feat(canvas): 图片编辑 - 合并打光预设与镜头参数控制
This commit is contained in:
commit
54351a9c1b
24 files changed
+1634
-19
No files matched your search
@@ -26,7 +26,8 @@ import { flushCanvasStorePersistence } from "@/stores/canvas/use-canvas-store";
|
||||
import { ensureCanvasNodeAsset } from "@/services/project-asset-sync";
|
||||
import { useThemeStore } from "@/stores/use-theme-store";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
import { App, Button, Modal } from "antd";
|
||||
import { App, Button } from "antd";
|
||||
import { AppModal } from "@/components/ui/product/app-modal";
|
||||
import { getNodeSpec } from "@/constant/canvas";
|
||||
import { CanvasConfigComposer } from "@/components/canvas/canvas-config-composer";
|
||||
import { CanvasConfigNodePanel } from "@/components/canvas/canvas-config-node-panel";
|
||||
@@ -48,6 +49,7 @@ import { CanvasVideoSegmentDialog } from "@/components/canvas/canvas-video-segme
|
||||
import { CanvasTimelineDialog } from "@/components/canvas/canvas-timeline-dialog";
|
||||
import { syncNodeSubtitlesToTimeline } from "@/lib/timeline/timeline-build";
|
||||
import { CanvasNodeAnglePanel } from "@/components/canvas/canvas-node-angle-dialog";
|
||||
import { CanvasNodeLightingPanel } from "@/components/canvas/canvas-node-lighting-dialog";
|
||||
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";
|
||||
@@ -783,6 +785,7 @@ function InfiniteCanvasPage() {
|
||||
|
||||
const {
|
||||
angleNodeId,
|
||||
lightingNodeId,
|
||||
emotionNodeId,
|
||||
annotationNodeId,
|
||||
createImageReversePromptNodes,
|
||||
@@ -796,6 +799,7 @@ function InfiniteCanvasPage() {
|
||||
extractingVideoFramesNodeId,
|
||||
frameDialogNodeId,
|
||||
generateAngleNode,
|
||||
generateLightingNode,
|
||||
generateEmotionNode,
|
||||
handleSegmentConfirm,
|
||||
maskEditImageNode,
|
||||
@@ -810,6 +814,7 @@ function InfiniteCanvasPage() {
|
||||
setFrameDialogNodeId,
|
||||
setSegmentDialogNodeId,
|
||||
setAngleNodeId,
|
||||
setLightingNodeId,
|
||||
setEmotionNodeId,
|
||||
setAnnotationNodeId,
|
||||
setCropNodeId,
|
||||
@@ -862,6 +867,7 @@ function InfiniteCanvasPage() {
|
||||
setSplitNodeId(clearDeletedId);
|
||||
setUpscaleNodeId(clearDeletedId);
|
||||
setAngleNodeId(clearDeletedId);
|
||||
setLightingNodeId(clearDeletedId);
|
||||
setEmotionNodeId(clearDeletedId);
|
||||
setSuperResolveNodeId(clearDeletedId);
|
||||
setPreviewNodeId(clearDeletedId);
|
||||
@@ -879,7 +885,7 @@ function InfiniteCanvasPage() {
|
||||
}
|
||||
cleanupCanvasFiles({ projectId, nodes: nextNodes, chatSessions });
|
||||
},
|
||||
[chatSessions, cleanupCanvasFiles, message, projectId, setAngleNodeId, setAnnotationNodeId, setArtCritiqueNodeId, setCropNodeId, setEmotionNodeId, setFrameDialogNodeId, setMaskEditNodeId, setSegmentDialogNodeId, setSplitNodeId, setUpscaleNodeId, setRunningNodeId],
|
||||
[chatSessions, cleanupCanvasFiles, message, projectId, setAngleNodeId, setAnnotationNodeId, setArtCritiqueNodeId, setCropNodeId, setEmotionNodeId, setFrameDialogNodeId, setLightingNodeId, setMaskEditNodeId, setSegmentDialogNodeId, setSplitNodeId, setUpscaleNodeId, setRunningNodeId],
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -1218,6 +1224,7 @@ function InfiniteCanvasPage() {
|
||||
activeScriptNode,
|
||||
activeStylePresetId,
|
||||
angleNode,
|
||||
lightingNode,
|
||||
emotionNode,
|
||||
annotationNode,
|
||||
batchChildCountById,
|
||||
@@ -1268,6 +1275,7 @@ function InfiniteCanvasPage() {
|
||||
upscaleNodeId,
|
||||
superResolveNodeId,
|
||||
angleNodeId,
|
||||
lightingNodeId,
|
||||
emotionNodeId,
|
||||
previewNodeId,
|
||||
contextMenu,
|
||||
@@ -1521,6 +1529,7 @@ function InfiniteCanvasPage() {
|
||||
setMaskEditNodeId(null);
|
||||
setAnnotationNodeId(null);
|
||||
setAngleNodeId(null);
|
||||
setLightingNodeId(null);
|
||||
setEmotionNodeId(null);
|
||||
setPreviewNodeId(null);
|
||||
setRunningNodeId(null);
|
||||
@@ -2436,13 +2445,13 @@ function InfiniteCanvasPage() {
|
||||
</div>
|
||||
|
||||
{angleNode?.metadata?.content ? (
|
||||
<Modal
|
||||
<AppModal
|
||||
flush
|
||||
open
|
||||
centered
|
||||
title="多角度编辑器"
|
||||
footer={null}
|
||||
width={620}
|
||||
destroyOnHidden
|
||||
onCancel={() => setAngleNodeId(null)}
|
||||
>
|
||||
<CanvasNodeAnglePanel
|
||||
@@ -2452,7 +2461,27 @@ function InfiniteCanvasPage() {
|
||||
void generateAngleNode(angleNode, params);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</AppModal>
|
||||
) : null}
|
||||
|
||||
{lightingNode?.metadata?.content ? (
|
||||
<AppModal
|
||||
flush
|
||||
open
|
||||
centered
|
||||
title="打光效果"
|
||||
footer={null}
|
||||
width={900}
|
||||
onCancel={() => setLightingNodeId(null)}
|
||||
>
|
||||
<CanvasNodeLightingPanel
|
||||
dataUrl={lightingNode.metadata.content}
|
||||
onClose={() => setLightingNodeId(null)}
|
||||
onConfirm={(options, prompt) => {
|
||||
void generateLightingNode(lightingNode, options, prompt);
|
||||
}}
|
||||
/>
|
||||
</AppModal>
|
||||
) : null}
|
||||
|
||||
{emotionNode?.metadata?.content && !isCanvasNodeMoving ? (
|
||||
@@ -2556,6 +2585,10 @@ function InfiniteCanvasPage() {
|
||||
setDialogNodeId(null);
|
||||
setAngleNodeId((current) => (current === node.id ? null : node.id));
|
||||
}}
|
||||
onLighting={(node) => {
|
||||
setDialogNodeId(null);
|
||||
setLightingNodeId((current) => (current === node.id ? null : node.id));
|
||||
}}
|
||||
onViewImage={(node) => setPreviewNodeId(node.id)}
|
||||
onExtractVideoFrames={openVideoFrameExtractor}
|
||||
onExtractAudioFromVideo={(node) => void extractAudioFromVideo(node)}
|
||||
|
||||
@@ -275,7 +275,7 @@ export default function SharedCanvasPage() {
|
||||
}} onHoverStart={keepToolbar} onHoverEnd={hideToolbar} onConnectStart={unauthorized} onResize={() => undefined} onContentChange={unauthorized} onRetry={unauthorized} onOpenTaskDetails={unauthorized} onViewImage={(target) => setInfoNodeId(target.id)} onContextMenu={(event, nodeId) => openContextMenu(event, nodeId)} />)}
|
||||
</InfiniteCanvas>
|
||||
|
||||
<CanvasNodeToolbar node={dragRef.current ? null : toolbarNode} viewport={viewport} containerRef={containerRef} onKeep={keepToolbar} onLeave={hideToolbar} onInfo={(node) => setInfoNodeId(node.id)} onEditText={unauthorized} onDecreaseFont={unauthorized} onIncreaseFont={unauthorized} onToggleDialog={unauthorized} onAnnotate={unauthorized} onGenerateImage={unauthorized} onUpload={unauthorized} onDownload={unauthorized} onSaveAsset={unauthorized} onMaskEdit={unauthorized} onEmotion={unauthorized} onPortraitTexture={unauthorized} onCrop={unauthorized} onSplit={unauthorized} onUpscale={unauthorized} onSuperResolve={unauthorized} onAngle={unauthorized} onViewImage={unauthorized} onExtractVideoFrames={unauthorized} onExtractAudioFromVideo={unauthorized} onTrimVideoSegments={unauthorized} extractingVideoFrames={false} extractingAudio={false} trimmingVideo={false} onSubtitles={unauthorized} onTimeline={unauthorized} onReversePrompt={unauthorized} onRetry={unauthorized} onToggleFreeResize={unauthorized} onToggleLocked={unauthorized} onDelete={unauthorized} />
|
||||
<CanvasNodeToolbar node={dragRef.current ? null : toolbarNode} viewport={viewport} containerRef={containerRef} onKeep={keepToolbar} onLeave={hideToolbar} onInfo={(node) => setInfoNodeId(node.id)} onEditText={unauthorized} onDecreaseFont={unauthorized} onIncreaseFont={unauthorized} onToggleDialog={unauthorized} onAnnotate={unauthorized} onGenerateImage={unauthorized} onUpload={unauthorized} onDownload={unauthorized} onSaveAsset={unauthorized} onMaskEdit={unauthorized} onEmotion={unauthorized} onPortraitTexture={unauthorized} onCrop={unauthorized} onSplit={unauthorized} onUpscale={unauthorized} onSuperResolve={unauthorized} onAngle={unauthorized} onLighting={unauthorized} onViewImage={unauthorized} onExtractVideoFrames={unauthorized} onExtractAudioFromVideo={unauthorized} onTrimVideoSegments={unauthorized} extractingVideoFrames={false} extractingAudio={false} trimmingVideo={false} onSubtitles={unauthorized} onTimeline={unauthorized} onReversePrompt={unauthorized} onRetry={unauthorized} onToggleFreeResize={unauthorized} onToggleLocked={unauthorized} onDelete={unauthorized} />
|
||||
|
||||
<div className="absolute bottom-5 left-5 z-[var(--z-panel-floating)]"><CanvasZoomControls scale={viewport.k} containerRef={containerRef} onScaleChange={setZoom} onFitContent={resetViewport} isMiniMapOpen={false} onToggleMiniMap={unauthorized} onOpenShortcuts={unauthorized} /></div>
|
||||
<div className="pointer-events-none absolute bottom-5 right-5 z-[var(--z-panel-floating)] max-w-[340px] text-right text-xs leading-5" style={{ color: theme.node.muted }}>访客操作仅在当前页面临时生效</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { buildGenerationConfig, isGenerationCanceled } from "@/lib/canvas/canvas
|
||||
import { canvasGenerationPromptMetadata, canvasGenerationRequestFingerprint, runCanvasGenerationSubmissionOnce } from "@/lib/canvas/canvas-generation-submission";
|
||||
import { isGenerationTaskCapacityError } from "@/lib/canvas/canvas-generation-batch";
|
||||
import { buildPortraitTexturePrompt } from "@/lib/canvas/canvas-portrait-texture";
|
||||
import { buildCameraPrompt } from "@/lib/canvas/camera-prompt-library";
|
||||
import { buildTextRewritePrompt } from "@/lib/prompts";
|
||||
import { resolveCanvasStyleExecution } from "@/lib/canvas/canvas-style-execution";
|
||||
import { generationErrorMessage, generationFailureMetadata } from "@/lib/generation-error";
|
||||
@@ -130,7 +131,12 @@ export function useCanvasGenerationExecutor({
|
||||
|
||||
const sourceTextContent = sourceNode?.type === CanvasNodeType.Text ? sourceNode.metadata?.content?.trim() || "" : "";
|
||||
const editingTextNode = mode === "text" && Boolean(sourceTextContent);
|
||||
const generationPrompt = mode === "image" && sourceNode?.metadata?.portraitTexture ? buildPortraitTexturePrompt(prompt, sourceNode.metadata.portraitTexture) : prompt;
|
||||
let generationPrompt = mode === "image" && sourceNode?.metadata?.portraitTexture ? buildPortraitTexturePrompt(prompt, sourceNode.metadata.portraitTexture) : prompt;
|
||||
if (mode === "image" && sourceNode?.metadata?.cameraControl?.enabled) {
|
||||
const cameraControl = sourceNode.metadata.cameraControl;
|
||||
const cameraPrompt = buildCameraPrompt({ cameraId: cameraControl.camera, lensId: cameraControl.lens, focalLengthMm: cameraControl.focalLength, apertureF: cameraControl.aperture });
|
||||
generationPrompt = `${generationPrompt}\n${cameraPrompt}`;
|
||||
}
|
||||
const isPreparingEmptyImage = mode === "image" && sourceNode?.type === CanvasNodeType.Image && !sourceNode.metadata?.content;
|
||||
|
||||
let rawGenerationContext: Awaited<ReturnType<typeof hydrateNodeGenerationContext>>;
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { CanvasImageMaskEditPayload } from "@/components/canvas/canvas-node
|
||||
import type { CanvasImageSplitParams } from "@/components/canvas/canvas-node-split-dialog";
|
||||
import type { CanvasImageUpscaleParams } from "@/components/canvas/canvas-node-upscale-dialog";
|
||||
import type { CanvasImageAngleParams } from "@/components/canvas/canvas-node-angle-dialog";
|
||||
import { buildLightingLabel, type CanvasImageLightingOptions } from "@/components/canvas/canvas-node-lighting-dialog";
|
||||
import type { CanvasImageEmotionPayload } from "@/components/canvas/canvas-node-emotion-panel";
|
||||
import type { CanvasVideoFrameParams } from "@/components/canvas/canvas-video-frame-dialog";
|
||||
import type { CanvasVideoSegmentParams } from "@/components/canvas/canvas-video-segment-dialog";
|
||||
@@ -104,6 +105,7 @@ export function useCanvasMediaTools({
|
||||
const [splitNodeId, setSplitNodeId] = useState<string | null>(null);
|
||||
const [upscaleNodeId, setUpscaleNodeId] = useState<string | null>(null);
|
||||
const [angleNodeId, setAngleNodeId] = useState<string | null>(null);
|
||||
const [lightingNodeId, setLightingNodeId] = useState<string | null>(null);
|
||||
const [emotionNodeId, setEmotionNodeId] = useState<string | null>(null);
|
||||
const [frameDialogNodeId, setFrameDialogNodeId] = useState<string | null>(null);
|
||||
const [extractingVideoFramesNodeId, setExtractingVideoFramesNodeId] = useState<string | null>(null);
|
||||
@@ -780,6 +782,50 @@ export function useCanvasMediaTools({
|
||||
}
|
||||
}, [bindGenerationTask, effectiveConfig, finishGenerationRequest, isAiConfigReady, nodesRef, persistMediaNodes, projectId, resolveImageEditStyle, setConnections, setDialogNodeId, setNodes, setRunningNodeId, setSelectedNodeIds, startGenerationRequest]);
|
||||
|
||||
const generateLightingNode = useCallback(async (node: CanvasNodeData, options: CanvasImageLightingOptions, prompt: string) => {
|
||||
if (!node.metadata?.content) return;
|
||||
const generationConfig = { ...buildGenerationConfig(effectiveConfig, node, "image"), count: "1" };
|
||||
if (!isAiConfigReady(generationConfig, generationConfig.model)) {
|
||||
navigateToSettings({ continueCreation: true });
|
||||
return;
|
||||
}
|
||||
const childId = nanoid();
|
||||
const imageSpec = NODE_DEFAULT_SIZE[CanvasNodeType.Image];
|
||||
const title = buildLightingLabel(options);
|
||||
const source = nodeReferenceImage(node);
|
||||
if (!source) return;
|
||||
const styleExecution = resolveImageEditStyle(node, prompt, generationConfig);
|
||||
if (!styleExecution) return;
|
||||
const { prompt: effectivePrompt, metadata: styleMetadata } = styleExecution;
|
||||
const generationMetadata = buildImageGenerationMetadata("edit", generationConfig, 1, [source]);
|
||||
setLightingNodeId(null);
|
||||
setRunningNodeId(childId);
|
||||
setNodes((current) => [...current, { id: childId, type: CanvasNodeType.Image, title, position: { x: node.position.x + node.width + 96, y: node.position.y }, width: imageSpec.width, height: imageSpec.height, metadata: { prompt: effectivePrompt, status: NODE_STATUS_LOADING, ...generationMetadata, ...styleMetadata } }]);
|
||||
setConnections((current) => [...current, { id: nanoid(), fromNodeId: node.id, toNodeId: childId }]);
|
||||
setSelectedNodeIds(new Set([childId]));
|
||||
setDialogNodeId(childId);
|
||||
const controller = startGenerationRequest(childId, node.id, childId);
|
||||
try {
|
||||
const result = await runBackendCanvasGenerationTask({ projectId, nodeId: childId, mode: "image", prompt: effectivePrompt, config: generationConfig, referenceImages: [source], signal: controller.signal, metadata: { sourceNodeId: node.id, edit: "lighting", lighting: options, ...styleMetadata }, onTaskCreated: (task) => bindGenerationTask(childId, task) });
|
||||
const image = result.images?.[0];
|
||||
if (!image?.dataUrl) throw new Error("后端任务没有返回图片");
|
||||
const uploaded = await uploadImage(image.dataUrl);
|
||||
const size = fitNodeSize(uploaded.width, uploaded.height, imageSpec.width, imageSpec.height);
|
||||
const currentNode = nodesRef.current.find((item) => item.id === childId);
|
||||
if (!currentNode) throw new Error("打光生成节点已被删除");
|
||||
const finalizedNode = { ...currentNode, width: size.width, height: size.height, metadata: { ...currentNode.metadata, ...imageMetadata(uploaded), prompt: effectivePrompt, ...generationMetadata } };
|
||||
setNodes((current) => current.map((item) => item.id === childId ? finalizedNode : item));
|
||||
await persistMediaNodes([finalizedNode]);
|
||||
} catch (error) {
|
||||
if (isGenerationCanceled(error)) return;
|
||||
const details = generationErrorMessage(error);
|
||||
setNodes((current) => current.map((item) => item.id === childId ? { ...item, metadata: { ...item.metadata, status: NODE_STATUS_ERROR, errorDetails: details } } : item));
|
||||
} finally {
|
||||
finishGenerationRequest(childId, controller);
|
||||
setRunningNodeId(null);
|
||||
}
|
||||
}, [bindGenerationTask, effectiveConfig, finishGenerationRequest, isAiConfigReady, nodesRef, persistMediaNodes, projectId, resolveImageEditStyle, setConnections, setDialogNodeId, setNodes, setRunningNodeId, setSelectedNodeIds, startGenerationRequest]);
|
||||
|
||||
const generateEmotionNode = useCallback(async (node: CanvasNodeData, payload: CanvasImageEmotionPayload) => {
|
||||
if (!node.metadata?.content) return;
|
||||
const baseConfig = buildGenerationConfig(effectiveConfig, node, "image");
|
||||
@@ -845,6 +891,7 @@ export function useCanvasMediaTools({
|
||||
|
||||
return {
|
||||
angleNodeId,
|
||||
lightingNodeId,
|
||||
emotionNodeId,
|
||||
annotationNodeId,
|
||||
createImageReversePromptNodes,
|
||||
@@ -859,6 +906,7 @@ export function useCanvasMediaTools({
|
||||
frameDialogNodeId,
|
||||
handleSegmentConfirm,
|
||||
generateAngleNode,
|
||||
generateLightingNode,
|
||||
maskEditImageNode,
|
||||
maskEditNodeId,
|
||||
mergeSelectedVideos,
|
||||
@@ -871,6 +919,7 @@ export function useCanvasMediaTools({
|
||||
setFrameDialogNodeId,
|
||||
setSegmentDialogNodeId,
|
||||
setAngleNodeId,
|
||||
setLightingNodeId,
|
||||
generateEmotionNode,
|
||||
setEmotionNodeId,
|
||||
setAnnotationNodeId,
|
||||
|
||||
@@ -35,6 +35,7 @@ type UseCanvasRenderModelOptions = {
|
||||
upscaleNodeId: string | null;
|
||||
superResolveNodeId: string | null;
|
||||
angleNodeId: string | null;
|
||||
lightingNodeId: string | null;
|
||||
emotionNodeId: string | null;
|
||||
previewNodeId: string | null;
|
||||
contextMenu: ContextMenuState | null;
|
||||
@@ -65,6 +66,7 @@ export function useCanvasRenderModel({
|
||||
upscaleNodeId,
|
||||
superResolveNodeId,
|
||||
angleNodeId,
|
||||
lightingNodeId,
|
||||
emotionNodeId,
|
||||
previewNodeId,
|
||||
contextMenu,
|
||||
@@ -225,6 +227,7 @@ export function useCanvasRenderModel({
|
||||
const upscaleNode = upscaleNodeId ? nodeById.get(upscaleNodeId) || null : null;
|
||||
const superResolveNode = superResolveNodeId ? nodeById.get(superResolveNodeId) || null : null;
|
||||
const angleNode = angleNodeId ? nodeById.get(angleNodeId) || null : null;
|
||||
const lightingNode = lightingNodeId ? nodeById.get(lightingNodeId) || null : null;
|
||||
const emotionNode = emotionNodeId ? nodeById.get(emotionNodeId) || null : null;
|
||||
const previewNode = previewNodeId ? nodeById.get(previewNodeId) || null : null;
|
||||
const contextMenuNode = contextMenu?.type === "node" ? nodeById.get(contextMenu.nodeId) || null : null;
|
||||
@@ -361,6 +364,7 @@ export function useCanvasRenderModel({
|
||||
activeScriptNode,
|
||||
activeStylePresetId,
|
||||
angleNode,
|
||||
lightingNode,
|
||||
emotionNode,
|
||||
annotationNode,
|
||||
batchChildCountById,
|
||||
|
||||
Reference in new issue
Block a user