Share scoped display leases and retain incremental resource views
This commit is contained in:
@@ -13,6 +13,7 @@ import {createGeometryProjectionCache} from "@/lib/canvas-bindings/geometry-cach
|
||||
import {hitProjectedCanvas} from "@/lib/canvas-bindings/hit-query";
|
||||
import {compatConnector,measuredConnector} from "@/lib/canvas-bindings/connectors";
|
||||
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 type { ProjectEngine } from "@/lib/canvas-runtime/workspace";
|
||||
@@ -42,7 +43,9 @@ function CanvasView({ engine, board, viewId, renderers }: { engine: ProjectEngin
|
||||
useEffect(() => { focusEpoch.current++; setEditorFocused(false); return () => { focusEpoch.current++; }; }, [engine,viewId]);
|
||||
const [copying, setCopying] = useState(false);
|
||||
const [showAI, setShowAI] = useState(false), [showRecovery, setShowRecovery] = useState(false);
|
||||
const [error, setError] = useState(""), [sources, setSources] = useState(new Map<string, string>());
|
||||
const [error, setError] = useState("");
|
||||
const [displayState, setDisplayState] = useState<{engine:ProjectEngine|null;viewId:Id|null;sources:Map<Id,ResourceViewSource>;failures:number}>({engine:null,viewId:null,sources:new Map(),failures:0});
|
||||
const resourceView=useRef<ReturnType<typeof createResourceView>|null>(null);
|
||||
const pointerStart = useRef<{ x: number; y: number } | null>(null), drag = useRef<{ token: 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();
|
||||
@@ -122,18 +125,15 @@ function CanvasView({ engine, board, viewId, renderers }: { engine: ProjectEngin
|
||||
if([source.id,destination.id].some(id=>projectionCache.current!.getNode(id)?.type==='unsupported')){setError('此元素暂不支持创建新连线');return;}
|
||||
report(engine.editor.dispatch({ type: 'element.create', element: { id: crypto.randomUUID(), incarnation: 1, componentId: 'connection', dataVersion: 1, structure: 'connection', ownerCanvasId: commonCanvas(document, source.ownerCanvasId, destination.ownerCanvasId), source: { elementId: source.id, ...(connection.sourceHandle ? { portId: connection.sourceHandle } : {}) }, target: { elementId: destination.id, ...(connection.targetHandle ? { portId: connection.targetHandle } : {}) }, data: {}, path: null } }));
|
||||
}
|
||||
const resourceKey = JSON.stringify(projection.nodes.filter(node => node.data.assetId).map(node => [node.id, node.data.assetId]));
|
||||
const resourceKey = JSON.stringify(projection.nodes.filter(node => typeof node.data.assetId==='string'&&node.data.assetId).map(node => [node.id, node.data.assetId]));
|
||||
useEffect(() => {
|
||||
if (!liveView) return;
|
||||
let active = true;
|
||||
const handles: { release(): void }[] = [], entries: [Id, string][] = [];
|
||||
void Promise.all((JSON.parse(resourceKey) as [Id, Id][]).map(async ([id, resource]) => {
|
||||
const handle = await engine.displayResource(resource);
|
||||
if (!active) { handle.release(); return; }
|
||||
handles.push(handle); entries.push([id, handle.url]);
|
||||
})).then(() => { if (active) setSources(new Map(entries)); }).catch(e => { if (active) setError(errorText(e)); });
|
||||
return () => { active = false; for (const handle of handles) handle.release(); };
|
||||
}, [engine, resourceKey, Boolean(liveView)]);
|
||||
const view=createResourceView({resolve:(id,signal)=>engine.displayResource(id,{signal}),onChange:(sources,failures)=>{if(resourceView.current===view)setDisplayState({engine,viewId,sources,failures});},onError:error=>{if(resourceView.current===view)setError(errorText(error));}});
|
||||
resourceView.current=view;
|
||||
return ()=>{if(resourceView.current===view)resourceView.current=null;view.dispose();};
|
||||
}, [engine,viewId,Boolean(liveView)]);
|
||||
useEffect(()=>{try{resourceView.current?.sync(JSON.parse(resourceKey) as [Id,Id][]);}catch(error){setError(errorText(error));}},[engine,viewId,resourceKey,Boolean(liveView)]);
|
||||
const sources=useMemo(()=>{if(displayState.engine!==engine||displayState.viewId!==viewId)return new Map<Id,string>();const wanted=new Map(JSON.parse(resourceKey) as [Id,Id][]);return new Map([...displayState.sources].filter(([id,source])=>wanted.get(id)===source.resourceId).map(([id,source])=>[id,source.url]));},[displayState,engine,viewId,resourceKey]);
|
||||
function removeSelected() { report(engine.editor.transaction(tx => { for (const id of current.current.view.selection) if (tx.getDocument().elements.has(id)) tx.dispatch({ type: 'element.remove', target: target(id) }); }, { label: 'Delete selection' })); }
|
||||
const pending = [...root.runtime.ledger.operations.values()].filter(operation => operation.phase !== 'resolved');
|
||||
const blocked = pending.some(operation => operation.phase === 'blocked' && operation.blockers.some(reason => reason !== 'waiting_digest')) || [...root.runtime.ledger.requests.values()].some(request => request.phase !== 'resolved' && request.error && request.retryAt === undefined) || root.runtime.conflicts.size > 0;
|
||||
@@ -258,7 +258,7 @@ function CanvasView({ engine, board, viewId, renderers }: { engine: ProjectEngin
|
||||
<HeadlessImports engine={engine} canvasId={view.canvasId} />
|
||||
{showAI && aiElement && <HeadlessAIPanel key={`${aiElement.id}:${aiElement.incarnation}:${aiElement.ownerCanvasId}`} engine={engine} element={aiElement} close={() => setShowAI(false)} />}
|
||||
{showRecovery && <HeadlessRecoveryDialog engine={engine} close={() => setShowRecovery(false)} />}
|
||||
{error && <div className={styles.error} role="alert">{error} <button onClick={() => setError('')}>关闭</button></div>}
|
||||
{error && <div className={styles.error} role="alert">{error} {displayState.engine===engine&&displayState.viewId===viewId&&displayState.failures>0&&<button onClick={()=>{setError('');resourceView.current?.retry();}}>重试图片</button>}<button onClick={() => setError('')}>关闭</button></div>}
|
||||
</div>;
|
||||
}
|
||||
export default function HeadlessGraphCanvas({ engine, board, renderers=defaultCanvasRenderers }: { engine: ProjectEngine; board: Board; renderers?:ReactRenderers }) {
|
||||
|
||||
Reference in New Issue
Block a user