Auto-layout (dagre)
A messy 14-node DAG snaps into a clean layered layout. Toggle LR/TB; Cmd+Z restores the mess in a single undo step.
// Vanilla mount for the Auto-Layout example. No React, no framework — just `XenolithEditor.init`,
// the shared scene builder, and two DOM buttons in the editor's overlay root for arrange / direction.
import { XenolithEditor } from '@xenolithengine/graph-editor'
import { buildAutoLayout } from '@xenolithengine/demo/auto-layout'
export async function mount(target: HTMLElement): Promise<() => void> {
// resizeToWindow:false — this demo is embedded in a fixed-size DemoFrame, not full-window.
// Default `true` would size the canvas to window dimensions, overflowing the container and
// visually colliding with the chip switcher below (`.dfr-bar`).
const editor = await XenolithEditor.init(target, { minimap: false, resizeToWindow: false })
const scene = buildAutoLayout(editor)
type Direction = 'LR' | 'TB'
let dir: Direction = 'LR'
let busy = false
// Tiny in-editor panel — themed by --xeno-* tokens like every other panel.
const panel = document.createElement('div')
panel.setAttribute('data-xeno-panel', '')
// pointer-events:auto opts back in — overlayRoot has pointer-events:none so the editor canvas
// underneath stays interactive; every chrome element that needs clicks must opt in explicitly.
panel.style.cssText = 'position:absolute;pointer-events:auto;top:12px;left:12px;display:flex;gap:6px;padding:6px;background:var(--xeno-panel,#1d1d1d);border:1px solid var(--xeno-border,#333);border-radius:8px;font:12px Inter,system-ui,sans-serif;'
const mkBtn = (label: string, primary: () => boolean, onClick: () => Promise<void>): HTMLButtonElement => {
const b = document.createElement('button')
b.textContent = label
b.addEventListener('click', () => { void onClick() })
const paint = (): void => {
const isP = primary()
b.style.cssText = `padding:6px 12px;font-size:12px;border-radius:6px;cursor:pointer;border:1px solid ${isP ? 'var(--xeno-accent,#FCB400)' : 'var(--xeno-border,#333)'};background:${isP ? 'var(--xeno-accent,#FCB400)' : 'transparent'};color:${isP ? 'var(--xeno-canvas,#111)' : 'var(--xeno-text,#cfcfcf)'};`
}
paint()
;(b as HTMLButtonElement & { _repaint: () => void })._repaint = paint
return b
}
const repaintAll = (): void => { for (const c of panel.children) (c as HTMLButtonElement & { _repaint?: () => void })._repaint?.() }
const run = async (next: Direction = dir): Promise<void> => {
if (busy) return
busy = true; runBtn.textContent = 'Arranging…'
try { await scene.arrange({ direction: next }) } finally { busy = false; runBtn.textContent = 'Auto-arrange' }
}
const flip = async (next: Direction): Promise<void> => { dir = next; repaintAll(); await run(next) }
const runBtn = mkBtn('Auto-arrange', () => true, () => run())
const lrBtn = mkBtn('LR', () => dir === 'LR', () => flip('LR'))
const tbBtn = mkBtn('TB', () => dir === 'TB', () => flip('TB'))
panel.append(runBtn, lrBtn, tbBtn)
editor.overlayRoot.appendChild(panel)
return () => { panel.remove(); editor.destroy() }
} // Auto-layout showcase: ~14 ill-placed nodes wired as a fan-in/fan-out DAG. Calling `arrange()`
// runs the dagre engine and applies positions in a single transaction (one undo restores the
// messy original). Reused by both the React demo wrapper and the vanilla mounter — no framework
// in the core; same data + same setup + same plugin.
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import { autoLayoutPlugin, type AutoLayoutPlugin, type LayoutOpts } from '@xenolithengine/graph-plugin-autolayout'
import { dagreEngine } from '@xenolithengine/graph-plugin-autolayout/dagre'
import type { Node, Edge, NodeId, PinId, EdgeId } from '@xenolithengine/graph-core'
const NODES: { id: string; title: string; x: number; y: number }[] = [
{ id: 'in', title: 'Input', x: 480, y: 60 },
{ id: 'a1', title: 'Tokenize', x: 80, y: 320 },
{ id: 'a2', title: 'Embed', x: 730, y: 410 },
{ id: 'a3', title: 'Encode', x: 250, y: 80 },
{ id: 'b1', title: 'Attention', x: 600, y: 220 },
{ id: 'b2', title: 'Norm', x: 30, y: 200 },
{ id: 'b3', title: 'FFN', x: 410, y: 480 },
{ id: 'c1', title: 'Residual', x: 770, y: 100 },
{ id: 'c2', title: 'Merge', x: 120, y: 480 },
{ id: 'd1', title: 'Project', x: 340, y: 260 },
{ id: 'd2', title: 'Logits', x: 540, y: 350 },
{ id: 'd3', title: 'Sample', x: 200, y: 410 },
{ id: 'd4', title: 'Decode', x: 640, y: 60 },
{ id: 'out', title: 'Output', x: 90, y: 130 },
]
const EDGES: [string, string][] = [
['in', 'a1'], ['in', 'a3'], ['in', 'a2'],
['a1', 'b2'], ['a3', 'b1'], ['a2', 'b1'], ['a2', 'b3'],
['b1', 'c1'], ['b1', 'd1'], ['b2', 'c2'], ['b3', 'd1'], ['b3', 'd2'],
['c1', 'd4'], ['c2', 'd3'], ['d1', 'd2'], ['d2', 'd3'],
['d3', 'out'], ['d4', 'out'],
]
export interface AutoLayoutScene {
/** Installed plugin instance — call `plugin.arrange(opts)` to lay out the graph. */
plugin: AutoLayoutPlugin
/** Convenience: run arrange + refit the viewport. */
arrange: (opts?: LayoutOpts) => Promise<void>
}
// Per-editor plugin handle. The React panel reads this via `runAutoLayout(editor, opts)` so it
// doesn't have to keep a scene object around — `setupAutoLayout` is called in `onReady` and the
// plugin is stashed here for later operations.
const PLUGINS = new WeakMap<XenolithEditor, AutoLayoutPlugin>()
/** Idempotent setup: install the plugin, load the demo graph, fit the view. Safe to pass directly
* to `<XenolithGraph onReady>` — synchronous, no first-paint flicker. */
export function setupAutoLayout(editor: XenolithEditor): void {
const plugin = autoLayoutPlugin({
engine: dagreEngine(),
defaults: { direction: 'LR', spacing: { node: 40, layer: 90 }, animate: { durationMs: 600 } },
})
editor.use(plugin)
PLUGINS.set(editor, plugin)
loadAutoLayoutGraph(editor)
}
/** Run the layout engine in the requested direction and refit. No-op if `setupAutoLayout` hasn't
* run yet on this editor. */
export async function runAutoLayout(editor: XenolithEditor, opts?: LayoutOpts): Promise<void> {
const plugin = PLUGINS.get(editor)
if (!plugin) return
await plugin.arrange(opts)
editor.fitView({ padding: 56, maxZoom: 1 })
}
function loadAutoLayoutGraph(editor: XenolithEditor): void {
const nodes: Node[] = NODES.map((n) => ({
id: n.id as NodeId, type: 'Step', position: { x: n.x, y: n.y }, size: { x: 160, y: 64 },
state: {},
render: { title: n.title, category: 'logic' } as never,
pins: [
{ id: `${n.id}_in` as PinId, kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: `${n.id}_out` as PinId, kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
],
}))
const edges: Edge[] = EDGES.map(([from, to], i) => ({
id: `e${i}` as EdgeId,
from: { node: from as NodeId, pin: `${from}_out` as PinId },
to: { node: to as NodeId, pin: `${to}_in` as PinId },
}))
editor.loadJSON({ version: 'xenolith.v1', nodes, edges })
editor.fitView({ padding: 56, maxZoom: 1 })
}
/** @deprecated Prefer `setupAutoLayout(editor)` + `runAutoLayout(editor, opts)`. Kept for the
* vanilla examples; will be removed in a follow-up. */
export function buildAutoLayout(editor: XenolithEditor): AutoLayoutScene {
setupAutoLayout(editor)
const plugin = PLUGINS.get(editor)!
return {
plugin,
arrange: (opts) => runAutoLayout(editor, opts),
}
} import { useState } from 'react'
import { XenolithGraph, XenolithPanel, useEditor } from '@xenolithengine/graph-react'
import { setupAutoLayout, runAutoLayout } from '@xenolithengine/demo/auto-layout'
import { DemoStage } from '../Layout.js'
type Direction = 'LR' | 'TB'
// Canon: direction lives in the panel (only the panel needs it), the panel calls runAutoLayout
// against `useEditor()` directly. No scene handle, no lifted state, no refs.
function AutoLayoutPanel() {
const editor = useEditor()
const [dir, setDir] = useState<Direction>('LR')
const [busy, setBusy] = useState(false)
const arrange = async (next: Direction = dir): Promise<void> => {
if (busy) return
setBusy(true)
try { await runAutoLayout(editor, { direction: next }) } finally { setBusy(false) }
}
const flip = async (next: Direction): Promise<void> => {
setDir(next)
await arrange(next)
}
return (
<XenolithPanel position="top-left" style={{ display: 'flex', gap: 6, padding: 6 }}>
<button onClick={() => arrange()} disabled={busy} style={btnStyle(true)}>
{busy ? 'Arranging…' : 'Auto-arrange'}
</button>
<button onClick={() => flip('LR')} disabled={busy} style={btnStyle(dir === 'LR')}>LR</button>
<button onClick={() => flip('TB')} disabled={busy} style={btnStyle(dir === 'TB')}>TB</button>
</XenolithPanel>
)
}
/** Island: Auto-Layout. */
export function AutoLayoutDemo() {
return (
<DemoStage>
<XenolithGraph className="xeno" resizeToWindow={false} onReady={setupAutoLayout}>
<AutoLayoutPanel />
</XenolithGraph>
</DemoStage>
)
}
const btnStyle = (primary: boolean): React.CSSProperties => ({
padding: '6px 12px',
fontSize: 12,
borderRadius: 6,
border: `1px solid ${primary ? 'var(--xeno-accent, #FCB400)' : 'var(--xeno-border, #333)'}`,
background: primary ? 'var(--xeno-accent, #FCB400)' : 'var(--xeno-panel, #1d1d1d)',
color: primary ? 'var(--xeno-canvas, #111)' : 'var(--xeno-text, #cfcfcf)',
cursor: 'pointer',
}) // Auto-layout showcase: ~14 ill-placed nodes wired as a fan-in/fan-out DAG. Calling `arrange()`
// runs the dagre engine and applies positions in a single transaction (one undo restores the
// messy original). Reused by both the React demo wrapper and the vanilla mounter — no framework
// in the core; same data + same setup + same plugin.
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import { autoLayoutPlugin, type AutoLayoutPlugin, type LayoutOpts } from '@xenolithengine/graph-plugin-autolayout'
import { dagreEngine } from '@xenolithengine/graph-plugin-autolayout/dagre'
import type { Node, Edge, NodeId, PinId, EdgeId } from '@xenolithengine/graph-core'
const NODES: { id: string; title: string; x: number; y: number }[] = [
{ id: 'in', title: 'Input', x: 480, y: 60 },
{ id: 'a1', title: 'Tokenize', x: 80, y: 320 },
{ id: 'a2', title: 'Embed', x: 730, y: 410 },
{ id: 'a3', title: 'Encode', x: 250, y: 80 },
{ id: 'b1', title: 'Attention', x: 600, y: 220 },
{ id: 'b2', title: 'Norm', x: 30, y: 200 },
{ id: 'b3', title: 'FFN', x: 410, y: 480 },
{ id: 'c1', title: 'Residual', x: 770, y: 100 },
{ id: 'c2', title: 'Merge', x: 120, y: 480 },
{ id: 'd1', title: 'Project', x: 340, y: 260 },
{ id: 'd2', title: 'Logits', x: 540, y: 350 },
{ id: 'd3', title: 'Sample', x: 200, y: 410 },
{ id: 'd4', title: 'Decode', x: 640, y: 60 },
{ id: 'out', title: 'Output', x: 90, y: 130 },
]
const EDGES: [string, string][] = [
['in', 'a1'], ['in', 'a3'], ['in', 'a2'],
['a1', 'b2'], ['a3', 'b1'], ['a2', 'b1'], ['a2', 'b3'],
['b1', 'c1'], ['b1', 'd1'], ['b2', 'c2'], ['b3', 'd1'], ['b3', 'd2'],
['c1', 'd4'], ['c2', 'd3'], ['d1', 'd2'], ['d2', 'd3'],
['d3', 'out'], ['d4', 'out'],
]
export interface AutoLayoutScene {
/** Installed plugin instance — call `plugin.arrange(opts)` to lay out the graph. */
plugin: AutoLayoutPlugin
/** Convenience: run arrange + refit the viewport. */
arrange: (opts?: LayoutOpts) => Promise<void>
}
// Per-editor plugin handle. The React panel reads this via `runAutoLayout(editor, opts)` so it
// doesn't have to keep a scene object around — `setupAutoLayout` is called in `onReady` and the
// plugin is stashed here for later operations.
const PLUGINS = new WeakMap<XenolithEditor, AutoLayoutPlugin>()
/** Idempotent setup: install the plugin, load the demo graph, fit the view. Safe to pass directly
* to `<XenolithGraph onReady>` — synchronous, no first-paint flicker. */
export function setupAutoLayout(editor: XenolithEditor): void {
const plugin = autoLayoutPlugin({
engine: dagreEngine(),
defaults: { direction: 'LR', spacing: { node: 40, layer: 90 }, animate: { durationMs: 600 } },
})
editor.use(plugin)
PLUGINS.set(editor, plugin)
loadAutoLayoutGraph(editor)
}
/** Run the layout engine in the requested direction and refit. No-op if `setupAutoLayout` hasn't
* run yet on this editor. */
export async function runAutoLayout(editor: XenolithEditor, opts?: LayoutOpts): Promise<void> {
const plugin = PLUGINS.get(editor)
if (!plugin) return
await plugin.arrange(opts)
editor.fitView({ padding: 56, maxZoom: 1 })
}
function loadAutoLayoutGraph(editor: XenolithEditor): void {
const nodes: Node[] = NODES.map((n) => ({
id: n.id as NodeId, type: 'Step', position: { x: n.x, y: n.y }, size: { x: 160, y: 64 },
state: {},
render: { title: n.title, category: 'logic' } as never,
pins: [
{ id: `${n.id}_in` as PinId, kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: `${n.id}_out` as PinId, kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
],
}))
const edges: Edge[] = EDGES.map(([from, to], i) => ({
id: `e${i}` as EdgeId,
from: { node: from as NodeId, pin: `${from}_out` as PinId },
to: { node: to as NodeId, pin: `${to}_in` as PinId },
}))
editor.loadJSON({ version: 'xenolith.v1', nodes, edges })
editor.fitView({ padding: 56, maxZoom: 1 })
}
/** @deprecated Prefer `setupAutoLayout(editor)` + `runAutoLayout(editor, opts)`. Kept for the
* vanilla examples; will be removed in a follow-up. */
export function buildAutoLayout(editor: XenolithEditor): AutoLayoutScene {
setupAutoLayout(editor)
const plugin = PLUGINS.get(editor)!
return {
plugin,
arrange: (opts) => runAutoLayout(editor, opts),
}
} <script setup lang="ts">
// Vue — one arrange call is one undo step. Direction is panel state passed to runAutoLayout.
import { ref } from 'vue'
import { XenolithGraph, XenolithPanel, XenolithButton } from '@xenolithengine/graph-vue'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import { setupAutoLayout, runAutoLayout } from '@xenolithengine/demo/auto-layout'
type Direction = 'LR' | 'TB'
const editor = ref<XenolithEditor | null>(null)
const dir = ref<Direction>('LR')
const busy = ref(false)
function onReady(e: XenolithEditor): void {
editor.value = e
setupAutoLayout(e)
}
async function arrange(next: Direction = dir.value): Promise<void> {
const e = editor.value
if (!e || busy.value) return
busy.value = true
try { await runAutoLayout(e, { direction: next }) } finally { busy.value = false }
}
async function flip(next: Direction): Promise<void> {
dir.value = next
await arrange(next)
}
</script>
<template>
<div class="app" style="position:absolute;inset:0;">
<XenolithGraph class="xeno" :resize-to-window="false" @ready="onReady">
<XenolithPanel position="top-left">
<div style="display:flex;gap:6px;">
<XenolithButton :active="true" :disabled="busy" @click="arrange()">
{{ busy ? 'Arranging…' : 'Auto-arrange' }}
</XenolithButton>
<XenolithButton :active="dir === 'LR'" :disabled="busy" @click="flip('LR')">LR</XenolithButton>
<XenolithButton :active="dir === 'TB'" :disabled="busy" @click="flip('TB')">TB</XenolithButton>
</div>
</XenolithPanel>
</XenolithGraph>
</div>
</template> // Auto-layout showcase: ~14 ill-placed nodes wired as a fan-in/fan-out DAG. Calling `arrange()`
// runs the dagre engine and applies positions in a single transaction (one undo restores the
// messy original). Reused by both the React demo wrapper and the vanilla mounter — no framework
// in the core; same data + same setup + same plugin.
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import { autoLayoutPlugin, type AutoLayoutPlugin, type LayoutOpts } from '@xenolithengine/graph-plugin-autolayout'
import { dagreEngine } from '@xenolithengine/graph-plugin-autolayout/dagre'
import type { Node, Edge, NodeId, PinId, EdgeId } from '@xenolithengine/graph-core'
const NODES: { id: string; title: string; x: number; y: number }[] = [
{ id: 'in', title: 'Input', x: 480, y: 60 },
{ id: 'a1', title: 'Tokenize', x: 80, y: 320 },
{ id: 'a2', title: 'Embed', x: 730, y: 410 },
{ id: 'a3', title: 'Encode', x: 250, y: 80 },
{ id: 'b1', title: 'Attention', x: 600, y: 220 },
{ id: 'b2', title: 'Norm', x: 30, y: 200 },
{ id: 'b3', title: 'FFN', x: 410, y: 480 },
{ id: 'c1', title: 'Residual', x: 770, y: 100 },
{ id: 'c2', title: 'Merge', x: 120, y: 480 },
{ id: 'd1', title: 'Project', x: 340, y: 260 },
{ id: 'd2', title: 'Logits', x: 540, y: 350 },
{ id: 'd3', title: 'Sample', x: 200, y: 410 },
{ id: 'd4', title: 'Decode', x: 640, y: 60 },
{ id: 'out', title: 'Output', x: 90, y: 130 },
]
const EDGES: [string, string][] = [
['in', 'a1'], ['in', 'a3'], ['in', 'a2'],
['a1', 'b2'], ['a3', 'b1'], ['a2', 'b1'], ['a2', 'b3'],
['b1', 'c1'], ['b1', 'd1'], ['b2', 'c2'], ['b3', 'd1'], ['b3', 'd2'],
['c1', 'd4'], ['c2', 'd3'], ['d1', 'd2'], ['d2', 'd3'],
['d3', 'out'], ['d4', 'out'],
]
export interface AutoLayoutScene {
/** Installed plugin instance — call `plugin.arrange(opts)` to lay out the graph. */
plugin: AutoLayoutPlugin
/** Convenience: run arrange + refit the viewport. */
arrange: (opts?: LayoutOpts) => Promise<void>
}
// Per-editor plugin handle. The React panel reads this via `runAutoLayout(editor, opts)` so it
// doesn't have to keep a scene object around — `setupAutoLayout` is called in `onReady` and the
// plugin is stashed here for later operations.
const PLUGINS = new WeakMap<XenolithEditor, AutoLayoutPlugin>()
/** Idempotent setup: install the plugin, load the demo graph, fit the view. Safe to pass directly
* to `<XenolithGraph onReady>` — synchronous, no first-paint flicker. */
export function setupAutoLayout(editor: XenolithEditor): void {
const plugin = autoLayoutPlugin({
engine: dagreEngine(),
defaults: { direction: 'LR', spacing: { node: 40, layer: 90 }, animate: { durationMs: 600 } },
})
editor.use(plugin)
PLUGINS.set(editor, plugin)
loadAutoLayoutGraph(editor)
}
/** Run the layout engine in the requested direction and refit. No-op if `setupAutoLayout` hasn't
* run yet on this editor. */
export async function runAutoLayout(editor: XenolithEditor, opts?: LayoutOpts): Promise<void> {
const plugin = PLUGINS.get(editor)
if (!plugin) return
await plugin.arrange(opts)
editor.fitView({ padding: 56, maxZoom: 1 })
}
function loadAutoLayoutGraph(editor: XenolithEditor): void {
const nodes: Node[] = NODES.map((n) => ({
id: n.id as NodeId, type: 'Step', position: { x: n.x, y: n.y }, size: { x: 160, y: 64 },
state: {},
render: { title: n.title, category: 'logic' } as never,
pins: [
{ id: `${n.id}_in` as PinId, kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: `${n.id}_out` as PinId, kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
],
}))
const edges: Edge[] = EDGES.map(([from, to], i) => ({
id: `e${i}` as EdgeId,
from: { node: from as NodeId, pin: `${from}_out` as PinId },
to: { node: to as NodeId, pin: `${to}_in` as PinId },
}))
editor.loadJSON({ version: 'xenolith.v1', nodes, edges })
editor.fitView({ padding: 56, maxZoom: 1 })
}
/** @deprecated Prefer `setupAutoLayout(editor)` + `runAutoLayout(editor, opts)`. Kept for the
* vanilla examples; will be removed in a follow-up. */
export function buildAutoLayout(editor: XenolithEditor): AutoLayoutScene {
setupAutoLayout(editor)
const plugin = PLUGINS.get(editor)!
return {
plugin,
arrange: (opts) => runAutoLayout(editor, opts),
}
} <!-- Svelte — one arrange call is one undo step. Direction is component state. -->
<script lang="ts">
import { xenolith, createXenolithEditorContext } from '@xenolithengine/graph-svelte'
import { XenolithPanel, XenolithButton } from '@xenolithengine/graph-svelte/components'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import { setupAutoLayout, runAutoLayout } from '@xenolithengine/demo/auto-layout'
type Direction = 'LR' | 'TB'
const panelEditor = createXenolithEditorContext()
let editor = $state<XenolithEditor | null>(null)
let dir = $state<Direction>('LR')
let busy = $state(false)
async function arrange(next: Direction = dir): Promise<void> {
if (!editor || busy) return
busy = true
try { await runAutoLayout(editor, { direction: next }) } finally { busy = false }
}
async function flip(next: Direction): Promise<void> {
dir = next
await arrange(next)
}
</script>
<div
use:xenolith={{ resizeToWindow: false }}
onready={(e) => { editor = e.detail; panelEditor.set(e.detail); setupAutoLayout(e.detail) }}
style="position:absolute;inset:0"
></div>
<XenolithPanel position="top-left">
<div style="display:flex;gap:6px;">
<XenolithButton active disabled={busy} onclick={() => arrange()}>{busy ? 'Arranging…' : 'Auto-arrange'}</XenolithButton>
<XenolithButton active={dir === 'LR'} disabled={busy} onclick={() => flip('LR')}>LR</XenolithButton>
<XenolithButton active={dir === 'TB'} disabled={busy} onclick={() => flip('TB')}>TB</XenolithButton>
</div>
</XenolithPanel> // Auto-layout showcase: ~14 ill-placed nodes wired as a fan-in/fan-out DAG. Calling `arrange()`
// runs the dagre engine and applies positions in a single transaction (one undo restores the
// messy original). Reused by both the React demo wrapper and the vanilla mounter — no framework
// in the core; same data + same setup + same plugin.
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import { autoLayoutPlugin, type AutoLayoutPlugin, type LayoutOpts } from '@xenolithengine/graph-plugin-autolayout'
import { dagreEngine } from '@xenolithengine/graph-plugin-autolayout/dagre'
import type { Node, Edge, NodeId, PinId, EdgeId } from '@xenolithengine/graph-core'
const NODES: { id: string; title: string; x: number; y: number }[] = [
{ id: 'in', title: 'Input', x: 480, y: 60 },
{ id: 'a1', title: 'Tokenize', x: 80, y: 320 },
{ id: 'a2', title: 'Embed', x: 730, y: 410 },
{ id: 'a3', title: 'Encode', x: 250, y: 80 },
{ id: 'b1', title: 'Attention', x: 600, y: 220 },
{ id: 'b2', title: 'Norm', x: 30, y: 200 },
{ id: 'b3', title: 'FFN', x: 410, y: 480 },
{ id: 'c1', title: 'Residual', x: 770, y: 100 },
{ id: 'c2', title: 'Merge', x: 120, y: 480 },
{ id: 'd1', title: 'Project', x: 340, y: 260 },
{ id: 'd2', title: 'Logits', x: 540, y: 350 },
{ id: 'd3', title: 'Sample', x: 200, y: 410 },
{ id: 'd4', title: 'Decode', x: 640, y: 60 },
{ id: 'out', title: 'Output', x: 90, y: 130 },
]
const EDGES: [string, string][] = [
['in', 'a1'], ['in', 'a3'], ['in', 'a2'],
['a1', 'b2'], ['a3', 'b1'], ['a2', 'b1'], ['a2', 'b3'],
['b1', 'c1'], ['b1', 'd1'], ['b2', 'c2'], ['b3', 'd1'], ['b3', 'd2'],
['c1', 'd4'], ['c2', 'd3'], ['d1', 'd2'], ['d2', 'd3'],
['d3', 'out'], ['d4', 'out'],
]
export interface AutoLayoutScene {
/** Installed plugin instance — call `plugin.arrange(opts)` to lay out the graph. */
plugin: AutoLayoutPlugin
/** Convenience: run arrange + refit the viewport. */
arrange: (opts?: LayoutOpts) => Promise<void>
}
// Per-editor plugin handle. The React panel reads this via `runAutoLayout(editor, opts)` so it
// doesn't have to keep a scene object around — `setupAutoLayout` is called in `onReady` and the
// plugin is stashed here for later operations.
const PLUGINS = new WeakMap<XenolithEditor, AutoLayoutPlugin>()
/** Idempotent setup: install the plugin, load the demo graph, fit the view. Safe to pass directly
* to `<XenolithGraph onReady>` — synchronous, no first-paint flicker. */
export function setupAutoLayout(editor: XenolithEditor): void {
const plugin = autoLayoutPlugin({
engine: dagreEngine(),
defaults: { direction: 'LR', spacing: { node: 40, layer: 90 }, animate: { durationMs: 600 } },
})
editor.use(plugin)
PLUGINS.set(editor, plugin)
loadAutoLayoutGraph(editor)
}
/** Run the layout engine in the requested direction and refit. No-op if `setupAutoLayout` hasn't
* run yet on this editor. */
export async function runAutoLayout(editor: XenolithEditor, opts?: LayoutOpts): Promise<void> {
const plugin = PLUGINS.get(editor)
if (!plugin) return
await plugin.arrange(opts)
editor.fitView({ padding: 56, maxZoom: 1 })
}
function loadAutoLayoutGraph(editor: XenolithEditor): void {
const nodes: Node[] = NODES.map((n) => ({
id: n.id as NodeId, type: 'Step', position: { x: n.x, y: n.y }, size: { x: 160, y: 64 },
state: {},
render: { title: n.title, category: 'logic' } as never,
pins: [
{ id: `${n.id}_in` as PinId, kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: `${n.id}_out` as PinId, kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
],
}))
const edges: Edge[] = EDGES.map(([from, to], i) => ({
id: `e${i}` as EdgeId,
from: { node: from as NodeId, pin: `${from}_out` as PinId },
to: { node: to as NodeId, pin: `${to}_in` as PinId },
}))
editor.loadJSON({ version: 'xenolith.v1', nodes, edges })
editor.fitView({ padding: 56, maxZoom: 1 })
}
/** @deprecated Prefer `setupAutoLayout(editor)` + `runAutoLayout(editor, opts)`. Kept for the
* vanilla examples; will be removed in a follow-up. */
export function buildAutoLayout(editor: XenolithEditor): AutoLayoutScene {
setupAutoLayout(editor)
const plugin = PLUGINS.get(editor)!
return {
plugin,
arrange: (opts) => runAutoLayout(editor, opts),
}
} // Solid — one arrange call is one undo step. Direction is a signal passed to runAutoLayout.
import { createSignal } from 'solid-js'
import { xenolith } from '@xenolithengine/graph-solid'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import { setupAutoLayout, runAutoLayout } from '@xenolithengine/demo/auto-layout'
type Direction = 'LR' | 'TB'
function btn(on: boolean, disabled: boolean): string {
return `font:inherit;font-size:12px;padding:6px 12px;border-radius:6px;cursor:${disabled ? 'default' : 'pointer'};opacity:${disabled ? 0.4 : 1};border:1px solid ${on ? 'var(--xeno-accent)' : 'var(--xeno-border)'};background:${on ? 'var(--xeno-accent)' : 'var(--xeno-panel)'};color:${on ? 'var(--xeno-canvas)' : 'var(--xeno-text)'};`
}
export function AutoLayoutDemo() {
const [editor, setEditor] = createSignal<XenolithEditor | null>(null)
const [dir, setDir] = createSignal<Direction>('LR')
const [busy, setBusy] = createSignal(false)
async function arrange(next: Direction = dir()): Promise<void> {
const e = editor()
if (!e || busy()) return
setBusy(true)
try { await runAutoLayout(e, { direction: next }) } finally { setBusy(false) }
}
async function flip(next: Direction): Promise<void> {
setDir(next)
await arrange(next)
}
return (
<div
use:xenolith={{ resizeToWindow: false }}
on:ready={(e) => { setEditor(e.detail); setupAutoLayout(e.detail) }}
style="position:absolute;inset:0"
>
<div style="position:absolute;top:12px;left:12px;z-index:5;display:flex;gap:6px;padding:6px;background:var(--xeno-panel);border:1px solid var(--xeno-border);border-radius:10px;">
<button type="button" style={btn(true, busy())} disabled={busy()} onClick={() => void arrange()}>{busy() ? 'Arranging…' : 'Auto-arrange'}</button>
<button type="button" style={btn(dir() === 'LR', busy())} disabled={busy()} onClick={() => void flip('LR')}>LR</button>
<button type="button" style={btn(dir() === 'TB', busy())} disabled={busy()} onClick={() => void flip('TB')}>TB</button>
</div>
</div>
)
} // Auto-layout showcase: ~14 ill-placed nodes wired as a fan-in/fan-out DAG. Calling `arrange()`
// runs the dagre engine and applies positions in a single transaction (one undo restores the
// messy original). Reused by both the React demo wrapper and the vanilla mounter — no framework
// in the core; same data + same setup + same plugin.
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import { autoLayoutPlugin, type AutoLayoutPlugin, type LayoutOpts } from '@xenolithengine/graph-plugin-autolayout'
import { dagreEngine } from '@xenolithengine/graph-plugin-autolayout/dagre'
import type { Node, Edge, NodeId, PinId, EdgeId } from '@xenolithengine/graph-core'
const NODES: { id: string; title: string; x: number; y: number }[] = [
{ id: 'in', title: 'Input', x: 480, y: 60 },
{ id: 'a1', title: 'Tokenize', x: 80, y: 320 },
{ id: 'a2', title: 'Embed', x: 730, y: 410 },
{ id: 'a3', title: 'Encode', x: 250, y: 80 },
{ id: 'b1', title: 'Attention', x: 600, y: 220 },
{ id: 'b2', title: 'Norm', x: 30, y: 200 },
{ id: 'b3', title: 'FFN', x: 410, y: 480 },
{ id: 'c1', title: 'Residual', x: 770, y: 100 },
{ id: 'c2', title: 'Merge', x: 120, y: 480 },
{ id: 'd1', title: 'Project', x: 340, y: 260 },
{ id: 'd2', title: 'Logits', x: 540, y: 350 },
{ id: 'd3', title: 'Sample', x: 200, y: 410 },
{ id: 'd4', title: 'Decode', x: 640, y: 60 },
{ id: 'out', title: 'Output', x: 90, y: 130 },
]
const EDGES: [string, string][] = [
['in', 'a1'], ['in', 'a3'], ['in', 'a2'],
['a1', 'b2'], ['a3', 'b1'], ['a2', 'b1'], ['a2', 'b3'],
['b1', 'c1'], ['b1', 'd1'], ['b2', 'c2'], ['b3', 'd1'], ['b3', 'd2'],
['c1', 'd4'], ['c2', 'd3'], ['d1', 'd2'], ['d2', 'd3'],
['d3', 'out'], ['d4', 'out'],
]
export interface AutoLayoutScene {
/** Installed plugin instance — call `plugin.arrange(opts)` to lay out the graph. */
plugin: AutoLayoutPlugin
/** Convenience: run arrange + refit the viewport. */
arrange: (opts?: LayoutOpts) => Promise<void>
}
// Per-editor plugin handle. The React panel reads this via `runAutoLayout(editor, opts)` so it
// doesn't have to keep a scene object around — `setupAutoLayout` is called in `onReady` and the
// plugin is stashed here for later operations.
const PLUGINS = new WeakMap<XenolithEditor, AutoLayoutPlugin>()
/** Idempotent setup: install the plugin, load the demo graph, fit the view. Safe to pass directly
* to `<XenolithGraph onReady>` — synchronous, no first-paint flicker. */
export function setupAutoLayout(editor: XenolithEditor): void {
const plugin = autoLayoutPlugin({
engine: dagreEngine(),
defaults: { direction: 'LR', spacing: { node: 40, layer: 90 }, animate: { durationMs: 600 } },
})
editor.use(plugin)
PLUGINS.set(editor, plugin)
loadAutoLayoutGraph(editor)
}
/** Run the layout engine in the requested direction and refit. No-op if `setupAutoLayout` hasn't
* run yet on this editor. */
export async function runAutoLayout(editor: XenolithEditor, opts?: LayoutOpts): Promise<void> {
const plugin = PLUGINS.get(editor)
if (!plugin) return
await plugin.arrange(opts)
editor.fitView({ padding: 56, maxZoom: 1 })
}
function loadAutoLayoutGraph(editor: XenolithEditor): void {
const nodes: Node[] = NODES.map((n) => ({
id: n.id as NodeId, type: 'Step', position: { x: n.x, y: n.y }, size: { x: 160, y: 64 },
state: {},
render: { title: n.title, category: 'logic' } as never,
pins: [
{ id: `${n.id}_in` as PinId, kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: `${n.id}_out` as PinId, kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
],
}))
const edges: Edge[] = EDGES.map(([from, to], i) => ({
id: `e${i}` as EdgeId,
from: { node: from as NodeId, pin: `${from}_out` as PinId },
to: { node: to as NodeId, pin: `${to}_in` as PinId },
}))
editor.loadJSON({ version: 'xenolith.v1', nodes, edges })
editor.fitView({ padding: 56, maxZoom: 1 })
}
/** @deprecated Prefer `setupAutoLayout(editor)` + `runAutoLayout(editor, opts)`. Kept for the
* vanilla examples; will be removed in a follow-up. */
export function buildAutoLayout(editor: XenolithEditor): AutoLayoutScene {
setupAutoLayout(editor)
const plugin = PLUGINS.get(editor)!
return {
plugin,
arrange: (opts) => runAutoLayout(editor, opts),
}
} // Angular — one arrange call is one undo step. Direction is a signal passed to runAutoLayout.
import { AfterViewInit, Component, ElementRef, OnDestroy, inject, signal, viewChild } from '@angular/core'
import { XenolithGraphService } from '@xenolithengine/graph-angular'
import { setupAutoLayout, runAutoLayout } from '@xenolithengine/demo/auto-layout'
type Direction = 'LR' | 'TB'
@Component({
selector: 'auto-layout-demo',
standalone: true,
providers: [XenolithGraphService],
template: `
<div style="position:absolute;inset:0;">
<div #host class="xeno" style="position:absolute;inset:0;"></div>
<div class="card">
<button type="button" class="btn on" [disabled]="busy()" (click)="arrange()">
{{ busy() ? 'Arranging…' : 'Auto-arrange' }}
</button>
<button type="button" class="btn" [class.on]="dir() === 'LR'" [disabled]="busy()" (click)="flip('LR')">LR</button>
<button type="button" class="btn" [class.on]="dir() === 'TB'" [disabled]="busy()" (click)="flip('TB')">TB</button>
</div>
</div>
`,
styles: [`
.card { position:absolute; top:12px; left:12px; z-index:5; display:flex; gap:6px; padding:6px;
background:var(--xeno-panel,#1d1d1d); border:1px solid var(--xeno-border,#333);
border-radius:10px; font:13px system-ui,sans-serif; }
.btn { font:inherit; font-size:12px; padding:6px 12px; cursor:pointer; border-radius:6px;
border:1px solid var(--xeno-border,#333); background:var(--xeno-panel,#1d1d1d);
color:var(--xeno-text,#cfcfcf); }
.btn.on { border-color:var(--xeno-accent,#FCB400); background:var(--xeno-accent,#FCB400);
color:var(--xeno-canvas,#111); }
.btn:disabled { opacity:0.4; cursor:default; }
`],
})
export class AutoLayoutDemoComponent implements AfterViewInit, OnDestroy {
private graph = inject(XenolithGraphService)
private host = viewChild.required<ElementRef<HTMLDivElement>>('host')
dir = signal<Direction>('LR')
busy = signal(false)
async ngAfterViewInit(): Promise<void> {
const editor = await this.graph.mount(this.host().nativeElement, { resizeToWindow: false })
setupAutoLayout(editor)
}
async arrange(next: Direction = this.dir()): Promise<void> {
const editor = this.graph.editor
if (!editor || this.busy()) return
this.busy.set(true)
try { await runAutoLayout(editor, { direction: next }) } finally { this.busy.set(false) }
}
async flip(next: Direction): Promise<void> {
this.dir.set(next)
await this.arrange(next)
}
ngOnDestroy(): void { this.graph.destroy() }
} // Auto-layout showcase: ~14 ill-placed nodes wired as a fan-in/fan-out DAG. Calling `arrange()`
// runs the dagre engine and applies positions in a single transaction (one undo restores the
// messy original). Reused by both the React demo wrapper and the vanilla mounter — no framework
// in the core; same data + same setup + same plugin.
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import { autoLayoutPlugin, type AutoLayoutPlugin, type LayoutOpts } from '@xenolithengine/graph-plugin-autolayout'
import { dagreEngine } from '@xenolithengine/graph-plugin-autolayout/dagre'
import type { Node, Edge, NodeId, PinId, EdgeId } from '@xenolithengine/graph-core'
const NODES: { id: string; title: string; x: number; y: number }[] = [
{ id: 'in', title: 'Input', x: 480, y: 60 },
{ id: 'a1', title: 'Tokenize', x: 80, y: 320 },
{ id: 'a2', title: 'Embed', x: 730, y: 410 },
{ id: 'a3', title: 'Encode', x: 250, y: 80 },
{ id: 'b1', title: 'Attention', x: 600, y: 220 },
{ id: 'b2', title: 'Norm', x: 30, y: 200 },
{ id: 'b3', title: 'FFN', x: 410, y: 480 },
{ id: 'c1', title: 'Residual', x: 770, y: 100 },
{ id: 'c2', title: 'Merge', x: 120, y: 480 },
{ id: 'd1', title: 'Project', x: 340, y: 260 },
{ id: 'd2', title: 'Logits', x: 540, y: 350 },
{ id: 'd3', title: 'Sample', x: 200, y: 410 },
{ id: 'd4', title: 'Decode', x: 640, y: 60 },
{ id: 'out', title: 'Output', x: 90, y: 130 },
]
const EDGES: [string, string][] = [
['in', 'a1'], ['in', 'a3'], ['in', 'a2'],
['a1', 'b2'], ['a3', 'b1'], ['a2', 'b1'], ['a2', 'b3'],
['b1', 'c1'], ['b1', 'd1'], ['b2', 'c2'], ['b3', 'd1'], ['b3', 'd2'],
['c1', 'd4'], ['c2', 'd3'], ['d1', 'd2'], ['d2', 'd3'],
['d3', 'out'], ['d4', 'out'],
]
export interface AutoLayoutScene {
/** Installed plugin instance — call `plugin.arrange(opts)` to lay out the graph. */
plugin: AutoLayoutPlugin
/** Convenience: run arrange + refit the viewport. */
arrange: (opts?: LayoutOpts) => Promise<void>
}
// Per-editor plugin handle. The React panel reads this via `runAutoLayout(editor, opts)` so it
// doesn't have to keep a scene object around — `setupAutoLayout` is called in `onReady` and the
// plugin is stashed here for later operations.
const PLUGINS = new WeakMap<XenolithEditor, AutoLayoutPlugin>()
/** Idempotent setup: install the plugin, load the demo graph, fit the view. Safe to pass directly
* to `<XenolithGraph onReady>` — synchronous, no first-paint flicker. */
export function setupAutoLayout(editor: XenolithEditor): void {
const plugin = autoLayoutPlugin({
engine: dagreEngine(),
defaults: { direction: 'LR', spacing: { node: 40, layer: 90 }, animate: { durationMs: 600 } },
})
editor.use(plugin)
PLUGINS.set(editor, plugin)
loadAutoLayoutGraph(editor)
}
/** Run the layout engine in the requested direction and refit. No-op if `setupAutoLayout` hasn't
* run yet on this editor. */
export async function runAutoLayout(editor: XenolithEditor, opts?: LayoutOpts): Promise<void> {
const plugin = PLUGINS.get(editor)
if (!plugin) return
await plugin.arrange(opts)
editor.fitView({ padding: 56, maxZoom: 1 })
}
function loadAutoLayoutGraph(editor: XenolithEditor): void {
const nodes: Node[] = NODES.map((n) => ({
id: n.id as NodeId, type: 'Step', position: { x: n.x, y: n.y }, size: { x: 160, y: 64 },
state: {},
render: { title: n.title, category: 'logic' } as never,
pins: [
{ id: `${n.id}_in` as PinId, kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' },
{ id: `${n.id}_out` as PinId, kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' },
],
}))
const edges: Edge[] = EDGES.map(([from, to], i) => ({
id: `e${i}` as EdgeId,
from: { node: from as NodeId, pin: `${from}_out` as PinId },
to: { node: to as NodeId, pin: `${to}_in` as PinId },
}))
editor.loadJSON({ version: 'xenolith.v1', nodes, edges })
editor.fitView({ padding: 56, maxZoom: 1 })
}
/** @deprecated Prefer `setupAutoLayout(editor)` + `runAutoLayout(editor, opts)`. Kept for the
* vanilla examples; will be removed in a follow-up. */
export function buildAutoLayout(editor: XenolithEditor): AutoLayoutScene {
setupAutoLayout(editor)
const plugin = PLUGINS.get(editor)!
return {
plugin,
arrange: (opts) => runAutoLayout(editor, opts),
}
}