Your Name c7ffcda87b
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add infinite canvas feature with AI image generation
- Add canvas project and node database tables
- Create canvas workspace with drag, resize, zoom and pan
- Add ImageNode with background removal, enhance, extend, angle edit, scene fusion
- Add VideoNode with playback controls
- Add TextNode for text editing
- Integrate AI image generation API (Gemini/Alibaba)
- Implement viewport culling for performance optimization
- Add canvas entry in top navigation bar
2026-01-24 19:48:13 +08:00

398 lines
12 KiB
TypeScript

import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import { devtools, persist } from 'zustand/middleware';
import type {
CanvasNode,
CanvasEdge,
CanvasState,
Viewport,
ToolType,
Position,
Size,
NodeType,
} from '@/types/canvas';
// 生成唯一 ID
const generateId = () => crypto.randomUUID();
// 默认视口
const defaultViewport: Viewport = {
x: 0,
y: 0,
zoom: 1,
};
// Store 状态接口
interface CanvasStore {
// 项目信息
projectId: string | null;
projectTitle: string;
// 画布状态
nodes: CanvasNode[];
edges: CanvasEdge[];
selectedNodeIds: string[];
viewport: Viewport;
tool: ToolType;
// 历史记录(用于撤销/重做)
history: CanvasState[];
historyIndex: number;
// UI 状态
isPanelOpen: boolean;
isGenerating: boolean;
// 项目操作
setProject: (id: string | null, title?: string) => void;
setProjectTitle: (title: string) => void;
// 节点操作
addNode: (node: Omit<CanvasNode, 'id' | 'createdAt' | 'updatedAt'>) => string;
updateNode: (id: string, updates: Partial<CanvasNode>) => void;
deleteNode: (id: string) => void;
deleteSelectedNodes: () => void;
// 选择操作
selectNode: (id: string, multi?: boolean) => void;
selectNodes: (ids: string[]) => void;
clearSelection: () => void;
selectAll: () => void;
// 边操作
addEdge: (edge: Omit<CanvasEdge, 'id'>) => string;
deleteEdge: (id: string) => void;
// 视口操作
setViewport: (viewport: Partial<Viewport>) => void;
zoomIn: () => void;
zoomOut: () => void;
zoomTo: (zoom: number) => void;
fitToScreen: () => void;
centerNode: (nodeId: string) => void;
// 工具操作
setTool: (tool: ToolType) => void;
// 历史操作
undo: () => void;
redo: () => void;
pushHistory: () => void;
// UI 操作
togglePanel: () => void;
setGenerating: (isGenerating: boolean) => void;
// 批量操作
loadCanvas: (state: Partial<CanvasState>) => void;
clearCanvas: () => void;
getCanvasState: () => CanvasState;
// 节点位置操作
moveNode: (id: string, position: Position) => void;
resizeNode: (id: string, size: Size) => void;
// 辅助方法
getNode: (id: string) => CanvasNode | undefined;
getSelectedNodes: () => CanvasNode[];
getNodesByType: (type: NodeType) => CanvasNode[];
}
export const useCanvasStore = create<CanvasStore>()(
devtools(
persist(
immer((set, get) => ({
// 初始状态
projectId: null,
projectTitle: '未命名画布',
nodes: [],
edges: [],
selectedNodeIds: [],
viewport: defaultViewport,
tool: 'select',
history: [],
historyIndex: -1,
isPanelOpen: true,
isGenerating: false,
// 项目操作
setProject: (id, title) => set((state) => {
state.projectId = id;
if (title) state.projectTitle = title;
}),
setProjectTitle: (title) => set((state) => {
state.projectTitle = title;
}),
// 节点操作
addNode: (nodeData) => {
const id = generateId();
const now = new Date();
const node: CanvasNode = {
...nodeData,
id,
createdAt: now,
updatedAt: now,
};
set((state) => {
state.nodes.push(node);
});
// 添加到历史
get().pushHistory();
return id;
},
updateNode: (id, updates) => set((state) => {
const index = state.nodes.findIndex(n => n.id === id);
if (index !== -1) {
state.nodes[index] = {
...state.nodes[index],
...updates,
updatedAt: new Date(),
};
}
}),
deleteNode: (id) => {
set((state) => {
state.nodes = state.nodes.filter(n => n.id !== id);
state.edges = state.edges.filter(e => e.source !== id && e.target !== id);
state.selectedNodeIds = state.selectedNodeIds.filter(nid => nid !== id);
});
get().pushHistory();
},
deleteSelectedNodes: () => {
const { selectedNodeIds } = get();
set((state) => {
state.nodes = state.nodes.filter(n => !selectedNodeIds.includes(n.id));
state.edges = state.edges.filter(
e => !selectedNodeIds.includes(e.source) && !selectedNodeIds.includes(e.target)
);
state.selectedNodeIds = [];
});
get().pushHistory();
},
// 选择操作
selectNode: (id, multi = false) => set((state) => {
if (multi) {
const index = state.selectedNodeIds.indexOf(id);
if (index === -1) {
state.selectedNodeIds.push(id);
} else {
state.selectedNodeIds.splice(index, 1);
}
} else {
state.selectedNodeIds = [id];
}
}),
selectNodes: (ids) => set((state) => {
state.selectedNodeIds = ids;
}),
clearSelection: () => set((state) => {
state.selectedNodeIds = [];
}),
selectAll: () => set((state) => {
state.selectedNodeIds = state.nodes.map(n => n.id);
}),
// 边操作
addEdge: (edgeData) => {
const id = generateId();
const edge: CanvasEdge = { ...edgeData, id };
set((state) => {
// 检查是否已存在相同的边
const exists = state.edges.some(
e => e.source === edge.source && e.target === edge.target
);
if (!exists) {
state.edges.push(edge);
}
});
return id;
},
deleteEdge: (id) => set((state) => {
state.edges = state.edges.filter(e => e.id !== id);
}),
// 视口操作
setViewport: (viewport) => set((state) => {
state.viewport = { ...state.viewport, ...viewport };
}),
zoomIn: () => set((state) => {
state.viewport.zoom = Math.min(state.viewport.zoom * 1.2, 4);
}),
zoomOut: () => set((state) => {
state.viewport.zoom = Math.max(state.viewport.zoom / 1.2, 0.1);
}),
zoomTo: (zoom) => set((state) => {
state.viewport.zoom = Math.max(0.1, Math.min(zoom, 4));
}),
fitToScreen: () => set((state) => {
// TODO: 计算所有节点的边界并调整视口
state.viewport = { x: 0, y: 0, zoom: 1 };
}),
centerNode: (nodeId) => {
const node = get().getNode(nodeId);
if (node) {
set((state) => {
state.viewport.x = -node.position.x - node.size.width / 2;
state.viewport.y = -node.position.y - node.size.height / 2;
});
}
},
// 工具操作
setTool: (tool) => set((state) => {
state.tool = tool;
}),
// 历史操作
undo: () => set((state) => {
if (state.historyIndex > 0) {
state.historyIndex -= 1;
const prevState = state.history[state.historyIndex];
state.nodes = prevState.nodes;
state.edges = prevState.edges;
state.selectedNodeIds = prevState.selectedNodeIds;
}
}),
redo: () => set((state) => {
if (state.historyIndex < state.history.length - 1) {
state.historyIndex += 1;
const nextState = state.history[state.historyIndex];
state.nodes = nextState.nodes;
state.edges = nextState.edges;
state.selectedNodeIds = nextState.selectedNodeIds;
}
}),
pushHistory: () => set((state) => {
const currentState: CanvasState = {
nodes: JSON.parse(JSON.stringify(state.nodes)),
edges: JSON.parse(JSON.stringify(state.edges)),
selectedNodeIds: [...state.selectedNodeIds],
viewport: { ...state.viewport },
tool: state.tool,
};
// 移除当前位置之后的历史
state.history = state.history.slice(0, state.historyIndex + 1);
state.history.push(currentState);
state.historyIndex = state.history.length - 1;
// 限制历史记录数量
if (state.history.length > 50) {
state.history = state.history.slice(-50);
state.historyIndex = state.history.length - 1;
}
}),
// UI 操作
togglePanel: () => set((state) => {
state.isPanelOpen = !state.isPanelOpen;
}),
setGenerating: (isGenerating) => set((state) => {
state.isGenerating = isGenerating;
}),
// 批量操作
loadCanvas: (canvasState) => set((state) => {
if (canvasState.nodes) state.nodes = canvasState.nodes;
if (canvasState.edges) state.edges = canvasState.edges;
if (canvasState.selectedNodeIds) state.selectedNodeIds = canvasState.selectedNodeIds;
if (canvasState.viewport) state.viewport = canvasState.viewport;
if (canvasState.tool) state.tool = canvasState.tool;
}),
clearCanvas: () => set((state) => {
state.nodes = [];
state.edges = [];
state.selectedNodeIds = [];
state.viewport = defaultViewport;
state.history = [];
state.historyIndex = -1;
}),
getCanvasState: () => {
const state = get();
return {
nodes: state.nodes,
edges: state.edges,
selectedNodeIds: state.selectedNodeIds,
viewport: state.viewport,
tool: state.tool,
};
},
// 节点位置操作
moveNode: (id, position) => set((state) => {
const node = state.nodes.find(n => n.id === id);
if (node) {
node.position = position;
node.updatedAt = new Date();
}
}),
resizeNode: (id, size) => set((state) => {
const node = state.nodes.find(n => n.id === id);
if (node) {
node.size = size;
node.updatedAt = new Date();
}
}),
// 辅助方法
getNode: (id) => get().nodes.find(n => n.id === id),
getSelectedNodes: () => {
const { nodes, selectedNodeIds } = get();
return nodes.filter(n => selectedNodeIds.includes(n.id));
},
getNodesByType: (type) => get().nodes.filter(n => n.type === type),
})),
{
name: 'canvas-storage',
partialize: (state) => ({
// 只持久化部分状态
projectId: state.projectId,
projectTitle: state.projectTitle,
}),
}
),
{ name: 'canvas-store' }
)
);
// 导出选择器
export const canvasSelectors = {
nodes: (state: CanvasStore) => state.nodes,
edges: (state: CanvasStore) => state.edges,
selectedNodeIds: (state: CanvasStore) => state.selectedNodeIds,
viewport: (state: CanvasStore) => state.viewport,
tool: (state: CanvasStore) => state.tool,
isGenerating: (state: CanvasStore) => state.isGenerating,
hasSelection: (state: CanvasStore) => state.selectedNodeIds.length > 0,
canUndo: (state: CanvasStore) => state.historyIndex > 0,
canRedo: (state: CanvasStore) => state.historyIndex < state.history.length - 1,
};