import { createGraphEngine } from '@graphora/engine' import { applyMeasuredNoOverlap, applyComponentPacking, createHierarchicalLayout, type LayoutGeometrySnapshot, type LayoutResult } from '@graphora/layouts' import { CanvasGraphRenderer, GRAPHORA_STYLE_ATTRIBUTE, createViewport, fitBounds, worldToScreen } from '@graphora/renderer' import './style.css' const container = document.querySelector('#graph')! const status = document.querySelector('#layout-status')! const crowdButton = document.querySelector('#crowd-nodes')! const separateButton = document.querySelector('#separate-nodes')! const overlapComponentsButton = document.querySelector( '#overlap-components' )! const packComponentsButton = document.querySelector('#pack-components')! const routeButton = document.querySelector('#route-view')! const moveGatewayButton = document.querySelector('#move-gateway')! const renderer = new CanvasGraphRenderer() const engine = createGraphEngine({ graph: { nodes: [ { id: 'gateway', label: 'Gateway', attributes: { [GRAPHORA_STYLE_ATTRIBUTE]: { shape: 'circle', radius: 48, fill: '#0f766e' } } }, { id: 'identity', label: 'Identity', attributes: { [GRAPHORA_STYLE_ATTRIBUTE]: { shape: 'square', radius: 28, fill: '#2563eb' } } }, { id: 'payments', label: 'Payments', attributes: { [GRAPHORA_STYLE_ATTRIBUTE]: { shape: 'diamond', radius: 40, fill: '#7c3aed' } } }, { id: 'audit', label: 'Audit', attributes: { [GRAPHORA_STYLE_ATTRIBUTE]: { shape: 'rounded-rect', radius: 18, fill: '#d97706' } } } ], edges: [ { source: 'gateway', target: 'identity', directed: true }, { source: 'gateway', target: 'payments', directed: true }, { source: 'identity', target: 'audit', directed: true }, { source: 'payments', target: 'audit', directed: true } ] }, layout: createHierarchicalLayout({ direction: 'LR', rankSpacing: 72, nodeSpacing: 36 }), renderer, viewport: createViewport({ width: container.clientWidth, height: container.clientHeight }), theme: { node: { strokeWidth: 2 }, edge: { strokeWidth: 2 } } }) const geometry = engine.createLayoutGeometry({ padding: 10 }) engine.setLayoutGeometry(geometry) engine.mount(container) crowdButton.disabled = true separateButton.disabled = true void engine .runLayout({ fitToView: { padding: 72 } }) .then(() => { reportOverlapStatus(engine.getLayoutGeometry()!, 'ready') status.dataset.ready = 'true' crowdButton.disabled = false separateButton.disabled = false overlapComponentsButton.disabled = false routeButton.disabled = false }) .catch((error: unknown) => { status.textContent = error instanceof Error ? error.message : String(error) status.dataset.ready = 'error' }) crowdButton.addEventListener('click', () => { const snapshot = engine.snapshot() if (engine.getLayoutGeometry() === null) engine.setLayoutGeometry(engine.createLayoutGeometry({ padding: 10 })) const positions = snapshot.nodes.map((node) => ({ id: node.id, key: node.key, position: { x: 0, y: 0 } })) engine.applyLayoutResult( { positions, bounds: { minX: 0, minY: 0, maxX: 0, maxY: 0, width: 0, height: 0 } }, { fitToView: { padding: 72 } } ) reportOverlapStatus(engine.getLayoutGeometry()!, 'crowded') }) separateButton.addEventListener('click', () => { const snapshot = engine.snapshot() const currentGeometry = engine.getLayoutGeometry() ?? engine.createLayoutGeometry({ padding: 10 }) engine.setLayoutGeometry(currentGeometry) const candidate: LayoutResult = { positions: snapshot.nodes.map((node) => ({ id: node.id, key: node.key, position: node.position! })), bounds: snapshot.bounds } const separated = applyMeasuredNoOverlap(snapshot, candidate, currentGeometry) engine.applyLayoutResult(separated.result, { fitToView: { padding: 72 } }) reportOverlapStatus(engine.getLayoutGeometry()!, separated.diagnostics.status) status.dataset.pairChecks = String(separated.diagnostics.pairChecks) }) function reportOverlapStatus( currentGeometry: LayoutGeometrySnapshot, state: string ): void { const positions = new Map( engine.snapshot().nodes.map((node) => [node.key, node.position!]) ) let overlaps = 0 for (let first = 0; first < currentGeometry.nodes.length; first += 1) { for ( let second = first + 1; second < currentGeometry.nodes.length; second += 1 ) { const a = currentGeometry.nodes[first] const b = currentGeometry.nodes[second] const ap = positions.get(a.key)! const bp = positions.get(b.key)! const separated = ap.x + a.size.width / 2 + a.padding.right <= bp.x - b.size.width / 2 - b.padding.left || bp.x + b.size.width / 2 + b.padding.right <= ap.x - a.size.width / 2 - a.padding.left || ap.y + a.size.height / 2 + a.padding.bottom <= bp.y - b.size.height / 2 - b.padding.top || bp.y + b.size.height / 2 + b.padding.bottom <= ap.y - a.size.height / 2 - a.padding.top if (!separated) overlaps += 1 } } status.textContent = `Measured boxes: ${currentGeometry.nodes.length} · overlaps: ${overlaps}` status.dataset.state = state } // Make two overlapping disconnected pairs, retaining the same four visible bodies. overlapComponentsButton.addEventListener('click', () => { engine.setViewFilters([ { id: 'two-components', kind: 'edge-filter', predicate: (edge) => (edge.sourceKey === 'gateway' && edge.targetKey === 'identity') || (edge.sourceKey === 'payments' && edge.targetKey === 'audit') } ]) engine.setLayoutGeometry(engine.createLayoutGeometry({ padding: 10 })) const snapshot = engine.viewSnapshot() engine.applyLayoutResult( { positions: snapshot.nodes.map((node) => ({ id: node.id, key: node.key, position: { x: node.key === 'gateway' || node.key === 'payments' ? 0 : 150, y: node.key === 'payments' || node.key === 'audit' ? 20 : 0 } })), bounds: null }, { fitToView: { padding: 72 } } ) status.textContent = 'Two overlapping components' status.dataset.state = 'components-overlap' packComponentsButton.disabled = false }) packComponentsButton.addEventListener('click', () => { const snapshot = engine.viewSnapshot() if (engine.getLayoutGeometry() === null) engine.setLayoutGeometry(engine.createLayoutGeometry({ padding: 10 })) const packed = applyComponentPacking( snapshot, { positions: snapshot.nodes.map((node) => ({ id: node.id, key: node.key, position: node.position! })), bounds: snapshot.bounds }, engine.getLayoutGeometry()!, { gap: 60, rowWidth: 320 } ) engine.applyLayoutResult(packed.result, { fitToView: { padding: 72 } }) status.textContent = `Packed components: ${packed.diagnostics.componentCount}` status.dataset.state = packed.diagnostics.status }) routeButton.addEventListener('click', () => { const snapshot = engine.viewSnapshot() const positions = new Map( snapshot.nodes.map((node) => [node.key, node.position!]) ) const top = Math.min(...snapshot.nodes.map((node) => node.position!.y)) - 130 const edgeRoutes = snapshot.edges.map((edge, index) => { const source = positions.get(edge.sourceKey)! const target = positions.get(edge.targetKey)! const y = top - index * 40 return { id: edge.id, key: edge.key, kind: 'polyline' as const, points: [source, { x: source.x, y }, { x: target.x, y }, target] } }) const geometry = engine.createLayoutGeometry({ padding: 10 }) engine.setLayoutGeometry({ ...geometry, labels: [ { id: 'node:gateway:label', ownerType: 'node', ownerId: 'gateway', ownerKey: 'gateway', size: { width: 80, height: 12 }, padding: { left: 0, right: 0, top: 0, bottom: 0 } } ] }) engine.applyLayoutResult({ positions: [], bounds: snapshot.bounds, provenance: engine.getLayoutGeometry()!.provenance, edgeRoutes: { mode: 'replace', items: edgeRoutes }, labelPlacements: { mode: 'replace', items: [ { id: 'node:gateway:label', ownerType: 'node', ownerId: 'gateway', ownerKey: 'gateway', center: { x: positions.get('gateway')!.x, y: top - 175 } } ] } }) const bounds = snapshot.bounds! const minY = top - 200 engine.setViewport( fitBounds( engine.getViewport(), { ...bounds, minY, height: bounds.maxY - minY }, { padding: 72 } ) ) status.textContent = `Routes: ${edgeRoutes.length} · placed labels: 1` status.dataset.state = 'artifacts-ready' moveGatewayButton.disabled = false const route = edgeRoutes[0] if (route !== undefined) { requestAnimationFrame(() => requestAnimationFrame(() => { const screen = worldToScreen(engine.getViewport(), { x: (route.points[1].x + route.points[2].x) / 2, y: route.points[1].y }) status.dataset.routeX = String(screen.x) status.dataset.routeY = String(screen.y) status.dataset.routeId = String(route.id) }) ) } }) moveGatewayButton.addEventListener('click', () => { const node = engine.snapshot().nodes.find((item) => item.key === 'gateway')! engine.store.updateNodePosition(node.id, { x: node.position!.x + 25, y: node.position!.y }) status.textContent = 'Node moved · route and label stages cleared' status.dataset.state = 'artifacts-cleared' }) container.addEventListener('pointerdown', (event) => { const rect = container.getBoundingClientRect() const picked = engine.pick({ x: event.clientX - rect.left, y: event.clientY - rect.top }) status.dataset.pickedEdge = picked?.type === 'edge' ? String(picked.edgeId) : '' })