feat: 完成短剧项目空间与结构化资产创作闭环 (#38)
* feat(canvas): 短剧项目空间 - 建立项目章节与画布关联 MVP * feat(canvas): 短剧项目空间 - 完善项目资产、镜头与流程闭环 Refs #30 * fix(deploy): 本地部署 - 修复前端构建并忽略本地 PostgreSQL 配置 * feat(*): 短剧项目空间 - 完成项目、章节、资产、角色与画布创作闭环 Refs #30 --------- Co-authored-by: ddCat <ddcat666@88.com>
This commit is contained in:
@@ -4,12 +4,14 @@ import { nanoid } from "nanoid";
|
||||
|
||||
import type { PendingConnectionCreate } from "@/components/canvas/canvas-workspace-overlays";
|
||||
import { attachNodeToStoryboardRow, createCanvasNode, getConnectionTargetAnchor, isHiddenBatchChild, normalizeConnection, storyboardHandleAtY, storyboardRowFromHandle } from "@/lib/canvas/canvas-project-domain";
|
||||
import { createCanvasDrawingFromImage } from "@/lib/canvas/canvas-drawing-storage";
|
||||
import { isFrameNode, isNodeHiddenByCollapsedFrame } from "@/lib/canvas/canvas-frame";
|
||||
import { getGenerationCount } from "@/lib/canvas/canvas-project-generation";
|
||||
import { useEffectiveConfig } from "@/stores/use-config-store";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type ConnectionHandle, type ContextMenuState, type Position, type ViewportTransform } from "@/types/canvas";
|
||||
|
||||
type UseCanvasConnectionControllerOptions = {
|
||||
projectId: string;
|
||||
nodesRef: { current: CanvasNodeData[] };
|
||||
connectionsRef: { current: CanvasConnection[] };
|
||||
viewportRef: { current: ViewportTransform };
|
||||
@@ -21,6 +23,7 @@ type UseCanvasConnectionControllerOptions = {
|
||||
setSelectedConnectionId: Dispatch<SetStateAction<string | null>>;
|
||||
setContextMenu: Dispatch<SetStateAction<ContextMenuState | null>>;
|
||||
setDialogNodeId: Dispatch<SetStateAction<string | null>>;
|
||||
setDrawingNodeId: Dispatch<SetStateAction<string | null>>;
|
||||
};
|
||||
|
||||
type ConnectionDropTarget = {
|
||||
@@ -34,6 +37,7 @@ const CONNECTION_NODE_HIT_PADDING = 32;
|
||||
const NODE_STATUS_IDLE = "idle" as const;
|
||||
|
||||
export function useCanvasConnectionController({
|
||||
projectId,
|
||||
nodesRef,
|
||||
connectionsRef,
|
||||
viewportRef,
|
||||
@@ -45,6 +49,7 @@ export function useCanvasConnectionController({
|
||||
setSelectedConnectionId,
|
||||
setContextMenu,
|
||||
setDialogNodeId,
|
||||
setDrawingNodeId,
|
||||
}: UseCanvasConnectionControllerOptions) {
|
||||
const { message } = App.useApp();
|
||||
const effectiveConfig = useEffectiveConfig();
|
||||
@@ -98,7 +103,7 @@ export function useCanvasConnectionController({
|
||||
setContextMenu(null);
|
||||
}, [connectionsRef, message, nodesRef, setConnections, setContextMenu, setNodes]);
|
||||
|
||||
const createConnectedNode = useCallback((type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Config | CanvasNodeType.Video | CanvasNodeType.Audio, pending: PendingConnectionCreate) => {
|
||||
const createConnectedNode = useCallback(async (type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Config | CanvasNodeType.Video | CanvasNodeType.Audio | CanvasNodeType.Drawing, pending: PendingConnectionCreate) => {
|
||||
const storyboardRow = type === CanvasNodeType.Video ? storyboardRowFromHandle(nodesRef.current, pending.connection.nodeId, pending.connection.handleId) : undefined;
|
||||
const videoPrompt = storyboardRow ? (storyboardRow.videoMotionPrompt || storyboardRow.plotDescription).trim() : "";
|
||||
const metadata = type === CanvasNodeType.Config
|
||||
@@ -113,6 +118,35 @@ export function useCanvasConnectionController({
|
||||
message.warning("配置节点之间不能连接");
|
||||
return;
|
||||
}
|
||||
if (type === CanvasNodeType.Drawing) {
|
||||
const sourceNode = nodesRef.current.find((node) => node.id === pending.connection.nodeId);
|
||||
const sourceUrl = sourceNode?.type === CanvasNodeType.Image ? sourceNode.metadata?.content : "";
|
||||
if (pending.connection.handleType !== "source" || !sourceNode || !sourceUrl || !newNode.metadata?.drawingId) {
|
||||
message.error("只有已有图片内容的输出连线可以创建绘图");
|
||||
return;
|
||||
}
|
||||
closeConnectionCreateMenu();
|
||||
setConnecting(null);
|
||||
try {
|
||||
const saved = await createCanvasDrawingFromImage(projectId, newNode.metadata.drawingId, {
|
||||
url: sourceUrl,
|
||||
storageKey: sourceNode.metadata?.storageKey,
|
||||
name: sourceNode.title || "来源图片",
|
||||
mimeType: sourceNode.metadata?.mimeType,
|
||||
});
|
||||
newNode.title = `${sourceNode.title || "图片"} · 绘图`;
|
||||
newNode.metadata = {
|
||||
...newNode.metadata,
|
||||
drawingRevision: saved.revision,
|
||||
drawingUpdatedAt: saved.updatedAt,
|
||||
drawingShapeCount: saved.shapeCount,
|
||||
drawingPageCount: saved.pageCount,
|
||||
};
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? `创建绘图失败:${error.message}` : "创建绘图失败");
|
||||
return;
|
||||
}
|
||||
}
|
||||
const fromHandleId = connection.fromNodeId === pending.connection.nodeId ? pending.connection.handleId : undefined;
|
||||
const toHandleId = connection.toNodeId === pending.connection.nodeId ? pending.connection.handleId : undefined;
|
||||
const connected = { ...connection, fromHandleId, toHandleId };
|
||||
@@ -120,10 +154,11 @@ export function useCanvasConnectionController({
|
||||
setConnections((currentConnections) => [...currentConnections, { id: nanoid(), ...connected }]);
|
||||
setSelectedNodeIds(new Set([newNode.id]));
|
||||
setSelectedConnectionId(null);
|
||||
if (type !== CanvasNodeType.Text && type !== CanvasNodeType.Audio) setDialogNodeId(newNode.id);
|
||||
if (type === CanvasNodeType.Drawing) setDrawingNodeId(newNode.id);
|
||||
else if (type !== CanvasNodeType.Text && type !== CanvasNodeType.Audio) setDialogNodeId(newNode.id);
|
||||
closeConnectionCreateMenu();
|
||||
setConnecting(null);
|
||||
}, [closeConnectionCreateMenu, effectiveConfig.canvasImageCount, effectiveConfig.count, effectiveConfig.imageModel, effectiveConfig.model, effectiveConfig.size, message, nodesRef, setConnecting, setConnections, setDialogNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds]);
|
||||
}, [closeConnectionCreateMenu, effectiveConfig.canvasImageCount, effectiveConfig.count, effectiveConfig.imageModel, effectiveConfig.model, effectiveConfig.size, message, nodesRef, projectId, setConnecting, setConnections, setDialogNodeId, setDrawingNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds]);
|
||||
|
||||
const getConnectionDropTarget = useCallback((clientX: number, clientY: number, current: ConnectionHandle): ConnectionDropTarget => {
|
||||
const world = screenToCanvas(clientX, clientY);
|
||||
|
||||
Reference in New Issue
Block a user