feat(assets): 素材库与画布 - 新增可交互通用文件夹
This commit is contained in:
@@ -4,15 +4,29 @@ export const FRAME_HEADER_HEIGHT = 36;
|
||||
export const FRAME_PADDING = 24;
|
||||
export const FRAME_COLLAPSED_WIDTH = 240;
|
||||
export const FRAME_COLLAPSED_HEIGHT = 144;
|
||||
export const FOLDER_COLLAPSED_WIDTH = 360;
|
||||
export const FOLDER_COLLAPSED_HEIGHT = 280;
|
||||
|
||||
export function isFrameNode(node?: CanvasNodeData | null): node is CanvasNodeData & { type: CanvasNodeType.Frame } {
|
||||
return node?.type === CanvasNodeType.Frame;
|
||||
}
|
||||
|
||||
export function isCanvasFolderNode(node?: CanvasNodeData | null) {
|
||||
return isFrameNode(node) && Boolean(node.metadata?.folder);
|
||||
}
|
||||
|
||||
export function canFrameContain(node: CanvasNodeData) {
|
||||
return node.type === CanvasNodeType.Image || node.type === CanvasNodeType.Text || node.type === CanvasNodeType.Drawing || node.type === CanvasNodeType.Script || node.type === CanvasNodeType.Video;
|
||||
}
|
||||
|
||||
export function canFolderContain(node: CanvasNodeData) {
|
||||
return node.type !== CanvasNodeType.Frame;
|
||||
}
|
||||
|
||||
export function canLinkedFolderArchive(node: CanvasNodeData) {
|
||||
return (node.type === CanvasNodeType.Image || node.type === CanvasNodeType.Video || node.type === CanvasNodeType.Audio || node.type === CanvasNodeType.Text) && Boolean(node.metadata?.assetId || node.metadata?.content?.trim());
|
||||
}
|
||||
|
||||
export function getFrameChildren(frameId: string, nodes: CanvasNodeData[]) {
|
||||
return nodes.filter((node) => node.parentId === frameId);
|
||||
}
|
||||
@@ -32,16 +46,18 @@ export function isNodeHiddenByCollapsedFrame(node: CanvasNodeData, nodes: Canvas
|
||||
}
|
||||
|
||||
export function findFrameDropTarget(nodes: CanvasNodeData[], draggedNodeIds: Set<string>) {
|
||||
const dragged = nodes.filter((node) => draggedNodeIds.has(node.id) && canFrameContain(node));
|
||||
const dragged = nodes.filter((node) => draggedNodeIds.has(node.id));
|
||||
if (!dragged.length) return null;
|
||||
|
||||
return (
|
||||
[...nodes]
|
||||
.reverse()
|
||||
.find((frame) => {
|
||||
if (!isFrameNode(frame) || frame.metadata?.frame?.collapsed || draggedNodeIds.has(frame.id)) return false;
|
||||
if (!isFrameNode(frame) || (frame.metadata?.frame?.collapsed && !isCanvasFolderNode(frame)) || draggedNodeIds.has(frame.id)) return false;
|
||||
const canContain = frame.metadata?.folder?.assetFolderId ? canLinkedFolderArchive : isCanvasFolderNode(frame) ? canFolderContain : canFrameContain;
|
||||
if (!dragged.every(canContain)) return false;
|
||||
const left = frame.position.x;
|
||||
const top = frame.position.y + FRAME_HEADER_HEIGHT;
|
||||
const top = frame.position.y + (isCanvasFolderNode(frame) && frame.metadata?.frame?.collapsed ? 0 : FRAME_HEADER_HEIGHT);
|
||||
const right = frame.position.x + frame.width;
|
||||
const bottom = frame.position.y + frame.height;
|
||||
return dragged.every((node) => {
|
||||
@@ -54,7 +70,13 @@ export function findFrameDropTarget(nodes: CanvasNodeData[], draggedNodeIds: Set
|
||||
}
|
||||
|
||||
export function applyFrameDrop(nodes: CanvasNodeData[], draggedNodeIds: Set<string>, frameId: string | null) {
|
||||
const next = nodes.map((node) => (draggedNodeIds.has(node.id) && canFrameContain(node) ? { ...node, parentId: frameId || undefined } : node));
|
||||
const target = frameId ? nodes.find((node) => node.id === frameId && isFrameNode(node)) : null;
|
||||
const canContain = target?.metadata?.folder?.assetFolderId ? canLinkedFolderArchive : target && isCanvasFolderNode(target) ? canFolderContain : canFrameContain;
|
||||
const next = nodes.map((node) => {
|
||||
if (!draggedNodeIds.has(node.id)) return node;
|
||||
if (!frameId) return node.parentId ? { ...node, parentId: undefined } : node;
|
||||
return target && canContain(node) ? { ...node, parentId: frameId } : node;
|
||||
});
|
||||
if (!frameId) return next;
|
||||
|
||||
const children = getFrameChildren(frameId, next);
|
||||
@@ -62,6 +84,10 @@ export function applyFrameDrop(nodes: CanvasNodeData[], draggedNodeIds: Set<stri
|
||||
const frame = next.find((node) => node.id === frameId);
|
||||
if (!frame || !isFrameNode(frame)) return next;
|
||||
|
||||
if (isCanvasFolderNode(frame) && frame.metadata?.frame?.collapsed) {
|
||||
return layoutCollapsedFolderChildren(next, frame);
|
||||
}
|
||||
|
||||
const left = Math.min(frame.position.x, ...children.map((node) => node.position.x - FRAME_PADDING));
|
||||
const top = Math.min(frame.position.y, ...children.map((node) => node.position.y - FRAME_HEADER_HEIGHT - FRAME_PADDING));
|
||||
const right = Math.max(frame.position.x + frame.width, ...children.map((node) => node.position.x + node.width + FRAME_PADDING));
|
||||
@@ -87,6 +113,43 @@ export function applyFrameDrop(nodes: CanvasNodeData[], draggedNodeIds: Set<stri
|
||||
);
|
||||
}
|
||||
|
||||
function layoutCollapsedFolderChildren(nodes: CanvasNodeData[], folder: CanvasNodeData) {
|
||||
const children = getFrameChildren(folder.id, nodes);
|
||||
if (!children.length) return nodes;
|
||||
const gap = 24;
|
||||
const padding = FRAME_PADDING;
|
||||
const columns = Math.min(3, Math.ceil(Math.sqrt(children.length)));
|
||||
const rows = Math.ceil(children.length / columns);
|
||||
const cellWidth = Math.max(...children.map((node) => node.width));
|
||||
const cellHeight = Math.max(...children.map((node) => node.height));
|
||||
const expandedWidth = padding * 2 + columns * cellWidth + Math.max(columns - 1, 0) * gap;
|
||||
const expandedHeight = FRAME_HEADER_HEIGHT + padding * 2 + rows * cellHeight + Math.max(rows - 1, 0) * gap;
|
||||
const indexById = new Map(children.map((node, index) => [node.id, index]));
|
||||
|
||||
return nodes.map((node) => {
|
||||
if (node.id === folder.id) {
|
||||
return {
|
||||
...node,
|
||||
metadata: {
|
||||
...node.metadata,
|
||||
frame: { collapsed: true, expandedWidth, expandedHeight },
|
||||
},
|
||||
};
|
||||
}
|
||||
const index = indexById.get(node.id);
|
||||
if (index === undefined) return node;
|
||||
const column = index % columns;
|
||||
const row = Math.floor(index / columns);
|
||||
return {
|
||||
...node,
|
||||
position: {
|
||||
x: folder.position.x + padding + column * (cellWidth + gap) + (cellWidth - node.width) / 2,
|
||||
y: folder.position.y + FRAME_HEADER_HEIGHT + padding + row * (cellHeight + gap) + (cellHeight - node.height) / 2,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveFrameConnection(connection: CanvasConnection, nodes: CanvasNodeData[]) {
|
||||
const from = nodes.find((node) => node.id === connection.fromNodeId);
|
||||
const to = nodes.find((node) => node.id === connection.toNodeId);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Clapperboard, FolderOpen, Image as ImageIcon, Layers3, Music2, Palette, PanelTop, Pencil, Type, UploadCloud, UserRound, Video } from "lucide-react";
|
||||
import { Clapperboard, Folder, FolderOpen, Image as ImageIcon, Layers3, Music2, Palette, PanelTop, Pencil, Type, UploadCloud, UserRound, Video } from "lucide-react";
|
||||
|
||||
import { registerAddNodeMenuCommands, type AddNodeMenuCommand } from "@/lib/canvas/tool-registry";
|
||||
|
||||
@@ -10,6 +10,7 @@ export const addNodeMenuCommands: AddNodeMenuCommand[] = [
|
||||
{ id: "drawing", label: "绘图", icon: <Pencil />, section: "node", defaultOrder: 20, run: (ctx) => ctx.handlers.onAddDrawing() },
|
||||
{ id: "script", label: "分镜脚本", icon: <Clapperboard />, badge: "核心", section: "node", defaultOrder: 30, run: (ctx) => ctx.handlers.onAddScript() },
|
||||
{ id: "frame", label: "背板", icon: <PanelTop />, section: "node", defaultOrder: 40, applicable: (ctx) => ctx.workspaceMode !== "simple", run: (ctx) => ctx.handlers.onAddFrame() },
|
||||
{ id: "folder", label: "文件夹", icon: <Folder />, badge: "6 款", section: "node", defaultOrder: 45, run: (ctx) => ctx.handlers.onAddFolder() },
|
||||
{ id: "image", label: "图片", icon: <ImageIcon />, section: "node", defaultOrder: 50, run: (ctx) => ctx.handlers.onAddImage() },
|
||||
{ id: "video", label: "视频", icon: <Video />, section: "node", defaultOrder: 60, run: (ctx) => ctx.handlers.onAddVideo() },
|
||||
{ id: "director", label: "导演台", icon: <Layers3 />, badge: "3D", section: "node", defaultOrder: 70, applicable: (ctx) => ctx.workspaceMode !== "simple", run: (ctx) => ctx.handlers.onOpenDirector() },
|
||||
|
||||
@@ -36,6 +36,7 @@ export type ToolbarHandlers = {
|
||||
onAddAudio: () => void;
|
||||
onAddScript: () => void;
|
||||
onAddFrame: () => void;
|
||||
onAddFolder: () => void;
|
||||
onAddDrawing: () => void;
|
||||
onChooseStyle: () => void;
|
||||
onOpenDirector: () => void;
|
||||
@@ -131,6 +132,7 @@ export type AddNodeMenuContext = {
|
||||
| "onAddAudio"
|
||||
| "onAddScript"
|
||||
| "onAddFrame"
|
||||
| "onAddFolder"
|
||||
| "onAddDrawing"
|
||||
| "onChooseStyle"
|
||||
| "onOpenDirector"
|
||||
|
||||
Reference in New Issue
Block a user