// React Flow-backed diagram canvas (M3). // - Custom block/actor/constraint/system nodes // - Custom edges (association/composition/constraint) // - Drag-create from the left palette // - Click-edit via the right inspector // - Drag handles between nodes to connect // // State is in-memory for M3; M5 swaps it for the bidirectional sync engine. "use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ReactFlow, ReactFlowProvider, Background, BackgroundVariant, Controls, MarkerType, applyNodeChanges, applyEdgeChanges, addEdge, useReactFlow, type Node, type Edge, type NodeChange, type EdgeChange, type Connection, type NodeMouseHandler, } from "@xyflow/react"; import { BlockNode, type BlockNodeData } from "./nodes/BlockNode"; import { SysmlEdge, type SysmlEdgeData } from "./edges/SysmlEdge"; import { Palette } from "./Palette"; import { NodeInspector } from "./NodeInspector"; import { fixtureToFlow } from "./fixtureToFlow"; import type { Density } from "../socrates/SocratesDock"; import type { FixtureData, AssociationKind, BlockKind } from "../../lib/fixtures/aristotle"; export type DiagramVariant = "softened" | "formal" | "graph"; interface DiagramCanvasProps { data: FixtureData; density?: Density; variant?: DiagramVariant; focusBlockId: string | null; onSelect?: (id: string | null) => void; } const nodeTypes = { sysmlBlock: BlockNode }; const edgeTypes = { sysml: SysmlEdge }; let nextNodeId = 1000; let nextEdgeId = 1000; function freshNodeId(): string { return `n${nextNodeId++}`; } function freshEdgeId(): string { return `e${nextEdgeId++}`; } export function DiagramCanvas(props: DiagramCanvasProps) { return ( ); } function DiagramInner({ data, focusBlockId, onSelect }: DiagramCanvasProps) { const initial = useMemo(() => fixtureToFlow(data), [data]); const [nodes, setNodes] = useState[]>(initial.nodes); const [edges, setEdges] = useState[]>(initial.edges); const wrapperRef = useRef(null); const { screenToFlowPosition } = useReactFlow(); const onNodesChange = useCallback( (changes: NodeChange[]) => setNodes(ns => applyNodeChanges(changes, ns) as Node[]), [] ); const onEdgesChange = useCallback( (changes: EdgeChange[]) => setEdges(es => applyEdgeChanges(changes, es) as Edge[]), [] ); const onConnect = useCallback((connection: Connection) => { const newEdge: Edge = { id: freshEdgeId(), source: connection.source, target: connection.target, type: "sysml", data: { label: "relates_to", kind: "association" satisfies AssociationKind }, markerEnd: { type: MarkerType.Arrow, color: "var(--edge)", width: 18, height: 18 }, }; setEdges(es => addEdge(newEdge, es) as Edge[]); }, []); // Highlight focusBlockId from sibling components (e.g. narrative chip hover). useEffect(() => { setNodes(ns => ns.map(n => ({ ...n, selected: n.id === focusBlockId, })) ); }, [focusBlockId]); const onNodeClick: NodeMouseHandler = useCallback( (_event, node) => { onSelect?.(node.id); }, [onSelect] ); const onPaneClick = useCallback(() => { onSelect?.(null); }, [onSelect]); const selectedNode = nodes.find(n => n.id === focusBlockId); // Drag-and-drop from the palette const onDragOver = useCallback((event: React.DragEvent) => { event.preventDefault(); event.dataTransfer.dropEffect = "move"; }, []); const onDrop = useCallback( (event: React.DragEvent) => { event.preventDefault(); const kindRaw = event.dataTransfer.getData("application/sysml-kind"); if (!kindRaw) return; const kind = kindRaw as BlockKind | "system"; const position = screenToFlowPosition({ x: event.clientX, y: event.clientY }); const id = freshNodeId(); const newNode: Node = { id, type: "sysmlBlock", position, data: { label: kind === "system" ? "System" : kind === "actor" ? "Actor" : kind === "constraint" ? "Constraint" : "Block", kind, properties: kind === "constraint" ? [] : ["new_property"], ...(kind === "constraint" ? { expression: "{ }" } : {}), }, }; setNodes(ns => [...ns, newNode]); onSelect?.(id); }, [onSelect, screenToFlowPosition] ); function patchSelected(patch: Partial) { if (!focusBlockId) return; setNodes(ns => ns.map(n => (n.id === focusBlockId ? { ...n, data: { ...(n.data as BlockNodeData), ...patch } } : n)) ); } function deleteSelected() { if (!focusBlockId) return; setNodes(ns => ns.filter(n => n.id !== focusBlockId)); setEdges(es => es.filter(e => e.source !== focusBlockId && e.target !== focusBlockId)); onSelect?.(null); } return (
{selectedNode && ( onSelect?.(null)} /> )}
); }