refactor(canvas): 预演台 - 重构工作台与场景交互链路

This commit is contained in:
ddcat666 committed 2026-10-06 09:11:15 +08:00
1 parent 1dfc8ce7ac
commit 112916587c
128 files changed
+42372 -5011

No files matched your search

@@ -17,7 +17,7 @@ type CanvasProjectContextMenuProps = {
onAddNode: (type: CanvasNodeTypeId, position: Position) => void;
onAddFolder: (position: Position) => void;
onChooseStyle: () => void;
onOpenDirector: (position?: Position) => void;
onOpenPrevis: (position?: Position) => void;
onUpload: (nodeId: string | undefined, position: Position) => void;
onOpenAssets: (position: Position) => void;
onOpenProjectCharacters: (position: Position) => void;
@@ -66,7 +66,7 @@ export function CanvasProjectContextMenu({ menu, node, screenToCanvas, ...props
if (menu.type === "canvas") props.onAddFolder(menu.position);
}}
onChooseStyle={props.onChooseStyle}
onOpenDirector={props.onOpenDirector}
onOpenPrevis={props.onOpenPrevis}
onUpload={() => props.onUpload(menu.type === "node" ? menu.nodeId : undefined, menuPosition())}
onOpenAssets={() => props.onOpenAssets(menuPosition())}
onOpenProjectCharacters={() => props.onOpenProjectCharacters(menuPosition())}
@@ -76,7 +76,7 @@ type CanvasProjectWorldLayersProps = {
onViewImage: (node: CanvasNodeData) => void;
onReplaceMedia: (node: CanvasNodeData) => void;
onOpenTextEditor: (node: CanvasNodeData) => void;
onOpenDirector: (node: CanvasNodeData) => void;
onOpenPrevis: (node: CanvasNodeData) => void;
onOpenDrawing: (node: CanvasNodeData) => void;
onStartBatchConnection: (event: ReactPointerEvent, sourceNodeIds: string[]) => void;
};
@@ -197,7 +197,7 @@ export const CanvasProjectWorldLayers = memo(function CanvasProjectWorldLayers(p
onViewImage={props.onViewImage}
onReplaceMedia={props.onReplaceMedia}
onOpenTextEditor={props.onOpenTextEditor}
onOpenDirector={props.onOpenDirector}
onOpenPrevis={props.onOpenPrevis}
onOpenDrawing={props.onOpenDrawing}
onContextMenu={props.onNodeContextMenu}
/>
+43 -38
View File
@@ -55,7 +55,7 @@ import { CanvasNodeLightingPanel } from "@/components/canvas/canvas-node-lightin
import { CanvasTextEditorModal } from "@/components/canvas/canvas-text-editor-modal";
import { CanvasNodeSearchModal } from "@/components/canvas/canvas-node-search-modal";
import { CanvasStylePickerModal } from "@/components/canvas/canvas-style-picker-modal";
import { CanvasDirectorTemplateModal } from "@/components/canvas/director/canvas-director-template-modal";
import { CanvasPrevisTemplateModal } from "@/components/canvas/previs/canvas-previs-template-modal";
import { CanvasFileDropOverlay } from "@/components/canvas/canvas-file-drop-overlay";
import { CanvasUploadModal } from "@/components/canvas/canvas-upload-modal";
import { CanvasPanoramaConfigModal } from "@/components/canvas/canvas-panorama-config-modal";
@@ -74,7 +74,7 @@ import { CanvasBatchTableNodeContent } from "@/components/canvas/canvas-batch-ta
import { BatchGenerationSettingsDialog } from "@/components/canvas/batch-generation-settings-dialog";
import { batchReferenceColumns, promoteLegacyBatchTableSize } from "@/lib/canvas/canvas-batch-table";
import { storyboardMinNodeHeight } from "@/lib/canvas/canvas-storyboard-layout";
import { CanvasDirectorNodePanel } from "@/components/canvas/director/canvas-director-node-panel";
import { CanvasPrevisNodePanel } from "@/components/canvas/previs/canvas-previs-node-panel";
import { CanvasVersionCompareModal } from "@/components/canvas/canvas-version-compare-modal";
import { useFocusMode } from "@/hooks/use-focus-mode";
import { connectCanvasTextMention } from "@/lib/canvas/canvas-text-mention";
@@ -132,7 +132,7 @@ import { useCanvasOperationHistory } from "./use-canvas-operation-history";
import { useCanvasAssistantVisibility } from "./use-canvas-assistant-visibility";
import { useCanvasActiveTasks } from "./use-canvas-active-tasks";
import { useCanvasStyleWorkflow } from "./use-canvas-style-workflow";
import { useCanvasDirector } from "./use-canvas-director";
import { useCanvasPrevis } from "./use-canvas-previs";
import { useCanvasGeneration } from "./use-canvas-generation";
import { useCanvasGenerationBatches } from "./use-canvas-generation-batches";
import { useCanvasBatchTable } from "./use-canvas-batch-table";
@@ -172,7 +172,7 @@ import {
import { ART_CRITIQUE_NODE_TYPE } from "@/lib/art-critique/contracts";
import { copyImageToSystemClipboard } from "./canvas-clipboard";
const CanvasDirectorWorkbench = lazy(() => import("@/components/canvas/director/canvas-director-workbench").then((module) => ({ default: module.CanvasDirectorWorkbench })));
const CanvasPrevisWorkbench = lazy(() => import("@/components/canvas/previs/canvas-previs-workbench").then((module) => ({ default: module.CanvasPrevisWorkbench })));
const CanvasDrawingEditorModal = lazy(() => import("@/components/canvas/canvas-drawing-editor-modal").then((module) => ({ default: module.CanvasDrawingEditorModal })));
const NODE_STATUS_SUCCESS = "success" as const;
@@ -221,7 +221,7 @@ function InfiniteCanvasPage() {
const theme = canvasThemes[colorTheme];
const defaultDrawingEngine = useUserStore((state) => state.drawingEngine.defaultEngine);
const shortDramaEnabled = useUserStore((state) => state.features.shortDramaEnabled);
const directorOnboardingScope = useUserStore((state) => state.user?.id?.trim() || "");
const previsOnboardingScope = useUserStore((state) => state.user?.id?.trim() || "");
const nodesRef = useRef<CanvasNodeData[]>([]);
const [nodes, setNodesState] = useState<CanvasNodeData[]>([]);
const setNodes = useMemo(() => createCanvasStateWriter(nodesRef, setNodesState, stampCanvasNodeChanges), []);
@@ -265,8 +265,8 @@ function InfiniteCanvasPage() {
const [characterLibraryPosition, setCharacterLibraryPosition] = useState<Position | undefined>();
const [drawingNodeId, setDrawingNodeId] = useState<string | null>(null);
const [stylePickerOpen, setStylePickerOpen] = useState(false);
// 新建导演台镜头必须先选模板:null 表示未在选择中,undefined position 表示用画布中心。
const [directorTemplateRequest, setDirectorTemplateRequest] = useState<{ position?: Position } | null>(null);
// 新建预演台镜头必须先选模板:null 表示未在选择中,undefined position 表示用画布中心。
const [previsTemplateRequest, setPrevisTemplateRequest] = useState<{ position?: Position } | null>(null);
const [infoNodeId, setInfoNodeId] = useState<string | null>(null);
const [subtitleNodeId, setSubtitleNodeId] = useState<string | null>(null);
const [timelineNodeId, setTimelineNodeId] = useState<string | null>(null);
@@ -277,7 +277,7 @@ function InfiniteCanvasPage() {
const artCritiqueRunningRef = useRef(false);
const [artCritiqueStartRequest, setArtCritiqueStartRequest] = useState<{ nodeId: string; id: string; restart: boolean } | null>(null);
const [scriptScrollTopById, setScriptScrollTopById] = useState<Record<string, number>>({});
const [directorNodeId, setDirectorNodeId] = useState<string | null>(null);
const [previsNodeId, setPrevisNodeId] = useState<string | null>(null);
const [versionCompareRootId, setVersionCompareRootId] = useState<string | null>(null);
const [libTVImportOpen, setLibTVImportOpen] = useState(false);
const [titleEditing, setTitleEditing] = useState(false);
@@ -922,7 +922,7 @@ function InfiniteCanvasPage() {
setRunningNodeId(clearDeletedId);
setScriptEditorNodeId(clearDeletedId);
setArtCritiqueNodeId(clearDeletedId);
setDirectorNodeId(clearDeletedId);
setPrevisNodeId(clearDeletedId);
setVersionCompareRootId(clearDeletedId);
setScriptScrollTopById((current) => Object.fromEntries(Object.entries(current).filter(([id]) => !removedIds.has(id))));
setContextMenu((current) => (current?.type === "node" && removedIds.has(current.nodeId) ? null : current));
@@ -1361,7 +1361,7 @@ function InfiniteCanvasPage() {
}, [linkedProjectQuery.data?.assetFolders, setNodes]);
const {
activeDirectorScene,
activePrevisScene,
activeNodeId,
activeScriptNode,
activeStylePresetId,
@@ -1407,7 +1407,7 @@ function InfiniteCanvasPage() {
dragPreview,
collapsingBatchIds,
addedSkills,
directorScenes: currentProject?.directorScenes,
previsScenes: currentProject?.previsScenes,
infoNodeId,
cropNodeId,
maskEditNodeId,
@@ -1422,7 +1422,7 @@ function InfiniteCanvasPage() {
previewNodeId,
contextMenu,
versionCompareRootId,
directorNodeId,
previsNodeId,
scriptEditorNodeId,
dialogNodeId,
});
@@ -1593,11 +1593,11 @@ function InfiniteCanvasPage() {
setStylePickerOpen,
});
const { applyDirectorOutput, createDirectorShot, openDirectorWorkbench, saveDirectorScene } = useCanvasDirector({
const { applyPrevisOutput, createPrevisShot, openPrevisWorkbench, savePrevisScene } = useCanvasPrevis({
projectId,
domainProjectId: currentProject?.projectId,
directorNodeId,
directorScenes: currentProject?.directorScenes || [],
previsNodeId,
previsScenes: currentProject?.previsScenes || [],
nodesRef,
connectionsRef,
getCanvasCenter,
@@ -1605,7 +1605,7 @@ function InfiniteCanvasPage() {
setConnections,
setSelectedNodeIds,
setSelectedConnectionId,
setDirectorNodeId,
setPrevisNodeId,
updateProject,
});
@@ -2312,14 +2312,17 @@ function InfiniteCanvasPage() {
/>
);
}
if (contentNode.metadata?.directorSceneId) {
if (contentNode.metadata?.previsSceneId) {
return (
<CanvasDirectorNodePanel
<CanvasPrevisNodePanel
node={contentNode}
scene={currentProject?.directorScenes?.find((scene) => scene.id === contentNode.metadata?.directorSceneId) || null}
readNodeContent={(nodeId) => (nodeId ? nodesRef.current.find((item) => item.id === nodeId)?.metadata?.content : undefined)}
scene={currentProject?.previsScenes?.find((scene) => scene.id === contentNode.metadata?.previsSceneId) || null}
readNodeContent={(nodeId) => {
const previewNode = nodeId ? nodesRef.current.find((item) => item.id === nodeId) : undefined;
return previewNode ? { content: previewNode.metadata?.content, storageKey: previewNode.metadata?.storageKey } : undefined;
}}
professional={workspaceMode === "professional"}
onOpen={() => openDirectorWorkbench(contentNode.id)}
onOpen={() => openPrevisWorkbench(contentNode.id)}
/>
);
}
@@ -2349,7 +2352,7 @@ function InfiniteCanvasPage() {
createStoryboardFromBatchTable,
createScriptImageNodes,
createScriptVideoNodes,
currentProject?.directorScenes,
currentProject?.previsScenes,
fillRowsFromConnections,
focusCanvasImageNode,
generateBatchRows,
@@ -2365,7 +2368,7 @@ function InfiniteCanvasPage() {
handleNodeResize,
mentionReferencesByNodeId,
mergeVideosByIds,
openDirectorWorkbench,
openPrevisWorkbench,
openStoryInput,
patchBatchTable,
removeBatchReferenceColumn,
@@ -2436,7 +2439,7 @@ function InfiniteCanvasPage() {
);
const openCanvasNodeVersions = useCallback((node: CanvasNodeData) => setVersionCompareRootId(node.metadata?.versionOfNodeId || node.id), []);
const viewCanvasNodeImage = useCallback((node: CanvasNodeData) => setPreviewNodeId(node.id), []);
const editCanvasDirector = useCallback((node: CanvasNodeData) => openDirectorWorkbench(node.id), [openDirectorWorkbench]);
const editCanvasPrevis = useCallback((node: CanvasNodeData) => openPrevisWorkbench(node.id), [openPrevisWorkbench]);
const locateProjectStyleNode = useCallback(() => {
const styleNode = nodesRef.current.find((node) => node.type === CanvasNodeType.Text && node.metadata?.workflowKind === "styleboard");
if (!styleNode) {
@@ -2460,7 +2463,7 @@ function InfiniteCanvasPage() {
onAddWorkflow: () => createNode(CanvasNodeType.Config),
onAddExtensionNode: (type) => createNode(type),
onChooseStyle: () => setStylePickerOpen(true),
onOpenDirector: () => setDirectorTemplateRequest({}),
onOpenPrevis: () => setPrevisTemplateRequest({}),
onUpload: () => handleUploadRequest(),
onOpenMyAssets: () => openCanvasAssetLibrary(),
onOpenProjectCharacters: () => openCharacterLibrary(),
@@ -2602,7 +2605,7 @@ function InfiniteCanvasPage() {
<CanvasStylePickerModal open={stylePickerOpen} value={activeStylePresetId} applying={styleApplying} onClose={() => setStylePickerOpen(false)} onSelect={selectCanvasStyle} />
<CanvasDirectorTemplateModal open={Boolean(directorTemplateRequest)} onClose={() => setDirectorTemplateRequest(null)} onSelect={(templateId) => createDirectorShot(templateId, directorTemplateRequest?.position)} />
<CanvasPrevisTemplateModal open={Boolean(previsTemplateRequest)} onClose={() => setPrevisTemplateRequest(null)} onSelect={(templateId) => createPrevisShot(templateId, previsTemplateRequest?.position)} />
<div className="relative flex min-h-0 min-w-0 flex-1">
<div className="relative min-w-0 flex-1 overflow-hidden">
@@ -2716,7 +2719,7 @@ function InfiniteCanvasPage() {
onViewImage={viewCanvasNodeImage}
onReplaceMedia={replaceCanvasNodeMedia}
onOpenTextEditor={openTextNodeEditor}
onOpenDirector={editCanvasDirector}
onOpenPrevis={editCanvasPrevis}
onOpenDrawing={openDrawingNode}
onStartBatchConnection={startBatchConnection}
/>
@@ -2768,7 +2771,7 @@ function InfiniteCanvasPage() {
onAddDrawing={() => createNode(CanvasNodeType.Drawing)}
onAddExtensionNode={(type) => createNode(type)}
onAddWorkflow={() => createNode(CanvasNodeType.Config)}
onOpenDirector={() => setDirectorTemplateRequest({})}
onOpenPrevis={() => setPrevisTemplateRequest({})}
onUndo={undoCanvas}
onRedo={redoCanvas}
onUpload={() => handleUploadRequest()}
@@ -2861,6 +2864,7 @@ function InfiniteCanvasPage() {
dialogNode.type !== CanvasNodeType.Drawing &&
dialogNode.type !== CanvasNodeType.Panorama &&
!(dialogNode.metadata?.workflowKind === "character" && dialogNode.metadata.characterAssetId) &&
!dialogNode.metadata?.previsSceneId &&
!selectionBox &&
!isCanvasNodeMoving ? (
<CanvasNodePanelOverlay
@@ -3031,7 +3035,7 @@ function InfiniteCanvasPage() {
onAddNode={(type, position) => createNode(type, position)}
onAddFolder={createFolder}
onChooseStyle={() => setStylePickerOpen(true)}
onOpenDirector={(position) => setDirectorTemplateRequest({ position })}
onOpenPrevis={(position) => setPrevisTemplateRequest({ position })}
onUpload={(nodeId, position) => handleUploadRequest(nodeId, position)}
onOpenAssets={openCanvasAssetLibrary}
onOpenProjectCharacters={(position) => openCharacterLibrary(position)}
@@ -3235,24 +3239,25 @@ function InfiniteCanvasPage() {
onVideoInputModeChange={(storyboardVideoInputMode) => activeScriptNode && handleConfigNodeChange(activeScriptNode.id, { storyboardVideoInputMode })}
/>
{directorNodeId && activeDirectorScene ? (
{previsNodeId && activePrevisScene ? (
<Suspense
fallback={
<div className="fixed inset-0 z-[var(--z-toast)] grid place-items-center px-5" style={{ background: theme.canvas.background, color: theme.node.text }}>
<WorkspaceState icon="loading" title="正在加载 3D 导演台" description="准备场景、镜头与空间控制。" />
<WorkspaceState icon="loading" title="正在加载 3D 预演台" description="准备场景、镜头与空间控制。" />
</div>
}
>
<CanvasDirectorWorkbench
<CanvasPrevisWorkbench
open
scene={activeDirectorScene}
imageNodes={nodes.filter((node) => node.type === CanvasNodeType.Image && Boolean(node.metadata?.content))}
onClose={() => setDirectorNodeId(null)}
onChange={saveDirectorScene}
onApply={applyDirectorOutput}
canvasId={projectId}
scene={activePrevisScene}
imageNodes={nodes.filter((node) => node.type === CanvasNodeType.Image && Boolean(node.metadata?.content || node.metadata?.storageKey))}
onClose={() => setPrevisNodeId(null)}
onChange={savePrevisScene}
onApply={applyPrevisOutput}
onDeleteImageNode={(nodeId) => deleteNodes(new Set([nodeId]))}
onFlush={() => flushCanvasStorePersistence()}
onboardingScope={directorOnboardingScope}
onboardingScope={previsOnboardingScope}
/>
</Suspense>
) : null}
@@ -5,20 +5,20 @@ import { nanoid } from "nanoid";
import { imageMetadata, videoMetadata } from "@/lib/canvas/canvas-generation-task-sync";
import { fitNodeSize } from "@/lib/canvas/canvas-node-size";
import { createCanvasNode } from "@/lib/canvas/canvas-project-domain";
import { createDirectorSceneFromTemplate, type DirectorTemplateId } from "@/lib/canvas/director/director-templates";
import { mergeDirectorOutputPreview, upsertDirectorSceneById } from "@/lib/canvas/director/director-session";
import { createPrevisSceneFromTemplate, type PrevisTemplateId } from "@/lib/canvas/previs/previs-templates";
import { mergePrevisOutputPreview, upsertPrevisSceneById } from "@/lib/canvas/previs/previs-session";
import { uploadImage } from "@/services/image-storage";
import { uploadMediaFile } from "@/services/file-storage";
import { ensureCanvasNodeAsset } from "@/services/project-asset-sync";
import { useCanvasStore } from "@/stores/canvas/use-canvas-store";
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type CanvasNodeMetadata, type Position } from "@/types/canvas";
import type { DirectorScene, DirectorSceneOutput } from "@/types/director";
import type { PrevisScene, PrevisSceneOutput } from "@/types/previs";
type UseCanvasDirectorOptions = {
type UseCanvasPrevisOptions = {
projectId: string;
domainProjectId?: string;
directorNodeId: string | null;
directorScenes: DirectorScene[];
previsNodeId: string | null;
previsScenes: PrevisScene[];
nodesRef: { current: CanvasNodeData[] };
connectionsRef: { current: CanvasConnection[] };
getCanvasCenter: () => Position;
@@ -26,25 +26,34 @@ type UseCanvasDirectorOptions = {
setConnections: Dispatch<SetStateAction<CanvasConnection[]>>;
setSelectedNodeIds: Dispatch<SetStateAction<Set<string>>>;
setSelectedConnectionId: Dispatch<SetStateAction<string | null>>;
setDirectorNodeId: Dispatch<SetStateAction<string | null>>;
updateProject: (projectId: string, patch: { directorScenes: DirectorScene[] }) => void;
setPrevisNodeId: Dispatch<SetStateAction<string | null>>;
updateProject: (projectId: string, patch: { previsScenes: PrevisScene[] }) => void;
};
const NODE_STATUS_IDLE = "idle" as const;
const PREVIS_NODE_SIZE = { width: 520, height: 340 } as const;
const PREVIS_LEGACY_NODE_SIZE = { width: 760, height: 500 } as const;
function shouldUpgradePrevisNodeSize(node: CanvasNodeData) {
// 只迁移预演台曾经写入的默认尺寸;用户后续手动调整过的节点保持原样。
const isLegacyDefault = node.width === PREVIS_LEGACY_NODE_SIZE.width && node.height === PREVIS_LEGACY_NODE_SIZE.height;
const isEarlyCompactDefault = node.width <= 720 && node.height <= 320;
return isLegacyDefault || isEarlyCompactDefault;
}
/**
* 项目真实内存权威在 useCanvasStore.getState().projects;
* 闭包里的 directorScenes 只作为 store 尚未就绪时的兜底。
* 闭包里的 previsScenes 只作为 store 尚未就绪时的兜底。
*/
function currentDirectorScenes(projectId: string, fallback: DirectorScene[]) {
function currentPrevisScenes(projectId: string, fallback: PrevisScene[]) {
const project = useCanvasStore.getState().projects.find((item) => item.id === projectId);
return project?.directorScenes ?? fallback;
return project?.previsScenes ?? fallback;
}
export function useCanvasDirector({
export function useCanvasPrevis({
projectId,
domainProjectId,
directorNodeId,
directorScenes,
previsNodeId,
previsScenes,
nodesRef,
connectionsRef,
getCanvasCenter,
@@ -52,9 +61,9 @@ export function useCanvasDirector({
setConnections,
setSelectedNodeIds,
setSelectedConnectionId,
setDirectorNodeId,
setPrevisNodeId,
updateProject,
}: UseCanvasDirectorOptions) {
}: UseCanvasPrevisOptions) {
const { message } = App.useApp();
const projectIdRef = useRef<string | null>(projectId);
projectIdRef.current = projectId;
@@ -70,10 +79,10 @@ export function useCanvasDirector({
* 新建镜头。templateId 由调用方(模板选择弹窗)显式给出 —— 这里不设默认模板,
* 否则又会回到「无条件塞一个默认演员」的老问题。
*/
const createDirectorShot = useCallback((templateId: DirectorTemplateId, position?: Position) => {
const createPrevisShot = useCallback((templateId: PrevisTemplateId, position?: Position) => {
const shots = nodesRef.current.filter((node) => node.metadata?.workflowKind === "shot");
const shotIndex = Math.max(0, ...shots.map((node) => node.metadata?.shotIndex || 0)) + 1;
let scene = createDirectorSceneFromTemplate(templateId, `镜头 ${shotIndex}`);
let scene = createPrevisSceneFromTemplate(templateId, `镜头 ${shotIndex}`);
const shot = scene.shots[0];
scene = { ...scene, shots: [{ ...shot, name: `镜头 ${shotIndex}` }] };
const node = createCanvasNode(CanvasNodeType.Video, position || getCanvasCenter(), {
@@ -84,61 +93,67 @@ export function useCanvasDirector({
videoEditOperation: "text_to_video",
status: NODE_STATUS_IDLE,
composerContent: "",
directorSceneId: scene.id,
directorShotId: shot.id,
previsSceneId: scene.id,
previsShotId: shot.id,
});
node.title = `镜头 ${shotIndex}`;
node.height = 300;
node.width = PREVIS_NODE_SIZE.width;
node.height = PREVIS_NODE_SIZE.height;
const nextNodes = [...nodesRef.current, node];
nodesRef.current = nextNodes;
setNodes(nextNodes);
setSelectedNodeIds(new Set([node.id]));
setSelectedConnectionId(null);
updateProject(projectId, { directorScenes: upsertDirectorSceneById(currentDirectorScenes(projectId, directorScenes), scene) });
message.success("已创建导演台节点,点击缩略图进入编辑");
}, [directorScenes, getCanvasCenter, message, nodesRef, projectId, setNodes, setSelectedConnectionId, setSelectedNodeIds, updateProject]);
updateProject(projectId, { previsScenes: upsertPrevisSceneById(currentPrevisScenes(projectId, previsScenes), scene) });
message.success("已创建预演台节点,点击缩略图进入编辑");
}, [previsScenes, getCanvasCenter, message, nodesRef, projectId, setNodes, setSelectedConnectionId, setSelectedNodeIds, updateProject]);
const openDirectorWorkbench = useCallback((nodeId: string) => {
const openPrevisWorkbench = useCallback((nodeId: string) => {
const node = nodesRef.current.find((item) => item.id === nodeId);
if (!node || node.metadata?.workflowKind !== "shot") return;
let scene = currentDirectorScenes(projectId, directorScenes).find((item) => item.id === node.metadata?.directorSceneId);
if (shouldUpgradePrevisNodeSize(node)) {
const upgradedNodes = nodesRef.current.map((item) => item.id === nodeId ? { ...item, width: PREVIS_NODE_SIZE.width, height: PREVIS_NODE_SIZE.height } : item);
nodesRef.current = upgradedNodes;
setNodes(upgradedNodes);
}
let scene = currentPrevisScenes(projectId, previsScenes).find((item) => item.id === node.metadata?.previsSceneId);
if (!scene) {
// 孤儿节点修复路径:节点存在但场景丢了。这不是「新建」,不弹模板选择,
// 用空场景兜底 —— 绝不在用户没选过的情况下塞演员进去。
scene = createDirectorSceneFromTemplate("empty", node.metadata?.workflowTitle || node.title || "镜头场景");
scene = createPrevisSceneFromTemplate("empty", node.metadata?.workflowTitle || node.title || "镜头场景");
const shot = scene.shots[0];
scene = { ...scene, shots: [{ ...shot, name: node.metadata?.workflowTitle || node.title || shot.name, prompt: node.metadata?.workflowDescription || "" }] };
const directorSceneId = scene.id;
const directorShotId = shot.id;
setNodes((current) => current.map((item) => item.id === nodeId ? { ...item, metadata: { ...item.metadata, directorSceneId, directorShotId } } : item));
updateProject(projectId, { directorScenes: upsertDirectorSceneById(currentDirectorScenes(projectId, directorScenes), scene) });
const previsSceneId = scene.id;
const previsShotId = shot.id;
setNodes((current) => current.map((item) => item.id === nodeId ? { ...item, metadata: { ...item.metadata, previsSceneId, previsShotId } } : item));
updateProject(projectId, { previsScenes: upsertPrevisSceneById(currentPrevisScenes(projectId, previsScenes), scene) });
}
setDirectorNodeId(nodeId);
}, [directorScenes, nodesRef, projectId, setDirectorNodeId, setNodes, updateProject]);
setPrevisNodeId(nodeId);
}, [previsScenes, nodesRef, projectId, setPrevisNodeId, setNodes, updateProject]);
/** 每次保存都基于 store 中最新 directorScenes upsert,避免旧闭包数组覆盖并发保存。 */
const saveDirectorScene = useCallback((scene: DirectorScene) => {
updateProject(projectId, { directorScenes: upsertDirectorSceneById(currentDirectorScenes(projectId, directorScenes), scene) });
}, [directorScenes, projectId, updateProject]);
/** 每次保存都基于 store 中最新 previsScenes upsert,避免旧闭包数组覆盖并发保存。 */
const savePrevisScene = useCallback((scene: PrevisScene) => {
updateProject(projectId, { previsScenes: upsertPrevisSceneById(currentPrevisScenes(projectId, previsScenes), scene) });
}, [previsScenes, projectId, updateProject]);
const applyDirectorOutput = useCallback(async (output: DirectorSceneOutput) => {
const applyPrevisOutput = useCallback(async (output: PrevisSceneOutput) => {
const outputProjectId = projectId;
if (projectIdRef.current !== outputProjectId) throw new Error("画布项目已切换,请重试");
const sourceNodeAtStart = nodesRef.current.find((item) => item.id === directorNodeId);
if (!sourceNodeAtStart || sourceNodeAtStart.metadata?.directorSceneId !== output.scene.id) throw new Error("镜头节点不存在或场景已切换");
const sourceNodeAtStart = nodesRef.current.find((item) => item.id === previsNodeId);
if (!sourceNodeAtStart || sourceNodeAtStart.metadata?.previsSceneId !== output.scene.id) throw new Error("镜头节点不存在或场景已切换");
const sourceNodeId = sourceNodeAtStart.id;
const [image, videoUpload] = await Promise.all([
uploadImage(output.beauty),
output.clayVideo ? uploadMediaFile(output.clayVideo, "director-clay") : Promise.resolve(null),
output.clayVideo ? uploadMediaFile(output.clayVideo, "previs-clay") : Promise.resolve(null),
]);
// 上传期间项目、节点和镜头都可能变化。以当前权威状态重新核验并合并,
// 不允许旧输出写入另一项目,也不允许旧 scene 快照覆盖并发编辑。
const outputProject = useCanvasStore.getState().projects.find((item) => item.id === outputProjectId);
const sourceNode = nodesRef.current.find((item) => item.id === sourceNodeId);
const latestScene = outputProject?.directorScenes.find((item) => item.id === output.scene.id);
if (projectIdRef.current !== outputProjectId || !outputProject || !sourceNode || sourceNode.metadata?.directorSceneId !== output.scene.id || !latestScene || !latestScene.shots.some((shot) => shot.id === output.shot.id)) throw new Error("输出期间项目或镜头已切换、删除,请重试");
const previewId = sourceNode.metadata?.directorPreviewNodeId || `image-director-${Date.now()}`;
const mergedScene = mergeDirectorOutputPreview(latestScene, { sceneId: output.scene.id, shotId: output.shot.id, previewNodeId: previewId });
const latestScene = outputProject?.previsScenes.find((item) => item.id === output.scene.id);
if (projectIdRef.current !== outputProjectId || !outputProject || !sourceNode || sourceNode.metadata?.previsSceneId !== output.scene.id || !latestScene || !latestScene.shots.some((shot) => shot.id === output.shot.id)) throw new Error("输出期间项目或镜头已切换、删除,请重试");
const previewId = sourceNode.metadata?.previsPreviewNodeId || `image-previs-${Date.now()}`;
const mergedScene = mergePrevisOutputPreview(latestScene, { sceneId: output.scene.id, shotId: output.shot.id, previewNodeId: previewId });
if (!mergedScene) throw new Error("输出期间镜头已切换或删除,请重试");
const previewSize = fitNodeSize(image.width, image.height);
const nextNodes = [...nodesRef.current];
@@ -148,18 +163,18 @@ export function useCanvasDirector({
...existingPreview,
id: previewId,
type: CanvasNodeType.Image,
title: `${sourceNode.title} · 导演台构图`,
title: `${sourceNode.title} · 预演台构图`,
position: existingPreview?.position || { x: sourceNode.position.x - previewSize.width - 36, y: sourceNode.position.y },
width: previewSize.width,
height: previewSize.height,
metadata: { ...existingPreview?.metadata, ...imageMetadata(image), prompt: output.prompt, workflowKind: "reference_set", assetTags: ["导演台构图", `镜头:${sourceNode.title}`], producedModel: undefined, producedModelCandidate: undefined },
metadata: { ...existingPreview?.metadata, ...imageMetadata(image), prompt: output.prompt, workflowKind: "reference_set", assetTags: ["预演台构图", `镜头:${sourceNode.title}`], producedModel: undefined, producedModelCandidate: undefined },
};
if (previewIndex >= 0) nextNodes[previewIndex] = previewNode;
else nextNodes.push(previewNode);
let clayVideoId = sourceNode.metadata?.directorClayVideoNodeId;
let clayVideoId = sourceNode.metadata?.previsClayVideoNodeId;
if (videoUpload) {
clayVideoId ||= `video-director-clay-${Date.now()}`;
clayVideoId ||= `video-previs-clay-${Date.now()}`;
const videoIndex = nextNodes.findIndex((item) => item.id === clayVideoId);
const existingVideo = videoIndex >= 0 ? nextNodes[videoIndex] : null;
const videoNode: CanvasNodeData = {
@@ -170,7 +185,7 @@ export function useCanvasDirector({
position: existingVideo?.position || { x: sourceNode.position.x, y: sourceNode.position.y + sourceNode.height + 48 },
width: existingVideo?.width || 360,
height: existingVideo?.height || 220,
metadata: { ...existingVideo?.metadata, ...videoMetadata(videoUpload), prompt: output.prompt, workflowKind: "reference_video", assetTags: ["导演台白膜", `镜头:${sourceNode.title}`], producedModel: undefined, producedModelCandidate: undefined },
metadata: { ...existingVideo?.metadata, ...videoMetadata(videoUpload), prompt: output.prompt, workflowKind: "reference_video", assetTags: ["预演台白膜", `镜头:${sourceNode.title}`], producedModel: undefined, producedModelCandidate: undefined },
};
if (videoIndex >= 0) nextNodes[videoIndex] = videoNode;
else nextNodes.push(videoNode);
@@ -180,19 +195,19 @@ export function useCanvasDirector({
[previewId, videoUpload ? clayVideoId : null].filter((id): id is string => Boolean(id)).forEach((id) => {
if (!nextConnections.some((connection) => connection.fromNodeId === id && connection.toNodeId === sourceNode.id)) nextConnections.push({ id: nanoid(), fromNodeId: id, toNodeId: sourceNode.id });
});
const retiredReferenceIds = new Set([sourceNode.metadata?.directorDepthNodeId, sourceNode.metadata?.directorNormalNodeId].filter(Boolean));
const retiredReferenceIds = new Set([sourceNode.metadata?.previsDepthNodeId, sourceNode.metadata?.previsNormalNodeId].filter(Boolean));
const referenceAssetNodeIds = Array.from(new Set([
...(sourceNode.metadata?.referenceAssetNodeIds || []).filter((id) => !retiredReferenceIds.has(id)),
previewId,
...(clayVideoId ? [clayVideoId] : []),
]));
const directorMetadata: Partial<CanvasNodeMetadata> = {
directorSceneId: output.scene.id,
directorShotId: output.shot.id,
directorPreviewNodeId: previewId,
directorDepthNodeId: undefined,
directorNormalNodeId: undefined,
directorClayVideoNodeId: clayVideoId,
const previsMetadata: Partial<CanvasNodeMetadata> = {
previsSceneId: output.scene.id,
previsShotId: output.shot.id,
previsPreviewNodeId: previewId,
previsDepthNodeId: undefined,
previsNormalNodeId: undefined,
previsClayVideoNodeId: clayVideoId,
composerContent: output.prompt,
prompt: output.prompt,
videoCameraMoveId: output.shot.cameraMove,
@@ -208,14 +223,14 @@ export function useCanvasDirector({
const finalizedNodes = nextNodes.map((item) => {
const assetId = assetIds.get(item.id);
if (assetId) return { ...item, metadata: { ...item.metadata, assetId } };
return item.id === sourceNode.id ? { ...item, metadata: { ...item.metadata, ...directorMetadata } } : item;
return item.id === sourceNode.id ? { ...item, metadata: { ...item.metadata, ...previsMetadata } } : item;
});
nodesRef.current = finalizedNodes;
connectionsRef.current = nextConnections;
setNodes(finalizedNodes);
setConnections(nextConnections);
saveDirectorScene(mergedScene);
}, [connectionsRef, directorNodeId, domainProjectId, nodesRef, projectId, saveDirectorScene, setConnections, setNodes]);
savePrevisScene(mergedScene);
}, [connectionsRef, previsNodeId, domainProjectId, nodesRef, projectId, savePrevisScene, setConnections, setNodes]);
return { applyDirectorOutput, createDirectorShot, openDirectorWorkbench, saveDirectorScene };
return { applyPrevisOutput, createPrevisShot, openPrevisWorkbench, savePrevisScene };
}
@@ -12,7 +12,7 @@ import { nodeGenerationPrompt } from "@/lib/canvas/generation-contract";
import { intersectsCanvasBounds, selectCanvasVisibleNodes } from "@/lib/canvas/canvas-node-visibility";
import type { Skill } from "@/services/api/skills";
import type { Asset, ImageAsset } from "@/stores/use-asset-store";
import type { DirectorScene } from "@/types/director";
import type { PrevisScene } from "@/types/previs";
import { CanvasNodeType, type CanvasConnection, type CanvasDisplayConnection, type CanvasMediaPerformanceMode, type CanvasNodeData, type ContextMenuState, type ViewportTransform } from "@/types/canvas";
type DragPreview = { x: number; y: number; nodeIds: Set<string> } | null;
@@ -29,7 +29,7 @@ type UseCanvasRenderModelOptions = {
dragPreview: DragPreview;
collapsingBatchIds: Set<string>;
addedSkills: Skill[];
directorScenes?: DirectorScene[];
previsScenes?: PrevisScene[];
infoNodeId: string | null;
cropNodeId: string | null;
maskEditNodeId: string | null;
@@ -44,7 +44,7 @@ type UseCanvasRenderModelOptions = {
previewNodeId: string | null;
contextMenu: ContextMenuState | null;
versionCompareRootId: string | null;
directorNodeId: string | null;
previsNodeId: string | null;
scriptEditorNodeId: string | null;
dialogNodeId: string | null;
};
@@ -61,7 +61,7 @@ export function useCanvasRenderModel({
dragPreview,
collapsingBatchIds,
addedSkills,
directorScenes,
previsScenes,
infoNodeId,
cropNodeId,
maskEditNodeId,
@@ -76,7 +76,7 @@ export function useCanvasRenderModel({
previewNodeId,
contextMenu,
versionCompareRootId,
directorNodeId,
previsNodeId,
scriptEditorNodeId,
dialogNodeId,
}: UseCanvasRenderModelOptions) {
@@ -389,10 +389,10 @@ export function useCanvasRenderModel({
configNodeIds.forEach((nodeId) => map.set(nodeId, buildNodeGenerationInputs(nodeId, semanticNodes, connections)));
return map;
}, [connections, dialogNodeId, nodeById, selectedNodeIds, semanticNodes, visibleNodes]);
const activeDirectorNode = useMemo(() => semanticNodes.find((node) => node.id === directorNodeId) || null, [directorNodeId, semanticNodes]);
const activePrevisNode = useMemo(() => semanticNodes.find((node) => node.id === previsNodeId) || null, [previsNodeId, semanticNodes]);
const activeStylePresetId = useMemo(() => semanticNodes.find((node) => node.metadata?.workflowKind === "styleboard")?.metadata?.stylePresetId, [semanticNodes]);
const activeScriptNode = useMemo(() => semanticNodes.find((node) => node.id === scriptEditorNodeId && node.type === CanvasNodeType.Script) || null, [scriptEditorNodeId, semanticNodes]);
const activeDirectorScene = useMemo(() => directorScenes?.find((scene) => scene.id === activeDirectorNode?.metadata?.directorSceneId) || null, [activeDirectorNode?.metadata?.directorSceneId, directorScenes]);
const activePrevisScene = useMemo(() => previsScenes?.find((scene) => scene.id === activePrevisNode?.metadata?.previsSceneId) || null, [activePrevisNode?.metadata?.previsSceneId, previsScenes]);
const resourceReferenceTargetNodes = useMemo(() => {
const targetNodes = [...visibleNodes];
const activeId = dialogNodeId || activeNodeId;
@@ -437,8 +437,8 @@ export function useCanvasRenderModel({
}, [connections, semanticNodes, skillMentionReferences, toolMentionReferencesByNodeId, visibleNodes]);
return {
activeDirectorNode,
activeDirectorScene,
activePrevisNode,
activePrevisScene,
activeNodeId,
activeScriptNode,
activeStylePresetId,
@@ -2,18 +2,18 @@ import { useCallback, useMemo, useState } from "react";
import { Button, Table } from "antd";
import { Switch } from "@/components/ui/base/switch";
import { CanvasDirectorWorkbench } from "@/components/canvas/director/canvas-director-workbench";
import { DIRECTOR_REPRO_MATRIX, createDirectorReproScene, directorReproSceneIsOffline, injectDirectorReproModel, type DirectorReproModelVariant } from "@/lib/canvas/director/director-repro-fixture";
import { readDirectorReproSnapshot, type DirectorReproSnapshot } from "@/lib/canvas/director/director-repro-runtime";
import { resetDirectorDiagnosticDedupe } from "@/lib/canvas/director/director-diagnostics-recorder";
import { CanvasPrevisWorkbench } from "@/components/canvas/previs/canvas-previs-workbench";
import { PREVIS_REPRO_MATRIX, createPrevisReproScene, previsReproSceneIsOffline, injectPrevisReproModel, type PrevisReproModelVariant } from "@/lib/canvas/previs/previs-repro-fixture";
import { readPrevisReproSnapshot, type PrevisReproSnapshot } from "@/lib/canvas/previs/previs-repro-runtime";
import { resetPrevisDiagnosticDedupe } from "@/lib/canvas/previs/previs-diagnostics-recorder";
import { getClientDiagnosticEvents } from "@/services/diagnostics/client-diagnostics";
import { StatusBadge } from "@/components/ui/base/badges";
import type { DirectorScene } from "@/types/director";
import type { PrevisScene } from "@/types/previs";
/**
* P0 手工复现入口(仅 DEV 注册)。
*
* 渲染真实 CanvasDirectorWorkbench + 确定性 fixture,不是静态 mock。
* 渲染真实 CanvasPrevisWorkbench + 确定性 fixture,不是静态 mock。
* 本页自己的 callback 都是本地确定性实现,不调用任何 API:
* onChange 只写本页 state,onFlush 只在「强制失败」开关打开时抛错,
* onApply 只累加本地计数。
@@ -22,21 +22,21 @@ import type { DirectorScene } from "@/types/director";
* 真实工作台自带的新增控件(默认演员 GLB、上传模型、画布图片立牌)
* 仍可能联网,这是刻意保留的真实行为 —— 不要据此宣称整个工作台永不发网。
*/
export default function DirectorReproLab() {
const [scene, setScene] = useState<DirectorScene>(() => createDirectorReproScene());
export default function PrevisReproLab() {
const [scene, setScene] = useState<PrevisScene>(() => createPrevisReproScene());
// workbench 是 fixed inset-0 全屏浮层,会完全遮住本页。默认关闭,
// 让环境快照 / 事件列表 / 复现矩阵先可读,再由用户显式打开进入复现。
const [workbenchOpen, setWorkbenchOpen] = useState(false);
const [forceSaveFailure, setForceSaveFailure] = useState(false);
const [appliedCount, setAppliedCount] = useState(0);
const [flushCount, setFlushCount] = useState(0);
const [events, setEvents] = useState(() => readDirectorEvents());
const snapshot = useMemo(() => readDirectorReproSnapshot(), []);
const [events, setEvents] = useState(() => readPrevisEvents());
const snapshot = useMemo(() => readPrevisReproSnapshot(), []);
const refreshEvents = useCallback(() => setEvents(readDirectorEvents()), []);
const refreshEvents = useCallback(() => setEvents(readPrevisEvents()), []);
/** 本地确定性保存:只写本页 state,不触碰项目 store,也不发请求。 */
const onChange = useCallback((next: DirectorScene) => setScene(next), []);
const onChange = useCallback((next: PrevisScene) => setScene(next), []);
/** 强制失败开关用于复现「保存失败 / 关闭恢复」,抛错后由 coordinator 走真实 error 语义。 */
const onFlush = useCallback(() => {
@@ -49,8 +49,8 @@ export default function DirectorReproLab() {
}, []);
const reset = useCallback(() => {
resetDirectorDiagnosticDedupe();
setScene(createDirectorReproScene());
resetPrevisDiagnosticDedupe();
setScene(createPrevisReproScene());
setForceSaveFailure(false);
setAppliedCount(0);
setFlushCount(0);
@@ -59,15 +59,15 @@ export default function DirectorReproLab() {
}, [refreshEvents]);
/** 显式注入模型对象:固定 id,重复点击替换同一对象而不是堆叠。 */
const injectModel = useCallback((variant: DirectorReproModelVariant) => {
setScene((current) => injectDirectorReproModel(current, variant));
const injectModel = useCallback((variant: PrevisReproModelVariant) => {
setScene((current) => injectPrevisReproModel(current, variant));
}, []);
return (
<div className="min-h-dvh overflow-y-auto p-5" style={{ background: "var(--bg)", color: "var(--fg)" }}>
<header className="mb-4 flex flex-wrap items-center gap-3">
<h1 className="text-lg font-semibold">导演台 P0 复现台</h1>
<StatusBadge tone={directorReproSceneIsOffline(scene) ? "error" : "success"} data-testid="offline-tag" label={directorReproSceneIsOffline(scene) ? "fixture 无网络资产" : "已注入模型资产"} />
<h1 className="text-lg font-semibold">预演台 P0 复现台</h1>
<StatusBadge tone={previsReproSceneIsOffline(scene) ? "error" : "success"} data-testid="offline-tag" label={previsReproSceneIsOffline(scene) ? "fixture 无网络资产" : "已注入模型资产"} />
<span className="text-[var(--fs-tiny)] opacity-70" data-testid="object-count">
对象数 {scene.objects.length}
</span>
@@ -81,7 +81,7 @@ export default function DirectorReproLab() {
<span className="text-[var(--fs-tiny)] opacity-70">强制保存失败</span>
<Switch checked={forceSaveFailure} onChange={setForceSaveFailure} data-testid="force-save-failure" />
<Button size="small" data-testid="toggle-workbench" onClick={() => setWorkbenchOpen((open) => !open)}>
{workbenchOpen ? "关闭导演台" : "打开导演台"}
{workbenchOpen ? "关闭预演台" : "打开预演台"}
</Button>
<Button size="small" data-testid="refresh-events" onClick={refreshEvents}>
刷新事件
@@ -97,23 +97,23 @@ export default function DirectorReproLab() {
<ReproMatrix />
{workbenchOpen ? (
<CanvasDirectorWorkbench open scene={scene} imageNodes={[]} onboardingScope="director-repro-lab" onClose={() => setWorkbenchOpen(false)} onChange={onChange} onApply={onApply} onDeleteImageNode={() => undefined} onFlush={onFlush} />
<CanvasPrevisWorkbench canvasId="previs-repro-lab" open scene={scene} imageNodes={[]} onboardingScope="previs-repro-lab" onClose={() => setWorkbenchOpen(false)} onChange={onChange} onApply={onApply} onDeleteImageNode={() => undefined} onFlush={onFlush} />
) : null}
</div>
);
}
type DirectorEventRow = { id: string; timestamp: string; level: string; code: string; message: string };
type PrevisEventRow = { id: string; timestamp: string; level: string; code: string; message: string };
/** 只显示导演台自己的稳定码事件,避免整条诊断流噪声淹没复现现场。 */
function readDirectorEvents(): DirectorEventRow[] {
/** 只显示预演台自己的稳定码事件,避免整条诊断流噪声淹没复现现场。 */
function readPrevisEvents(): PrevisEventRow[] {
return getClientDiagnosticEvents()
.filter((event) => (event.code || "").startsWith("DIRECTOR_"))
.filter((event) => (event.code || "").startsWith("PREVIS_"))
.map((event) => ({ id: event.id, timestamp: event.timestamp, level: event.level, code: event.code || "", message: event.message }))
.reverse();
}
function EnvironmentSnapshot({ snapshot, appliedCount, flushCount, sceneRevisionHint }: { snapshot: DirectorReproSnapshot; appliedCount: number; flushCount: number; sceneRevisionHint: string }) {
function EnvironmentSnapshot({ snapshot, appliedCount, flushCount, sceneRevisionHint }: { snapshot: PrevisReproSnapshot; appliedCount: number; flushCount: number; sceneRevisionHint: string }) {
const { runtime, webgl } = snapshot;
const rows: Array<[string, string]> = [
["应用版本", runtime.appVersion],
@@ -158,11 +158,11 @@ function EnvironmentSnapshot({ snapshot, appliedCount, flushCount, sceneRevision
);
}
function DiagnosticEventList({ events }: { events: DirectorEventRow[] }) {
function DiagnosticEventList({ events }: { events: PrevisEventRow[] }) {
return (
<section className="mb-4">
<h2 className="mb-2 text-[var(--fs-label)] font-semibold opacity-75">结构化事件({events.length})</h2>
<p className="mb-2 text-[var(--fs-tiny)] opacity-60">来自统一 client-diagnostics 缓冲区,只筛选 DIRECTOR_ 稳定码;不含 stack、URL 与业务正文。</p>
<p className="mb-2 text-[var(--fs-tiny)] opacity-60">来自统一 client-diagnostics 缓冲区,只筛选 PREVIS_ 稳定码;不含 stack、URL 与业务正文。</p>
{events.length ? (
<Table
size="small"
@@ -178,7 +178,7 @@ function DiagnosticEventList({ events }: { events: DirectorEventRow[] }) {
]}
/>
) : (
<p className="text-[var(--fs-tiny)] opacity-55">暂无导演台事件。触发下方矩阵中的失败场景后点击「刷新事件」。</p>
<p className="text-[var(--fs-tiny)] opacity-55">暂无预演台事件。触发下方矩阵中的失败场景后点击「刷新事件」。</p>
)}
</section>
);
@@ -187,11 +187,11 @@ function DiagnosticEventList({ events }: { events: DirectorEventRow[] }) {
function ReproMatrix() {
return (
<section className="mb-4">
<h2 className="mb-2 text-[var(--fs-label)] font-semibold opacity-75">P0 手工复现矩阵({DIRECTOR_REPRO_MATRIX.length})</h2>
<h2 className="mb-2 text-[var(--fs-label)] font-semibold opacity-75">P0 手工复现矩阵({PREVIS_REPRO_MATRIX.length})</h2>
<Table
size="small"
rowKey="id"
dataSource={[...DIRECTOR_REPRO_MATRIX]}
dataSource={[...PREVIS_REPRO_MATRIX]}
pagination={false}
columns={[
{ title: "分组", dataIndex: "group", width: 108 },
+1 -1
View File
@@ -115,6 +115,6 @@ function toCanvasProject(canvas: { id: string; title: string; createdAt: string;
backgroundMode: "dots",
showImageInfo: true,
viewport: { x: 0, y: 0, k: 1 },
directorScenes: [],
previsScenes: [],
};
}