feat(canvas): 多角度贴节点、宫格切分菜单与多选虚线框跟随

把多角度改成贴在图片节点下方的 3D 编辑器并跟随拖动;宫格切分并入图片工具,自定义面板不关菜单;切分后子图按网格排开;多选虚线框随拖动预览平移。
This commit is contained in:
soonstudio-ai committed 2026-09-14 17:19:56 +08:00
1 parent 9727656b2b
commit 8e82e67cb3
26 files changed
+1431 -153

No files matched your search

@@ -5,6 +5,7 @@ import { activeConnectionPath, canvasConnectionPath } from "@/components/canvas/
import type { CanvasBatchConnectionPreview } from "@/lib/canvas/canvas-batch-connection";
import { subscribeCanvasGraphicsViewportPreview, subscribeCanvasNodeDragPreview, subscribeCanvasSelectionPreview, type CanvasNodeDragPreview } from "@/lib/canvas/canvas-live-viewport";
import { calculateCanvasPreviewTransform, sameCanvasViewport, shouldRebaseCanvasRaster } from "@/lib/canvas/canvas-leafer-viewport";
import { offsetSelectedNodeBounds } from "@/lib/canvas/canvas-selection";
import type { CanvasTheme } from "@/lib/canvas-theme";
import type { CanvasDisplayConnection, CanvasNodeData, ConnectionHandle, Position, SelectionBox, ViewportTransform } from "@/types/canvas";
@@ -57,6 +58,7 @@ type OverlayScene = LeaferScene & {
guides: Path;
draft: Path;
batchDrafts: Group;
dragPreview: CanvasNodeDragPreview | null;
};
export function CanvasLeaferGraphicsLayer(props: CanvasLeaferGraphicsLayerProps) {
@@ -118,6 +120,8 @@ export function CanvasLeaferGraphicsLayer(props: CanvasLeaferGraphicsLayerProps)
});
const unsubscribeNodeDrag = subscribeCanvasNodeDragPreview(container, (preview) => {
applyConnectionDragPreview(underlay, propsRef.current, preview);
overlay.dragPreview = preview;
syncLiveSelectionBounds(overlay, propsRef.current, viewportRef.current.k);
});
resize();
@@ -206,7 +210,7 @@ function createOverlayScene(host: HTMLDivElement): OverlayScene {
world.add(draft);
world.add(batchDrafts);
leafer.add(world);
return { leafer, world, host, selection, selectionBounds, guides, draft, batchDrafts };
return { leafer, world, host, selection, selectionBounds, guides, draft, batchDrafts, dragPreview: null };
}
function rebuildConnections(scene: UnderlayScene, props: CanvasLeaferGraphicsLayerProps) {
@@ -323,7 +327,7 @@ function syncOverlayContent(scene: OverlayScene, props: CanvasLeaferGraphicsLaye
const bounds = props.selectedNodeBounds;
scene.selectionBounds.visible = Boolean(bounds && !selection);
if (bounds && !selection) {
syncSelectionBounds(scene.selectionBounds, bounds, viewportScale);
syncSelectionBounds(scene.selectionBounds, liveSelectedNodeBounds(bounds, scene.dragPreview), viewportScale);
scene.selectionBounds.stroke = props.theme.node.label;
}
@@ -388,7 +392,7 @@ function syncViewport(viewport: ViewportTransform, width: number, height: number
cornerRadius: 2 / scale,
dashPattern: [4 / scale, 4 / scale],
});
if (props.selectedNodeBounds) syncSelectionBounds(overlay.selectionBounds, props.selectedNodeBounds, scale);
if (props.selectedNodeBounds) syncSelectionBounds(overlay.selectionBounds, liveSelectedNodeBounds(props.selectedNodeBounds, overlay.dragPreview), scale);
overlay.selectionBounds.set({
strokeWidth: 1 / scale,
cornerRadius: 2 / scale,
@@ -438,6 +442,19 @@ function forceSceneRender(...scenes: LeaferScene[]) {
for (const scene of scenes) scene.leafer.forceRender(undefined, true);
}
function liveSelectedNodeBounds(bounds: NonNullable<NodeBounds>, preview: CanvasNodeDragPreview | null) {
return offsetSelectedNodeBounds(bounds, preview);
}
function syncLiveSelectionBounds(scene: OverlayScene, props: CanvasLeaferGraphicsLayerProps, viewportScale: number) {
const bounds = props.selectedNodeBounds;
const selection = props.selectionBox;
scene.selectionBounds.visible = Boolean(bounds && !selection);
if (!bounds || selection) return;
syncSelectionBounds(scene.selectionBounds, liveSelectedNodeBounds(bounds, scene.dragPreview), viewportScale);
scene.selectionBounds.stroke = props.theme.node.label;
}
function syncSelectionBounds(rect: Rect, bounds: NonNullable<NodeBounds>, viewportScale: number) {
const padding = 12 / Math.max(viewportScale, 0.05);
rect.set({