Retain explicit canvas owners without disabling viewport culling
This commit is contained in:
@@ -11,7 +11,7 @@ import type { Data, ElementRecord, Geometry, Id, Json, Result } from "@/lib/canv
|
||||
import { equal } from "@/lib/canvas-core/values";
|
||||
import {createSceneProjection} from "@/lib/canvas-bindings/scene-projection";
|
||||
import {createConnectionPaintIndex} from "@/lib/canvas-bindings/connection-paint";
|
||||
import {nativeRectVisible} from "@/lib/canvas-bindings/visibility";
|
||||
import {createRetainedProjection} from "./retained-projection";
|
||||
import {createOverlayIndex} from "@/lib/canvas-bindings/overlays";
|
||||
import {CanvasOverlays} from "./canvas-overlays";
|
||||
import {createProjectedPaintIndex} from "@/lib/canvas-bindings/projected-paint";
|
||||
@@ -73,6 +73,9 @@ function CanvasView({ engine, board, viewId, renderers,fitLoading=true }: { engi
|
||||
const preparingCopy = [...(root.runtime.imports?.values() ?? [])].some(job => job.status === 'preparing' && job.source?.targetCanvasId === view.canvasId);
|
||||
const [focusedEditor, setFocusedEditor] = useState<HTMLElement | null>(null), focusEpoch = useRef(0);
|
||||
useEffect(() => { focusEpoch.current++; setFocusedEditor(null); return () => { focusEpoch.current++; }; }, [engine,viewId]);
|
||||
// Removing an active renderer need not dispatch a native blur. Retire only
|
||||
// that detached input, without clearing a newer focus acquired in the meantime.
|
||||
useEffect(()=>{if(focusedEditor&&!focusedEditor.isConnected)setFocusedEditor(current=>current===focusedEditor?null:current);});
|
||||
const [copying, setCopying] = useState(false);
|
||||
const [resourcePicker,setResourcePicker]=useState<{canvasId:Id;position:{x:number;y:number}}|null>(null);
|
||||
const copyAvailability=useMemo(()=>engine.imports.canDuplicate([...view.selection].map(id=>root.document.elements.get(id)).filter((element):element is ElementRecord=>!!element).map(({id,incarnation})=>({id,incarnation})),{viewCanvasId:view.canvasId}),[engine,root,view.selection,view.canvasId]);
|
||||
@@ -106,13 +109,12 @@ function CanvasView({ engine, board, viewId, renderers,fitLoading=true }: { engi
|
||||
const paint=useMemo(()=>paintCache.sync(projection,engine.editor.registry),[paintCache,projection,engine]);
|
||||
const viewport=useViewport(),surfaceWidth=useStore(state=>state.width),surfaceHeight=useStore(state=>state.height);
|
||||
const sceneViewport={x:-viewport.x/viewport.zoom,y:-viewport.y/viewport.zoom,width:surfaceWidth/viewport.zoom,height:surfaceHeight/viewport.zoom};
|
||||
const nativeVisible=(id:Id)=>{const node=flow.getInternalNode(id);if(!node||!node.internals.handleBounds||(node as typeof node&{dragging?:boolean}).dragging)return true;return nativeRectVisible({x:node.internals.positionAbsolute.x,y:node.internals.positionAbsolute.y,width:node.measured.width??node.width??0,height:node.measured.height??node.height??0},sceneViewport);};
|
||||
const overlayVisible=!!overlaySnapshot.length&&(!surfaceBounds||overlays.visible(surfaceBounds,target=>root.document.elements.get(target.id)?.incarnation===target.incarnation).some(entry=>!nativeVisible(entry.target.id)));
|
||||
const paintVisible=!surfaceWidth||!surfaceHeight||paint.unknown.some(id=>!nativeVisible(id))||[...paint.query(sceneViewport)].some(id=>!nativeVisible(id));
|
||||
const visibleOverlays=overlaySnapshot.length?(surfaceBounds?overlays.visible(surfaceBounds,target=>root.document.elements.get(target.id)?.incarnation===target.incarnation):overlaySnapshot.filter(entry=>root.document.elements.get(entry.target.id)?.incarnation===entry.target.incarnation)):[];
|
||||
const paintedNodes=[...paint.unknown,...paint.query(sceneViewport)];
|
||||
const nativeNodes=useStore(state=>state.nodes);
|
||||
const connectionPaintCache=useMemo(()=>createConnectionPaintIndex(),[engine,view.canvasId]);
|
||||
const connectionPaint=useMemo(()=>connectionPaintCache.sync(projection,engine.editor.registry,{anchor:(node,handle,end,edge)=>{const measured=flow.getInternalNode(node.id);return node.data.__headlessRecord||edge.type==='unsupported'?measuredConnector(measured,handle,end):compatConnector(node,handle,end==='source'?'right':'left');}}),[connectionPaintCache,projection,engine,flow,nativeNodes]);
|
||||
const connectionPaintVisible=connectionPaint.unknown.length>0||!surfaceWidth||!surfaceHeight||connectionPaint.query({x:-viewport.x/viewport.zoom,y:-viewport.y/viewport.zoom,width:surfaceWidth/viewport.zoom,height:surfaceHeight/viewport.zoom}).size>0;
|
||||
const paintedEdges=[...connectionPaint.unknown,...connectionPaint.query(sceneViewport)];
|
||||
const nodeTypes=useMemo(()=>({...renderers.nodeTypes,unsupported:UnsupportedNode}),[renderers]),edgeTypes=useMemo(()=>({...renderers.edgeTypes,unsupported:UnsupportedEdge}),[renderers]);
|
||||
const current = useRef({ root, projection, view, engine, sessions }); current.current = { root, projection, view, engine, sessions };
|
||||
const report = useCallback((result: Result | { enabled: boolean; code?: string }) => {
|
||||
@@ -202,15 +204,23 @@ function CanvasView({ engine, board, viewId, renderers,fitLoading=true }: { engi
|
||||
return () => { window.removeEventListener('blur', cancel); window.removeEventListener('keydown', escape); };
|
||||
}, [engine, viewId]);
|
||||
const modes = view.modes;
|
||||
// Visible native owners already survive React Flow culling. Fall back only if
|
||||
// culling could discard an active editor/lease (including unknown custom owners).
|
||||
const interactionOwners = new Set(modes.keys());
|
||||
if (sessions.gesture?.viewId === viewId) for (const target of sessions.gesture.targets) interactionOwners.add(target.id);
|
||||
if (sessions.connectionGesture?.viewId === viewId) interactionOwners.add(sessions.connectionGesture.target.id);
|
||||
const focusedNode = focusedEditor?.closest('.react-flow__node')?.getAttribute('data-id');
|
||||
if (focusedNode) interactionOwners.add(focusedNode);
|
||||
const retainInteraction = !!focusedEditor && !focusedNode || [...interactionOwners].some(id => !flow.getInternalNode(id) || !nativeVisible(id));
|
||||
const cullVisible = !overlayVisible && !connectionPaintVisible && !paintVisible && !retainInteraction;
|
||||
// Retain explicit owners in the native renderer, leaving the full graph in its
|
||||
// lookup for edges/queries. No synthetic dragging or altered geometry is used.
|
||||
const retained=new Set<Id>([...modes.keys(),...paintedNodes,...paintedEdges]);
|
||||
for(const entry of visibleOverlays)retained.add(entry.target.id);
|
||||
if(sessions.gesture?.viewId===viewId)for(const target of sessions.gesture.targets)retained.add(target.id);
|
||||
if(sessions.connectionGesture?.viewId===viewId)retained.add(sessions.connectionGesture.target.id);
|
||||
const activeEditor=focusedEditor?.isConnected?focusedEditor:null;
|
||||
const focusedOverlay=activeEditor?.closest<HTMLElement>('[data-canvas-overlay-target]');
|
||||
const focusedNative=activeEditor?.closest<HTMLElement>('.react-flow__node,.react-flow__edge');
|
||||
const focusedId=focusedOverlay?.dataset.canvasOverlayView===viewId?focusedOverlay.dataset.canvasOverlayTarget:focusedNative?.closest('[data-headless-canvas]')===surface.current?focusedNative?.dataset.id:undefined;
|
||||
if(focusedId)retained.add(focusedId);
|
||||
// Retained connections also keep their measured endpoint owners alive.
|
||||
for(const id of [...retained]){const element=root.document.elements.get(id);if(element?.structure==='connection'){retained.add(element.source.elementId);retained.add(element.target.elementId);}}
|
||||
const scoped=!(activeEditor&&!focusedId)&&[...retained].every(id=>root.document.elements.has(id));
|
||||
const cullVisible=!!surfaceWidth&&!!surfaceHeight&&scoped;
|
||||
const retainedProjection=useMemo(()=>createRetainedProjection(),[engine,viewId]);
|
||||
const rendered=retainedProjection(projection.nodes,projection.edges,retained);
|
||||
const selected = view.selection.size === 1 ? root.document.elements.get([...view.selection][0]) : undefined;
|
||||
const selectedResource=selected?.structure==='node'&&engine.editor.registry.resolve(selected)==='known'&&engine.editor.registry.supports(selected,'geometry.resize')&&typeof selected.data.assetId==='string'&&engine.editor.registry.components.get(selected.componentId)?.resourceFields?.includes('assetId')?root.runtime.resources.get(selected.data.assetId):undefined;
|
||||
const canFitImage=!!selectedResource&&(selectedResource.image?.version===1&&selectedResource.image.status==='ready'||!!selectedResource.stored)&&!sessions.gesture;
|
||||
@@ -276,7 +286,7 @@ function CanvasView({ engine, board, viewId, renderers,fitLoading=true }: { engi
|
||||
<CanvasRendererCommandsContext.Provider value={rendererCommands}>
|
||||
<NodeEditing.Provider value={nodeEditing}>
|
||||
<EdgeEditing.Provider value={edgeEditing}>
|
||||
<ReactFlow<ProjectionNode> elevateEdgesOnSelect onlyRenderVisibleElements={cullVisible} nodes={projection.nodes} edges={projection.edges} nodeTypes={nodeTypes} edgeTypes={edgeTypes} connectionMode={ConnectionMode.Loose}
|
||||
<ReactFlow<ProjectionNode> elevateEdgesOnSelect onlyRenderVisibleElements={cullVisible} nodes={rendered.nodes} edges={rendered.edges} nodeTypes={nodeTypes} edgeTypes={edgeTypes} connectionMode={ConnectionMode.Loose}
|
||||
nodesConnectable={view.tool === 'select'} nodesDraggable={view.tool === 'select'} panOnDrag={view.tool === 'pan' ? true : [1, 2]} selectionOnDrag={view.tool === 'select'} panOnScroll zoomOnScroll={false} minZoom={.1} maxZoom={4} deleteKeyCode={null} defaultViewport={view.viewport}
|
||||
|
||||
onMoveEnd={(_, viewport) => engine.sessions.viewport(viewId, viewport)}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/** Adapter-only flags consumed by the app's pinned React Flow compiler extension.
|
||||
* They never enter the document, history, persistence or renderer business data. */
|
||||
export function createRetainedProjection(){
|
||||
const tagged=new WeakMap<object,object>();
|
||||
let previous:{nodes:readonly object[];edges:readonly object[];ids:Set<string>;result:{nodes:object[];edges:object[]}}|undefined;
|
||||
return <N extends {id:string},E extends {id:string}>(nodes:N[],edges:E[],ids:ReadonlySet<string>):{nodes:N[];edges:E[]}=>{
|
||||
if(!ids.size){previous=undefined;return{nodes,edges};}
|
||||
if(previous?.nodes===nodes&&previous.edges===edges&&previous.ids.size===ids.size&&[...ids].every(id=>previous!.ids.has(id)))return previous.result as {nodes:N[];edges:E[]};
|
||||
const retain=<T extends {id:string}>(row:T):T=>{
|
||||
if(!ids.has(row.id))return row;
|
||||
let value=tagged.get(row);if(!value){value={...row,__endoleapRetain:true};tagged.set(row,value);}return value as T;
|
||||
};
|
||||
const result={nodes:nodes.map(retain),edges:edges.map(retain)};
|
||||
previous={nodes,edges,ids:new Set(ids),result};return result;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user