Connection validation
Typed Blueprint pins refuse mismatched wires automatically (a string won’t plug into a number). A custom guard adds cycle prevention on top. Every attempt is logged live.
{
"version": "xenolith.v1",
"nodes": [
{ "id": "a", "type": "A", "position": { "x": 0, "y": 40 }, "render": { "title": "A", "category": "data" },
"pins": [
{ "id": "a:in", "kind": "data", "direction": "in", "type": "float", "multiple": false, "label": "In" },
{ "id": "a:out", "kind": "data", "direction": "out", "type": "float", "multiple": true, "label": "Out" }
] },
{ "id": "b", "type": "B", "position": { "x": 230, "y": 40 }, "render": { "title": "B", "category": "data" },
"pins": [
{ "id": "b:in", "kind": "data", "direction": "in", "type": "float", "multiple": false, "label": "In" },
{ "id": "b:out", "kind": "data", "direction": "out", "type": "float", "multiple": true, "label": "Out" }
] },
{ "id": "c", "type": "C", "position": { "x": 460, "y": 40 }, "render": { "title": "C", "category": "data" },
"pins": [
{ "id": "c:in", "kind": "data", "direction": "in", "type": "float", "multiple": false, "label": "In" },
{ "id": "c:out", "kind": "data", "direction": "out", "type": "float", "multiple": true, "label": "Out" }
] },
{ "id": "text", "type": "Text", "position": { "x": 40, "y": 210 }, "render": { "title": "Text", "category": "logic" },
"pins": [
{ "id": "text:out", "kind": "data", "direction": "out", "type": "string", "multiple": true, "label": "Out" }
] },
{ "id": "caption", "type": "Caption", "position": { "x": 460, "y": 210 }, "render": { "title": "Caption", "category": "logic" },
"pins": [
{ "id": "caption:in", "kind": "data", "direction": "in", "type": "string", "multiple": false, "label": "In" }
] }
],
"edges": [
{ "id": "e-ab", "from": { "node": "a", "pin": "a:out" }, "to": { "node": "b", "pin": "b:in" } },
{ "id": "e-bc", "from": { "node": "b", "pin": "b:out" }, "to": { "node": "c", "pin": "c:in" } },
{ "id": "e-tc", "from": { "node": "text", "pin": "text:out" }, "to": { "node": "caption", "pin": "caption:in" } }
]
} import { useState } from 'react'
import { XenolithGraph, XenolithPanel, XenolithControls } from '@xenolithengine/graph-react'
import { buildConnectionValidation, type Attempt } from '@xenolithengine/demo/connection-validation'
import { DemoStage } from '../Layout.js'
// Showcase: typed-pin validation + a custom cycle-prevention guard. The graph + the guard live in the
// framework-agnostic core (@xenolithengine/demo/connection-validation); this React file just renders the
// live attempt log fed by the core's `log` callback.
function RulesPanel({ log }: { log: Attempt[] }): React.ReactElement {
return (
<XenolithPanel position="top-right" style={{ display: 'flex', flexDirection: 'column', gap: 8, width: 230 }}>
<p style={{ margin: 0, fontSize: 11, textTransform: 'uppercase', letterSpacing: '.05em', color: 'var(--xeno-muted)' }}>Connection rules</p>
<span style={{ color: 'var(--xeno-muted)', fontSize: 11, lineHeight: 1.5 }}>
Pins are typed. Drag <b style={{ color: 'var(--xeno-text)' }}>Text</b> → a float input — refused (snaps back).
Drag <b style={{ color: 'var(--xeno-text)' }}>C</b> → <b style={{ color: 'var(--xeno-text)' }}>A</b> — blocked (cycle).
</span>
<div style={{ display: 'flex', flexDirection: 'column', gap: 3, maxHeight: 168, overflow: 'hidden' }}>
{log.length === 0 && <span style={{ color: 'var(--xeno-muted)', fontSize: 11 }}>No attempts yet.</span>}
{log.map((a, i) => (
<span key={i} style={{ fontSize: 11, fontFamily: 'var(--xeno-mono, monospace)', color: a.ok ? '#39d98a' : '#ff5b6e' }}>
{a.ok ? '✓' : '✗'} {a.text}
</span>
))}
</div>
</XenolithPanel>
)
}
/** Showcase: typed-pin validation + custom cycle-prevention guard. */
export function ConnectionValidationDemo(): React.ReactElement {
const [log, setLog] = useState<Attempt[]>([])
const push = (a: Attempt): void => setLog((l) => [a, ...l].slice(0, 8))
return (
<DemoStage>
<XenolithGraph className="xeno" resizeToWindow={false} onReady={(editor) => buildConnectionValidation(editor, push)}>
<XenolithControls position="bottom-left" />
<RulesPanel log={log} />
</XenolithGraph>
</DemoStage>
)
} // Showcase: connection rules. Pins are typed (Blueprint-style) so the built-in check refuses a
// string→float wire automatically. On top of that, a custom isValidConnection guard uses the core
// wouldCreateCycle() helper to forbid loops. The graph is DATA (connection-validation.json); the only
// host-specific piece is the `log` sink the guard + edge:connected event report attempts to.
import { wouldCreateCycle } from '@xenolithengine/graph-core'
import type { XenolithEditor, NodeId } from '@xenolithengine/graph-editor'
import graph from './connection-validation.json'
export interface Attempt { ok: boolean; text: string }
export function buildConnectionValidation(editor: XenolithEditor, log: (a: Attempt) => void): void {
editor.loadJSON(graph)
const name = (id: NodeId): string => editor.graph.getNode(id)?.type ?? '?'
editor.setIsValidConnection((conn) => {
if (wouldCreateCycle(editor.graph, conn.source, conn.target)) {
log({ ok: false, text: `${name(conn.source)} → ${name(conn.target)} · would create a cycle` })
return false
}
return true
})
editor.on('edge:connected', (e) => log({ ok: true, text: `${name(e.edge.from.node)} → ${name(e.edge.to.node)} · connected` }))
editor.fitView({ padding: 64, maxZoom: 1 })
}