Files
Endoleap/src/components/canvas/loading-graph-canvas.tsx
T

44 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import {useEffect,useMemo,useRef} from 'react';
import {Background,BackgroundVariant,ReactFlow,useReactFlow,type Viewport,type NodeTypes, type NodeProps,Handle,Position} from '@xyflow/react';
import type {Registry} from '@/lib/canvas-core/types';
import type {ModelPreview} from '@/lib/canvas-runtime/model-preview';
import type {ReactRenderers} from '@/lib/canvas-bindings/react-flow';
import {createLoadingRendererCatalog} from '@/lib/canvas-bindings/renderers';
import {projectCanvas} from '@/lib/canvas-bindings/projection';
import {CanvasRendererCommandsContext,type CanvasRendererCommands} from '@/lib/canvas-bindings/react-flow';
import {UnsupportedNode,UnsupportedEdge} from './unsupported-elements';
import styles from './canvas.module.css';
function PreparingNode({data}:NodeProps){return <div style={{width:'100%',height:'100%',padding:14,border:'1px solid #ded7ce',borderRadius:8,background:'#fffdfa'}} aria-label="正在准备元素"><span>{typeof data.title==='string'?data.title:'正在准备元素…'}</span>{(data.__previewHandles as {id?:string;type:'source'|'target'}[]??[]).map((handle,index)=><Handle key={index} type={handle.type} id={handle.id} position={handle.type==='source'?Position.Right:Position.Left} isConnectable={false}/>)}</div>;}
const reject=()=>({status:'rejected' as const,code:'model_loading',targets:[]});
const commands:CanvasRendererCommands=Object.freeze({dispatch:reject,transaction:reject});
function InitialPreviewViewport({onViewport,nodeCount}:{onViewport(viewport:Viewport):void;nodeCount:number}){
const flow=useReactFlow(),fitted=useRef(false),notify=useRef(onViewport);notify.current=onViewport;
useEffect(()=>{if(!flow.viewportInitialized||fitted.current||!nodeCount)return;fitted.current=true;let active=true;void flow.fitView({padding:.2,maxZoom:1}).then(()=>{if(active)notify.current(flow.getViewport());});return()=>{active=false;};},[flow,nodeCount]);
return null;
}
/** Render the validated read prefix directly. No temporary editor, ACK, ledger or
* writable engine is constructed, and no partial model enters durable storage. */
export default function LoadingGraphCanvas({preview,canvasId,registry,renderers,workspaceId,onViewport}: {preview:ModelPreview;canvasId:string;workspaceId:string;registry:Registry;renderers:ReactRenderers;onViewport(viewport:Viewport):void}){
// Without a declared reference schema, use the existing opaque placeholder
// until the complete document is available to the installed renderer.
const catalog=useMemo(()=>createLoadingRendererCatalog(registry,renderers.catalog),[registry,renderers]);
const projection=useMemo(()=>{
const value=projectCanvas(preview.document,canvasId,registry,new Set(),()=>undefined,undefined,catalog);
const handles=new Map<string,{id?:string;type:'source'|'target'}[]>();for(const edge of value.edges)for(const type of ['source','target'] as const){const id=edge[type],rows=handles.get(id)??[];rows.push({type,id:edge[type==='source'?'sourceHandle':'targetHandle']??undefined});handles.set(id,rows);}
return{nodes:value.nodes.map(node=>({...node,draggable:false,selectable:false,focusable:false,connectable:false,style:{...node.style,pointerEvents:'none' as const},data:{...node.data,__previewHandles:handles.get(node.id)??[],...(registry.resolve(preview.document.elements.get(node.id)!)==='known'&&typeof node.data.assetId==='string'&&registry.components.get(preview.document.elements.get(node.id)!.componentId)?.resourceFields?.includes('assetId')?{src:`/api/foundry/workspaces/${encodeURIComponent(workspaceId)}/assets/${encodeURIComponent(node.data.assetId)}?variant=preview`}:{})}})),edges:value.edges.map(edge=>({...edge,selectable:false,focusable:false}))};
},[preview.document,canvasId,workspaceId,registry,catalog]);
const nodeTypes=useMemo<NodeTypes>(()=>{const result:NodeTypes={};for(const [id,Renderer]of Object.entries({...Object.fromEntries(Object.keys(renderers.nodeTypes).map(id=>[id,renderers.previewNodeTypes[id]??PreparingNode])),unsupported:UnsupportedNode}))result[id]=props=><div inert style={{width:'100%',height:'100%'}}><Renderer {...props}/></div>;return result;},[renderers]),edgeTypes=useMemo(()=>({...Object.fromEntries(Object.keys(renderers.edgeTypes).map(id=>[id,renderers.previewEdgeTypes[id]??UnsupportedEdge])),unsupported:UnsupportedEdge}),[renderers]);
return <div className={styles.editor} data-canvas-loading-preview aria-label="正在加载的画布,只读预览">
<CanvasRendererCommandsContext.Provider value={commands}>
<ReactFlow nodes={projection.nodes} edges={projection.edges} nodeTypes={nodeTypes} edgeTypes={edgeTypes} nodesDraggable={false} nodesConnectable={false} elementsSelectable={false} nodesFocusable={false} edgesFocusable={false} panOnDrag panOnScroll zoomOnScroll={false} minZoom={.1} maxZoom={4} deleteKeyCode={null} onMoveEnd={(_,viewport)=>onViewport(viewport)}>
<InitialPreviewViewport onViewport={onViewport} nodeCount={projection.nodes.length}/>
<Background variant={BackgroundVariant.Dots} gap={24} size={1} />
</ReactFlow>
</CanvasRendererCommandsContext.Provider>
<p role="status" style={{position:'absolute',left:'50%',transform:'translateX(-50%)',bottom:16,pointerEvents:'none'}}>正在加载… {preview.receivedElements}/{preview.totalElements},加载期间可浏览</p>
</div>;
}