Edge path styles
Per-edge `pathStyle`: bezier (default Xen S-curve), smoothstep (rounded orthogonal), step (90° elbows), linear (straight). Set on construction or live via `editor.setEdgeOptions(id, { pathStyle })`. Same wire colour / animated dash / arrowhead contract regardless of shape.
// Vanilla mount for G9 — edge path styles. DOM buttons in the overlay root flip every wire's
// pathStyle via `editor.setEdgeOptions(id, { pathStyle })`.
import { XenolithEditor } from '@xenolithengine/graph-editor'
import type { EdgePathStyle } from '@xenolithengine/graph-render-pixi'
import { buildEdgePaths } from '@xenolithengine/demo/edge-paths'
const STYLES: EdgePathStyle[] = ['bezier', 'smoothstep', 'step', 'linear']
export async function mount(target: HTMLElement): Promise<() => void> {
const editor = await XenolithEditor.init(target, { resizeToWindow: false, minimap: false })
const scene = buildEdgePaths(editor)
let active: EdgePathStyle | null = null
const panel = document.createElement('div')
panel.setAttribute('data-xeno-panel', '')
panel.style.cssText = 'position:absolute;pointer-events:auto;top:12px;left:12px;display:flex;flex-direction:column;gap:6px;padding:8px;min-width:200px;background:var(--xeno-panel,#1d1d1d);border:1px solid var(--xeno-border,#333);border-radius:8px;font:12px Inter,system-ui,sans-serif;'
const hdr = document.createElement('div')
hdr.style.cssText = 'font-size:11px;color:var(--xeno-muted,#999);text-transform:uppercase;letter-spacing:0.6px;'
hdr.textContent = 'Apply to all'
panel.appendChild(hdr)
const buttons: { s: EdgePathStyle; el: HTMLButtonElement }[] = []
const paint = (): void => {
for (const { s, el } of buttons) {
const on = s === active
el.style.cssText = `padding:6px 12px;font-size:12px;border-radius:6px;cursor:pointer;text-align:left;border:1px solid ${on ? 'var(--xeno-accent,#FCB400)' : 'var(--xeno-border,#333)'};background:${on ? 'var(--xeno-accent,#FCB400)' : 'transparent'};color:${on ? 'var(--xeno-canvas,#111)' : 'var(--xeno-text,#cfcfcf)'};`
}
}
for (const s of STYLES) {
const b = document.createElement('button')
b.textContent = s
b.addEventListener('click', () => { active = s; scene.setAll(s); paint() })
buttons.push({ s, el: b })
panel.appendChild(b)
}
paint()
editor.overlayRoot.appendChild(panel)
return () => { panel.remove(); editor.destroy() }
} // G9 showcase — pluggable edge path styles. Same graph (4 source→sink pairs) rendered with each
// style at once, plus a "set all to X" panel that flips every wire in place via
// `editor.setEdgeOptions(id, { pathStyle })`. No render hacks; uses the public per-edge option.
import type { XenolithEditor, XenolithGraphV1 } from '@xenolithengine/graph-editor'
import type { EdgeId } from '@xenolithengine/graph-core'
import type { EdgePathStyle } from '@xenolithengine/graph-render-pixi'
const STYLES: EdgePathStyle[] = ['bezier', 'smoothstep', 'step', 'linear']
export interface EdgePathsScene {
/** Set every edge in the demo to the chosen style. */
setAll: (style: EdgePathStyle) => void
/** Current per-edge styles (id → style). */
styles: () => Map<EdgeId, EdgePathStyle>
}
/** Edge path styles this demo wires (one row per style). */
export const EDGE_PATH_STYLES: readonly EdgePathStyle[] = STYLES
/** Idempotent setup: load the source/sink rows + apply the per-row pathStyle. */
export function setupEdgePaths(editor: XenolithEditor): void { void buildEdgePaths(editor) }
/** Flip every edge in the loaded demo graph to the same style. */
export function setAllEdgePaths(editor: XenolithEditor, style: EdgePathStyle): void {
for (const s of STYLES) editor.setEdgeOptions(`e_${s}` as EdgeId, { pathStyle: style })
}
/** @deprecated Prefer `setupEdgePaths` + `setAllEdgePaths`. Kept for vanilla examples. */
export function buildEdgePaths(editor: XenolithEditor): EdgePathsScene {
const nodes: XenolithGraphV1['nodes'] = []
const edges: XenolithGraphV1['edges'] = []
const edgeStyle = new Map<EdgeId, EdgePathStyle>()
const colW = 360
// Sink is offset vertically from its source so every style's signature shape actually shows up:
// a horizontal pair makes bezier/smoothstep/step/linear all look like the same flat wire. Half
// the sinks dip down, the other half rise — alternating directions surfaces the curves AND
// keeps each row visually separated from its neighbours.
const ROW_SPACING = 200
const SINK_OFFSET_Y = 90
for (let i = 0; i < STYLES.length; i++) {
const style = STYLES[i]!
const srcY = 60 + i * ROW_SPACING
const sinkY = srcY + (i % 2 === 0 ? SINK_OFFSET_Y : -SINK_OFFSET_Y)
const srcId = `src_${style}`
const sinkId = `sink_${style}`
nodes.push({
id: srcId, type: 'Src', position: { x: 60, y: srcY }, size: { x: 140, y: 60 },
state: {}, render: { title: 'Source', category: 'data' },
pins: [{ id: `${srcId}_o`, kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' }],
})
nodes.push({
id: sinkId, type: 'Sink', position: { x: 60 + colW, y: sinkY }, size: { x: 140, y: 60 },
state: {}, render: { title: style, category: 'utility' },
pins: [{ id: `${sinkId}_i`, kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' }],
})
const eId = `e_${style}`
edges.push({ id: eId, from: { node: srcId, pin: `${srcId}_o` }, to: { node: sinkId, pin: `${sinkId}_i` } })
edgeStyle.set(eId as EdgeId, style)
}
editor.loadJSON({ version: 'xenolith.v1', nodes, edges })
// Apply per-edge pathStyle AFTER load so each wire takes its row's style. setEdgeOptions
// preserves the existing options (sourceType, label, etc.) — only the listed keys change.
for (const [id, style] of edgeStyle) editor.setEdgeOptions(id, { pathStyle: style })
editor.fitView({ padding: 56, maxZoom: 1 })
return {
setAll: (style) => {
for (const id of edgeStyle.keys()) {
edgeStyle.set(id, style)
editor.setEdgeOptions(id, { pathStyle: style })
}
},
styles: () => new Map(edgeStyle),
}
} import { useState } from 'react'
import { XenolithGraph, XenolithPanel, useEditor } from '@xenolithengine/graph-react'
import { setupEdgePaths, setAllEdgePaths, EDGE_PATH_STYLES } from '@xenolithengine/demo/edge-paths'
import type { EdgePathStyle } from '@xenolithengine/graph-render-pixi'
import { DemoStage } from '../Layout.js'
// Canon: active style lives in the panel; flipping it dispatches through the editor directly.
// The initial 'each' view (per-row distinct styles) is what `setupEdgePaths` lays down — once
// the user picks a single style there's no going back without a re-mount, by design.
function EdgePathsPanel() {
const editor = useEditor()
const [active, setActive] = useState<EdgePathStyle | 'each'>('each')
const flip = (s: EdgePathStyle): void => {
setActive(s)
setAllEdgePaths(editor, s)
}
return (
<XenolithPanel position="top-left" style={{ display: 'flex', flexDirection: 'column', gap: 6, padding: 8, minWidth: 200 }}>
<div style={{ fontSize: 11, color: 'var(--xeno-muted, #999)', textTransform: 'uppercase', letterSpacing: 0.6 }}>Apply to all</div>
{EDGE_PATH_STYLES.map((s) => (
<button key={s} onClick={() => flip(s)} style={btn(active === s)}>{s}</button>
))}
</XenolithPanel>
)
}
/** Island: G9 — edge path styles. */
export function EdgePathsDemo() {
return (
<DemoStage>
<XenolithGraph className="xeno" resizeToWindow={false} onReady={setupEdgePaths}>
<EdgePathsPanel />
</XenolithGraph>
</DemoStage>
)
}
const btn = (on: boolean): React.CSSProperties => ({
padding: '6px 12px',
fontSize: 12,
borderRadius: 6,
border: `1px solid ${on ? 'var(--xeno-accent, #FCB400)' : 'var(--xeno-border, #333)'}`,
background: on ? 'var(--xeno-accent, #FCB400)' : 'transparent',
color: on ? 'var(--xeno-canvas, #111)' : 'var(--xeno-text, #cfcfcf)',
cursor: 'pointer',
textAlign: 'left',
}) // G9 showcase — pluggable edge path styles. Same graph (4 source→sink pairs) rendered with each
// style at once, plus a "set all to X" panel that flips every wire in place via
// `editor.setEdgeOptions(id, { pathStyle })`. No render hacks; uses the public per-edge option.
import type { XenolithEditor, XenolithGraphV1 } from '@xenolithengine/graph-editor'
import type { EdgeId } from '@xenolithengine/graph-core'
import type { EdgePathStyle } from '@xenolithengine/graph-render-pixi'
const STYLES: EdgePathStyle[] = ['bezier', 'smoothstep', 'step', 'linear']
export interface EdgePathsScene {
/** Set every edge in the demo to the chosen style. */
setAll: (style: EdgePathStyle) => void
/** Current per-edge styles (id → style). */
styles: () => Map<EdgeId, EdgePathStyle>
}
/** Edge path styles this demo wires (one row per style). */
export const EDGE_PATH_STYLES: readonly EdgePathStyle[] = STYLES
/** Idempotent setup: load the source/sink rows + apply the per-row pathStyle. */
export function setupEdgePaths(editor: XenolithEditor): void { void buildEdgePaths(editor) }
/** Flip every edge in the loaded demo graph to the same style. */
export function setAllEdgePaths(editor: XenolithEditor, style: EdgePathStyle): void {
for (const s of STYLES) editor.setEdgeOptions(`e_${s}` as EdgeId, { pathStyle: style })
}
/** @deprecated Prefer `setupEdgePaths` + `setAllEdgePaths`. Kept for vanilla examples. */
export function buildEdgePaths(editor: XenolithEditor): EdgePathsScene {
const nodes: XenolithGraphV1['nodes'] = []
const edges: XenolithGraphV1['edges'] = []
const edgeStyle = new Map<EdgeId, EdgePathStyle>()
const colW = 360
// Sink is offset vertically from its source so every style's signature shape actually shows up:
// a horizontal pair makes bezier/smoothstep/step/linear all look like the same flat wire. Half
// the sinks dip down, the other half rise — alternating directions surfaces the curves AND
// keeps each row visually separated from its neighbours.
const ROW_SPACING = 200
const SINK_OFFSET_Y = 90
for (let i = 0; i < STYLES.length; i++) {
const style = STYLES[i]!
const srcY = 60 + i * ROW_SPACING
const sinkY = srcY + (i % 2 === 0 ? SINK_OFFSET_Y : -SINK_OFFSET_Y)
const srcId = `src_${style}`
const sinkId = `sink_${style}`
nodes.push({
id: srcId, type: 'Src', position: { x: 60, y: srcY }, size: { x: 140, y: 60 },
state: {}, render: { title: 'Source', category: 'data' },
pins: [{ id: `${srcId}_o`, kind: 'data', direction: 'out', type: 'float', multiple: true, label: 'out' }],
})
nodes.push({
id: sinkId, type: 'Sink', position: { x: 60 + colW, y: sinkY }, size: { x: 140, y: 60 },
state: {}, render: { title: style, category: 'utility' },
pins: [{ id: `${sinkId}_i`, kind: 'data', direction: 'in', type: 'float', multiple: false, label: 'in' }],
})
const eId = `e_${style}`
edges.push({ id: eId, from: { node: srcId, pin: `${srcId}_o` }, to: { node: sinkId, pin: `${sinkId}_i` } })
edgeStyle.set(eId as EdgeId, style)
}
editor.loadJSON({ version: 'xenolith.v1', nodes, edges })
// Apply per-edge pathStyle AFTER load so each wire takes its row's style. setEdgeOptions
// preserves the existing options (sourceType, label, etc.) — only the listed keys change.
for (const [id, style] of edgeStyle) editor.setEdgeOptions(id, { pathStyle: style })
editor.fitView({ padding: 56, maxZoom: 1 })
return {
setAll: (style) => {
for (const id of edgeStyle.keys()) {
edgeStyle.set(id, style)
editor.setEdgeOptions(id, { pathStyle: style })
}
},
styles: () => new Map(edgeStyle),
}
}