Reuse canvas scenes across viewport and interaction metadata changes

This commit is contained in:
2026-11-09 21:46:58 +08:00
parent bb5bc9d8e9
commit 732c15f7bf
5 changed files with 69 additions and 5 deletions
@@ -9,7 +9,7 @@ import { CanvasProvider, CanvasRendererCommandsContext, type CanvasRendererComma
import { defaultCanvasRenderers } from "./canvas-renderers";
import type { Data, ElementRecord, Geometry, Id, Json, Result } from "@/lib/canvas-core/types";
import { equal } from "@/lib/canvas-core/values";
import {createGeometryProjectionCache} from "@/lib/canvas-bindings/geometry-cache";
import {createSceneProjection} from "@/lib/canvas-bindings/scene-projection";
import {createConnectionPaintIndex} from "@/lib/canvas-bindings/connection-paint";
import {nativeRectVisible} from "@/lib/canvas-bindings/visibility";
import {createOverlayIndex} from "@/lib/canvas-bindings/overlays";
@@ -23,7 +23,7 @@ import {createLoadingRendererCatalog} from '@/lib/canvas-bindings/renderers';
import { createProjectionCache } from "@/lib/canvas-bindings/projection-cache";
import { createResourceView, type ResourceViewSource } from "@/lib/canvas-bindings/resource-view";
import { ancestors, commonCanvas } from "@/lib/canvas-core/document";
import { projectCanvas, toLocal, transformPath, type ProjectionNode } from "@/lib/canvas-bindings/projection";
import { toLocal, transformPath, type ProjectionNode } from "@/lib/canvas-bindings/projection";
import type { ProjectEngine } from "@/lib/canvas-runtime/workspace";
import { errorText } from "@/lib/api";
import { IconButton } from "@/components/ui/icon-button";
@@ -65,20 +65,21 @@ function CanvasView({ engine, board, viewId, renderers,fitLoading=true }: { engi
const pointerStart = useRef<{ x: number; y: number;pointerId?:number } | null>(null), drag = useRef<{ token: number;pointerId?:number; start: { x: number; y: number }; positions: Map<Id, { x: number; y: number }> } | null>(null);
const projectionCache = useRef<ReturnType<typeof createProjectionCache> | null>(null);
projectionCache.current ??= createProjectionCache();
const geometryCache=useRef<ReturnType<typeof createGeometryProjectionCache>|null>(null);geometryCache.current??=createGeometryProjectionCache();
const sceneCache=useMemo(()=>createSceneProjection(),[engine,viewId]);
const measurements = useRef(new Map<Id, { width: number; height: number; measured: { width: number; height: number } }>());
const imageInput = useRef<HTMLInputElement>(null), groups = useRef(new Map<Id,{incarnation:number;groupId:Id}>());
useEffect(()=>{for(const [id,group]of groups.current)if(root.document.elements.get(id)?.incarnation!==group.incarnation)groups.current.delete(id);},[root.document]);
const catalog=useMemo(()=>loading?createLoadingRendererCatalog(engine.editor.registry,renderers.catalog):renderers.catalog,[engine,renderers,!!loading]);
const scene=sceneCache.project(engine,root.document,view,sessions,engine.editor.registry,catalog,engine.editor.getQueries(root.document));
const projection = useMemo(() => {
const result = projectCanvas(root.document, view.canvasId, engine.editor.registry, view.selection, engine.sessions.effectiveGeometry, sessions.connectionGesture,catalog,{queries:engine.editor.getQueries(root.document),geometry:geometryCache.current!,owner:engine});
const result = scene;
// Preserve renderer measurements across metadata/preview updates. Dropping them
// makes React Flow discard handle bounds and unmount active edge controls.
const visibleIds = new Set(result.nodes.map(node => node.id));
for (const id of measurements.current.keys()) if (!visibleIds.has(id)) measurements.current.delete(id);
for (const node of result.nodes) { const cached = measurements.current.get(node.id); if (cached && cached.width === node.width && cached.height === node.height && projectionCache.current!.canRetainMeasurement(engine,root.document,view.canvasId,node)) node.measured = cached.measured; }
return projectionCache.current!.reuse(engine,root.document,view.canvasId,result);
}, [engine, root.document, sessions, view,renderers,catalog]);
}, [engine, root.document, scene,view.canvasId]);
const surface=useRef<HTMLDivElement>(null),overlays=useMemo(()=>createOverlayIndex(viewId),[engine,viewId]);
const overlayScope=useMemo(()=>({index:overlays,engine}),[overlays,engine]);
const overlaySnapshot=useSyncExternalStore(overlays.subscribe,overlays.getSnapshot,overlays.getSnapshot);