Save & restore
The whole graph is JSON. Download / upload a .json file, and autosave to localStorage on every edit (driven by useGraphJSON). Reload the page — it comes back.
import { useEffect, useRef, useState } from 'react'
import { XenolithGraph, XenolithControls, XenolithPanel, XenolithButton, useEditor, useGraphJSON } from '@xenolithengine/graph-react'
import { initSaveRestore, downloadGraph, uploadGraph, saveToLocal, restoreFromLocal, hasSaved } from '@xenolithengine/demo/save-restore'
import { DemoStage } from '../Layout.js'
// Persistence: the whole graph is JSON (editor.toJSON ⇄ loadJSON). The file + localStorage helpers
// live in the framework-agnostic core; only the autosave-on-edit is React here, since it rides
// useGraphJSON's reactive change stream. `useEditor()` is strict — the panel only renders inside
// <XenolithGraph>, after the editor has mounted, so we never hold null.
function SavePanel() {
const editor = useEditor()
const json = useGraphJSON()
const [savedAt, setSavedAt] = useState<number | null>(null)
const fileRef = useRef<HTMLInputElement | null>(null)
const first = useRef(true)
// Autosave (debounced) whenever the graph changes — useGraphJSON re-emits on edits.
useEffect(() => {
if (!json) return
if (first.current) { first.current = false; return } // skip the initial load
const t = setTimeout(() => { saveToLocal(editor); setSavedAt(Date.now()) }, 500)
return () => clearTimeout(t)
}, [json, editor])
const btn: React.CSSProperties = { width: '100%' }
return (
<XenolithPanel position="top-left" style={{ display: 'flex', flexDirection: 'column', gap: 6, width: 180 }}>
<p style={{ margin: 0, fontSize: 11, textTransform: 'uppercase', letterSpacing: '.05em', color: 'var(--xeno-muted)' }}>Save / restore</p>
<XenolithButton style={btn} onClick={() => downloadGraph(editor)}>↓ Download .json</XenolithButton>
<XenolithButton style={btn} onClick={() => fileRef.current?.click()}>↑ Upload .json</XenolithButton>
<XenolithButton style={btn} onClick={() => restoreFromLocal(editor)} disabled={savedAt === null && !hasSaved()}>↺ Restore last</XenolithButton>
<span style={{ color: 'var(--xeno-muted)', fontSize: 11, lineHeight: 1.4 }}>
{savedAt ? '✓ Autosaved to your browser' : 'Edit the graph — it autosaves to localStorage.'}
</span>
<input ref={fileRef} type="file" accept="application/json,.json" hidden onChange={(e) => { const f = e.target.files?.[0]; if (f) uploadGraph(editor, f) }} />
</XenolithPanel>
)
}
/** Showcase: save & restore. Restores the last autosave on load, else the demo graph. */
export function SaveRestoreDemo() {
return (
<DemoStage>
<XenolithGraph className="xeno" resizeToWindow={false} onReady={initSaveRestore}>
<XenolithControls position="bottom-left" />
<SavePanel />
</XenolithGraph>
</DemoStage>
)
} // Persistence: the whole graph is JSON (editor.exportJSON / loadJSON). These framework-agnostic
// helpers cover the real save/restore surface — download/upload a .json file, and save/restore the
// last graph from localStorage. The host wires them to buttons; the only framework-reactive bit
// (autosave on every edit) stays in the host because it rides that framework's change subscription.
import { loadDemo } from './scene.js'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
export const SAVE_KEY = 'xeno:save-restore-demo'
function download(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url; a.download = filename
document.body.appendChild(a); a.click(); a.remove()
setTimeout(() => URL.revokeObjectURL(url), 0)
}
export function downloadGraph(editor: XenolithEditor): void {
download(editor.exportJSON(), 'graph.json')
}
export function uploadGraph(editor: XenolithEditor, file: File): void {
void file.text().then((t) => {
try { editor.loadJSON(JSON.parse(t)); editor.fitView({ padding: 48, maxZoom: 1 }) } catch { /* bad file */ }
})
}
export function saveToLocal(editor: XenolithEditor): void {
localStorage.setItem(SAVE_KEY, JSON.stringify(editor.toJSON()))
}
export function hasSaved(): boolean {
return localStorage.getItem(SAVE_KEY) !== null
}
export function restoreFromLocal(editor: XenolithEditor): boolean {
const s = localStorage.getItem(SAVE_KEY)
if (!s) return false
try { editor.loadJSON(JSON.parse(s)); editor.fitView({ padding: 48, maxZoom: 1 }); return true } catch { return false }
}
/** Initial load: the demo graph, then the last autosave on top if present. */
export function initSaveRestore(editor: XenolithEditor): void {
loadDemo(editor)
restoreFromLocal(editor)
} <script setup lang="ts">
// Vue SFC — save & restore. Framework-agnostic helpers (download/upload/autosave/restore) live in
// the shared package; this SFC is just the Vue-specific glue: emit `@ready` for one-time setup,
// host the panel as a child component (uses `useEditor` inside).
import { XenolithGraph } from '@xenolithengine/graph-vue'
import { initSaveRestore } from '@xenolithengine/demo/save-restore'
import SavePanel from './SavePanel.vue'
</script>
<template>
<div class="app" style="position:absolute;inset:0;">
<XenolithGraph class="xeno" :resize-to-window="false" @ready="initSaveRestore">
<SavePanel />
</XenolithGraph>
</div>
</template> <script setup lang="ts">
// Autosave on every edit: subscribe to `graph:loaded` + `history:changed` (post-edit signal). The
// shared helpers do the actual IO; we only debounce + flash a status string.
import { ref } from 'vue'
import { useEditor, useEditorEvent } from '@xenolithengine/graph-vue'
import {
downloadGraph, uploadGraph, saveToLocal, restoreFromLocal, hasSaved,
} from '@xenolithengine/demo/save-restore'
const editor = useEditor()
const savedAt = ref<number | null>(null)
const fileInput = ref<HTMLInputElement | null>(null)
let timer: ReturnType<typeof setTimeout> | null = null
let first = true
function scheduleAutosave(): void {
if (first) { first = false; return }
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
const e = editor.value
if (!e) return
saveToLocal(e)
savedAt.value = Date.now()
}, 500)
}
useEditorEvent('history:changed', scheduleAutosave)
function onDownload(): void { const e = editor.value; if (e) downloadGraph(e) }
function onUpload(ev: Event): void {
const f = (ev.target as HTMLInputElement).files?.[0]
const e = editor.value
if (f && e) uploadGraph(e, f)
}
function onRestore(): void { const e = editor.value; if (e) restoreFromLocal(e) }
</script>
<template>
<div data-xeno-panel class="panel">
<p class="label">Save / restore</p>
<button class="btn" @click="onDownload">↓ Download .json</button>
<button class="btn" @click="fileInput?.click()">↑ Upload .json</button>
<button class="btn" :disabled="savedAt === null && !hasSaved()" @click="onRestore">↺ Restore last</button>
<span class="status">{{ savedAt ? '✓ Autosaved to your browser' : 'Edit the graph — it autosaves to localStorage.' }}</span>
<input ref="fileInput" type="file" accept="application/json,.json" hidden @change="onUpload" />
</div>
</template>
<style scoped>
.panel {
position: absolute; top: 12px; left: 12px;
display: flex; flex-direction: column; gap: 6px; width: 180px;
padding: 10px;
background: var(--xeno-panel, #1d1d1d);
border: 1px solid var(--xeno-border, #333);
border-radius: 8px;
font: 12px Inter, system-ui, sans-serif;
color: var(--xeno-text, #cfcfcf);
z-index: 5;
}
.label { margin: 0; font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--xeno-muted, #9a9a9a); }
.btn { width: 100%; padding: 6px 10px; font: inherit; font-size: 12px; cursor: pointer;
border: 1px solid var(--xeno-border, #333); border-radius: 6px;
background: transparent; color: var(--xeno-text, #cfcfcf); }
.btn:hover:not(:disabled) { background: #2a2a2a; }
.btn:disabled { opacity: .5; cursor: default; }
.status { color: var(--xeno-muted, #9a9a9a); font-size: 11px; line-height: 1.4; }
</style> // Persistence: the whole graph is JSON (editor.exportJSON / loadJSON). These framework-agnostic
// helpers cover the real save/restore surface — download/upload a .json file, and save/restore the
// last graph from localStorage. The host wires them to buttons; the only framework-reactive bit
// (autosave on every edit) stays in the host because it rides that framework's change subscription.
import { loadDemo } from './scene.js'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
export const SAVE_KEY = 'xeno:save-restore-demo'
function download(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url; a.download = filename
document.body.appendChild(a); a.click(); a.remove()
setTimeout(() => URL.revokeObjectURL(url), 0)
}
export function downloadGraph(editor: XenolithEditor): void {
download(editor.exportJSON(), 'graph.json')
}
export function uploadGraph(editor: XenolithEditor, file: File): void {
void file.text().then((t) => {
try { editor.loadJSON(JSON.parse(t)); editor.fitView({ padding: 48, maxZoom: 1 }) } catch { /* bad file */ }
})
}
export function saveToLocal(editor: XenolithEditor): void {
localStorage.setItem(SAVE_KEY, JSON.stringify(editor.toJSON()))
}
export function hasSaved(): boolean {
return localStorage.getItem(SAVE_KEY) !== null
}
export function restoreFromLocal(editor: XenolithEditor): boolean {
const s = localStorage.getItem(SAVE_KEY)
if (!s) return false
try { editor.loadJSON(JSON.parse(s)); editor.fitView({ padding: 48, maxZoom: 1 }); return true } catch { return false }
}
/** Initial load: the demo graph, then the last autosave on top if present. */
export function initSaveRestore(editor: XenolithEditor): void {
loadDemo(editor)
restoreFromLocal(editor)
} // Svelte adapter — save & restore. Imperative primitive (`createXenolithGraph`) for editor access;
// shared helpers (`downloadGraph` / `uploadGraph` / `saveToLocal` / `restoreFromLocal`) do the IO.
// Autosave rides the binding's `history:changed` event.
import { createXenolithGraph } from '@xenolithengine/graph-svelte'
import {
initSaveRestore, downloadGraph, uploadGraph, saveToLocal, restoreFromLocal, hasSaved,
} from '@xenolithengine/demo/save-restore'
export async function mount(target: HTMLElement): Promise<() => void> {
const slot = document.createElement('div')
slot.style.cssText = 'position:absolute;inset:0;'
target.appendChild(slot)
const binding = await createXenolithGraph(slot, { resizeToWindow: false })
initSaveRestore(binding.editor)
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;width:180px;padding:10px;background:var(--xeno-panel,#1d1d1d);border:1px solid var(--xeno-border,#333);border-radius:8px;font:12px Inter,system-ui,sans-serif;color:var(--xeno-text,#cfcfcf);z-index:5;'
const fileInput = document.createElement('input')
fileInput.type = 'file'; fileInput.accept = 'application/json,.json'; fileInput.hidden = true
const status = document.createElement('span')
status.style.cssText = 'color:var(--xeno-muted,#9a9a9a);font-size:11px;line-height:1.4;'
status.textContent = 'Edit the graph — it autosaves to localStorage.'
const btn = (text: string, on: () => void, opts?: { disabled?: () => boolean }) => {
const b = document.createElement('button')
b.textContent = text
b.style.cssText = 'width:100%;padding:6px 10px;font:inherit;font-size:12px;cursor:pointer;border:1px solid var(--xeno-border,#333);border-radius:6px;background:transparent;color:var(--xeno-text,#cfcfcf);'
b.addEventListener('click', on)
if (opts?.disabled) b.disabled = opts.disabled()
return b
}
const restoreBtn = btn('↺ Restore last', () => restoreFromLocal(binding.editor), { disabled: () => !hasSaved() })
panel.append(
Object.assign(document.createElement('p'), { textContent: 'Save / restore', style: 'margin:0;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--xeno-muted,#9a9a9a);' as unknown as string }),
btn('↓ Download .json', () => downloadGraph(binding.editor)),
btn('↑ Upload .json', () => fileInput.click()),
restoreBtn,
status,
fileInput,
)
fileInput.addEventListener('change', () => { const f = fileInput.files?.[0]; if (f) uploadGraph(binding.editor, f) })
let timer: ReturnType<typeof setTimeout> | null = null
let first = true
const off = binding.on('history:changed', () => {
if (first) { first = false; return }
if (timer) clearTimeout(timer)
timer = setTimeout(() => { saveToLocal(binding.editor); status.textContent = '✓ Autosaved to your browser'; restoreBtn.disabled = false }, 500)
})
binding.editor.overlayRoot.appendChild(panel)
return () => { off(); panel.remove(); binding.destroy(); slot.remove() }
} // Persistence: the whole graph is JSON (editor.exportJSON / loadJSON). These framework-agnostic
// helpers cover the real save/restore surface — download/upload a .json file, and save/restore the
// last graph from localStorage. The host wires them to buttons; the only framework-reactive bit
// (autosave on every edit) stays in the host because it rides that framework's change subscription.
import { loadDemo } from './scene.js'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
export const SAVE_KEY = 'xeno:save-restore-demo'
function download(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url; a.download = filename
document.body.appendChild(a); a.click(); a.remove()
setTimeout(() => URL.revokeObjectURL(url), 0)
}
export function downloadGraph(editor: XenolithEditor): void {
download(editor.exportJSON(), 'graph.json')
}
export function uploadGraph(editor: XenolithEditor, file: File): void {
void file.text().then((t) => {
try { editor.loadJSON(JSON.parse(t)); editor.fitView({ padding: 48, maxZoom: 1 }) } catch { /* bad file */ }
})
}
export function saveToLocal(editor: XenolithEditor): void {
localStorage.setItem(SAVE_KEY, JSON.stringify(editor.toJSON()))
}
export function hasSaved(): boolean {
return localStorage.getItem(SAVE_KEY) !== null
}
export function restoreFromLocal(editor: XenolithEditor): boolean {
const s = localStorage.getItem(SAVE_KEY)
if (!s) return false
try { editor.loadJSON(JSON.parse(s)); editor.fitView({ padding: 48, maxZoom: 1 }); return true } catch { return false }
}
/** Initial load: the demo graph, then the last autosave on top if present. */
export function initSaveRestore(editor: XenolithEditor): void {
loadDemo(editor)
restoreFromLocal(editor)
} // Solid adapter — save & restore. Mirrors the Svelte adapter version; uses the imperative
// `createXenolithGraph` primitive so we can register schemas and drive autosave via the binding's
// event channel. Solid hosts using the directive would do this via `createEffect` + `on:` bindings.
import { createXenolithGraph } from '@xenolithengine/graph-solid'
import {
initSaveRestore, downloadGraph, uploadGraph, saveToLocal, restoreFromLocal, hasSaved,
} from '@xenolithengine/demo/save-restore'
export async function mount(target: HTMLElement): Promise<() => void> {
const slot = document.createElement('div')
slot.style.cssText = 'position:absolute;inset:0;'
target.appendChild(slot)
const binding = await createXenolithGraph(slot, { resizeToWindow: false })
initSaveRestore(binding.editor)
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;width:180px;padding:10px;background:var(--xeno-panel,#1d1d1d);border:1px solid var(--xeno-border,#333);border-radius:8px;font:12px Inter,system-ui,sans-serif;color:var(--xeno-text,#cfcfcf);z-index:5;'
const fileInput = document.createElement('input')
fileInput.type = 'file'; fileInput.accept = 'application/json,.json'; fileInput.hidden = true
const status = document.createElement('span')
status.style.cssText = 'color:var(--xeno-muted,#9a9a9a);font-size:11px;line-height:1.4;'
status.textContent = 'Edit the graph — it autosaves to localStorage.'
const btn = (text: string, on: () => void) => {
const b = document.createElement('button')
b.textContent = text
b.style.cssText = 'width:100%;padding:6px 10px;font:inherit;font-size:12px;cursor:pointer;border:1px solid var(--xeno-border,#333);border-radius:6px;background:transparent;color:var(--xeno-text,#cfcfcf);'
b.addEventListener('click', on)
return b
}
const restoreBtn = btn('↺ Restore last', () => restoreFromLocal(binding.editor))
restoreBtn.disabled = !hasSaved()
const label = document.createElement('p')
label.textContent = 'Save / restore'
label.style.cssText = 'margin:0;font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:var(--xeno-muted,#9a9a9a);'
panel.append(
label,
btn('↓ Download .json', () => downloadGraph(binding.editor)),
btn('↑ Upload .json', () => fileInput.click()),
restoreBtn,
status,
fileInput,
)
fileInput.addEventListener('change', () => { const f = fileInput.files?.[0]; if (f) uploadGraph(binding.editor, f) })
let timer: ReturnType<typeof setTimeout> | null = null
let first = true
const off = binding.on('history:changed', () => {
if (first) { first = false; return }
if (timer) clearTimeout(timer)
timer = setTimeout(() => { saveToLocal(binding.editor); status.textContent = '✓ Autosaved to your browser'; restoreBtn.disabled = false }, 500)
})
binding.editor.overlayRoot.appendChild(panel)
return () => { off(); panel.remove(); binding.destroy(); slot.remove() }
} // Persistence: the whole graph is JSON (editor.exportJSON / loadJSON). These framework-agnostic
// helpers cover the real save/restore surface — download/upload a .json file, and save/restore the
// last graph from localStorage. The host wires them to buttons; the only framework-reactive bit
// (autosave on every edit) stays in the host because it rides that framework's change subscription.
import { loadDemo } from './scene.js'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
export const SAVE_KEY = 'xeno:save-restore-demo'
function download(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url; a.download = filename
document.body.appendChild(a); a.click(); a.remove()
setTimeout(() => URL.revokeObjectURL(url), 0)
}
export function downloadGraph(editor: XenolithEditor): void {
download(editor.exportJSON(), 'graph.json')
}
export function uploadGraph(editor: XenolithEditor, file: File): void {
void file.text().then((t) => {
try { editor.loadJSON(JSON.parse(t)); editor.fitView({ padding: 48, maxZoom: 1 }) } catch { /* bad file */ }
})
}
export function saveToLocal(editor: XenolithEditor): void {
localStorage.setItem(SAVE_KEY, JSON.stringify(editor.toJSON()))
}
export function hasSaved(): boolean {
return localStorage.getItem(SAVE_KEY) !== null
}
export function restoreFromLocal(editor: XenolithEditor): boolean {
const s = localStorage.getItem(SAVE_KEY)
if (!s) return false
try { editor.loadJSON(JSON.parse(s)); editor.fitView({ padding: 48, maxZoom: 1 }); return true } catch { return false }
}
/** Initial load: the demo graph, then the last autosave on top if present. */
export function initSaveRestore(editor: XenolithEditor): void {
loadDemo(editor)
restoreFromLocal(editor)
} // Angular standalone component — save & restore. Imperative IO via the shared helpers; autosave
// rides the `(historyChanged)` Output. Editor reference is captured from `(ready)` once.
import { Component, signal, ViewChild, ElementRef } from '@angular/core'
import { XenolithGraphComponent } from '@xenolithengine/graph-angular'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
import {
initSaveRestore, downloadGraph, uploadGraph, saveToLocal, restoreFromLocal, hasSaved,
} from '@xenolithengine/demo/save-restore'
@Component({
selector: 'save-restore-demo',
standalone: true,
imports: [XenolithGraphComponent],
template: `
<div class="app" style="position:absolute;inset:0;">
<xenolith-graph
class="xeno"
[resizeToWindow]="false"
(ready)="onReady($event)"
(historyChanged)="scheduleAutosave()">
</xenolith-graph>
<div data-xeno-panel class="panel">
<p class="label">Save / restore</p>
<button class="btn" (click)="download()">↓ Download .json</button>
<button class="btn" (click)="fileInput.click()">↑ Upload .json</button>
<button class="btn" [disabled]="savedAt() === null && !hasSaved" (click)="restore()">↺ Restore last</button>
<span class="status">{{ savedAt() ? '✓ Autosaved to your browser' : 'Edit the graph — it autosaves to localStorage.' }}</span>
<input #fileInput type="file" accept="application/json,.json" hidden (change)="onUpload($event)" />
</div>
</div>
`,
styles: [\`
.panel { position:absolute; top:12px; left:12px; display:flex; flex-direction:column; gap:6px;
width:180px; padding:10px; background:var(--xeno-panel,#1d1d1d);
border:1px solid var(--xeno-border,#333); border-radius:8px;
font:12px Inter,system-ui,sans-serif; color:var(--xeno-text,#cfcfcf); z-index:5; }
.label { margin:0; font-size:11px; text-transform:uppercase; letter-spacing:.05em; color:#9a9a9a; }
.btn { width:100%; padding:6px 10px; font-size:12px; cursor:pointer;
border:1px solid var(--xeno-border,#333); border-radius:6px;
background:transparent; color:var(--xeno-text,#cfcfcf); }
.status { color:#9a9a9a; font-size:11px; line-height:1.4; }
\`],
})
export class SaveRestoreDemoComponent {
private editor: XenolithEditor | null = null
private timer: ReturnType<typeof setTimeout> | null = null
private first = true
savedAt = signal<number | null>(null)
readonly hasSaved = hasSaved()
onReady(editor: XenolithEditor): void {
this.editor = editor
initSaveRestore(editor)
}
scheduleAutosave(): void {
if (this.first) { this.first = false; return }
if (!this.editor) return
if (this.timer) clearTimeout(this.timer)
const e = this.editor
this.timer = setTimeout(() => { saveToLocal(e); this.savedAt.set(Date.now()) }, 500)
}
download(): void { if (this.editor) downloadGraph(this.editor) }
restore(): void { if (this.editor) restoreFromLocal(this.editor) }
onUpload(ev: Event): void {
const f = (ev.target as HTMLInputElement).files?.[0]
if (f && this.editor) uploadGraph(this.editor, f)
}
} // Persistence: the whole graph is JSON (editor.exportJSON / loadJSON). These framework-agnostic
// helpers cover the real save/restore surface — download/upload a .json file, and save/restore the
// last graph from localStorage. The host wires them to buttons; the only framework-reactive bit
// (autosave on every edit) stays in the host because it rides that framework's change subscription.
import { loadDemo } from './scene.js'
import type { XenolithEditor } from '@xenolithengine/graph-editor'
export const SAVE_KEY = 'xeno:save-restore-demo'
function download(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url; a.download = filename
document.body.appendChild(a); a.click(); a.remove()
setTimeout(() => URL.revokeObjectURL(url), 0)
}
export function downloadGraph(editor: XenolithEditor): void {
download(editor.exportJSON(), 'graph.json')
}
export function uploadGraph(editor: XenolithEditor, file: File): void {
void file.text().then((t) => {
try { editor.loadJSON(JSON.parse(t)); editor.fitView({ padding: 48, maxZoom: 1 }) } catch { /* bad file */ }
})
}
export function saveToLocal(editor: XenolithEditor): void {
localStorage.setItem(SAVE_KEY, JSON.stringify(editor.toJSON()))
}
export function hasSaved(): boolean {
return localStorage.getItem(SAVE_KEY) !== null
}
export function restoreFromLocal(editor: XenolithEditor): boolean {
const s = localStorage.getItem(SAVE_KEY)
if (!s) return false
try { editor.loadJSON(JSON.parse(s)); editor.fitView({ padding: 48, maxZoom: 1 }); return true } catch { return false }
}
/** Initial load: the demo graph, then the last autosave on top if present. */
export function initSaveRestore(editor: XenolithEditor): void {
loadDemo(editor)
restoreFromLocal(editor)
}