1 parent
cfdf08d05f
commit
e7af466a16
16 files changed
+509
-80
No files matched your search
@@ -705,9 +705,10 @@ function InfiniteCanvasPage() {
|
||||
onNodesDeleted: handleNodesDeleted,
|
||||
});
|
||||
|
||||
const { cancelPendingConnectionCreate, closeConnectionCreateMenu, connectionTargetAnchorRatio, connectionTargetNodeId, connectingParams, createConnectedNode, handleConnectStart, mouseWorld, pendingConnectionCreate, setConnecting } =
|
||||
const { cancelPendingConnectionCreate, closeConnectionCreateMenu, connectionTargetAnchorRatio, connectionTargetNodeId, connectingParams, createConnectedNode, getConnectionCreateDisabledReason, handleConnectStart, mouseWorld, pendingConnectionCreate, setConnecting } =
|
||||
useCanvasConnectionController({
|
||||
projectId,
|
||||
config: effectiveConfig,
|
||||
defaultDrawingEngine,
|
||||
nodesRef,
|
||||
connectionsRef,
|
||||
@@ -1786,6 +1787,7 @@ function InfiniteCanvasPage() {
|
||||
viewportSize={size}
|
||||
containerRef={containerRef}
|
||||
canCreateDrawing={canCreateDrawingFromConnection}
|
||||
getDisabledReason={(type) => getConnectionCreateDisabledReason(type, pendingConnectionCreate)}
|
||||
onCreate={(type) => void createConnectedNode(type, pendingConnectionCreate)}
|
||||
onClose={cancelPendingConnectionCreate}
|
||||
/>
|
||||
|
||||
@@ -4,15 +4,18 @@ import { nanoid } from "nanoid";
|
||||
|
||||
import type { PendingConnectionCreate } from "@/components/canvas/canvas-workspace-overlays";
|
||||
import { getNodeSpec } from "@/constant/canvas";
|
||||
import { canvasConnectionError } from "@/lib/canvas/canvas-connection-policy";
|
||||
import { attachNodeToStoryboardRow, createCanvasNode, getConnectionTargetAnchor, isHiddenBatchChild, normalizeConnection, storyboardHandleAtY, storyboardPromptTemplateMetadata, storyboardRowFromHandle } from "@/lib/canvas/canvas-project-domain";
|
||||
import { createCanvasDrawingFromImage } from "@/lib/canvas/canvas-drawing-storage";
|
||||
import { isDrawingEngineAvailable, type CanvasDrawingEngine } from "@/lib/canvas/canvas-drawing-engine";
|
||||
import { isFrameNode, isNodeHiddenByCollapsedFrame } from "@/lib/canvas/canvas-frame";
|
||||
import type { AiConfig } from "@/stores/use-config-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasNodeData, type ConnectionHandle, type ContextMenuState, type Position, type ViewportTransform } from "@/types/canvas";
|
||||
|
||||
type UseCanvasConnectionControllerOptions = {
|
||||
projectId: string;
|
||||
config: AiConfig;
|
||||
defaultDrawingEngine: CanvasDrawingEngine;
|
||||
nodesRef: { current: CanvasNodeData[] };
|
||||
connectionsRef: { current: CanvasConnection[] };
|
||||
@@ -41,6 +44,7 @@ const NODE_STATUS_IDLE = "idle" as const;
|
||||
|
||||
export function useCanvasConnectionController({
|
||||
projectId,
|
||||
config,
|
||||
defaultDrawingEngine,
|
||||
nodesRef,
|
||||
connectionsRef,
|
||||
@@ -105,6 +109,11 @@ export function useCanvasConnectionController({
|
||||
const toHandleId = toNodeId === current.nodeId ? current.handleId : targetHandleId;
|
||||
const fromAnchorRatio = fromNodeId === current.nodeId ? current.anchorRatio : targetAnchorRatio;
|
||||
const toAnchorRatio = toNodeId === current.nodeId ? current.anchorRatio : targetAnchorRatio;
|
||||
const policyError = canvasConnectionError(config, nodesRef.current, connectionsRef.current, { fromNodeId, toNodeId });
|
||||
if (policyError) {
|
||||
message.warning(policyError);
|
||||
return;
|
||||
}
|
||||
const exists = connectionsRef.current.find((item) => item.fromNodeId === fromNodeId && item.toNodeId === toNodeId && item.fromHandleId === fromHandleId && item.toHandleId === toHandleId);
|
||||
if (exists) {
|
||||
setConnections((currentConnections) => currentConnections.map((item) => item.id === exists.id ? { ...item, fromAnchorRatio, toAnchorRatio } : item));
|
||||
@@ -113,7 +122,7 @@ export function useCanvasConnectionController({
|
||||
setNodes((currentNodes) => attachNodeToStoryboardRow(currentNodes, { fromNodeId, toNodeId, fromHandleId, toHandleId }));
|
||||
}
|
||||
setContextMenu(null);
|
||||
}, [connectionsRef, message, nodesRef, setConnections, setContextMenu, setNodes]);
|
||||
}, [config, connectionsRef, message, nodesRef, setConnections, setContextMenu, setNodes]);
|
||||
|
||||
const createConnectedNode = useCallback(async (type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Script | CanvasNodeType.Video | CanvasNodeType.Audio | CanvasNodeType.Drawing, pending: PendingConnectionCreate) => {
|
||||
if (type === CanvasNodeType.Drawing && !isDrawingEngineAvailable(defaultDrawingEngine, tldrawLicenseKey)) {
|
||||
@@ -149,6 +158,11 @@ export function useCanvasConnectionController({
|
||||
message.warning("配置节点之间不能连接");
|
||||
return;
|
||||
}
|
||||
const policyError = canvasConnectionError(config, [...nodesRef.current, newNode], connectionsRef.current, connection);
|
||||
if (policyError) {
|
||||
message.warning(policyError);
|
||||
return;
|
||||
}
|
||||
if (type === CanvasNodeType.Drawing) {
|
||||
const drawingSourceNode = nodesRef.current.find((node) => node.id === pending.connection.nodeId);
|
||||
const sourceUrl = drawingSourceNode?.type === CanvasNodeType.Image ? drawingSourceNode.metadata?.content : "";
|
||||
@@ -192,7 +206,16 @@ export function useCanvasConnectionController({
|
||||
else if (type !== CanvasNodeType.Text && type !== CanvasNodeType.Script && type !== CanvasNodeType.Audio) setDialogNodeId(newNode.id);
|
||||
closeConnectionCreateMenu();
|
||||
setConnecting(null);
|
||||
}, [closeConnectionCreateMenu, defaultDrawingEngine, message, nodesRef, projectId, setConnecting, setConnections, setDialogNodeId, setDrawingNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds, tldrawLicenseKey]);
|
||||
}, [closeConnectionCreateMenu, config, connectionsRef, defaultDrawingEngine, message, nodesRef, projectId, setConnecting, setConnections, setDialogNodeId, setDrawingNodeId, setNodes, setSelectedConnectionId, setSelectedNodeIds, tldrawLicenseKey]);
|
||||
|
||||
const getConnectionCreateDisabledReason = useCallback((type: CanvasNodeType.Image | CanvasNodeType.Text | CanvasNodeType.Script | CanvasNodeType.Video | CanvasNodeType.Audio | CanvasNodeType.Drawing, pending: PendingConnectionCreate) => {
|
||||
const spec = getNodeSpec(type);
|
||||
const pendingNode: CanvasNodeData = { id: "__pending-connection-node__", type, title: "", position: pending.position, width: spec.width, height: spec.height };
|
||||
const pendingNodes = [...nodesRef.current, pendingNode];
|
||||
const connection = normalizeConnection(pending.connection.nodeId, pendingNode.id, pendingNodes, pending.connection.handleType);
|
||||
if (!connection) return "当前节点类型不能这样连接";
|
||||
return canvasConnectionError(config, pendingNodes, connectionsRef.current, connection);
|
||||
}, [config, connectionsRef, nodesRef]);
|
||||
|
||||
const getConnectionDropTarget = useCallback((clientX: number, clientY: number, current: ConnectionHandle): ConnectionDropTarget => {
|
||||
const world = screenToCanvas(clientX, clientY);
|
||||
@@ -221,7 +244,8 @@ export function useCanvasConnectionController({
|
||||
const hitsExpanded = world.x >= node.position.x - padding && world.x <= node.position.x + node.width + padding && world.y >= node.position.y - padding && world.y <= node.position.y + node.height + padding;
|
||||
if (!hitsHandle && !hitsInside && !hitsExpanded) return;
|
||||
isNearNode = true;
|
||||
if (node.id === current.nodeId || !normalizeConnection(current.nodeId, node.id, nodesRef.current, current.handleType)) return;
|
||||
const normalized = node.id === current.nodeId ? null : normalizeConnection(current.nodeId, node.id, nodesRef.current, current.handleType);
|
||||
if (!normalized || canvasConnectionError(config, nodesRef.current, connectionsRef.current, normalized)) return;
|
||||
const priority = hitsInside ? 0 : hitsHandle ? 1 : 2;
|
||||
if (priority < bestPriority) {
|
||||
bestNodeId = node.id;
|
||||
@@ -231,7 +255,7 @@ export function useCanvasConnectionController({
|
||||
}
|
||||
});
|
||||
return { nodeId: bestNodeId, handleId: bestHandleId, anchorRatio: bestAnchorRatio, isNearNode };
|
||||
}, [nodesRef, screenToCanvas, scriptScrollTopById, viewportRef]);
|
||||
}, [config, connectionsRef, nodesRef, screenToCanvas, scriptScrollTopById, viewportRef]);
|
||||
|
||||
const finishConnection = useCallback((clientX: number, clientY: number) => {
|
||||
if (pendingConnectionCreateRef.current) return;
|
||||
@@ -313,6 +337,7 @@ export function useCanvasConnectionController({
|
||||
connectionTargetAnchorRatio,
|
||||
connectingParams,
|
||||
createConnectedNode,
|
||||
getConnectionCreateDisabledReason,
|
||||
handleConnectStart,
|
||||
mouseWorld,
|
||||
pendingConnectionCreate,
|
||||
|
||||
@@ -3,12 +3,13 @@ import { App } from "antd";
|
||||
|
||||
import { buildNodeGenerationContext, hydrateNodeGenerationContext } from "@/components/canvas/canvas-node-generation";
|
||||
import type { CanvasNodeGenerationMode } from "@/components/canvas/canvas-node-prompt-panel";
|
||||
import { buildGenerationConfig, isGenerationCanceled, supportsVideoReferenceAudio } from "@/lib/canvas/canvas-project-generation";
|
||||
import { buildGenerationConfig, isGenerationCanceled } from "@/lib/canvas/canvas-project-generation";
|
||||
import { isGenerationTaskCapacityError } from "@/lib/canvas/canvas-generation-batch";
|
||||
import { buildPortraitTexturePrompt } from "@/lib/canvas/canvas-portrait-texture";
|
||||
import { resolveCanvasStyleExecution } from "@/lib/canvas/canvas-style-execution";
|
||||
import { expandSkillMentions } from "@/lib/canvas/canvas-skill-mentions";
|
||||
import { generationErrorMessage, generationFailureMetadata } from "@/lib/generation-error";
|
||||
import { modelCompatibilityError, modelGroupReferenceLimits, type ModelRequirements } from "@/lib/model-selection";
|
||||
import { navigateToSettings } from "@/lib/settings-navigation";
|
||||
import type { Skill } from "@/services/api/skills";
|
||||
import type { GenerationTask } from "@/services/api/task-center";
|
||||
@@ -127,14 +128,25 @@ export function useCanvasGenerationExecutor({
|
||||
// 视频文本只保留输入框内容;连接的媒体仍作为结构化参考传递。
|
||||
const promptOnly = mode === "video";
|
||||
try {
|
||||
const baseContext = buildNodeGenerationContext(nodeId, nodesRef.current, connectionsRef.current, editingTextNode ? `请根据要求修改以下文本。\n\n原文:\n${sourceTextContent}\n\n修改要求:\n${prompt}` : generationPrompt, promptOnly);
|
||||
const requirements = generationModelRequirements(mode, baseContext, sourceNode, generationConfig.videoSeconds, true);
|
||||
generationConfig = buildGenerationConfig(effectiveConfig, sourceNode, mode, requirements);
|
||||
const compatibilityError = modelCompatibilityError(generationConfig, generationConfig.model, requirements);
|
||||
if (compatibilityError) throw new Error(`当前逻辑模型没有可用的细分模型:${compatibilityError}`);
|
||||
const referenceLimits = modelGroupReferenceLimits(effectiveConfig, generationConfig.model, mode, requirements);
|
||||
rawGenerationContext = await hydrateNodeGenerationContext(
|
||||
buildNodeGenerationContext(nodeId, nodesRef.current, connectionsRef.current, editingTextNode ? `请根据要求修改以下文本。\n\n原文:\n${sourceTextContent}\n\n修改要求:\n${prompt}` : generationPrompt, promptOnly),
|
||||
baseContext,
|
||||
projectId,
|
||||
domainProjectId,
|
||||
mode,
|
||||
mode === "video" && supportsVideoReferenceAudio(generationConfig),
|
||||
mode === "video" && Boolean(referenceLimits?.maxAudios),
|
||||
!promptOnly,
|
||||
referenceLimits,
|
||||
);
|
||||
const hydratedRequirements = generationModelRequirements(mode, rawGenerationContext, sourceNode, generationConfig.videoSeconds);
|
||||
generationConfig = buildGenerationConfig(effectiveConfig, sourceNode, mode, hydratedRequirements);
|
||||
const hydratedCompatibilityError = modelCompatibilityError(generationConfig, generationConfig.model, hydratedRequirements);
|
||||
if (hydratedCompatibilityError) throw new Error(`当前逻辑模型没有可用的细分模型:${hydratedCompatibilityError}`);
|
||||
} catch (error) {
|
||||
const errorDetails = generationErrorMessage(error);
|
||||
if (isPreparingEmptyImage) {
|
||||
@@ -258,3 +270,18 @@ export function useCanvasGenerationExecutor({
|
||||
[addedSkills, bindGenerationTask, domainProjectId, effectiveConfig, finishGenerationRequest, isAiConfigReady, message, nodesRef, connectionsRef, projectId, setConnections, setDialogNodeId, setNodes, setRunningNodeId, setSelectedConnectionId, setSelectedNodeIds, startGenerationRequest],
|
||||
);
|
||||
}
|
||||
|
||||
function generationModelRequirements(mode: CanvasNodeGenerationMode, input: Pick<Awaited<ReturnType<typeof hydrateNodeGenerationContext>>, "textCount" | "imageCount" | "videoCount" | "audioCount" | "characterReferences">, sourceNode: CanvasNodeData | undefined, videoSeconds: string, includeCharacterMinimum = false): ModelRequirements {
|
||||
return {
|
||||
capability: mode,
|
||||
input: {
|
||||
textCount: input.textCount,
|
||||
imageCount: input.imageCount,
|
||||
videoCount: input.videoCount,
|
||||
audioCount: input.audioCount,
|
||||
characterCount: includeCharacterMinimum ? input.characterReferences.length : 0,
|
||||
},
|
||||
videoOperation: sourceNode?.metadata?.videoEditOperation,
|
||||
videoSeconds,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user