mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-04 08:11:14 +08:00
feat(canvas): 左侧面板可调宽收起 + 元素多选导出与跳转动画
- 左侧画布面板支持拖拽调整宽度、展开/收起(带动画),状态与宽度持久化 - 顶栏菜单左侧新增面板开关按钮,并缩小菜单/开关按钮 - 顶栏菜单新增「导出当前画布」为压缩包 - 画布元素列表支持多选并批量导出:图片/视频/音频为文件、文本为 txt、其余各自为 json - 画布/资产切换改为滑动下划线动画,点击元素跳转带缓动动画 - 移除文本/配置/视频等非图片元素图标的灰色底色,并写入 AGENTS.md 规范 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,7 @@ import { useCanvasStore } from "@/stores/canvas/use-canvas-store";
|
||||
import { useAgentBridge } from "@/pages/canvas/hooks/use-agent-bridge";
|
||||
import { usePluginHost } from "@/pages/canvas/hooks/use-plugin-host";
|
||||
import { buildNodeMentionReferences, type CanvasResourceReference } from "@/lib/canvas/canvas-resource-references";
|
||||
import { exportCanvasProjects } from "@/lib/canvas/canvas-export";
|
||||
import { applyNodeConfigPatch, audioMetadata, buildAudioGenerationMetadata, buildImageGenerationMetadata, createCanvasNode, imageMetadata, videoMetadata } from "@/lib/canvas/canvas-node-factory";
|
||||
import { findContainingGroupId, findGroupDropTarget, getConnectionTargetAnchor, isHiddenBatchChild, isHiddenBatchConnectionEndpoint, normalizeConnection, snapNodesIntoGroup } from "@/lib/canvas/canvas-node-geometry";
|
||||
import {
|
||||
@@ -246,6 +247,7 @@ function InfiniteCanvasPage() {
|
||||
const connectionsRef = useRef(connections);
|
||||
const selectedNodeIdsRef = useRef(selectedNodeIds);
|
||||
const viewportRef = useRef(viewport);
|
||||
const focusAnimRef = useRef<number | null>(null);
|
||||
const generateNodeRef = useRef<((nodeId: string, mode: CanvasNodeGenerationMode, prompt: string) => Promise<void>) | null>(null);
|
||||
const connectingParamsRef = useRef(connectingParams);
|
||||
const connectionTargetNodeIdRef = useRef(connectionTargetNodeId);
|
||||
@@ -901,14 +903,30 @@ function InfiniteCanvasPage() {
|
||||
const worldX = node.position.x + node.width / 2;
|
||||
const worldY = node.position.y + node.height / 2;
|
||||
const k = Math.min(Math.max(Math.min((size.width * 0.6) / node.width, (size.height * 0.6) / node.height), 0.05), 1.5);
|
||||
setViewport({ x: size.width / 2 - worldX * k, y: size.height / 2 - worldY * k, k });
|
||||
const target = { x: size.width / 2 - worldX * k, y: size.height / 2 - worldY * k, k };
|
||||
setSelectedNodeIds(new Set([nodeId]));
|
||||
setSelectedConnectionId(null);
|
||||
setContextMenu(null);
|
||||
|
||||
if (focusAnimRef.current) cancelAnimationFrame(focusAnimRef.current);
|
||||
const start = { ...viewportRef.current };
|
||||
const duration = 450;
|
||||
const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3);
|
||||
let startTime: number | null = null;
|
||||
const step = (now: number) => {
|
||||
if (startTime === null) startTime = now;
|
||||
const progress = Math.min((now - startTime) / duration, 1);
|
||||
const t = easeOutCubic(progress);
|
||||
setViewport({ x: start.x + (target.x - start.x) * t, y: start.y + (target.y - start.y) * t, k: start.k + (target.k - start.k) * t });
|
||||
focusAnimRef.current = progress < 1 ? requestAnimationFrame(step) : null;
|
||||
};
|
||||
focusAnimRef.current = requestAnimationFrame(step);
|
||||
},
|
||||
[size.height, size.width],
|
||||
);
|
||||
|
||||
useEffect(() => () => void (focusAnimRef.current && cancelAnimationFrame(focusAnimRef.current)), []);
|
||||
|
||||
const setZoomScale = useCallback(
|
||||
(scale: number) => {
|
||||
const nextScale = Math.min(Math.max(scale, 0.05), 5);
|
||||
@@ -971,6 +989,21 @@ function InfiniteCanvasPage() {
|
||||
navigate("/canvas");
|
||||
}, [cleanupAssetImages, deleteProjects, navigate, projectId]);
|
||||
|
||||
const exportCurrentProject = useCallback(async () => {
|
||||
const project = useCanvasStore.getState().projects.find((item) => item.id === projectId);
|
||||
if (!project) return message.error("未找到当前画布");
|
||||
const hide = message.loading("正在导出当前画布…", 0);
|
||||
try {
|
||||
await exportCanvasProjects([project], project.title || "无限画布");
|
||||
message.success("已导出当前画布");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error("导出失败,请重试");
|
||||
} finally {
|
||||
hide();
|
||||
}
|
||||
}, [message, projectId]);
|
||||
|
||||
const handleCanvasMouseDown = useCallback(
|
||||
(event: ReactPointerEvent<HTMLDivElement>) => {
|
||||
setContextMenu(null);
|
||||
@@ -2689,6 +2722,7 @@ function InfiniteCanvasPage() {
|
||||
onProjects={() => navigate("/canvas")}
|
||||
onCreateProject={createAndOpenProject}
|
||||
onDeleteProject={deleteCurrentProject}
|
||||
onExportProject={exportCurrentProject}
|
||||
onImportImage={() => handleUploadRequest()}
|
||||
onOpenPlugins={() => setPluginManagerOpen(true)}
|
||||
onUndo={undoCanvas}
|
||||
|
||||
Reference in New Issue
Block a user