refactor(canvas): 预演台 - 重构工作台与场景交互链路
This commit is contained in:
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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
+77
-62
@@ -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 },
|
||||
@@ -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: [],
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user