feat(assets): 文件夹主题 - 新增多主题皮肤并同步主线
This commit is contained in:
@@ -3,7 +3,7 @@ import { nanoid } from "nanoid";
|
||||
import { NODE_DEFAULT_SIZE } from "@/constant/canvas";
|
||||
import { canGenerateImageInPlace, findAvailableGenerationGroupPosition, imageGenerationChildPosition, imageGenerationGroupSize } from "@/lib/canvas/canvas-generation-layout";
|
||||
import { nodeSizeFromRatio } from "@/lib/canvas/canvas-node-size";
|
||||
import { buildImageGenerationMetadata, getGenerationCount, isGenerationCanceled, limitCanvasImageReferences, runCanvasGenerationTaskToConsumer } from "@/lib/canvas/canvas-project-generation";
|
||||
import { canvasImageReferenceLimitError, buildImageGenerationMetadata, getGenerationCount, isGenerationCanceled, runCanvasGenerationTaskToConsumer } from "@/lib/canvas/canvas-project-generation";
|
||||
import { CONTENT_MODERATION_ERROR_CODE, generationFailureMetadata, type GenerationFailureMetadata } from "@/lib/generation-error";
|
||||
import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas";
|
||||
|
||||
@@ -39,13 +39,18 @@ export async function executeImageGeneration({
|
||||
showError,
|
||||
registerPendingNodeIds,
|
||||
}: CanvasGenerationExecution) {
|
||||
const referenceLimitError = canvasImageReferenceLimitError(generationConfig, generationContext.referenceImages);
|
||||
if (referenceLimitError) {
|
||||
showError(referenceLimitError);
|
||||
return;
|
||||
}
|
||||
const count = getGenerationCount(generationConfig.count);
|
||||
const isConfigNode = sourceNode?.type === CanvasNodeType.Config;
|
||||
const isImageNode = sourceNode?.type === CanvasNodeType.Image;
|
||||
const reuseSourceNode = canGenerateImageInPlace(sourceNode);
|
||||
const directCopiedBatch = count > 1 && isImageNode && Boolean(sourceNode?.metadata?.content) && (Boolean(sourceNode?.metadata?.copiedFromNodeId) || sourceNode?.title.endsWith(" Copy"));
|
||||
// 已有图片生成新结果并保留旧版本;参考图只来自入边,避免把旧结果误当成自身输入。
|
||||
const referenceImages = limitCanvasImageReferences(generationConfig, generationContext.referenceImages);
|
||||
const referenceImages = generationContext.referenceImages;
|
||||
const generationType = referenceImages.length ? ("edit" as const) : ("generation" as const);
|
||||
const generationMetadata = buildImageGenerationMetadata(generationType, generationConfig, count, referenceImages);
|
||||
const parentConfig = NODE_DEFAULT_SIZE[isConfigNode ? CanvasNodeType.Config : isImageNode ? CanvasNodeType.Image : CanvasNodeType.Text];
|
||||
|
||||
@@ -7,7 +7,7 @@ import { CanvasNode } from "@/components/canvas/canvas-node";
|
||||
import type { CanvasBatchConnectionPreview } from "@/lib/canvas/canvas-batch-connection";
|
||||
import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
|
||||
import { isFrameNode } from "@/lib/canvas/canvas-frame";
|
||||
import type { CanvasDisplayConnection, CanvasFolderStyle, CanvasNodeData, ConnectionHandle, Position, SelectionBox } from "@/types/canvas";
|
||||
import type { CanvasDisplayConnection, CanvasFolderStyle, CanvasFolderTheme, CanvasNodeData, ConnectionHandle, Position, SelectionBox } from "@/types/canvas";
|
||||
|
||||
type DragPreview = { x: number; y: number; nodeIds: Set<string> } | null;
|
||||
type NodeBounds = { left: number; top: number; width: number; height: number; count: number } | null;
|
||||
@@ -57,6 +57,7 @@ type CanvasProjectWorldLayersProps = {
|
||||
onNodeResize: (nodeId: string, width: number, height: number, position?: Position) => void;
|
||||
onToggleFrame: (nodeId: string) => void;
|
||||
onFolderStyleChange: (nodeId: string, style: CanvasFolderStyle) => void;
|
||||
onFolderThemeChange: (nodeId: string, theme: CanvasFolderTheme) => void;
|
||||
onNodeTitleChange: (nodeId: string, title: string) => void;
|
||||
onNodeContextMenu: (event: ReactMouseEvent, nodeId: string) => void;
|
||||
onNodeContentChange: (nodeId: string, content: string) => void;
|
||||
@@ -123,6 +124,7 @@ export function CanvasProjectWorldLayers(props: CanvasProjectWorldLayersProps) {
|
||||
onResize={props.onNodeResize}
|
||||
onToggleCollapsed={props.onToggleFrame}
|
||||
onFolderStyleChange={props.onFolderStyleChange}
|
||||
onFolderThemeChange={props.onFolderThemeChange}
|
||||
onTitleChange={props.onNodeTitleChange}
|
||||
onContextMenu={props.onNodeContextMenu}
|
||||
/>
|
||||
|
||||
@@ -110,6 +110,7 @@ import {
|
||||
type CanvasAssistantSession,
|
||||
type CanvasConnection,
|
||||
type CanvasFolderStyle,
|
||||
type CanvasFolderTheme,
|
||||
type CanvasNodeData,
|
||||
type CanvasMediaPerformanceMode,
|
||||
type StoryboardColumn,
|
||||
@@ -916,6 +917,7 @@ function InfiniteCanvasPage() {
|
||||
downloadNodeImage,
|
||||
handleConfigNodeChange,
|
||||
handleFolderStyleChange,
|
||||
handleFolderThemeChange,
|
||||
handleFontSizeChange,
|
||||
handleNodeContentChange,
|
||||
handleNodePromptChange,
|
||||
@@ -943,7 +945,8 @@ function InfiniteCanvasPage() {
|
||||
const folder = linkedProjectQuery.data?.assetFolders.find((item) => item.id === folderId);
|
||||
if (!folder || !linkedProjectId) throw new Error("素材文件夹已不存在,请刷新后重试");
|
||||
const style: CanvasFolderStyle = folder.style === "stacked" || folder.style === "midnight" || folder.style === "paper" || folder.style === "cinema" || folder.style === "compact" ? folder.style : "glass";
|
||||
createFolder(projectAssetInsertPosition, { id: folder.id, projectId: linkedProjectId, title: folder.name, style, createdAt: folder.createdAt });
|
||||
const theme: CanvasFolderTheme = folder.theme === "obsidian" || folder.theme === "ember" || folder.theme === "pearl" ? folder.theme : "aurora";
|
||||
createFolder(projectAssetInsertPosition, { id: folder.id, projectId: linkedProjectId, title: folder.name, style, theme, createdAt: folder.createdAt });
|
||||
}, [createFolder, linkedProjectId, linkedProjectQuery.data?.assetFolders, projectAssetInsertPosition]);
|
||||
|
||||
const handleFrameToggle = useCallback((nodeId: string) => {
|
||||
@@ -985,9 +988,10 @@ function InfiniteCanvasPage() {
|
||||
const folder = folderId ? byId.get(folderId) : undefined;
|
||||
if (!folder) return node;
|
||||
const style: CanvasFolderStyle = folder.style === "stacked" || folder.style === "midnight" || folder.style === "paper" || folder.style === "cinema" || folder.style === "compact" ? folder.style : "glass";
|
||||
if (node.title === folder.name && node.metadata?.folder?.style === style) return node;
|
||||
const theme: CanvasFolderTheme = folder.theme === "obsidian" || folder.theme === "ember" || folder.theme === "pearl" ? folder.theme : "aurora";
|
||||
if (node.title === folder.name && node.metadata?.folder?.style === style && node.metadata?.folder?.theme === theme) return node;
|
||||
changed = true;
|
||||
return { ...node, title: folder.name, metadata: { ...node.metadata, folder: { ...node.metadata!.folder!, style } } };
|
||||
return { ...node, title: folder.name, metadata: { ...node.metadata, folder: { ...node.metadata!.folder!, style, theme, themeCover: undefined } } };
|
||||
});
|
||||
return changed ? next : current;
|
||||
});
|
||||
@@ -1860,6 +1864,7 @@ function InfiniteCanvasPage() {
|
||||
onNodeResize={handleNodeResize}
|
||||
onToggleFrame={handleFrameToggle}
|
||||
onFolderStyleChange={handleFolderStyleChange}
|
||||
onFolderThemeChange={handleFolderThemeChange}
|
||||
onNodeTitleChange={handleNodeTitleChange}
|
||||
onNodeContextMenu={handleNodeContextMenu}
|
||||
onNodeContentChange={handleNodeContentChange}
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
findRetrySourceNode,
|
||||
generationReferenceUrls,
|
||||
isGenerationCanceled,
|
||||
limitCanvasImageReferences,
|
||||
canvasImageReferenceLimitError,
|
||||
resolveMetadataReferences,
|
||||
resolveStoredReferenceImages,
|
||||
runBackendCanvasGenerationTask,
|
||||
@@ -108,7 +108,10 @@ export function useCanvasGenerationRetry({
|
||||
try {
|
||||
const promptOnly = retryMode === "video";
|
||||
const baseContext = buildNodeGenerationContext(sourceNode.id, nodesRef.current, connectionsRef.current, retryContextPrompt, assets, promptOnly);
|
||||
rawContext = hasSavedImageMetadata && !baseContext.characterReferences.length ? null : await hydrateNodeGenerationContext(baseContext, projectId, domainProjectId, retryMode, retryMode === "video" && supportsVideoReferenceAudio(generationConfig), !promptOnly);
|
||||
rawContext =
|
||||
hasSavedImageMetadata && !baseContext.characterReferences.length
|
||||
? null
|
||||
: await hydrateNodeGenerationContext(baseContext, projectId, domainProjectId, retryMode, retryMode === "video" && supportsVideoReferenceAudio(generationConfig), !promptOnly);
|
||||
} catch (error) {
|
||||
const failure = generationFailureMetadata(error, retryPromptSource);
|
||||
message.error(failure.errorDetails);
|
||||
@@ -168,7 +171,14 @@ export function useCanvasGenerationRetry({
|
||||
message.error("参考图片已丢失,无法继续重试");
|
||||
return;
|
||||
}
|
||||
const retryImages = retryMode === "image" ? limitCanvasImageReferences(generationConfig, retryReferenceImages || []) : retryReferenceImages || [];
|
||||
const retryImages = retryReferenceImages || [];
|
||||
if (retryMode === "image") {
|
||||
const referenceLimitError = canvasImageReferenceLimitError(generationConfig, retryImages);
|
||||
if (referenceLimitError) {
|
||||
message.error(referenceLimitError);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const storedVideoImages = node.type === CanvasNodeType.Video && !context?.referenceImages.length ? await resolveStoredReferenceImages(node.metadata?.references) : [];
|
||||
if (storedVideoImages === null) {
|
||||
markMissingReferences(node.id, setNodes);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { applyBatchPrimaryImage, applyNodeConfigPatch } from "@/lib/canvas/canva
|
||||
import { audioExtension, imageExtension, resetGenerationTaskMetadata } from "@/lib/canvas/canvas-project-generation";
|
||||
import { CONTENT_MODERATION_ERROR_CODE, isContentModerationError } from "@/lib/generation-error";
|
||||
import { ensureCanvasNodeAsset } from "@/services/project-asset-sync";
|
||||
import { CanvasNodeType, type CanvasFolderStyle, type CanvasNodeData, type CanvasNodeMetadata, type Position } from "@/types/canvas";
|
||||
import { CanvasNodeType, type CanvasFolderStyle, type CanvasFolderTheme, type CanvasNodeData, type CanvasNodeMetadata, type Position } from "@/types/canvas";
|
||||
|
||||
type UseCanvasNodeEditorOptions = {
|
||||
canvasId: string;
|
||||
@@ -89,6 +89,14 @@ export function useCanvasNodeEditor({
|
||||
}));
|
||||
}, [setNodes]);
|
||||
|
||||
const handleFolderThemeChange = useCallback((nodeId: string, theme: CanvasFolderTheme) => {
|
||||
setNodes((current) => current.map((node) => {
|
||||
if (node.id !== nodeId || !isCanvasFolderNode(node)) return node;
|
||||
const folder = node.metadata!.folder!;
|
||||
return { ...node, metadata: { ...node.metadata, folder: { ...folder, theme, themeCover: undefined, createdAt: folder.createdAt || new Date().toISOString() } } };
|
||||
}));
|
||||
}, [setNodes]);
|
||||
|
||||
const toggleNodeFreeResize = useCallback((nodeId: string) => {
|
||||
setNodes((current) =>
|
||||
current.map((node) => {
|
||||
@@ -190,6 +198,7 @@ export function useCanvasNodeEditor({
|
||||
downloadNodeImage,
|
||||
handleConfigNodeChange,
|
||||
handleFolderStyleChange,
|
||||
handleFolderThemeChange,
|
||||
handleFontSizeChange,
|
||||
handleNodeContentChange,
|
||||
handleNodePromptChange,
|
||||
|
||||
@@ -8,7 +8,7 @@ import { FOLDER_COLLAPSED_HEIGHT, FOLDER_COLLAPSED_WIDTH, FRAME_HEADER_HEIGHT, g
|
||||
import { alignCanvasNodes, layoutCanvasFlow, layoutCanvasNodes, nextCanvasVersionLabel, type CanvasAlignmentMode } from "@/lib/canvas/canvas-layout";
|
||||
import { createCanvasNode, removeCanvasNodes } from "@/lib/canvas/canvas-project-domain";
|
||||
import { isolateCopiedNodeMetadata } from "@/lib/canvas/canvas-node-copy";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasFolderStyle, type CanvasNodeData, type ContextMenuState, type Position } from "@/types/canvas";
|
||||
import { CanvasNodeType, type CanvasConnection, type CanvasFolderStyle, type CanvasFolderTheme, type CanvasNodeData, type ContextMenuState, type Position } from "@/types/canvas";
|
||||
import { cloneCanvasDrawing } from "@/lib/canvas/canvas-drawing-storage";
|
||||
import { isDrawingEngineAvailable, type CanvasDrawingEngine } from "@/lib/canvas/canvas-drawing-engine";
|
||||
import { useUserStore } from "@/stores/use-user-store";
|
||||
@@ -133,7 +133,7 @@ export function useCanvasNodeOperations({
|
||||
if (type !== CanvasNodeType.Text && type !== CanvasNodeType.Script && type !== CanvasNodeType.Audio && type !== CanvasNodeType.Frame && type !== CanvasNodeType.Drawing) setDialogNodeId(node.id);
|
||||
}, [commitNodes, defaultDrawingEngine, getCanvasCenter, message, nodesRef, selectNodes, setDialogNodeId, tldrawLicenseKey]);
|
||||
|
||||
const createFolder = useCallback((position?: Position, linked?: { id: string; projectId: string; title: string; style: CanvasFolderStyle; createdAt: string }) => {
|
||||
const createFolder = useCallback((position?: Position, linked?: { id: string; projectId: string; title: string; style: CanvasFolderStyle; theme: CanvasFolderTheme; createdAt: string }) => {
|
||||
const folder = createCanvasNode(CanvasNodeType.Frame, position || getCanvasCenter(), {
|
||||
frame: {
|
||||
collapsed: true,
|
||||
@@ -142,6 +142,7 @@ export function useCanvasNodeOperations({
|
||||
},
|
||||
folder: {
|
||||
style: linked?.style || "glass",
|
||||
theme: linked?.theme || "aurora",
|
||||
createdAt: linked?.createdAt || new Date().toISOString(),
|
||||
assetFolderId: linked?.id,
|
||||
projectId: linked?.projectId,
|
||||
|
||||
Reference in New Issue
Block a user