From d622b18ddf6dac9385d16eb7273e394a5a786bd0 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 26 Apr 2026 17:55:43 +0800 Subject: [PATCH] fix canvas grid split selection workflow --- .../canvas/LiblibVideoCanvasReplica.tsx | 498 +++++++++++++++--- services/canvas/index.ts | 1 + 2 files changed, 440 insertions(+), 59 deletions(-) diff --git a/components/canvas/LiblibVideoCanvasReplica.tsx b/components/canvas/LiblibVideoCanvasReplica.tsx index 222a5ef75..5b0a64fc5 100644 --- a/components/canvas/LiblibVideoCanvasReplica.tsx +++ b/components/canvas/LiblibVideoCanvasReplica.tsx @@ -307,6 +307,15 @@ type GridSplitSpec = { rows: number; }; +type GridSplitState = { + columns: number; + detectedDivider: boolean; + rects: GridCropRect[]; + rows: number; + selectedIndices: number[]; + sourceMode: string; +}; + type AngleEditorSettings = { horizontalAngle: number; promptEnabled: boolean; @@ -1213,6 +1222,10 @@ function getNodeReferenceMediaUrl(node: CloneNode | null | undefined) { return getNodeMediaUrl(node) || getDraftString(node, "sourceUrl"); } +function getNodeGridSplitSourceUrl(node: CloneNode | null | undefined) { + return getNodeMediaUrl(node) || getNodeSourceMediaUrl(node); +} + function getOfficialImagePresetFunctionKey(value?: unknown) { const text = String(value || "").trim(); if (!text) return null; @@ -1474,6 +1487,56 @@ function createEqualGridRects(rows: number, columns: number): GridCropRect[] { }); } +function normalizeGridRect(value: unknown, fallback: GridCropRect): GridCropRect { + if (!value || typeof value !== "object") return fallback; + const record = value as Record; + return { + height: Math.min(1, Math.max(0.02, Number(record.height ?? fallback.height))), + width: Math.min(1, Math.max(0.02, Number(record.width ?? fallback.width))), + x: Math.min(0.98, Math.max(0, Number(record.x ?? fallback.x))), + y: Math.min(0.98, Math.max(0, Number(record.y ?? fallback.y))), + }; +} + +function normalizeGridSplitState(value: unknown): GridSplitState | null { + if (!value || typeof value !== "object") return null; + const record = value as Record; + const rows = Math.max(1, Math.min(5, Math.round(Number(record.rows || 1)))); + const columns = Math.max(1, Math.min(5, Math.round(Number(record.columns || 1)))); + const cellCount = rows * columns; + const fallbackRects = createEqualGridRects(rows, columns); + const rawRects = Array.isArray(record.rects) ? record.rects.slice(0, cellCount) : []; + const rects = Array.from({ length: cellCount }).map((_, index) => + normalizeGridRect(rawRects[index], fallbackRects[index] || fallbackRects[0]) + ); + const selectedIndices = Array.from( + new Set( + (Array.isArray(record.selectedIndices) ? record.selectedIndices : []) + .map((item) => Math.round(Number(item))) + .filter((index) => Number.isFinite(index) && index >= 0 && index < cellCount) + ) + ).sort((left, right) => left - right); + + return { + columns, + detectedDivider: Boolean(record.detectedDivider), + rects, + rows, + selectedIndices, + sourceMode: String(record.sourceMode || "detected-lines"), + }; +} + +function getNodeGridSplitState(node: CloneNode | null | undefined) { + return normalizeGridSplitState(node?.draftValues?.gridSplit); +} + +function clearGridSplitDraftValues(draftValues: Record | undefined) { + const nextDraftValues = { ...(draftValues || {}) }; + delete nextDraftValues.gridSplit; + return nextDraftValues; +} + function buildGridRectsFromLines({ columns, height, @@ -1630,6 +1693,109 @@ async function detectGridSplitFromImage(url: string, rows: number, columns: numb } } +async function loadCanvasImage(url: string): Promise { + return new Promise((resolve, reject) => { + const image = new Image(); + if (!url.startsWith("blob:") && !url.startsWith("data:")) { + image.crossOrigin = "anonymous"; + image.referrerPolicy = "no-referrer"; + } + image.onload = () => resolve(image); + image.onerror = () => reject(new Error("image-load-failed")); + image.src = url; + }); +} + +async function cropRectToBlobUrlSource(url: string, rect: GridCropRect): Promise { + if (typeof window === "undefined" || !url) return null; + const image = await loadCanvasImage(url); + const naturalWidth = image.naturalWidth || image.width; + const naturalHeight = image.naturalHeight || image.height; + if (!naturalWidth || !naturalHeight) return null; + + const cropX = Math.max(0, Math.floor(rect.x * naturalWidth)); + const cropY = Math.max(0, Math.floor(rect.y * naturalHeight)); + const cropWidth = Math.max(1, Math.round(rect.width * naturalWidth)); + const cropHeight = Math.max(1, Math.round(rect.height * naturalHeight)); + const canvas = document.createElement("canvas"); + canvas.width = cropWidth; + canvas.height = cropHeight; + const context = canvas.getContext("2d"); + if (!context) return null; + + context.drawImage(image, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth, cropHeight); + + return await new Promise((resolve) => { + canvas.toBlob((blob) => resolve(blob), "image/png", 1); + }); +} + +async function createGridSplitReferenceAsset({ + index, + nodeId, + rect, + sourceUrl, +}: { + index: number; + nodeId: string; + rect: GridCropRect; + sourceUrl: string; +}) { + if (/^https?:\/\//i.test(sourceUrl)) { + const response = await imageGenerationService.boxCutout({ + cropRect: rect, + imageUrl: sourceUrl, + paddingRatio: 0, + sourceNodeId: nodeId, + }); + if (response.success && response.data?.url) return response.data.url; + } + + const blob = await cropRectToBlobUrlSource(sourceUrl, rect); + if (!blob) return ""; + const file = new File([blob], `video-canvas-grid-${nodeId}-${index + 1}.png`, { + type: blob.type || "image/png", + }); + const upload = await imageGenerationService.uploadImage(file); + return upload.success ? upload.data?.url || "" : ""; +} + +async function resolveNodeReferenceImages(node: CloneNode | null | undefined, limit = 12): Promise { + const fallbackUrl = getNodeReferenceMediaUrl(node); + const split = getNodeGridSplitState(node); + const sourceUrl = getNodeGridSplitSourceUrl(node); + if (!split || split.selectedIndices.length === 0 || !sourceUrl) { + return fallbackUrl ? [fallbackUrl] : []; + } + + const selectedIndices = split.selectedIndices.slice(0, limit); + const resolved = await Promise.all( + selectedIndices.map(async (index) => { + const rect = split.rects[index]; + if (!rect) return ""; + try { + return await createGridSplitReferenceAsset({ + index, + nodeId: node?.id || "grid", + rect, + sourceUrl, + }); + } catch { + return ""; + } + }) + ); + + const urls = Array.from( + new Set( + resolved + .map((item) => String(item || "").trim()) + .filter(Boolean) + ) + ); + return urls.length > 0 ? urls : fallbackUrl ? [fallbackUrl] : []; +} + function getPresetMode(node: CloneNode | null | undefined): ActiveStarterScene { const value = getDraftString(node, "targetPreset"); if ( @@ -2743,15 +2909,15 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib async (nodeId: string, action: ImageToolbarAction, option?: string) => { const anchor = nodes.find((node) => node.id === nodeId); if (!anchor) return; - const sourceUrl = getNodeReferenceMediaUrl(anchor) || CHARACTER_IMAGE_URL; + const anchorSourceUrl = getNodeReferenceMediaUrl(anchor) || CHARACTER_IMAGE_URL; if (action === "download") { - if (!sourceUrl) { + if (!anchorSourceUrl) { toast.error("当前素材没有可下载地址"); return; } const link = document.createElement("a"); - link.href = sourceUrl; + link.href = anchorSourceUrl; link.download = `${anchor.title || "libtv-image"}.png`; link.rel = "noreferrer"; document.body.appendChild(link); @@ -2818,16 +2984,31 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib const rightX = Math.round(anchor.x + anchor.width + OFFICIAL_NODE_GAP); const centerY = (height: number) => Math.round(anchor.y + anchor.height / 2 - height / 2); - const nextNodes: CloneNode[] = []; - const nextEdges: CloneEdge[] = []; if (action === "crop") { const { columns, rows, tileCount } = normalizeGridOption(option); - const tileSize = tileCount === 25 ? 142 : tileCount === 16 ? 156 : 180; - const gap = 26; - const startY = Math.round(anchor.y + anchor.height / 2 - (rows * tileSize + (rows - 1) * gap) / 2); const anchorMediaUrl = getNodeMediaUrl(anchor); + const currentSplit = getNodeGridSplitState(anchor); const isSyntheticGrid = Boolean(anchor.draftValues?.nineGridComposite) && !anchorMediaUrl; + if (currentSplit && currentSplit.columns === columns && currentSplit.rows === rows) { + setNodes((current) => + current.map((node) => + node.id === anchor.id + ? { + ...node, + draftValues: clearGridSplitDraftValues(node.draftValues), + } + : node + ) + ); + setSelectedNodeId(anchor.id); + setCanvasCommand(null); + setNodeContextMenu(null); + setConnectionMenu(null); + setActivePanel(null); + toast.success("已取消宫格切分"); + return; + } const detectedSplit = !isSyntheticGrid && anchorMediaUrl ? await detectGridSplitFromImage(anchorMediaUrl, rows, columns) @@ -2836,59 +3017,43 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib detectedSplit?.rects && detectedSplit.rects.length >= tileCount ? detectedSplit.rects : createEqualGridRects(rows, columns); - const sourceVisuals = Array.isArray(anchor.draftValues?.nineGridCells) - ? (anchor.draftValues?.nineGridCells as GridCellVisual[]) - : []; - - Array.from({ length: tileCount }).forEach((_, index) => { - const tile = createMediaResourceNode({ - action: "IMAGE_RESOURCE", - height: tileSize, - mediaType: "image", - mediaUrl: isSyntheticGrid ? sourceUrl : anchorMediaUrl || sourceUrl, - prompt: option || "宫格切分", - title: `镜头 ${String(index + 1).padStart(2, "0")}`, - width: tileSize, - x: rightX + (index % columns) * (tileSize + gap), - y: startY + Math.floor(index / columns) * (tileSize + gap), - }); - nextNodes.push({ - ...tile, - subtitle: "宫格切片", - draftValues: { - ...(tile.draftValues || {}), - cropTile: { - columns, - detectedDivider: Boolean(detectedSplit?.detected), - index, - rect: rects[index], - rows, - sourceCompositeNodeId: anchor.id, - sourceMode: isSyntheticGrid ? "synthetic-grid" : "detected-lines", - }, - gridCellVisual: isSyntheticGrid ? sourceVisuals[index] || getGridCellVisual(index) : undefined, - sourceNodeId: anchor.id, - sourceUrl, - }, - }); - nextEdges.push({ id: makeId("edge"), source: anchor.id, target: tile.id }); - }); - - setNodes((current) => [...current, ...nextNodes]); - setEdges((current) => [...current, ...nextEdges]); - setSelectedNodeId(nextNodes[0]?.id || anchor.id); + setNodes((current) => + current.map((node) => + node.id === anchor.id + ? { + ...node, + draftValues: { + ...(node.draftValues || {}), + gridSplit: { + columns, + detectedDivider: Boolean(detectedSplit?.detected), + rects, + rows, + selectedIndices: [], + sourceMode: isSyntheticGrid ? "synthetic-grid" : "detected-lines", + }, + }, + } + : node + ) + ); + setSelectedNodeId(anchor.id); setCanvasCommand(null); setNodeContextMenu(null); setConnectionMenu(null); setActivePanel(null); toast.success( detectedSplit?.detected - ? `已识别黑色分割线并切分为 ${tileCount} 张图片` - : `已按 ${columns}x${rows} 宫格切分为 ${tileCount} 张图片` + ? `已在当前图上识别黑色分割线,进入 ${tileCount} 宫格选择模式` + : `已在当前图上按 ${columns}x${rows} 进入宫格选择模式` ); return; } + const resolvedReferenceImages = await resolveNodeReferenceImages(anchor); + const sourceUrl = resolvedReferenceImages[0] || anchorSourceUrl; + const extraReferenceImages = resolvedReferenceImages.slice(1); + const labels: Record, string> = { grid: "九宫格", hd: "高清", @@ -2936,6 +3101,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib mode: "grid", option: resultTitle, prompt, + referenceImages: [sourceUrl, ...extraReferenceImages], sourceNodeId: anchor.id, sourceUrl, }, @@ -2965,6 +3131,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib prompt, promptExtend: false, referenceImage: sourceUrl, + referenceImages: extraReferenceImages, }); const generatedUrl = response.success ? getFirstGeneratedImageUrl(response) : ""; if (generatedUrl) { @@ -2987,6 +3154,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib mode: "grid", option: resultTitle, prompt, + referenceImages: [sourceUrl, ...extraReferenceImages], }, prompt, sourceNodeId: anchor.id, @@ -3051,6 +3219,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib mode: functionKey === CHARACTER_THREE_VIEW_FUNCTION_KEY ? "character-three-view" : "image", option: resultTitle, prompt, + referenceImages: [sourceUrl, ...extraReferenceImages], sourceNodeId: anchor.id, sourceUrl, }, @@ -3080,6 +3249,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib prompt, promptExtend: false, referenceImage: sourceUrl, + referenceImages: extraReferenceImages, }); const generatedUrl = response.success ? getFirstGeneratedImageUrl(response) : ""; if (generatedUrl) { @@ -3101,6 +3271,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib functionKey, option: resultTitle, prompt, + referenceImages: [sourceUrl, ...extraReferenceImages], }, prompt, sourceNodeId: anchor.id, @@ -3174,6 +3345,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib params: { mode: action, prompt, + referenceImages: [sourceUrl, ...extraReferenceImages], sourceNodeId: anchor.id, sourceUrl, }, @@ -3214,6 +3386,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib prompt, promptExtend: false, referenceImage: sourceUrl, + referenceImages: extraReferenceImages, }); generatedUrl = response.success ? getFirstGeneratedImageUrl(response) : ""; if (!generatedUrl && !response.success) toast.error(response.error || `${definition.label}处理失败`); @@ -3234,6 +3407,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib params: { mode: action, prompt, + referenceImages: [sourceUrl, ...extraReferenceImages], sourceNodeId: anchor.id, sourceUrl, }, @@ -3277,6 +3451,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib params: { mode: action, prompt, + referenceImages: [sourceUrl, ...extraReferenceImages], sourceNodeId: anchor.id, sourceUrl, }, @@ -3314,11 +3489,92 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib ); }, []); + const dismissNodeGridSplit = useCallback((nodeId: string) => { + setNodes((current) => + current.map((node) => + node.id === nodeId + ? { + ...node, + draftValues: clearGridSplitDraftValues(node.draftValues), + } + : node + ) + ); + }, []); + + const clearNodeGridSplitSelection = useCallback((nodeId: string) => { + setNodes((current) => + current.map((node) => { + if (node.id !== nodeId) return node; + const split = getNodeGridSplitState(node); + if (!split) return node; + return { + ...node, + draftValues: { + ...(node.draftValues || {}), + gridSplit: { + ...split, + selectedIndices: [], + }, + }, + }; + }) + ); + }, []); + + const toggleNodeGridSplitSelection = useCallback( + (nodeId: string, index: number, behavior: "replace" | "toggle" = "replace") => { + setNodes((current) => + current.map((node) => { + if (node.id !== nodeId) return node; + const split = getNodeGridSplitState(node); + if (!split || !split.rects[index]) return node; + const alreadySelected = split.selectedIndices.includes(index); + const nextSelected = + behavior === "toggle" + ? alreadySelected + ? split.selectedIndices.filter((item) => item !== index) + : [...split.selectedIndices, index].sort((left, right) => left - right) + : alreadySelected && split.selectedIndices.length === 1 + ? [] + : [index]; + + return { + ...node, + draftValues: { + ...(node.draftValues || {}), + gridSplit: { + ...split, + selectedIndices: nextSelected, + }, + }, + }; + }) + ); + }, + [] + ); + const submitSelectedImagePrompt = useCallback( async (nodeId: string, payload: ComposerSubmitPayload) => { const anchor = nodes.find((node) => node.id === nodeId); if (!anchor) return; - const sourceUrl = getNodeMediaUrl(anchor) || CHARACTER_IMAGE_URL; + const anchorSplit = getNodeGridSplitState(anchor); + const anchorReferenceImages = await resolveNodeReferenceImages(anchor); + const referencedNodes = payload.references + .filter((referenceId) => referenceId !== anchor.id) + .map((referenceId) => nodes.find((node) => node.id === referenceId)) + .filter((node): node is CloneNode => Boolean(node)); + const referencedImageLists = await Promise.all(referencedNodes.map((node) => resolveNodeReferenceImages(node))); + const mergedReferenceImages = Array.from( + new Set( + [...anchorReferenceImages, ...referencedImageLists.flat()] + .map((item) => String(item || "").trim()) + .filter(Boolean) + ) + ).slice(0, 12); + const sourceUrl = mergedReferenceImages[0] || getNodeReferenceMediaUrl(anchor) || CHARACTER_IMAGE_URL; + const extraReferenceImages = mergedReferenceImages.slice(1); const rightX = Math.round(anchor.x + anchor.width + OFFICIAL_NODE_GAP); const starterMode = getPresetMode(anchor); const slashScene = payload.slashScene || ""; @@ -3356,6 +3612,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib prompt: finalPrompt, promptExtend: !isWideGeneration, referenceImage: sourceUrl, + referenceImages: extraReferenceImages, }); generatedUrl = response.success ? getFirstGeneratedImageUrl(response) : ""; if (!generatedUrl && !response.success) { @@ -3424,6 +3681,8 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib : "image", prompt: finalPrompt, references: payload.references, + referenceImages: mergedReferenceImages, + selectedGridCellCount: anchorSplit?.selectedIndices.length || 0, settings: payload.settings, slashScene, sourceNodeId: anchor.id, @@ -3627,10 +3886,19 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib .filter((edge) => edge.target === nodeId) .map((edge) => nodes.find((item) => item.id === edge.source)) .filter((item): item is CloneNode => Boolean(item)); - const imageUrls = incomingNodes - .filter((item) => item.inputType === "image") - .map((item) => getNodeReferenceMediaUrl(item)) - .filter(Boolean); + const imageReferenceLists = await Promise.all( + incomingNodes + .filter((item) => item.inputType === "image") + .map((item) => resolveNodeReferenceImages(item)) + ); + const imageUrls = Array.from( + new Set( + imageReferenceLists + .flat() + .map((item) => String(item || "").trim()) + .filter(Boolean) + ) + ).slice(0, 12); const videoUrls = incomingNodes .filter((item) => item.inputType === "video") .map((item) => getNodeReferenceMediaUrl(item)) @@ -3665,6 +3933,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib : "image-repaint"; const definition = getConfiguredFunction(videoCanvasFunctionConfig, functionKey); const sourceUrl = imageUrls[0] || getNodeReferenceMediaUrl(node); + const extraReferenceImages = imageUrls.slice(1); const prompt = functionKey === MULTI_VIEW_NINE_GRID_FUNCTION_KEY ? composeMultiViewNineGridPrompt(definition, String(params.option || node.title)) @@ -3687,6 +3956,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib prompt, promptExtend: !isWideImageGenerationFunction(functionKey), referenceImage: sourceUrl || undefined, + referenceImages: extraReferenceImages, }); generatedUrl = response.success ? getFirstGeneratedImageUrl(response) : ""; if (!generatedUrl && !response.success) throw new Error(response.error || "图片生成失败"); @@ -3701,7 +3971,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib kind: "media", subtitle: "生成结果", draftValues: { - ...(item.draftValues || {}), + ...clearGridSplitDraftValues(item.draftValues), functionKey, generated: true, generationProgress: 100, @@ -3710,7 +3980,9 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib gridPrompt: gridLayout ? prompt : item.draftValues?.gridPrompt, gridRows: gridLayout?.rows || item.draftValues?.gridRows, nineGridCells: gridLayout - ? Array.from({ length: gridLayout.rows * gridLayout.columns }).map((_, index) => getGridCellVisual(index)) + ? Array.from({ length: gridLayout.rows * gridLayout.columns }).map((_, index) => + getGridCellVisual(index) + ) : item.draftValues?.nineGridCells, nineGridComposite: gridLayout ? true : item.draftValues?.nineGridComposite, mediaType: "image", @@ -3723,6 +3995,7 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib ...getNodeParams(item), functionKey, prompt, + referenceImages: sourceUrl ? [sourceUrl, ...extraReferenceImages] : extraReferenceImages, }, prompt, sourceUrl, @@ -4111,9 +4384,12 @@ export function LiblibVideoCanvasReplica({ canvasId, isLoading = false }: Liblib selected={node.id === selectedNodeId} onContextMenu={(event) => openNodeContext(event, node)} onCancelGeneration={() => cancelGenerationNode(node.id)} + onClearGridSplit={() => dismissNodeGridSplit(node.id)} + onClearGridSplitSelection={() => clearNodeGridSplitSelection(node.id)} onModuleAction={(actionLabel) => runTargetModuleAction(node.id, actionLabel)} onOpenConnectionMenu={(event, side) => openConnectionMenu(event, node, side)} onPointerDown={(event) => startNodeDrag(event, node)} + onToggleGridSplitSelection={(index, behavior) => toggleNodeGridSplitSelection(node.id, index, behavior)} onTextResourceChange={(content) => updateTextResourceContent(node.id, content)} /> ))} @@ -7235,21 +7511,27 @@ function EdgeFlowSegments({ } function CloneCanvasNode({ + onClearGridSplit, + onClearGridSplitSelection, node, onCancelGeneration, onContextMenu, onModuleAction, onOpenConnectionMenu, onPointerDown, + onToggleGridSplitSelection, onTextResourceChange, selected, }: { + onClearGridSplit: () => void; + onClearGridSplitSelection: () => void; node: CloneNode; onCancelGeneration: () => void; onContextMenu: (event: ReactMouseEvent) => void; onModuleAction: (actionLabel: TargetModuleActionLabel) => void; onOpenConnectionMenu: (event: ReactMouseEvent, side: ConnectionSide) => void; onPointerDown: (event: ReactPointerEvent) => void; + onToggleGridSplitSelection: (index: number, behavior: "replace" | "toggle") => void; onTextResourceChange: (content: string) => void; selected: boolean; }) { @@ -7273,7 +7555,14 @@ function CloneCanvasNode({ if (node.kind === "media") { return ( - + ); } @@ -7609,6 +7898,82 @@ function CroppedGridTileImage({ ); } +function GridSplitSelectionOverlay({ + onClearSelection, + onClose, + onToggleCell, + split, +}: { + onClearSelection: () => void; + onClose: () => void; + onToggleCell: (index: number, behavior: "replace" | "toggle") => void; + split: GridSplitState; +}) { + const selectedCount = split.selectedIndices.length; + const selectedSet = new Set(split.selectedIndices); + + return ( +
+
+ + {selectedCount > 0 ? `已选 ${selectedCount}` : `${split.columns} × ${split.rows} 切分`} + + {selectedCount > 0 ? ( + + ) : null} + +
+ {split.rects.map((rect, index) => { + const selected = selectedSet.has(index); + return ( +
+ ); +} + function OfficialAudioCard({ mediaUrl, selected }: { mediaUrl: string; selected: boolean }) { return (
void; + onClearGridSplitSelection: () => void; node: CloneNode; onCancelGeneration: () => void; + onToggleGridSplitSelection: (index: number, behavior: "replace" | "toggle") => void; selected: boolean; }) { const mediaType = getDraftString(node, "mediaType") || node.inputType || "image"; @@ -7752,6 +8123,7 @@ function PresetMediaCanvasNode({ const outputSizeLabel = getDraftString(node, "outputSizeLabel"); const isNineGridComposite = Boolean(node.draftValues?.nineGridComposite); const cropTile = normalizeGridCropTile(node.draftValues?.cropTile); + const gridSplit = getNodeGridSplitState(node); const gridCellVisual = node.draftValues?.gridCellVisual ? normalizeGridCellVisual(node.draftValues?.gridCellVisual) : null; @@ -7844,6 +8216,14 @@ function PresetMediaCanvasNode({ 主体
) : null} + {gridSplit ? ( + + ) : null} {!generated && !isGenerating && !isGenerationFailed ? (