feat(canvas): PR #500 - 合并多角度与切分并保留本地 Agent 改造
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { CanvasNodeAnnotationDialog } from "@/components/canvas/canvas-node-annotation-dialog";
|
||||
import { CanvasNodeCropDialog, type CanvasImageCropRect } from "@/components/canvas/canvas-node-crop-dialog";
|
||||
import { CanvasNodeMaskEditDialog, type CanvasImageMaskEditPayload } from "@/components/canvas/canvas-node-mask-edit-dialog";
|
||||
import { CanvasNodeSplitDialog, type CanvasImageSplitParams } from "@/components/canvas/canvas-node-split-dialog";
|
||||
import { CanvasNodeUpscaleDialog, type CanvasImageUpscaleParams } from "@/components/canvas/canvas-node-upscale-dialog";
|
||||
import type { CanvasNodeData } from "@/types/canvas";
|
||||
import type { AiConfig } from "@/stores/use-config-store";
|
||||
@@ -10,17 +9,14 @@ type CanvasProjectMediaDialogsProps = {
|
||||
cropNode: CanvasNodeData | null;
|
||||
annotationNode: CanvasNodeData | null;
|
||||
maskEditNode: CanvasNodeData | null;
|
||||
splitNode: CanvasNodeData | null;
|
||||
upscaleNode: CanvasNodeData | null;
|
||||
onCloseCrop: () => void;
|
||||
onCloseAnnotation: () => void;
|
||||
onCloseMaskEdit: () => void;
|
||||
onCloseSplit: () => void;
|
||||
onCloseUpscale: () => void;
|
||||
onCrop: (node: CanvasNodeData, crop: CanvasImageCropRect) => void;
|
||||
onAnnotate: (node: CanvasNodeData, dataUrl: string) => void;
|
||||
onMaskEdit: (node: CanvasNodeData, payload: CanvasImageMaskEditPayload) => void;
|
||||
onSplit: (node: CanvasNodeData, params: CanvasImageSplitParams) => void;
|
||||
onUpscale: (node: CanvasNodeData, params: CanvasImageUpscaleParams) => void;
|
||||
config: AiConfig;
|
||||
};
|
||||
@@ -29,17 +25,14 @@ export function CanvasProjectMediaDialogs({
|
||||
cropNode,
|
||||
annotationNode,
|
||||
maskEditNode,
|
||||
splitNode,
|
||||
upscaleNode,
|
||||
onCloseCrop,
|
||||
onCloseAnnotation,
|
||||
onCloseMaskEdit,
|
||||
onCloseSplit,
|
||||
onCloseUpscale,
|
||||
onCrop,
|
||||
onAnnotate,
|
||||
onMaskEdit,
|
||||
onSplit,
|
||||
onUpscale,
|
||||
config,
|
||||
}: CanvasProjectMediaDialogsProps) {
|
||||
@@ -48,7 +41,6 @@ export function CanvasProjectMediaDialogs({
|
||||
{cropNode?.metadata?.content ? <CanvasNodeCropDialog dataUrl={cropNode.metadata.content} open onClose={onCloseCrop} onConfirm={(crop) => onCrop(cropNode, crop)} /> : null}
|
||||
{annotationNode?.metadata?.content ? <CanvasNodeAnnotationDialog image={{ url: annotationNode.metadata.content, storageKey: annotationNode.metadata.storageKey }} open onClose={onCloseAnnotation} onConfirm={(dataUrl) => onAnnotate(annotationNode, dataUrl)} /> : null}
|
||||
{maskEditNode?.metadata?.content ? <CanvasNodeMaskEditDialog dataUrl={maskEditNode.metadata.content} config={{ ...config, model: maskEditNode.metadata.model || config.model, imageModel: maskEditNode.metadata.model || config.imageModel, size: maskEditNode.metadata.size || config.size, quality: maskEditNode.metadata.quality || config.quality, count: String(maskEditNode.metadata.count || config.count) }} open onClose={onCloseMaskEdit} onConfirm={(payload) => onMaskEdit(maskEditNode, payload)} /> : null}
|
||||
{splitNode?.metadata?.content ? <CanvasNodeSplitDialog dataUrl={splitNode.metadata.content} open onClose={onCloseSplit} onConfirm={(params) => onSplit(splitNode, params)} /> : null}
|
||||
{upscaleNode?.metadata?.content ? <CanvasNodeUpscaleDialog dataUrl={upscaleNode.metadata.content} open onClose={onCloseUpscale} onConfirm={(params) => onUpscale(upscaleNode, params)} /> : null}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -200,9 +200,10 @@ export const CanvasProjectWorldLayers = memo(function CanvasProjectWorldLayers(p
|
||||
),
|
||||
)}
|
||||
|
||||
{props.selectedNodeBounds && !props.selectionBox && !props.isNodeDragging ? (
|
||||
{props.selectedNodeBounds && !props.selectionBox ? (
|
||||
<div
|
||||
ref={props.selectionBoundsElementRef}
|
||||
data-canvas-selection-bounds
|
||||
className="pointer-events-none absolute z-[var(--z-panel-floating)] rounded-xl"
|
||||
style={{
|
||||
left: props.selectedNodeBounds.left - 12 / viewportScale,
|
||||
|
||||
@@ -823,10 +823,8 @@ function InfiniteCanvasPage() {
|
||||
setAnnotationNodeId,
|
||||
setCropNodeId,
|
||||
setMaskEditNodeId,
|
||||
setSplitNodeId,
|
||||
setUpscaleNodeId,
|
||||
splitImageNode,
|
||||
splitNodeId,
|
||||
openVideoFrameExtractor,
|
||||
openVideoSegmentExtractor,
|
||||
upscaleImageNode,
|
||||
@@ -868,7 +866,6 @@ function InfiniteCanvasPage() {
|
||||
setCropNodeId(clearDeletedId);
|
||||
setMaskEditNodeId(clearDeletedId);
|
||||
setAnnotationNodeId(clearDeletedId);
|
||||
setSplitNodeId(clearDeletedId);
|
||||
setUpscaleNodeId(clearDeletedId);
|
||||
setAngleNodeId(clearDeletedId);
|
||||
setLightingNodeId(clearDeletedId);
|
||||
@@ -902,7 +899,6 @@ function InfiniteCanvasPage() {
|
||||
setLightingNodeId,
|
||||
setMaskEditNodeId,
|
||||
setSegmentDialogNodeId,
|
||||
setSplitNodeId,
|
||||
setUpscaleNodeId,
|
||||
setRunningNodeId,
|
||||
],
|
||||
@@ -1337,7 +1333,6 @@ function InfiniteCanvasPage() {
|
||||
selectedNodeBounds,
|
||||
selectedVideoNodes,
|
||||
skillMentionReferences,
|
||||
splitNode,
|
||||
superResolveNode,
|
||||
toolbarNode,
|
||||
upscaleNode,
|
||||
@@ -1360,7 +1355,7 @@ function InfiniteCanvasPage() {
|
||||
cropNodeId,
|
||||
maskEditNodeId,
|
||||
annotationNodeId,
|
||||
splitNodeId,
|
||||
splitNodeId: null,
|
||||
upscaleNodeId,
|
||||
superResolveNodeId,
|
||||
angleNodeId,
|
||||
@@ -2528,7 +2523,16 @@ function InfiniteCanvasPage() {
|
||||
</div>
|
||||
|
||||
{angleNode?.metadata?.content ? (
|
||||
<AppModal flush open centered title="多角度编辑器" footer={null} width={620} onCancel={() => setAngleNodeId(null)}>
|
||||
<CanvasNodePanelOverlay
|
||||
node={angleNode}
|
||||
viewport={viewport}
|
||||
containerRef={containerRef}
|
||||
panelWidth={640}
|
||||
panelHeight={540}
|
||||
allowOverflow
|
||||
dragOffset={dragPreview?.nodeIds.has(angleNode.id) ? { x: dragPreview.x, y: dragPreview.y } : null}
|
||||
isDragging={isNodeDragging && Boolean(dragPreview?.nodeIds.has(angleNode.id))}
|
||||
>
|
||||
<CanvasNodeAnglePanel
|
||||
dataUrl={angleNode.metadata.content}
|
||||
onClose={() => setAngleNodeId(null)}
|
||||
@@ -2536,7 +2540,7 @@ function InfiniteCanvasPage() {
|
||||
void generateAngleNode(angleNode, params);
|
||||
}}
|
||||
/>
|
||||
</AppModal>
|
||||
</CanvasNodePanelOverlay>
|
||||
) : null}
|
||||
|
||||
{lightingNode?.metadata?.content ? (
|
||||
@@ -2644,7 +2648,7 @@ function InfiniteCanvasPage() {
|
||||
) : null}
|
||||
|
||||
<CanvasNodeToolbar
|
||||
node={isCanvasNodeMoving || nodeImageSettingsOpen || emotionNodeId ? null : toolbarNode}
|
||||
node={isCanvasNodeMoving || nodeImageSettingsOpen || emotionNodeId || angleNodeId ? null : toolbarNode}
|
||||
workspaceMode={workspaceMode}
|
||||
viewport={viewport}
|
||||
containerRef={containerRef}
|
||||
@@ -2667,7 +2671,7 @@ function InfiniteCanvasPage() {
|
||||
}}
|
||||
onPortraitTexture={openPortraitTextureEditor}
|
||||
onCrop={(node) => setCropNodeId(node.id)}
|
||||
onSplit={(node) => setSplitNodeId(node.id)}
|
||||
onSplit={(node, params) => void splitImageNode(node, params)}
|
||||
onUpscale={(node) => setUpscaleNodeId(node.id)}
|
||||
onSuperResolve={(node) => setSuperResolveNodeId(node.id)}
|
||||
onAngle={(node) => {
|
||||
@@ -2981,17 +2985,14 @@ function InfiniteCanvasPage() {
|
||||
cropNode={cropNode}
|
||||
annotationNode={annotationNode}
|
||||
maskEditNode={maskEditNode}
|
||||
splitNode={splitNode}
|
||||
upscaleNode={upscaleNode}
|
||||
onCloseCrop={() => setCropNodeId(null)}
|
||||
onCloseAnnotation={() => setAnnotationNodeId(null)}
|
||||
onCloseMaskEdit={() => setMaskEditNodeId(null)}
|
||||
onCloseSplit={() => setSplitNodeId(null)}
|
||||
onCloseUpscale={() => setUpscaleNodeId(null)}
|
||||
onCrop={(node, crop) => void cropImageNode(node, crop)}
|
||||
onAnnotate={(node, dataUrl) => void saveAnnotatedImageNode(node, dataUrl)}
|
||||
onMaskEdit={(node, payload) => void maskEditImageNode(node, payload)}
|
||||
onSplit={(node, params) => void splitImageNode(node, params)}
|
||||
onUpscale={(node, params) => void upscaleImageNode(node, params)}
|
||||
config={effectiveConfig}
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { PanoramaGenerateConfig } from "@/components/canvas/canvas-panorama
|
||||
import type { CanvasVideoFrameParams } from "@/components/canvas/canvas-video-frame-dialog";
|
||||
import { NODE_DEFAULT_SIZE } from "@/constant/canvas";
|
||||
import { cropDataUrl, splitDataUrl, upscaleDataUrl } from "@/lib/canvas/canvas-image-data";
|
||||
import { isValidGridSplit, layoutGridSplitCells } from "@/lib/canvas/canvas-grid-split";
|
||||
import { audioMetadata, imageMetadata, videoMetadata } from "@/lib/canvas/canvas-generation-task-sync";
|
||||
import { findAvailableGenerationGroupPosition, imageGenerationChildPosition, imageGenerationGroupSize } from "@/lib/canvas/canvas-generation-layout";
|
||||
import { canvasGenerationPromptMetadata } from "@/lib/canvas/canvas-generation-submission";
|
||||
@@ -113,7 +114,6 @@ export function useCanvasMediaTools({
|
||||
const [cropNodeId, setCropNodeId] = useState<string | null>(null);
|
||||
const [annotationNodeId, setAnnotationNodeId] = useState<string | null>(null);
|
||||
const [maskEditNodeId, setMaskEditNodeId] = useState<string | null>(null);
|
||||
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);
|
||||
@@ -615,25 +615,25 @@ export function useCanvasMediaTools({
|
||||
}, [connectionsRef, message, persistMediaNodes, setConnections, setNodes]);
|
||||
|
||||
const splitImageNode = useCallback(async (node: CanvasNodeData, params: CanvasImageSplitParams) => {
|
||||
if (!node.metadata?.content) return;
|
||||
setSplitNodeId(null);
|
||||
if (!node.metadata?.content || !isValidGridSplit(params)) return;
|
||||
const pieces = await splitDataUrl(node.metadata.content, params);
|
||||
const gap = 16;
|
||||
const cellWidth = node.width / params.columns;
|
||||
const cellHeight = node.height / params.rows;
|
||||
const startX = node.position.x + node.width + 96;
|
||||
const childNodes = await Promise.all(pieces.map(async (piece) => {
|
||||
const sizedPieces = await Promise.all(pieces.map(async (piece) => {
|
||||
const image = await uploadImage(piece.dataUrl);
|
||||
return {
|
||||
id: nanoid(),
|
||||
type: CanvasNodeType.Image,
|
||||
title: `${node.title || "图片"} · 宫格 ${piece.row + 1}-${piece.column + 1}`,
|
||||
position: { x: startX + piece.column * (cellWidth + gap), y: node.position.y + piece.row * (cellHeight + gap) },
|
||||
width: cellWidth,
|
||||
height: cellHeight,
|
||||
metadata: { ...imageMetadata(image), prompt: node.metadata?.prompt },
|
||||
} satisfies CanvasNodeData;
|
||||
return { piece, image, size: fitNodeSize(image.width, image.height) };
|
||||
}));
|
||||
const positions = layoutGridSplitCells(
|
||||
{ x: node.position.x + node.width + 96, y: node.position.y },
|
||||
sizedPieces.map(({ piece, size }) => ({ row: piece.row, column: piece.column, width: size.width, height: size.height })),
|
||||
);
|
||||
const childNodes = sizedPieces.map(({ piece, image, size }, index) => ({
|
||||
id: nanoid(),
|
||||
type: CanvasNodeType.Image,
|
||||
title: `${node.title || "图片"} · 宫格 ${piece.row + 1}-${piece.column + 1}`,
|
||||
position: positions[index] || { x: node.position.x + node.width + 96, y: node.position.y },
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
metadata: { ...imageMetadata(image), prompt: node.metadata?.prompt, manualSize: true },
|
||||
} satisfies CanvasNodeData));
|
||||
setNodes((current) => [...current, ...childNodes]);
|
||||
setConnections((current) => [...current, ...childNodes.map((child) => ({ id: nanoid(), fromNodeId: node.id, toNodeId: child.id }))]);
|
||||
setSelectedNodeIds(new Set(childNodes.map((child) => child.id)));
|
||||
@@ -998,10 +998,8 @@ export function useCanvasMediaTools({
|
||||
setAnnotationNodeId,
|
||||
setCropNodeId,
|
||||
setMaskEditNodeId,
|
||||
setSplitNodeId,
|
||||
setUpscaleNodeId,
|
||||
splitImageNode,
|
||||
splitNodeId,
|
||||
openVideoFrameExtractor,
|
||||
openVideoSegmentExtractor,
|
||||
upscaleImageNode,
|
||||
|
||||
Reference in New Issue
Block a user