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),
}
} <script setup lang="ts">
// Vue — one click sets every edge to that path style. 'each' is the layout setupEdgePaths starts with.
import { ref } from 'vue'
import { XenolithGraph, XenolithPanel, XenolithButton } from '@xenolithengine/graph-vue'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import type { EdgePathStyle } from '@xenolithengine/graph-render-pixi'
import { setupEdgePaths, setAllEdgePaths, EDGE_PATH_STYLES } from '@xenolithengine/demo/edge-paths'
const editor = ref<XenolithEditor | null>(null)
const active = ref<EdgePathStyle | 'each'>('each')
const styles = EDGE_PATH_STYLES
function onReady(e: XenolithEditor): void {
editor.value = e
setupEdgePaths(e)
}
function flip(style: EdgePathStyle): void {
const e = editor.value
if (!e) return
active.value = style
setAllEdgePaths(e, style)
}
</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;flex-direction:column;gap:6px;min-width:200px;">
<div style="font-size:11px;color:var(--xeno-muted);text-transform:uppercase;letter-spacing:.06em;">Apply to all</div>
<XenolithButton
v-for="style in styles"
:key="style"
style="text-align:left"
:active="active === style"
@click="flip(style)"
>{{ style }}</XenolithButton>
</div>
</XenolithPanel>
</XenolithGraph>
</div>
</template> // 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),
}
} <!-- Svelte — one click sets every edge to that path style. -->
<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 type { EdgePathStyle } from '@xenolithengine/graph-render-pixi'
import { setupEdgePaths, setAllEdgePaths, EDGE_PATH_STYLES } from '@xenolithengine/demo/edge-paths'
const panelEditor = createXenolithEditorContext()
let editor = $state<XenolithEditor | null>(null)
let active = $state<EdgePathStyle | 'each'>('each')
function flip(style: EdgePathStyle): void {
if (!editor) return
active = style
setAllEdgePaths(editor, style)
}
</script>
<div
use:xenolith={{ resizeToWindow: false }}
onready={(e) => { editor = e.detail; panelEditor.set(e.detail); setupEdgePaths(e.detail) }}
style="position:absolute;inset:0"
></div>
<XenolithPanel position="top-left">
<div style="display:flex;flex-direction:column;gap:6px;min-width:200px;">
<div style="font-size:11px;color:var(--xeno-muted);text-transform:uppercase;letter-spacing:.06em;">Apply to all</div>
{#each EDGE_PATH_STYLES as style (style)}
<XenolithButton style="text-align:left" active={active === style} onclick={() => flip(style)}>{style}</XenolithButton>
{/each}
</div>
</XenolithPanel> // 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),
}
} // Solid — one click sets every edge to that path style.
import { createSignal, For } from 'solid-js'
import { xenolith } from '@xenolithengine/graph-solid'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import type { EdgePathStyle } from '@xenolithengine/graph-render-pixi'
import { setupEdgePaths, setAllEdgePaths, EDGE_PATH_STYLES } from '@xenolithengine/demo/edge-paths'
export function EdgePathsDemo() {
const [editor, setEditor] = createSignal<XenolithEditor | null>(null)
const [active, setActive] = createSignal<EdgePathStyle | 'each'>('each')
function flip(style: EdgePathStyle): void {
const e = editor()
if (!e) return
setActive(style)
setAllEdgePaths(e, style)
}
return (
<div
use:xenolith={{ resizeToWindow: false }}
on:ready={(e) => { setEditor(e.detail); setupEdgePaths(e.detail) }}
style="position:absolute;inset:0"
>
<div style="position:absolute;top:12px;left:12px;z-index:5;display:flex;flex-direction:column;gap:6px;min-width:200px;padding:8px;background:var(--xeno-panel);border:1px solid var(--xeno-border);border-radius:10px;color:var(--xeno-text);font:13px system-ui,sans-serif;">
<div style="font-size:11px;color:var(--xeno-muted);text-transform:uppercase;letter-spacing:.06em;">Apply to all</div>
<For each={EDGE_PATH_STYLES}>{(style) =>
<button
type="button"
style={`padding:6px 12px;font-size:12px;text-align:left;cursor:pointer;border-radius:6px;border:1px solid ${active() === style ? 'var(--xeno-accent)' : 'var(--xeno-border)'};background:${active() === style ? 'var(--xeno-accent)' : 'transparent'};color:${active() === style ? 'var(--xeno-canvas)' : 'var(--xeno-text)'};`}
onClick={() => flip(style)}
>{style}</button>
}</For>
</div>
</div>
)
} // 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),
}
} // Angular — one click sets every edge to that path style.
import { AfterViewInit, Component, ElementRef, OnDestroy, inject, signal, viewChild } from '@angular/core'
import { XenolithGraphService } from '@xenolithengine/graph-angular'
import type { EdgePathStyle } from '@xenolithengine/graph-render-pixi'
import { setupEdgePaths, setAllEdgePaths, EDGE_PATH_STYLES } from '@xenolithengine/demo/edge-paths'
@Component({
selector: 'edge-paths-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">
<div class="label">Apply to all</div>
@for (style of styles; track style) {
<button type="button" class="btn" [class.on]="active() === style" (click)="flip(style)">{{ style }}</button>
}
</div>
</div>
`,
styles: [`
.card { position:absolute; top:12px; left:12px; z-index:5; display:flex; flex-direction:column;
gap:6px; min-width:200px; padding:8px; background:var(--xeno-panel,#1d1d1d);
border:1px solid var(--xeno-border,#333); border-radius:10px;
color:var(--xeno-text,#cfcfcf); font:13px system-ui,sans-serif; }
.label { font-size:11px; color:var(--xeno-muted,#9a9a9a); text-transform:uppercase; letter-spacing:.06em; }
.btn { padding:6px 12px; font-size:12px; text-align:left; cursor:pointer; border-radius:6px;
border:1px solid var(--xeno-border,#333); background:transparent; color:var(--xeno-text,#cfcfcf); }
.btn.on { border-color:var(--xeno-accent,#FCB400); background:var(--xeno-accent,#FCB400);
color:var(--xeno-canvas,#111); }
`],
})
export class EdgePathsDemoComponent implements AfterViewInit, OnDestroy {
private graph = inject(XenolithGraphService)
private host = viewChild.required<ElementRef<HTMLDivElement>>('host')
readonly styles = EDGE_PATH_STYLES
active = signal<EdgePathStyle | 'each'>('each')
async ngAfterViewInit(): Promise<void> {
const editor = await this.graph.mount(this.host().nativeElement, { resizeToWindow: false })
setupEdgePaths(editor)
}
flip(style: EdgePathStyle): void {
const editor = this.graph.editor
if (!editor) return
this.active.set(style)
setAllEdgePaths(editor, style)
}
ngOnDestroy(): void { this.graph.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),
}
}