// SysMLModel → React Flow nodes/edges. M5 replaces fixtureToFlow with this // so the diagram renders the canonical store state. import { MarkerType, type Edge, type Node } from "@xyflow/react"; import type { SysMLModel, AssociationKind } from "../../lib/sysml/model"; import type { BlockNodeData } from "./nodes/BlockNode"; import type { SysmlEdgeData } from "./edges/SysmlEdge"; // Default board dimensions used when the model lacks per-block positions. const BOARD_W = 720; const BOARD_H = 460; export interface NodePositions { [blockId: string]: { x: number; y: number }; } export interface ConvertOptions { /** Per-node positions tracked in component state (drag offsets). */ positions?: NodePositions; } function markerForKind(kind: AssociationKind) { if (kind === "composition") { return { type: MarkerType.ArrowClosed, color: "var(--edge)", width: 18, height: 18 }; } if (kind === "constraintApplies") return undefined; return { type: MarkerType.Arrow, color: "var(--edge)", width: 18, height: 18 }; } export function modelToFlow(model: SysMLModel, opts: ConvertOptions = {}): { nodes: Node[]; edges: Edge[] } { const positions = opts.positions ?? {}; // Auto-place blocks that don't have a tracked position. Spread them on a // grid so newly-created elements don't all stack at (0,0). const positioned = new Set(Object.keys(positions)); let autoIdx = 0; function autoPosition(blockId: string) { autoIdx++; const col = autoIdx % 4; const row = Math.floor(autoIdx / 4); return { x: 100 + col * 220, y: 100 + row * 160 }; } // Render constraints + blocks together so the user sees both. const allBlockLikeNodes: Node[] = []; model.blocks.forEach((b, i) => { const pos = positions[b.id] ?? defaultPositionForIndex(i, model.blocks.length); allBlockLikeNodes.push({ id: b.id, type: "sysmlBlock", position: pos, data: { label: b.label, kind: b.kind, properties: b.properties.map(p => p.name), }, }); }); model.constraints.forEach(c => { const pos = positions[c.id] ?? autoPosition(c.id); if (!positioned.has(c.id)) positioned.add(c.id); allBlockLikeNodes.push({ id: c.id, type: "sysmlBlock", position: pos, data: { label: c.label, kind: "constraint", properties: [], expression: c.expression || "{ }", }, }); }); const edges: Edge[] = []; for (const a of model.associations) { edges.push({ id: a.id, source: a.fromBlockId, target: a.toBlockId, type: "sysml", data: { label: a.label, kind: a.kind }, markerEnd: markerForKind(a.kind), }); } // Synthesize constraint→block edges from the constraint's appliesTo field. for (const c of model.constraints) { for (const target of c.appliesTo) { edges.push({ id: `${c.id}__applies__${target}`, source: c.id, target, type: "sysml", data: { label: "applies_to", kind: "constraintApplies" }, }); } } return { nodes: allBlockLikeNodes, edges }; } function defaultPositionForIndex(i: number, total: number): { x: number; y: number } { // Best-effort grid layout when no positions are tracked yet. const cols = Math.max(2, Math.ceil(Math.sqrt(total))); const col = i % cols; const row = Math.floor(i / cols); return { x: 60 + col * (BOARD_W / cols), y: 60 + row * 160 }; }