feat(canvas): 画布节点与背景 - 扩展节点能力并优化连接与网格
This commit is contained in:
@@ -62,7 +62,7 @@ import { CanvasVersionCompareModal } from "@/components/canvas/canvas-version-co
|
||||
import { CanvasLocalAgentPanel } from "@/components/canvas/canvas-local-agent-panel";
|
||||
import { useFocusMode } from "@/hooks/use-focus-mode";
|
||||
import { useCanvasAgentStore } from "@/stores/canvas/use-canvas-agent-store";
|
||||
import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
|
||||
import { getContextResourceNodes, type CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
|
||||
import { CanvasConnectionCreateMenu, CanvasNodePanelOverlay } from "@/components/canvas/canvas-workspace-overlays";
|
||||
import { CanvasLeaferGraphicsLayer } from "@/components/canvas/canvas-leafer-graphics-layer";
|
||||
import { CanvasFreeformEmptyState, CanvasLinkedProjectEmptyState, CanvasShortDramaEmptyState, CanvasShortDramaGuide, CanvasStoryInputNodeContent, CanvasStylePlaceholderNodeContent } from "@/components/canvas/canvas-short-drama-entry";
|
||||
@@ -83,6 +83,7 @@ import { CanvasProjectSelectionToolbar } from "./canvas-project-selection-toolba
|
||||
import { CanvasProjectStatusDialogs } from "./canvas-project-status-dialogs";
|
||||
import { CanvasProjectWorldLayers } from "./canvas-project-world-layers";
|
||||
import { CanvasNodeActionContext } from "@/components/canvas/canvas-node-action-context";
|
||||
import { CanvasNodeGraphContext, type CanvasNodeGraphContextValue } from "@/components/canvas/canvas-node-graph-context";
|
||||
import { CanvasRefreshShell } from "./canvas-refresh-shell";
|
||||
import type { CanvasImageEmotionPayload } from "@/components/canvas/canvas-node-emotion-panel";
|
||||
import { CanvasEmotionWorkspace } from "@/components/canvas/canvas-emotion-workspace";
|
||||
@@ -189,7 +190,7 @@ function InfiniteCanvasPage() {
|
||||
const [hoveredNodeId, setHoveredNodeId] = useState<string | null>(null);
|
||||
const [contextMenu, setContextMenu] = useState<ContextMenuState | null>(null);
|
||||
const [isMiniMapOpen, setIsMiniMapOpen] = useState(false);
|
||||
const [backgroundMode, setBackgroundMode] = useState<CanvasBackgroundMode>("dots");
|
||||
const [backgroundMode, setBackgroundMode] = useState<CanvasBackgroundMode>("lines");
|
||||
const [showImageInfo, setShowImageInfo] = useState(false);
|
||||
const [canvasTool, setCanvasTool] = useState<CanvasToolMode>("move");
|
||||
const [mediaPerformanceMode, setMediaPerformanceMode] = useState<CanvasMediaPerformanceMode>(readCanvasMediaPerformanceMode);
|
||||
@@ -409,6 +410,10 @@ function InfiniteCanvasPage() {
|
||||
setNodes((current) => refreshCanvasCharacterReferenceNodes(current, linkedProjectQuery.data.assets));
|
||||
}, [linkedProjectQuery.data, projectLoaded, setNodes]);
|
||||
const canvasContext = useMemo(() => summarizeCanvasContext(nodes, selectedNodeIds, linkedProjectQuery.data?.units), [linkedProjectQuery.data?.units, nodes, selectedNodeIds]);
|
||||
// 扩展节点(对比/图表/调色)要读自己的上游才能渲染,经 Context 下发;
|
||||
// 取上游复用 canvas-resource-references 的实现,别在这里另写一份。必须 memo——
|
||||
// 每帧新对象会让所有节点跟着重渲染,错题本里多条崩溃都出在画布高频更新。
|
||||
const nodeGraphContext = useMemo<CanvasNodeGraphContextValue>(() => ({ getUpstreamNodes: (nodeId: string) => getContextResourceNodes(nodeId, nodes, connections) }), [connections, nodes]);
|
||||
|
||||
const {
|
||||
applyGenerationTaskResult,
|
||||
@@ -1867,7 +1872,8 @@ function InfiniteCanvasPage() {
|
||||
onFileDragLeave={handleFileDragLeave}
|
||||
onFileDragOver={handleFileDragOver}
|
||||
>
|
||||
<CanvasNodeActionContext.Provider value={{ download: downloadNodeImage, duplicate: (node) => duplicateNode(node.id), deleteNode: (node) => deleteNodes(new Set([node.id])) }}>
|
||||
<CanvasNodeActionContext.Provider value={{ download: downloadNodeImage, duplicate: (node) => duplicateNode(node.id), deleteNode: (node) => deleteNodes(new Set([node.id])), updateMetadata: (nodeId, patch) => setNodes((current) => current.map((node) => (node.id === nodeId ? { ...node, metadata: { ...node.metadata, ...patch } } : node))), resizeNode: (nodeId, size) => setNodes((current) => current.map((node) => (node.id === nodeId ? { ...node, width: size.width, height: size.height } : node))) }}>
|
||||
<CanvasNodeGraphContext.Provider value={nodeGraphContext}>
|
||||
<CanvasProjectWorldLayers
|
||||
projectId={projectId}
|
||||
viewportScale={viewport.k}
|
||||
@@ -1938,6 +1944,7 @@ function InfiniteCanvasPage() {
|
||||
onOpenDrawing={openDrawingNode}
|
||||
onStartBatchConnection={startBatchConnection}
|
||||
/>
|
||||
</CanvasNodeGraphContext.Provider>
|
||||
</CanvasNodeActionContext.Provider>
|
||||
</InfiniteCanvas>
|
||||
|
||||
@@ -1981,6 +1988,7 @@ function InfiniteCanvasPage() {
|
||||
onAddFrame={() => createNode(CanvasNodeType.Frame)}
|
||||
onAddFolder={createFolder}
|
||||
onAddDrawing={() => createNode(CanvasNodeType.Drawing)}
|
||||
onAddExtensionNode={(type) => createNode(type)}
|
||||
onOpenDirector={() => createDirectorShot()}
|
||||
onUndo={undoCanvas}
|
||||
onRedo={redoCanvas}
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function SharedCanvasPage() {
|
||||
const [title, setTitle] = useState("共享画布");
|
||||
const [nodes, setNodes] = useState<CanvasNodeData[]>([]);
|
||||
const [connections, setConnections] = useState<Awaited<ReturnType<typeof getPublicCanvasShare>>["project"]["connections"]>([]);
|
||||
const [backgroundMode, setBackgroundMode] = useState<"lines" | "dots" | "blank">("dots");
|
||||
const [backgroundMode, setBackgroundMode] = useState<"lines" | "dots" | "blank">("lines");
|
||||
const [viewport, setViewport] = useState<ViewportTransform>({ x: 0, y: 0, k: 1 });
|
||||
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
||||
const [infoNodeId, setInfoNodeId] = useState<string | null>(null);
|
||||
@@ -80,7 +80,7 @@ export default function SharedCanvasPage() {
|
||||
setTitle(project.title || "共享画布");
|
||||
setNodes((project.nodes || []).map(ensureMediaNodeMinimumSize));
|
||||
setConnections(project.connections || []);
|
||||
setBackgroundMode(project.backgroundMode || "dots");
|
||||
setBackgroundMode(project.backgroundMode || "lines");
|
||||
const initial = project.viewport || { x: 0, y: 0, k: 1 };
|
||||
viewportRef.current = initial;
|
||||
setViewport(initial);
|
||||
|
||||
@@ -337,7 +337,7 @@ export function useCanvasConnectionController({
|
||||
const targetHandleId = node.type === CanvasNodeType.Script ? storyboardHandleAtY(node, world.y, scrollTop) : undefined;
|
||||
if (node.type === CanvasNodeType.Script && !targetHandleId) return;
|
||||
const targetAnchorRatio = node.type === CanvasNodeType.Script ? undefined : Math.min(0.94, Math.max(0.06, (world.y - node.position.y) / Math.max(node.height, 1)));
|
||||
const anchor = getConnectionTargetAnchor(node, current, targetHandleId, scrollTop, targetAnchorRatio);
|
||||
const anchor = getConnectionTargetAnchor(node, current, targetHandleId, scrollTop);
|
||||
const dx = world.x - anchor.x;
|
||||
const dy = world.y - anchor.y;
|
||||
const hitsHandle = dx * dx + dy * dy <= handleRadius * handleRadius;
|
||||
@@ -383,7 +383,7 @@ export function useCanvasConnectionController({
|
||||
const targetHandleId = node.type === CanvasNodeType.Script ? storyboardHandleAtY(node, world.y, scrollTop) : undefined;
|
||||
if (node.type === CanvasNodeType.Script && !targetHandleId) return;
|
||||
const targetAnchorRatio = node.type === CanvasNodeType.Script ? undefined : Math.min(0.94, Math.max(0.06, (world.y - node.position.y) / Math.max(node.height, 1)));
|
||||
const anchor = getConnectionTargetAnchor(node, current, targetHandleId, scrollTop, targetAnchorRatio);
|
||||
const anchor = getConnectionTargetAnchor(node, current, targetHandleId, scrollTop);
|
||||
const dx = world.x - anchor.x;
|
||||
const dy = world.y - anchor.y;
|
||||
const hitsHandle = dx * dx + dy * dy <= handleRadius * handleRadius;
|
||||
|
||||
@@ -47,9 +47,11 @@ export function useCanvasNodeEditor({
|
||||
const nextPosition = position || node.position;
|
||||
if (node.width === width && node.height === height && node.position.x === nextPosition.x && node.position.y === nextPosition.y) return node;
|
||||
changed = true;
|
||||
const resized = { ...node, width, height, position: nextPosition };
|
||||
// 打上「用户手动定过尺寸」标记:图片按真实比例自动适配时要避让它,
|
||||
// 否则每次图片重新加载都会把用户拉过的尺寸改回去。
|
||||
const resized = { ...node, width, height, position: nextPosition, metadata: { ...node.metadata, manualSize: true } };
|
||||
if (!isFrameNode(node) || node.metadata?.frame?.collapsed) return resized;
|
||||
return { ...resized, metadata: { ...node.metadata, frame: { collapsed: false, expandedWidth: width, expandedHeight: height } } };
|
||||
return { ...resized, metadata: { ...resized.metadata, frame: { collapsed: false, expandedWidth: width, expandedHeight: height } } };
|
||||
});
|
||||
return changed ? next : current;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user