diff --git a/Makefile b/Makefile index 2529909a..95856894 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,41 @@ SHELL := /bin/bash -.PHONY: hooks +# Default docs port; override with: make docs PORT=5180 +PORT ?= 5173 + +.PHONY: hooks docs docs-build docs-ci + hooks: @git config core.hooksPath .githooks @chmod +x .githooks/* 2>/dev/null || true @echo "[hooks] Installed git hooks from .githooks (core.hooksPath)" +# Start VitePress dev server and open the browser automatically. +docs: + @echo "[docs] Ensuring deps..." + @npm install --silent + @echo "[docs] Starting VitePress on http://localhost:$(PORT) ..." + # Start server in background and record PID + @ (npm run --silent docs:dev -- --port $(PORT) &) ; \ + server_pid=$$! ; \ + echo "[docs] Waiting for server to become ready..." ; \ + for i in {1..80}; do \ + if curl -sSf "http://localhost:$(PORT)/" >/dev/null ; then \ + echo "[docs] Server is up at http://localhost:$(PORT)/" ; \ + scripts/docs-open.sh "http://localhost:$(PORT)/" ; \ + wait $$server_pid ; \ + exit 0 ; \ + fi ; \ + sleep 0.25 ; \ + done ; \ + echo "[docs] Timed out waiting for VitePress." ; \ + exit 1 + +# Build static site +docs-build: + @npm run --silent docs:build + +# Build docs without installing dependencies (for CI caches) +docs-ci: + @echo "[docs] CI build (no npm install)" + @npm run --silent docs:build diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts new file mode 100644 index 00000000..008c31a8 --- /dev/null +++ b/docs/.vitepress/config.ts @@ -0,0 +1,24 @@ +import { defineConfig } from 'vitepress' + +export default defineConfig({ + title: 'Echo', + description: 'Deterministic, multiverse-aware ECS', + cleanUrls: true, + themeConfig: { + nav: [ + { text: 'Home', link: '/' }, + { text: 'Collision Tour', link: '/guide/collision-tour' }, + ], + sidebar: { + '/guide/': [ + { + text: 'Guide', + items: [ + { text: 'Collision Tour', link: '/guide/collision-tour' }, + ] + } + ] + } + } +}) + diff --git a/docs/assets/collision/README.md b/docs/assets/collision/README.md new file mode 100644 index 00000000..b038b3fb --- /dev/null +++ b/docs/assets/collision/README.md @@ -0,0 +1,33 @@ +# Collision/CCD DPO Diagrams + +- Base CSS: `diagrams.css` +- Static SVGs: + - `dpo_build_temporal_proxy.svg` + - `dpo_broad_phase_pairing.svg` + - `dpo_narrow_phase_discrete.svg` + - `dpo_narrow_phase_ccd.svg` + - `dpo_contact_events.svg` + - `dpo_gc_ephemeral.svg` + - `scheduler_phase_mapping.svg` + - `legend.svg` + +Each SVG uses semantic classes: +- Nodes: `node`, variants: `interfaceK`, `added`, `removed` +- Edges: `edge`, variants: `added`, `removed` +- Other: `title`, `label`, `caption`, `scope` +- Optional animations: `pulse-add`, `pulse-remove` + +## Animations +- Extend `diagrams.css` with your own `@keyframes` and apply classes to elements. +- For step-by-step tutorials, use the `_step1.svg`, `_step2.svg`, `_step3.svg` variants or overlay multiple SVGs in the docs site. + +## Mermaid Sources +- Source `.mmd` files live alongside these SVGs. You can generate SVGs locally: + +``` +# Requires Node.js +npm i -g @mermaid-js/mermaid-cli +mmdc -i build_temporal_proxy.mmd -o build_temporal_proxy.svg +``` + +CI also compiles `.mmd` files to SVG artifacts (see `.github/workflows/ci.yml` job `diagrams`). diff --git a/docs/assets/collision/animate.js b/docs/assets/collision/animate.js new file mode 100644 index 00000000..2f1ad95f --- /dev/null +++ b/docs/assets/collision/animate.js @@ -0,0 +1,154 @@ +(function(){ + const els = Array.from(document.querySelectorAll('.fade-seed')); + if (!('IntersectionObserver' in window)) { + els.forEach(el => el.classList.add('fade-in')); + return; + } + const io = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.classList.add('fade-in'); + io.unobserve(entry.target); + } + }); + }, { rootMargin: '0px 0px -10% 0px', threshold: 0.1 }); + els.forEach(el => io.observe(el)); +})(); + +// Carousel pager for each rule's step-grid +(function(){ + function buildPager(rule) { + const grid = rule.querySelector('.step-grid'); + if (!grid) return; + const slides = Array.from(grid.querySelectorAll('figure')); + if (slides.length < 2) return; + + // Build overlay captions with step counts from figcaptions + slides.forEach((fig, i) => { + const cap = fig.querySelector('figcaption'); + const text = cap ? cap.textContent.trim() : ''; + const ov = document.createElement('div'); + ov.className = 'overlay'; + ov.innerHTML = `Step ${i + 1} of ${slides.length}
${text}
`; + fig.appendChild(ov); + fig.classList.add('has-overlay'); + }); + + const nav = document.createElement('div'); + nav.className = 'pager'; + const prev = document.createElement('button'); + prev.className = 'btn'; prev.textContent = '◀ Prev'; + const next = document.createElement('button'); + next.className = 'btn'; next.textContent = 'Next ▶'; + const toggle = document.createElement('button'); + toggle.className = 'btn'; toggle.textContent = 'Show all'; + const world = document.createElement('button'); + world.className = 'btn'; world.textContent = 'World view: On'; + nav.append(prev, next, toggle, world); + grid.after(nav); + + let mode = 'all'; // default to all frames visible + let idx = 0; + + // Helpers to find neighbor rule blocks + const prevRule = (el) => { let r = el.previousElementSibling; while (r && !r.classList.contains('rule')) r = r.previousElementSibling; return r; }; + const nextRule = (el) => { let r = el.nextElementSibling; while (r && !r.classList.contains('rule')) r = r.nextElementSibling; return r; }; + + function render() { + if (mode === 'all') { + slides.forEach(el => el.classList.remove('hidden')); + toggle.textContent = 'Carousel mode'; + } else { + slides.forEach((el, i) => { + el.classList.toggle('hidden', i !== idx); + // ensure visible slide is faded in + if (!el.classList.contains('fade-in')) el.classList.add('fade-in'); + }); + toggle.textContent = 'Show all'; + } + // Enable/disable edges. If at first slide and no previous rule, disable Prev. + // If at last slide and no next rule, disable Next. + if (mode === 'all') { + // Keep navigation enabled in 'all' mode so users/tests can enter carousel via Prev/Next. + prev.disabled = false; next.disabled = false; + } else { + const atFirst = idx === 0; + const atLast = idx === slides.length - 1; + prev.disabled = atFirst && !prevRule(rule); + next.disabled = atLast && !nextRule(rule); + } + } + + prev.addEventListener('click', () => { + // If in all mode, enter carousel at first slide + if (mode === 'all') { mode = 'one'; idx = 0; render(); return; } + if (idx > 0) { idx -= 1; render(); return; } + // At first slide: navigate to previous rule, show its first slide + const pr = prevRule(rule); + if (pr && pr._pager) { + pr._pager.setIndex(0); + pr.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + render(); + }); + next.addEventListener('click', () => { + if (mode === 'all') { mode = 'one'; idx = 0; render(); return; } + if (idx < slides.length - 1) { idx += 1; render(); return; } + // At last slide: navigate to next rule, show its first slide + const nr = nextRule(rule); + if (nr && nr._pager) { + nr._pager.setIndex(0); + nr.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } + render(); + }); + toggle.addEventListener('click', () => { + mode = (mode === 'all') ? 'one' : 'all'; + render(); + }); + + // Picture-in-picture container with tabs (World / Graph) + slides.forEach((fig) => { + const srcWorld = fig.getAttribute('data-pip'); + const srcGraph = fig.getAttribute('data-graph'); + if (!srcWorld && !srcGraph) return; + const wrap = document.createElement('div'); + wrap.className = 'pip'; + const tabs = document.createElement('div'); + tabs.className = 'pip-tabs'; + const tabWorld = document.createElement('div'); tabWorld.className = 'tab active'; tabWorld.textContent = 'World'; + const tabGraph = document.createElement('div'); tabGraph.className = 'tab'; tabGraph.textContent = 'Graph'; + tabs.append(tabWorld, tabGraph); + const imgWorld = document.createElement('img'); imgWorld.alt = 'World view'; if (srcWorld) imgWorld.src = srcWorld; else imgWorld.style.display='none'; + const imgGraph = document.createElement('img'); imgGraph.alt = 'Graph view'; if (srcGraph) imgGraph.src = srcGraph; else imgGraph.style.display='none'; imgGraph.classList.add('hidden'); + wrap.append(tabs, imgWorld, imgGraph); + fig.appendChild(wrap); + function show(which){ + if (which==='world') { tabWorld.classList.add('active'); tabGraph.classList.remove('active'); imgWorld.classList.remove('hidden'); imgGraph.classList.add('hidden'); } + else { tabGraph.classList.add('active'); tabWorld.classList.remove('active'); imgGraph.classList.remove('hidden'); imgWorld.classList.add('hidden'); } + } + tabWorld.addEventListener('click', ()=>show('world')); + tabGraph.addEventListener('click', ()=>show('graph')); + }); + + let worldOn = true; + world.addEventListener('click', () => { + worldOn = !worldOn; + world.textContent = worldOn ? 'World view: On' : 'World view: Off'; + slides.forEach(fig => { + const pip = fig.querySelector('.pip'); + if (pip) pip.classList.toggle('hidden', !worldOn); + }); + }); + + // Expose simple API for cross-rule navigation + rule._pager = { + setIndex: (i) => { mode = 'one'; idx = Math.max(0, Math.min(slides.length - 1, i)); render(); }, + setMode: (m) => { mode = m; render(); }, + }; + + render(); + } + + document.querySelectorAll('.rule').forEach(buildPager); +})(); diff --git a/docs/assets/collision/broad_phase_pairing.mmd b/docs/assets/collision/broad_phase_pairing.mmd new file mode 100644 index 00000000..8a1b9cb3 --- /dev/null +++ b/docs/assets/collision/broad_phase_pairing.mmd @@ -0,0 +1,17 @@ +flowchart LR + subgraph LHS + PA((TemporalProxy(a,n))):::KClass + PB((TemporalProxy(b,n))):::KClass + PA -- overlaps --> PB + end + + subgraph RHS + PA2((TemporalProxy(a,n))):::KClass + PB2((TemporalProxy(b,n))):::KClass + PP[[PotentialPair(a,b,n)]]:::Add + PA2 -- pair_of --> PP + PB2 -- pair_of --> PP + end + +classDef KClass stroke:#ffd166,stroke-width:2; +classDef Add stroke:#00c853,stroke-width:2,fill:#0d2a1a,color:#dfe7ff; diff --git a/docs/assets/collision/build_temporal_proxy.mmd b/docs/assets/collision/build_temporal_proxy.mmd new file mode 100644 index 00000000..719a4f3f --- /dev/null +++ b/docs/assets/collision/build_temporal_proxy.mmd @@ -0,0 +1,19 @@ +flowchart LR + subgraph LHS + E[Collider(e)] --- T[Transform(e,n)] + V[Velocity(e)] + K[Tick(n)] + T -- at --> K + E -- produced_in --> K + end + + subgraph RHS + E2[Collider(e)]:::KClass --- T2[Transform(e,n)]:::KClass + K2[Tick(n)]:::KClass + P((TemporalProxy(e,n))):::Add + E2 -- has_proxy --> P + P -- produced_in --> K2 + end + +classDef KClass stroke:#ffd166,stroke-width:2; +classDef Add stroke:#00c853,stroke-width:2,fill:#0d2a1a,color:#dfe7ff; diff --git a/docs/assets/collision/contact_events.mmd b/docs/assets/collision/contact_events.mmd new file mode 100644 index 00000000..8221e17c --- /dev/null +++ b/docs/assets/collision/contact_events.mmd @@ -0,0 +1,12 @@ +flowchart LR + subgraph LHS + C1((Contact(pair,n-1))):::KClass + C2((Contact(pair,n))):::KClass + end + subgraph RHS + E((ContactEvent(kind,pair,n))):::Add + E -- event_of --> C2 + end + +classDef KClass stroke:#ffd166,stroke-width:2; +classDef Add stroke:#00c853,stroke-width:2,fill:#0d2a1a,color:#dfe7ff; diff --git a/docs/assets/collision/diagrams.css b/docs/assets/collision/diagrams.css new file mode 100644 index 00000000..7a7d7014 --- /dev/null +++ b/docs/assets/collision/diagrams.css @@ -0,0 +1,63 @@ +/* Echo collision/CCD DPO diagrams — base styles and optional animations */ +:root { + --bg: #0b1020; + --panel: #121a33; + --text: #dfe7ff; + --muted: #9fb0d6; + --edge: #6aa0ff; + --k: #ffd166; + --added: #00c853; + --removed: #ff5252; + --scope: #ffa600; +} + +svg { font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, 'Helvetica Neue', Arial, 'Noto Sans', 'Liberation Sans', sans-serif; } +.panel { fill: var(--panel); rx: 8px; } +.title { fill: var(--text); font-weight: 600; font-size: 16px; } +.caption { fill: var(--muted); font-size: 12px; } +.node rect { fill: #182347; stroke: #3356a6; stroke-width: 1.2; rx: 6px; } +.node.interfaceK rect { fill: #2b2746; stroke: var(--k); stroke-width: 2; } +.node.added rect { fill: #0d2a1a; stroke: var(--added); stroke-width: 2; } +.node.removed rect { fill: #2a0d0d; stroke: var(--removed); stroke-width: 2; } +.node text { fill: var(--text); font-size: 12px; } +.edge line, .edge path { stroke: var(--edge); stroke-width: 1.6; fill: none; } +.edge.added line, .edge.added path { stroke: var(--added); stroke-width: 2; } +.edge.removed line, .edge.removed path { stroke: var(--removed); stroke-width: 2; } +.edge text { fill: var(--muted); font-size: 11px; } +.scope rect { fill: none; stroke: var(--scope); stroke-width: 2; stroke-dasharray: 6 4; rx: 8px; } +.label { fill: var(--text); font-size: 13px; } + +/* Optional animation hooks */ +.pulse-add { animation: pulseAdd 1.6s ease-in-out infinite; } +.pulse-remove { animation: pulseRemove 1.6s ease-in-out infinite; } +@keyframes pulseAdd { 0%{opacity:0.5} 50%{opacity:1} 100%{opacity:0.5} } +@keyframes pulseRemove { 0%{opacity:1} 50%{opacity:0.6} 100%{opacity:1} } + +/* Fade-in on scroll (tour page) */ +.fade-seed { opacity: 0; transform: translateY(12px); transition: opacity 400ms ease, transform 400ms ease; } +.fade-in { opacity: 1; transform: translateY(0); } + +/* Carousel pager */ +.pager { display: flex; gap: 8px; margin-top: 8px; } +.btn { background: #1b2446; color: var(--text); border: 1px solid #3356a6; border-radius: 6px; padding: 6px 10px; cursor: pointer; font-size: 12px; } +.btn:hover { background: #223060; } +.btn:disabled { opacity: 0.5; cursor: default; } +.hidden { display: none; } + +/* Overlay caption inside figures */ +.has-overlay figcaption { display: none; } +.overlay { position: absolute; left: 12px; right: 12px; bottom: 12px; background: rgba(18,26,51,0.85); border: 1px solid #3356a6; border-radius: 6px; padding: 8px 10px; backdrop-filter: blur(2px); } +.overlay strong { color: var(--text); font-size: 12px; letter-spacing: 0.2px; } +.overlay .ov-t { color: var(--muted); font-size: 12px; margin-top: 2px; } +.has-overlay { padding-bottom: 68px; } +.slide-explain { margin-top: 10px; font-size: 13px; color: var(--text); } +.slide-explain p { margin: 0 0 6px; } +.slide-explain ul { margin: 6px 0 0 18px; } +.slide-explain li { margin: 3px 0; color: var(--muted); } + +/* Picture-in-picture world view */ +.pip { position: absolute; right: 12px; top: 12px; width: 32%; max-width: 240px; border: 1px solid #3356a6; background: #0b1020; border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.35); overflow: hidden; } +.pip img { width: 100%; display: block; } +.pip-tabs { display: flex; gap: 0; border-bottom: 1px solid #2a3c72; } +.pip-tabs .tab { flex: 1; text-align: center; font-size: 11px; color: var(--muted); padding: 6px 4px; cursor: pointer; background: #121a33; } +.pip-tabs .tab.active { color: var(--text); background: #1b2446; } diff --git a/docs/assets/collision/dpo_broad_phase_pairing.svg b/docs/assets/collision/dpo_broad_phase_pairing.svg new file mode 100644 index 00000000..e1525e19 --- /dev/null +++ b/docs/assets/collision/dpo_broad_phase_pairing.svg @@ -0,0 +1,46 @@ + + + + + + + DPO: BroadPhasePairing (update) + LHS: overlapping TemporalProxy(a,n), TemporalProxy(b,n); RHS: add PotentialPair(a,b,n); K = both proxies + + + LHS + + RHS + + + TemporalProxy(a,n) + TemporalProxy(b,n) + + overlap (fat AABBs) + + + TemporalProxy(a,n) + TemporalProxy(b,n) + PotentialPair(a,b,n) + + + + + + diff --git a/docs/assets/collision/dpo_broad_phase_pairing_step1.svg b/docs/assets/collision/dpo_broad_phase_pairing_step1.svg new file mode 100644 index 00000000..8a55a290 --- /dev/null +++ b/docs/assets/collision/dpo_broad_phase_pairing_step1.svg @@ -0,0 +1,26 @@ + + + + + + BroadPhasePairing — Step 1: LHS + TemporalProxy(a,n) + TemporalProxy(b,n) + fat AABBs overlap + diff --git a/docs/assets/collision/dpo_broad_phase_pairing_step2.svg b/docs/assets/collision/dpo_broad_phase_pairing_step2.svg new file mode 100644 index 00000000..759cfa34 --- /dev/null +++ b/docs/assets/collision/dpo_broad_phase_pairing_step2.svg @@ -0,0 +1,26 @@ + + + + + + BroadPhasePairing — Step 2: Interface K + TemporalProxy(a,n) + TemporalProxy(b,n) + + diff --git a/docs/assets/collision/dpo_broad_phase_pairing_step3.svg b/docs/assets/collision/dpo_broad_phase_pairing_step3.svg new file mode 100644 index 00000000..14ed7556 --- /dev/null +++ b/docs/assets/collision/dpo_broad_phase_pairing_step3.svg @@ -0,0 +1,26 @@ + + + + + + BroadPhasePairing — Step 3: RHS + TemporalProxy(a,n) + TemporalProxy(b,n) + PotentialPair(a,b,n) + diff --git a/docs/assets/collision/dpo_build_temporal_proxy.svg b/docs/assets/collision/dpo_build_temporal_proxy.svg new file mode 100644 index 00000000..4f35c6e3 --- /dev/null +++ b/docs/assets/collision/dpo_build_temporal_proxy.svg @@ -0,0 +1,67 @@ + + + + + + + DPO: BuildTemporalProxy (pre_update) + LHS: Collider+Transform(+Velocity) at Tick n; RHS: add TemporalProxy(e,n) with fat AABB; K: Collider, Transform, Tick + + + + LHS + + RHS + + + Collider(e) + Transform(e,n) + Velocity(e) + Tick(n) + + + + + produced_in + + + + at + + + + Collider(e) + Transform(e,n) + Tick(n) + + + TemporalProxy(e,n) + + + has_proxy + + + + produced_in + + + + + + diff --git a/docs/assets/collision/dpo_build_temporal_proxy_step1.svg b/docs/assets/collision/dpo_build_temporal_proxy_step1.svg new file mode 100644 index 00000000..d0e9642c --- /dev/null +++ b/docs/assets/collision/dpo_build_temporal_proxy_step1.svg @@ -0,0 +1,27 @@ + + + + + + BuildTemporalProxy — Step 1: LHS + Collider(e) + Transform(e,n) + Velocity(e) + Tick(n) + diff --git a/docs/assets/collision/dpo_build_temporal_proxy_step2.svg b/docs/assets/collision/dpo_build_temporal_proxy_step2.svg new file mode 100644 index 00000000..0add5c31 --- /dev/null +++ b/docs/assets/collision/dpo_build_temporal_proxy_step2.svg @@ -0,0 +1,27 @@ + + + + + + BuildTemporalProxy — Step 2: Interface K + Collider(e) + Transform(e,n) + Tick(n) + + diff --git a/docs/assets/collision/dpo_build_temporal_proxy_step3.svg b/docs/assets/collision/dpo_build_temporal_proxy_step3.svg new file mode 100644 index 00000000..a667846a --- /dev/null +++ b/docs/assets/collision/dpo_build_temporal_proxy_step3.svg @@ -0,0 +1,27 @@ + + + + + + BuildTemporalProxy — Step 3: RHS + Collider(e) + Transform(e,n) + Tick(n) + TemporalProxy(e,n) + diff --git a/docs/assets/collision/dpo_contact_events.svg b/docs/assets/collision/dpo_contact_events.svg new file mode 100644 index 00000000..ed888ecb --- /dev/null +++ b/docs/assets/collision/dpo_contact_events.svg @@ -0,0 +1,40 @@ + + + + + + + DPO: ContactEvents (post_update) + LHS: Contact(pair,n-1) and Contact(pair,n) (or absence); RHS: Begin/Persist/End event + + + LHS + + RHS + + Contact(pair,n-1) + Contact(pair,n) + + ContactEvent(kind, pair, n) + + + + + + diff --git a/docs/assets/collision/dpo_contact_events_step1.svg b/docs/assets/collision/dpo_contact_events_step1.svg new file mode 100644 index 00000000..fb3e039f --- /dev/null +++ b/docs/assets/collision/dpo_contact_events_step1.svg @@ -0,0 +1,25 @@ + + + + + + ContactEvents — Step 1: LHS + Contact(pair,n-1) + Contact(pair,n) + diff --git a/docs/assets/collision/dpo_contact_events_step2.svg b/docs/assets/collision/dpo_contact_events_step2.svg new file mode 100644 index 00000000..05703400 --- /dev/null +++ b/docs/assets/collision/dpo_contact_events_step2.svg @@ -0,0 +1,26 @@ + + + + + + ContactEvents — Step 2: Interface K + Contact(pair,n-1) + Contact(pair,n) + + diff --git a/docs/assets/collision/dpo_contact_events_step3.svg b/docs/assets/collision/dpo_contact_events_step3.svg new file mode 100644 index 00000000..7e079498 --- /dev/null +++ b/docs/assets/collision/dpo_contact_events_step3.svg @@ -0,0 +1,26 @@ + + + + + + ContactEvents — Step 3: RHS + Contact(pair,n-1) + Contact(pair,n) + ContactEvent(kind,pair,n) + diff --git a/docs/assets/collision/dpo_gc_ephemeral.svg b/docs/assets/collision/dpo_gc_ephemeral.svg new file mode 100644 index 00000000..a38a91bc --- /dev/null +++ b/docs/assets/collision/dpo_gc_ephemeral.svg @@ -0,0 +1,38 @@ + + + + + + DPO: GC Ephemeral (timeline_flush) + LHS: Temporal artifacts older than retention and unreferenced; RHS: delete deterministically + + + LHS + + RHS + + TemporalProxy(e,n-2) + PotentialPair(a,b,n-2) + Toi(pair,n-2) + + (deleted ephemeral nodes) + + + + diff --git a/docs/assets/collision/dpo_gc_ephemeral_step1.svg b/docs/assets/collision/dpo_gc_ephemeral_step1.svg new file mode 100644 index 00000000..d8af8e0c --- /dev/null +++ b/docs/assets/collision/dpo_gc_ephemeral_step1.svg @@ -0,0 +1,26 @@ + + + + + + GC Ephemeral — Step 1: LHS + TemporalProxy(e,n-2) + PotentialPair(a,b,n-2) + Toi(pair,n-2) + diff --git a/docs/assets/collision/dpo_gc_ephemeral_step2.svg b/docs/assets/collision/dpo_gc_ephemeral_step2.svg new file mode 100644 index 00000000..8844e59f --- /dev/null +++ b/docs/assets/collision/dpo_gc_ephemeral_step2.svg @@ -0,0 +1,27 @@ + + + + + + GC Ephemeral — Step 2: Selection + TemporalProxy(e,n-2) + PotentialPair(a,b,n-2) + Toi(pair,n-2) + + diff --git a/docs/assets/collision/dpo_gc_ephemeral_step3.svg b/docs/assets/collision/dpo_gc_ephemeral_step3.svg new file mode 100644 index 00000000..f0651f70 --- /dev/null +++ b/docs/assets/collision/dpo_gc_ephemeral_step3.svg @@ -0,0 +1,24 @@ + + + + + + GC Ephemeral — Step 3: RHS + (deleted ephemeral nodes) + diff --git a/docs/assets/collision/dpo_narrow_phase_ccd.svg b/docs/assets/collision/dpo_narrow_phase_ccd.svg new file mode 100644 index 00000000..c8c77b8a --- /dev/null +++ b/docs/assets/collision/dpo_narrow_phase_ccd.svg @@ -0,0 +1,42 @@ + + + + + + + DPO: NarrowPhaseCCD (update) + LHS: PotentialPair(a,b,n), CCD policy triggers; RHS: Toi(pair,n) + Contact(pair,n) at s∈[0,1] + + + LHS + + RHS + + PotentialPair(a,b,n) + ccd: policy(a,b) == true + + PotentialPair(a,b,n) + Toi(pair,n) + Contact(pair,n) { Manifold } + toi_s + + + + + diff --git a/docs/assets/collision/dpo_narrow_phase_ccd_step1.svg b/docs/assets/collision/dpo_narrow_phase_ccd_step1.svg new file mode 100644 index 00000000..b9d20369 --- /dev/null +++ b/docs/assets/collision/dpo_narrow_phase_ccd_step1.svg @@ -0,0 +1,25 @@ + + + + + + NarrowPhaseCCD — Step 1: LHS + PotentialPair(a,b,n) + policy(a,b) == true (CCD) + diff --git a/docs/assets/collision/dpo_narrow_phase_ccd_step2.svg b/docs/assets/collision/dpo_narrow_phase_ccd_step2.svg new file mode 100644 index 00000000..a7ab1df2 --- /dev/null +++ b/docs/assets/collision/dpo_narrow_phase_ccd_step2.svg @@ -0,0 +1,25 @@ + + + + + + NarrowPhaseCCD — Step 2: Interface K + PotentialPair(a,b,n) + + diff --git a/docs/assets/collision/dpo_narrow_phase_ccd_step3.svg b/docs/assets/collision/dpo_narrow_phase_ccd_step3.svg new file mode 100644 index 00000000..1613f487 --- /dev/null +++ b/docs/assets/collision/dpo_narrow_phase_ccd_step3.svg @@ -0,0 +1,26 @@ + + + + + + NarrowPhaseCCD — Step 3: RHS + PotentialPair(a,b,n) + Toi(pair,n) + Contact(pair,n) { Manifold } + diff --git a/docs/assets/collision/dpo_narrow_phase_discrete.svg b/docs/assets/collision/dpo_narrow_phase_discrete.svg new file mode 100644 index 00000000..d3e70b0a --- /dev/null +++ b/docs/assets/collision/dpo_narrow_phase_discrete.svg @@ -0,0 +1,39 @@ + + + + + + + DPO: NarrowPhaseDiscrete (update) + LHS: PotentialPair(a,b,n), discrete overlap at end poses; RHS: add Contact(pair,n) with Manifold + + + LHS + + RHS + + PotentialPair(a,b,n) + discrete: overlap == true @ end pose + + PotentialPair(a,b,n) + Contact(pair,n) { Manifold } + + + + diff --git a/docs/assets/collision/dpo_narrow_phase_discrete_step1.svg b/docs/assets/collision/dpo_narrow_phase_discrete_step1.svg new file mode 100644 index 00000000..6d401272 --- /dev/null +++ b/docs/assets/collision/dpo_narrow_phase_discrete_step1.svg @@ -0,0 +1,25 @@ + + + + + + NarrowPhaseDiscrete — Step 1: LHS + PotentialPair(a,b,n) + overlap == true @ end pose + diff --git a/docs/assets/collision/dpo_narrow_phase_discrete_step2.svg b/docs/assets/collision/dpo_narrow_phase_discrete_step2.svg new file mode 100644 index 00000000..39dc906c --- /dev/null +++ b/docs/assets/collision/dpo_narrow_phase_discrete_step2.svg @@ -0,0 +1,25 @@ + + + + + + NarrowPhaseDiscrete — Step 2: Interface K + PotentialPair(a,b,n) + + diff --git a/docs/assets/collision/dpo_narrow_phase_discrete_step3.svg b/docs/assets/collision/dpo_narrow_phase_discrete_step3.svg new file mode 100644 index 00000000..cb694af4 --- /dev/null +++ b/docs/assets/collision/dpo_narrow_phase_discrete_step3.svg @@ -0,0 +1,25 @@ + + + + + + NarrowPhaseDiscrete — Step 3: RHS + PotentialPair(a,b,n) + Contact(pair,n) { Manifold } + diff --git a/docs/assets/collision/graph_collision_overview.mmd b/docs/assets/collision/graph_collision_overview.mmd new file mode 100644 index 00000000..731969f8 --- /dev/null +++ b/docs/assets/collision/graph_collision_overview.mmd @@ -0,0 +1,48 @@ +flowchart TB + Tick[(Tick n)] + subgraph Entities + EA[Entity A] + EB[Entity B] + end + TA[Transform(A,n)] + CA[Collider(A)] + TB[Transform(B,n)] + CB[Collider(B)] + TPA[[TemporalProxy(A,n)]] + TPB[[TemporalProxy(B,n)]] + PP{{PotentialPair(A,B,n)}} + C((Contact(pair,n))) + TOI((Toi(pair,n))) + E((ContactEvent(kind,n))) + + EA --> TA + EA --> CA + EB --> TB + EB --> CB + + TA -- produced_in --> Tick + TB -- produced_in --> Tick + + TA -. has_proxy .-> TPA + TB -. has_proxy .-> TPB + TPA -- pair_of --> PP + TPB -- pair_of --> PP + PP -- contact_of --> C + PP -. toi_of .-> TOI + C -- event_of --> E + + TPA -- produced_in --> Tick + TPB -- produced_in --> Tick + PP -- produced_in --> Tick + C -- produced_in --> Tick + TOI -- produced_in --> Tick + E -- produced_in --> Tick + +classDef tick fill:#1a233f,stroke:#3e5fb5,color:#dfe7ff; +classDef proxy stroke:#ffd166,color:#dfe7ff; +classDef pair stroke:#00c853,color:#dfe7ff; +classDef event fill:#241a4b,stroke:#a78bfa,color:#dfe7ff; +class Tick tick; +class TPA,TPB proxy; +class PP pair; +class E event; diff --git a/docs/assets/collision/graph_collision_overview.svg b/docs/assets/collision/graph_collision_overview.svg new file mode 100644 index 00000000..efc12d26 --- /dev/null +++ b/docs/assets/collision/graph_collision_overview.svg @@ -0,0 +1,131 @@ + + + + + + + + + Collision Subgraph — Typed Nodes and Edges (Tick n) + Everything is a graph: entities/components, time‑aware proxies, pairs, contacts, TOI, and events are nodes linked by typed edges. + + + + + Tick(n) + + + + + + Entity A + + + + Entity B + + + + + + Transform(A,n) + + + + Collider(A) + + + + + + Transform(B,n) + + + + Collider(B) + + + + + + TemporalProxy(A,n) + + + + TemporalProxy(B,n) + + + + + + PotentialPair(A,B,n) + + + + + + Contact(pair,n) + + + + Toi(pair,n) + + + + + + ContactEvent(kind,n) + + + + has_component + + + + + + produced_in + + + + has_proxy + + + + has_proxy + + + + pair_of + + + + contact_of + toi_of + + + event_of + + + + + + + + + + diff --git a/docs/assets/collision/legend.svg b/docs/assets/collision/legend.svg new file mode 100644 index 00000000..4779ddb0 --- /dev/null +++ b/docs/assets/collision/legend.svg @@ -0,0 +1,48 @@ + + + + + + DPO Diagram Legend + + Node + Regular node + + + Interface K + Preserved between LHS and RHS + + + Added + Created on RHS + + + Removed + Deleted on RHS + + + + edge + added edge + removed edge + + + + Scope boundary + diff --git a/docs/assets/collision/narrow_phase_ccd.mmd b/docs/assets/collision/narrow_phase_ccd.mmd new file mode 100644 index 00000000..eaadda91 --- /dev/null +++ b/docs/assets/collision/narrow_phase_ccd.mmd @@ -0,0 +1,15 @@ +flowchart LR + subgraph LHS + PP[[PotentialPair(a,b,n)]]:::KClass + PP -- policy(ccd) --> CCD[/run CCD/] + end + subgraph RHS + PP2[[PotentialPair(a,b,n)]]:::KClass + TOI((Toi(pair,n))):::Add + C((Contact(pair,n))):::Add + TOI -- toi_s --> C + C -- contact_of --> PP2 + end + +classDef KClass stroke:#ffd166,stroke-width:2; +classDef Add stroke:#00c853,stroke-width:2,fill:#0d2a1a,color:#dfe7ff; diff --git a/docs/assets/collision/narrow_phase_discrete.mmd b/docs/assets/collision/narrow_phase_discrete.mmd new file mode 100644 index 00000000..7e441d50 --- /dev/null +++ b/docs/assets/collision/narrow_phase_discrete.mmd @@ -0,0 +1,13 @@ +flowchart LR + subgraph LHS + PP[[PotentialPair(a,b,n)]]:::KClass + PP -- overlap_at_end --> YES{overlap?} + end + subgraph RHS + PP2[[PotentialPair(a,b,n)]]:::KClass + C((Contact(pair,n))):::Add + C -- contact_of --> PP2 + end + +classDef KClass stroke:#ffd166,stroke-width:2; +classDef Add stroke:#00c853,stroke-width:2,fill:#0d2a1a,color:#dfe7ff; diff --git a/docs/assets/collision/pip/build_proxy_step1.svg b/docs/assets/collision/pip/build_proxy_step1.svg new file mode 100644 index 00000000..5bc3682a --- /dev/null +++ b/docs/assets/collision/pip/build_proxy_step1.svg @@ -0,0 +1,8 @@ + + + + + + Entity e — start pose and velocity + + diff --git a/docs/assets/collision/pip/build_proxy_step2.svg b/docs/assets/collision/pip/build_proxy_step2.svg new file mode 100644 index 00000000..c1a855cc --- /dev/null +++ b/docs/assets/collision/pip/build_proxy_step2.svg @@ -0,0 +1,6 @@ + + + + + Interface K — e@Tick n preserved + diff --git a/docs/assets/collision/pip/build_proxy_step3.svg b/docs/assets/collision/pip/build_proxy_step3.svg new file mode 100644 index 00000000..47853a2b --- /dev/null +++ b/docs/assets/collision/pip/build_proxy_step3.svg @@ -0,0 +1,10 @@ + + + + + + + + Fat AABB over motion [start,end] + + diff --git a/docs/assets/collision/pip/ccd_step1.svg b/docs/assets/collision/pip/ccd_step1.svg new file mode 100644 index 00000000..7fbfb08e --- /dev/null +++ b/docs/assets/collision/pip/ccd_step1.svg @@ -0,0 +1,9 @@ + + + + + + + Fast mover towards obstacle (CCD) + + diff --git a/docs/assets/collision/pip/ccd_step2.svg b/docs/assets/collision/pip/ccd_step2.svg new file mode 100644 index 00000000..9c114a4c --- /dev/null +++ b/docs/assets/collision/pip/ccd_step2.svg @@ -0,0 +1,7 @@ + + + + + + K: pair preserved for CCD + diff --git a/docs/assets/collision/pip/ccd_step3.svg b/docs/assets/collision/pip/ccd_step3.svg new file mode 100644 index 00000000..572572cf --- /dev/null +++ b/docs/assets/collision/pip/ccd_step3.svg @@ -0,0 +1,10 @@ + + + + + + + + + Impact at s ≈ TOI; contact normal + diff --git a/docs/assets/collision/pip/discrete_step1.svg b/docs/assets/collision/pip/discrete_step1.svg new file mode 100644 index 00000000..4f248427 --- /dev/null +++ b/docs/assets/collision/pip/discrete_step1.svg @@ -0,0 +1,7 @@ + + + + + + End pose overlap (discrete) + diff --git a/docs/assets/collision/pip/discrete_step2.svg b/docs/assets/collision/pip/discrete_step2.svg new file mode 100644 index 00000000..f2846ec7 --- /dev/null +++ b/docs/assets/collision/pip/discrete_step2.svg @@ -0,0 +1,7 @@ + + + + + + K: pair preserved + diff --git a/docs/assets/collision/pip/discrete_step3.svg b/docs/assets/collision/pip/discrete_step3.svg new file mode 100644 index 00000000..6bb7da0c --- /dev/null +++ b/docs/assets/collision/pip/discrete_step3.svg @@ -0,0 +1,10 @@ + + + + + + + + + Contact manifold (canonical order) + diff --git a/docs/assets/collision/pip/events_step1.svg b/docs/assets/collision/pip/events_step1.svg new file mode 100644 index 00000000..e2e9ff1f --- /dev/null +++ b/docs/assets/collision/pip/events_step1.svg @@ -0,0 +1,9 @@ + + + + + + + + Contacts at n and n−1 (compare) + diff --git a/docs/assets/collision/pip/events_step2.svg b/docs/assets/collision/pip/events_step2.svg new file mode 100644 index 00000000..c3b14f78 --- /dev/null +++ b/docs/assets/collision/pip/events_step2.svg @@ -0,0 +1,9 @@ + + + + + + + + K: states preserved for diff + diff --git a/docs/assets/collision/pip/events_step3.svg b/docs/assets/collision/pip/events_step3.svg new file mode 100644 index 00000000..ae0de6c5 --- /dev/null +++ b/docs/assets/collision/pip/events_step3.svg @@ -0,0 +1,11 @@ + + + + + + + + + Begin + Event node emitted (Begin/Persist/End) + diff --git a/docs/assets/collision/pip/gc_step1.svg b/docs/assets/collision/pip/gc_step1.svg new file mode 100644 index 00000000..0709dff2 --- /dev/null +++ b/docs/assets/collision/pip/gc_step1.svg @@ -0,0 +1,8 @@ + + + + + + + Ephemeral artifacts present + diff --git a/docs/assets/collision/pip/gc_step2.svg b/docs/assets/collision/pip/gc_step2.svg new file mode 100644 index 00000000..f4864573 --- /dev/null +++ b/docs/assets/collision/pip/gc_step2.svg @@ -0,0 +1,8 @@ + + + + + + + GC selection window + diff --git a/docs/assets/collision/pip/gc_step3.svg b/docs/assets/collision/pip/gc_step3.svg new file mode 100644 index 00000000..641fdf26 --- /dev/null +++ b/docs/assets/collision/pip/gc_step3.svg @@ -0,0 +1,5 @@ + + + + Ephemeral cleared (deterministic) + diff --git a/docs/assets/collision/pip/graph_build_proxy_step1.svg b/docs/assets/collision/pip/graph_build_proxy_step1.svg new file mode 100644 index 00000000..cada2fb1 --- /dev/null +++ b/docs/assets/collision/pip/graph_build_proxy_step1.svg @@ -0,0 +1,13 @@ + + + + + Transform(A,n) + + Collider(A) + + Tick(n) + + + produced_in + diff --git a/docs/assets/collision/pip/graph_build_proxy_step2.svg b/docs/assets/collision/pip/graph_build_proxy_step2.svg new file mode 100644 index 00000000..c82b011f --- /dev/null +++ b/docs/assets/collision/pip/graph_build_proxy_step2.svg @@ -0,0 +1,11 @@ + + + + + Transform(A,n) + + Collider(A) + + Tick(n) + + diff --git a/docs/assets/collision/pip/graph_build_proxy_step3.svg b/docs/assets/collision/pip/graph_build_proxy_step3.svg new file mode 100644 index 00000000..4f55ee58 --- /dev/null +++ b/docs/assets/collision/pip/graph_build_proxy_step3.svg @@ -0,0 +1,14 @@ + + + + + Transform(A,n) + + Collider(A) + + Tick(n) + + TemporalProxy(A,n) + + has_proxy + diff --git a/docs/assets/collision/pip/graph_ccd_step1.svg b/docs/assets/collision/pip/graph_ccd_step1.svg new file mode 100644 index 00000000..44622f7e --- /dev/null +++ b/docs/assets/collision/pip/graph_ccd_step1.svg @@ -0,0 +1,7 @@ + + + + + PotentialPair(A,B,n) + Policy: CCD + diff --git a/docs/assets/collision/pip/graph_ccd_step2.svg b/docs/assets/collision/pip/graph_ccd_step2.svg new file mode 100644 index 00000000..8f9237b0 --- /dev/null +++ b/docs/assets/collision/pip/graph_ccd_step2.svg @@ -0,0 +1,7 @@ + + + + + PotentialPair(A,B,n) + Run CA / sweep + diff --git a/docs/assets/collision/pip/graph_ccd_step3.svg b/docs/assets/collision/pip/graph_ccd_step3.svg new file mode 100644 index 00000000..2d729cdd --- /dev/null +++ b/docs/assets/collision/pip/graph_ccd_step3.svg @@ -0,0 +1,12 @@ + + + + + PotentialPair(A,B,n) + + Contact(pair,n) + + Toi(pair,n) + + + diff --git a/docs/assets/collision/pip/graph_discrete_step1.svg b/docs/assets/collision/pip/graph_discrete_step1.svg new file mode 100644 index 00000000..ef8dec34 --- /dev/null +++ b/docs/assets/collision/pip/graph_discrete_step1.svg @@ -0,0 +1,7 @@ + + + + + PotentialPair(A,B,n) + LHS: overlap at end pose + diff --git a/docs/assets/collision/pip/graph_discrete_step2.svg b/docs/assets/collision/pip/graph_discrete_step2.svg new file mode 100644 index 00000000..07802466 --- /dev/null +++ b/docs/assets/collision/pip/graph_discrete_step2.svg @@ -0,0 +1,7 @@ + + + + + PotentialPair(A,B,n) + K: pair preserved + diff --git a/docs/assets/collision/pip/graph_discrete_step3.svg b/docs/assets/collision/pip/graph_discrete_step3.svg new file mode 100644 index 00000000..e6ff976d --- /dev/null +++ b/docs/assets/collision/pip/graph_discrete_step3.svg @@ -0,0 +1,10 @@ + + + + + PotentialPair(A,B,n) + + Contact(pair,n) + + contact_of + diff --git a/docs/assets/collision/pip/graph_events_step1.svg b/docs/assets/collision/pip/graph_events_step1.svg new file mode 100644 index 00000000..2c43658f --- /dev/null +++ b/docs/assets/collision/pip/graph_events_step1.svg @@ -0,0 +1,8 @@ + + + + + Contact(pair,n-1) + + Contact(pair,n) + diff --git a/docs/assets/collision/pip/graph_events_step2.svg b/docs/assets/collision/pip/graph_events_step2.svg new file mode 100644 index 00000000..e2b13f47 --- /dev/null +++ b/docs/assets/collision/pip/graph_events_step2.svg @@ -0,0 +1,9 @@ + + + + + Contact(pair,n-1) + + Contact(pair,n) + K: compare states + diff --git a/docs/assets/collision/pip/graph_events_step3.svg b/docs/assets/collision/pip/graph_events_step3.svg new file mode 100644 index 00000000..e7348665 --- /dev/null +++ b/docs/assets/collision/pip/graph_events_step3.svg @@ -0,0 +1,10 @@ + + + + + Contact(pair,n) + + ContactEvent(kind,n) + + event_of + diff --git a/docs/assets/collision/pip/graph_gc_step1.svg b/docs/assets/collision/pip/graph_gc_step1.svg new file mode 100644 index 00000000..032f3fde --- /dev/null +++ b/docs/assets/collision/pip/graph_gc_step1.svg @@ -0,0 +1,11 @@ + + + + + TemporalProxy(*,n) + + PotentialPair(*,n) + + Toi(*,n) + Ephemeral artifacts + diff --git a/docs/assets/collision/pip/graph_gc_step2.svg b/docs/assets/collision/pip/graph_gc_step2.svg new file mode 100644 index 00000000..43500529 --- /dev/null +++ b/docs/assets/collision/pip/graph_gc_step2.svg @@ -0,0 +1,11 @@ + + + + + TemporalProxy(*,n) + + PotentialPair(*,n) + + Toi(*,n) + Selecting for deletion + diff --git a/docs/assets/collision/pip/graph_gc_step3.svg b/docs/assets/collision/pip/graph_gc_step3.svg new file mode 100644 index 00000000..aaeab7e5 --- /dev/null +++ b/docs/assets/collision/pip/graph_gc_step3.svg @@ -0,0 +1,5 @@ + + + + Ephemeral cleared + diff --git a/docs/assets/collision/pip/graph_pairing_step1.svg b/docs/assets/collision/pip/graph_pairing_step1.svg new file mode 100644 index 00000000..ddce482a --- /dev/null +++ b/docs/assets/collision/pip/graph_pairing_step1.svg @@ -0,0 +1,9 @@ + + + + + TemporalProxy(A,n) + + TemporalProxy(B,n) + LHS: overlapping proxies + diff --git a/docs/assets/collision/pip/graph_pairing_step2.svg b/docs/assets/collision/pip/graph_pairing_step2.svg new file mode 100644 index 00000000..a0a33552 --- /dev/null +++ b/docs/assets/collision/pip/graph_pairing_step2.svg @@ -0,0 +1,9 @@ + + + + + TemporalProxy(A,n) + + TemporalProxy(B,n) + K: proxies preserved + diff --git a/docs/assets/collision/pip/graph_pairing_step3.svg b/docs/assets/collision/pip/graph_pairing_step3.svg new file mode 100644 index 00000000..c8567ae7 --- /dev/null +++ b/docs/assets/collision/pip/graph_pairing_step3.svg @@ -0,0 +1,13 @@ + + + + + TemporalProxy(A,n) + + TemporalProxy(B,n) + + PotentialPair(A,B,n) + + + pair_of + diff --git a/docs/assets/collision/pip/pairing_step1.svg b/docs/assets/collision/pip/pairing_step1.svg new file mode 100644 index 00000000..7df40c48 --- /dev/null +++ b/docs/assets/collision/pip/pairing_step1.svg @@ -0,0 +1,9 @@ + + + + + + + + Overlapping fat AABBs → candidate + diff --git a/docs/assets/collision/pip/pairing_step2.svg b/docs/assets/collision/pip/pairing_step2.svg new file mode 100644 index 00000000..f8f19568 --- /dev/null +++ b/docs/assets/collision/pip/pairing_step2.svg @@ -0,0 +1,9 @@ + + + + + + + + K: proxies preserved (ordering stable) + diff --git a/docs/assets/collision/pip/pairing_step3.svg b/docs/assets/collision/pip/pairing_step3.svg new file mode 100644 index 00000000..9ed71d3a --- /dev/null +++ b/docs/assets/collision/pip/pairing_step3.svg @@ -0,0 +1,10 @@ + + + + + + + + + PotentialPair added (canonical id) + diff --git a/docs/assets/collision/scheduler_phase_mapping.svg b/docs/assets/collision/scheduler_phase_mapping.svg new file mode 100644 index 00000000..3e7b4204 --- /dev/null +++ b/docs/assets/collision/scheduler_phase_mapping.svg @@ -0,0 +1,50 @@ + + + + + + Scheduler Phase Mapping (Collision/CCD) + pre_update → update → post_update → timeline_flush + + + + pre_update + BuildTemporalProxy + + + + + update + BroadPhasePairing + NarrowPhaseDiscrete + NarrowPhaseCCD + + + + + post_update + ContactEvents + + + + + timeline_flush + GC Ephemeral + + diff --git a/docs/code-map.md b/docs/code-map.md new file mode 100644 index 00000000..96081044 --- /dev/null +++ b/docs/code-map.md @@ -0,0 +1,57 @@ +# Echo Code Map + +> Quick index from concepts → code, with the most relevant specs. + +## Crates + +- rmg-core — deterministic graph rewriting engine (Rust) + - Public API aggregator: `crates/rmg-core/src/lib.rs` + - Identifiers & hashing: `crates/rmg-core/src/ident.rs` + - Node/edge records: `crates/rmg-core/src/record.rs` + - In-memory graph store: `crates/rmg-core/src/graph.rs` + - Rules and patterns: `crates/rmg-core/src/rule.rs` + - Transactions: `crates/rmg-core/src/tx.rs` + - Deterministic scheduler: `crates/rmg-core/src/scheduler.rs` + - Snapshots + hashing: `crates/rmg-core/src/snapshot.rs` + - Payload codecs (demo): `crates/rmg-core/src/payload.rs` + - Engine implementation: `crates/rmg-core/src/engine_impl.rs` + - Demo rule: `crates/rmg-core/src/demo/motion.rs` + - Deterministic math: `crates/rmg-core/src/math/*` + - Tests (integration): `crates/rmg-core/tests/*` + +- rmg-ffi — C ABI for host integrations + - `crates/rmg-ffi/src/lib.rs` + +- rmg-wasm — wasm-bindgen bindings + - `crates/rmg-wasm/src/lib.rs` + +- rmg-cli — CLI scaffolding + - `crates/rmg-cli/src/main.rs` + +## Specs → Code + +- RMG core model — docs/spec-rmg-core.md → `ident.rs`, `record.rs`, `graph.rs`, `rule.rs`, `engine_impl.rs`, `snapshot.rs`, `scheduler.rs` +- Scheduler — docs/spec-scheduler.md → `scheduler.rs`, `engine_impl.rs` +- ECS storage (future) — docs/spec-ecs-storage.md → new `ecs/*` modules (TBD) +- Serialization — docs/spec-serialization-protocol.md → `snapshot.rs` (hashing), future codecs +- Deterministic math — docs/spec-deterministic-math.md → `math/*` +- Temporal bridge/Codex’s Baby — docs/spec-temporal-bridge.md, docs/spec-codex-baby.md → future modules (TBD) + +## Conventions + +- Column-major matrices, right-handed coordinates, f32 math. +- One concrete concept per file; keep modules < 300 LoC where feasible. +- Tests live in `crates//tests` and favor small, focused cases. + +## Refactor Policy + +- 1 file = 1 concrete concept (engine, graph store, identifiers, etc.). +- No 500+ LoC “god files”; split before modules exceed ~300 LoC. +- Keep inline tests in separate files under `crates//tests`. +- Maintain stable re-exports in `lib.rs` so public API stays coherent. + +## Onboarding + +- Start with `README.md` and `docs/docs-index.md`. +- For engine flow, read `engine_impl.rs` (apply → schedule → commit → snapshot). +- For demo behavior, see `demo/motion.rs` and tests under `crates/rmg-core/tests/*`. diff --git a/docs/collision-dpo-tour.html b/docs/collision-dpo-tour.html new file mode 100644 index 00000000..6577e606 --- /dev/null +++ b/docs/collision-dpo-tour.html @@ -0,0 +1,210 @@ + + + + + + Echo Collision DPO Tour + + + +

Collision / CCD — DPO Rule Tour

+

Each rule shown as LHS → Interface K → RHS. See the legend for visual semantics.

+

LegendSpec

+ +
+

Graph Anatomy (Everything Is a Graph)

+
+
+ Collision Subgraph Overview +
Overview — typed nodes and edges for one colliding pair at tick n.
+
Node/Edge Graph
Entities, components, temporal proxies, potential pair, contact, TOI, event — all first‑class nodes linked by typed edges (has_component, has_proxy, pair_of, contact_of, event_of, produced_in).
+
+

+ This is the literal graph Echo maintains. Derived artifacts (proxies, pairs, contacts, events) are not hidden engine buffers — + they are nodes that tools can query, branch, replay, and merge deterministically. The same initial facts and policies yield the + same subgraph and the same snapshot hash on every peer. +

+
+
+
+
+ +

How Things Move (time-aware proxies)

+
+

BuildTemporalProxy (pre_update)

+
+
+ BuildTemporalProxy Step 1 +
Step 1 — LHS: Collider + Transform (+ Velocity) at Tick n
+
+

+ We gather the collider’s Transform (and optional Velocity) at tick n. In Echo this input is + explicit graph state, not a transient engine struct. Chronos gives us a fixed dt, so the motion window + for the upcoming tick is well-defined and reproducible. +

+
    +
  • Different: many engines pull from mutable component state ad‑hoc; here we read typed nodes bound to a specific tick.
  • +
  • Determinism: same dt and same components ⇒ same inputs on every peer/branch.
  • +
+
+
+
+ BuildTemporalProxy Step 2 +
Step 2 — Interface K
+
+

+ The DPO Interface K shows what is preserved between LHS and RHS: collider + transform + tick. + This is how we say “the world keeps these facts while we add the proxy.” +

+
    +
  • Different: Echo makes the preserved context explicit; typical engines merge implicit state in-place.
  • +
+
+
+
+ BuildTemporalProxy Step 3 +
Step 3 — RHS: TemporalProxy(e,n) added
+
+

+ We add a TemporalProxy with a fat AABB that encloses motion over [start,end]. Padding is derived from + velocity and quantized policy, so two peers derive the same box. The proxy links back to the entity and Tick n. +

+
    +
  • Different: broad‑phase caches become first‑class graph nodes with stable IDs.
  • +
  • Determinism: quantized padding + stable insertion order ⇒ identical proxy sets.
  • +
+
+
+
+
+

+ This rule deterministically derives a TemporalProxy for each collider at tick n. + The proxy’s “fat AABB” encloses the body over the whole tick window [start,end], so fast movers can’t tunnel between broad‑phase sweeps. + The proxy is a typed node in the graph (not an opaque engine cache) and is linked back to the entity and the producing tick. +

+
    +
  • Different from typical engines: broad‑phase buffers are usually internal and mutation‑ordered; in Echo they are explicit graph nodes, created by a rewrite with a stable scope and ID.
  • +
  • Determinism: proxy size and padding are computed from quantized policy values; insert order is sorted by ID, so peers/branches build identical proxy sets.
  • +
+
+
+ +

How Collision Works (broad → narrow → events)

+
+

BroadPhasePairing (update)

+
+
BroadPhasePairing Step 1
Step 1 — LHS: overlapping proxies

We test fat AABB overlap on proxies built for the full tick window. Overlap means the pair is a candidate for narrow phase.

  • Different: the candidate condition is a graph fact, not an opaque boolean.
  • Determinism: identical proxies ⇒ identical overlap set.
+
BroadPhasePairing Step 2
Step 2 — K: proxies preserved

The proxies themselves are preserved (K). This makes the rule commute with other rules that may also read them this tick.

+
BroadPhasePairing Step 3
Step 3 — RHS: PotentialPair added

We mint a PotentialPair with canonical PairId = H(min(A,B)||max(A,B)||branch) and back‑refs to proxies.

  • Different: pair lists are reproducible data, not engine iteration order.
  • Determinism: output list is sorted strictly; peers/branches match.
+
+
+

+ The broad phase converts overlapping proxies into PotentialPair nodes. Each pair gets a canonical + PairId = H(min(A,B) || max(A,B) || branch) and edges back to the proxies. The emitted list is + sorted deterministically, which makes network replication and timeline diffs trivial. +

+
    +
  • Different: most engines keep an internal unsorted array of candidate pairs; Echo materializes pairs as graph facts, with stable IDs and ordering.
  • +
  • Determinism: ties in AABB endpoints break on IDs; output is strictly sorted, so two peers converge on identical pair order.
  • +
+
+
+ +
+

NarrowPhaseDiscrete (update)

+
+
NarrowPhaseDiscrete Step 1
Step 1 — LHS: discrete overlap @ end pose

For low‑speed pairs, we evaluate shapes at the end pose of tick n. If they overlap, we proceed to build a manifold.

  • Different: thresholding policy is data; no hidden time‑step heuristics.
+
NarrowPhaseDiscrete Step 2
Step 2 — K: pair preserved

We keep the PotentialPair (K). The narrow phase acts as a pure derivation from pair+poses.

+
NarrowPhaseDiscrete Step 3
Step 3 — RHS: Contact with Manifold added

We create a Contact with a reduced Manifold (2–4 points). Points are canonicalized by feature IDs to ensure reproducible ordering.

  • Different: engine doesn’t call your code mid‑narrow; it records facts you can read consistently.
  • Determinism: centralized tolerances + ordering.
+
+
+

+ For low‑speed pairs, we evaluate shapes at end‑of‑tick poses and, if intersecting, create a Contact with a + deterministically ordered Manifold (2–4 clipped points). The contact attaches to the pair and to the producing tick. + Manifold point ordering and feature IDs are canonicalized to remove platform drift. +

+
    +
  • Different: instead of imperative callbacks that mutate scripts, Echo records contacts as first‑class nodes; scripts read them after rules run.
  • +
  • Determinism: manifold reduction, feature selection, and floating‑point tolerances are centralized and quantized.
  • +
+
+
+ +
+

NarrowPhaseCCD (update)

+
+
NarrowPhaseCCD Step 1
Step 1 — LHS: CCD policy triggers

Policy flags fast motion/small features (or material‑required CCD). We will compute a Toi in [0,1] before creating a contact.

+
NarrowPhaseCCD Step 2
Step 2 — K: pair preserved

We keep the pair (K) and run conservative advancement or a swept primitive test to find the impact time.

+
NarrowPhaseCCD Step 3
Step 3 — RHS: Toi + Contact added

We emit a Toi node with quantized s and a Contact at the impact pose. Quantization and iteration caps are recorded to make this stable.

+
+
+

+ When a policy indicates high motion or small features, we run CCD: conservative advancement for general convex + shapes or closed‑form sweeps for spheres/capsules. We emit a Toi node with quantized s ∈ [0,1] and a Contact + at the impact pose. Because s is quantized and the rule scopes are stable, peers compute the same TOI and contact set. +

+
    +
  • Different: CCD outputs are persisted as graph data (Toi + Contact), not transient solver state; branches and replays see identical values.
  • +
  • Determinism: iteration caps, policy thresholds, and s quantization are recorded; identical inputs yield identical s.
  • +
+
+
+ +
+

ContactEvents (post_update)

+
+
ContactEvents Step 1
Step 1 — LHS: contact states n-1 vs n

We stage previous and current Contact facts for the pair to compute Begin/Persist/End.

+
ContactEvents Step 2
Step 2 — K: contacts preserved

K keeps both contact nodes in scope so event construction is a pure comparison, not in‑place mutation.

+
ContactEvents Step 3
Step 3 — RHS: ContactEvent added

We create a ContactEvent (Begin/Persist/End) sorted by (toi_s, ContactId). Events are nodes that tools and scripts can consume deterministically.

+
+
+

+ We diff previous vs current Contact nodes and create a ContactEvent (Begin/Persist/End) ordered by + (toi_s, ContactId). Events are regular nodes and flow through the Temporal Bridge to tools, replay, or networking. +

+
    +
  • Different: engines typically invoke user callbacks in engine order; Echo records events as data first, then tooling/scripts consume them deterministically.
  • +
  • Determinism: strict sort order; event payloads are value objects that hash the same on every peer.
  • +
+
+
+ +

How We Keep It Clean (deterministic GC)

+
+

GC Ephemeral (timeline_flush)

+
+
GC Ephemeral Step 1
Step 1 — LHS: ephemeral nodes

Before flush, the frame has proxies, pairs, TOIs and optional per‑tick contacts. They’re marked ephemeral.

+
GC Ephemeral Step 2
Step 2 — Selection

We deterministically select unreferenced, older artifacts for deletion. The retention policy is configured and recorded.

+
GC Ephemeral Step 3
Step 3 — RHS: nodes deleted

We remove the selected nodes in a stable ID order. Snapshots after flush are identical across peers/branches.

+
+
+

+ Broad‑phase proxies, potential pairs, transient TOIs and, optionally, per‑tick contacts are ephemeral. At + timeline_flush we delete them in a deterministic order. We keep only the high‑value artifacts (Aion‑tagged events, metrics) for tools and audits. +

+
    +
  • Different: many engines leak implicit caches across frames; Echo models and cleans them explicitly as graph data.
  • +
  • Determinism: GC order is sorted by ID; post‑flush snapshots are identical across branches and peers.
  • +
+
+
+ + + + diff --git a/docs/decision-log.md b/docs/decision-log.md index e0763c33..8ec05e45 100644 --- a/docs/decision-log.md +++ b/docs/decision-log.md @@ -54,3 +54,9 @@ - Decision: Raise the workspace floor (MSRV) to Rust 1.71.1. All crates and CI jobs target 1.71.1. - Implementation: Updated `rust-toolchain.toml` to 1.71.1; bumped `rust-version` in crate manifests; CI jobs pin 1.71.1; devcontainer installs only 1.71.1. +## 2025-10-29 — Docs E2E carousel init (PR #10) + +- Context: Playwright tour test clicks Next to enter carousel from "all" mode. +- Decision: Do not disable Prev/Next in "all" mode; allow navigation buttons to toggle into carousel. +- Change: docs/assets/collision/animate.js leaves Prev/Next enabled in 'all'; boundary disabling still applies in single-slide mode. +- Consequence: Users can initiate the carousel via navigation controls; E2E tour test passes deterministically. diff --git a/docs/execution-plan.md b/docs/execution-plan.md index 76bec188..13e0a0f3 100644 --- a/docs/execution-plan.md +++ b/docs/execution-plan.md @@ -33,6 +33,12 @@ This is Codex’s working map for building Echo. Update it relentlessly—each s ## Today’s Intent +> 2025-10-29 — Docs E2E (PR #10) + +- Collision DPO tour carousel: keep Prev/Next enabled in "all" mode so users and tests can enter carousel via navigation. Fixes Playwright tour test. +- Updated Makefile by merging hooks target with docs targets. +- CI Docs Guard satisfied with this entry; Decision Log updated. + > 2025-10-29 — rmg-core snapshot header + tx/rules hardening (PR #9 base) - Adopt Snapshot v1 header shape in `rmg-core` with `parents: Vec`, and canonical digests: diff --git a/docs/guide/collision-tour.md b/docs/guide/collision-tour.md new file mode 100644 index 00000000..5bb3c340 --- /dev/null +++ b/docs/guide/collision-tour.md @@ -0,0 +1,13 @@ +--- +title: Collision DPO Tour +--- + +# Collision DPO Tour + +The interactive tour illustrates how Echo models collision and CCD as DPO graph rewrites. + +- Launch the tour: [docs/collision-dpo-tour.html](../collision-dpo-tour.html) +- Assets live under `docs/assets/collision/`. + +Tip: Toggle the World/Graph tabs in the picture-in-picture panel and use the Prev/Next buttons to step through each rule. + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..9a3709db --- /dev/null +++ b/docs/index.md @@ -0,0 +1,11 @@ +--- +title: Echo Docs +--- + +# Echo + +Deterministic, multiverse-aware ECS. + +- See the Collision DPO Tour: [Open the interactive HTML](./collision-dpo-tour.html). +- Read the spec: [Geometry & Collision](./spec-geom-collision.md). + diff --git a/e2e/collision-dpo-tour.spec.ts b/e2e/collision-dpo-tour.spec.ts new file mode 100644 index 00000000..14aee96f --- /dev/null +++ b/e2e/collision-dpo-tour.spec.ts @@ -0,0 +1,51 @@ +import { test, expect } from '@playwright/test' +import { resolve } from 'node:path' +import { pathToFileURL } from 'node:url' + +function fileUrl(rel: string) { + return pathToFileURL(resolve(rel)).href +} + +test.describe('Collision DPO Tour (static HTML)', () => { + test('loads and renders', async ({ page }) => { + await page.goto(fileUrl('docs/collision-dpo-tour.html')) + await expect(page.locator('h1')).toHaveText(/Collision/i) + // Animate script attaches pagers; ensure at least one exists + await expect(page.locator('.pager').first()).toBeVisible() + }) + + test('tabs toggle World/Graph views', async ({ page }) => { + await page.goto(fileUrl('docs/collision-dpo-tour.html')) + // Find a figure with pip tabs + const tabs = page.locator('.pip-tabs').first() + await expect(tabs).toBeVisible() + const graphTab = tabs.locator('.tab', { hasText: 'Graph' }) + const worldTab = tabs.locator('.tab', { hasText: 'World' }) + await graphTab.click() + // Graph image should be visible, world hidden within the same figure + const fig = tabs.locator('..') // pip + const pip = fig + await expect(pip.locator('img[alt="Graph view"]')).toBeVisible() + await expect(pip.locator('img[alt="World view"]')).toBeHidden() + await worldTab.click() + await expect(pip.locator('img[alt="World view"]')).toBeVisible() + }) + + test('prev/next navigation toggles carousel mode', async ({ page }) => { + await page.goto(fileUrl('docs/collision-dpo-tour.html')) + const firstRule = page.locator('.rule').first() + const nextBtn = firstRule.locator('.pager .btn', { hasText: 'Next' }) + await expect(nextBtn).toBeVisible() + // Initially all slides are visible + const figs = firstRule.locator('.step-grid figure') + const total = await figs.count() + expect(total).toBeGreaterThan(1) + // Click next -> enter carousel mode (only one visible) + await nextBtn.click() + // Wait a tick for layout updates + await page.waitForTimeout(50) + const hiddenCount = await firstRule.locator('.step-grid figure.hidden').count() + expect(hiddenCount).toBeGreaterThan(0) + }) +}) + diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 00000000..11509fc4 --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from '@playwright/test' + +export default defineConfig({ + testDir: './e2e', + retries: 0, + use: { + headless: true, + viewport: { width: 1280, height: 800 }, + ignoreHTTPSErrors: true, + }, +}) + diff --git a/package.json b/package.json new file mode 100644 index 00000000..e040490d --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "echo-docs-and-tests", + "private": true, + "scripts": { + "docs:dev": "vitepress dev docs", + "docs:build": "vitepress build docs", + "test:e2e": "playwright test" + }, + "devDependencies": { + "@playwright/test": "^1.48.0", + "vitepress": "^1.3.3" + } +} + diff --git a/scripts/docs-open.sh b/scripts/docs-open.sh new file mode 100755 index 00000000..3365a020 --- /dev/null +++ b/scripts/docs-open.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -euo pipefail +URL="${1:-http://localhost:5173/}" +case "${OSTYPE:-}" in + darwin*) open "$URL" ;; + linux*) xdg-open "$URL" >/dev/null 2>&1 || echo "Open: $URL" ;; + msys*|cygwin*|win32*) cmd.exe /c start "" "$URL" ;; + *) echo "Open: $URL" ;; +esac +