Compare commits
37 Commits
test/billi
...
claude/arc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f0fd79a76 | ||
|
|
bdced9ce86 | ||
|
|
5d83ee5f3c | ||
|
|
cd7c28ed92 | ||
|
|
0b425dfd80 | ||
|
|
0a880faad8 | ||
|
|
b8f26177c9 | ||
|
|
244e549116 | ||
|
|
d7f425b51b | ||
|
|
1a9091605a | ||
|
|
805ff2db31 | ||
|
|
ac5c291990 | ||
|
|
e2f0add917 | ||
|
|
dadc5a076f | ||
|
|
ddb6562ff6 | ||
|
|
7294072f9e | ||
|
|
3ee2cd37c8 | ||
|
|
cf4bf78aa0 | ||
|
|
08eee56a68 | ||
|
|
c3617e0af8 | ||
|
|
7c976e128e | ||
|
|
886bc1ef7c | ||
|
|
1cfaaac511 | ||
|
|
a984872ae3 | ||
|
|
b78db34e42 | ||
|
|
6c89fa9385 | ||
|
|
1471684821 | ||
|
|
94791b4e65 | ||
|
|
fc8775bf38 | ||
|
|
6e81d71f1b | ||
|
|
f92205ca46 | ||
|
|
96c737bc0e | ||
|
|
301cdda9f0 | ||
|
|
91fd59f246 | ||
|
|
0b924e3ae1 | ||
|
|
b468ea83ea | ||
|
|
5892720f69 |
1
.gitignore
vendored
@@ -66,6 +66,7 @@ dist.zip
|
||||
|
||||
/temp/
|
||||
/tmp/
|
||||
.superpowers/
|
||||
|
||||
# Generated JSON Schemas
|
||||
/schemas/
|
||||
|
||||
16
apps/architecture-adventure/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Codebase Caverns — ComfyUI Architecture Adventure</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="A prestige-driven architecture adventure game. Discover problems, learn patterns, make decisions, and watch the consequences unfold."
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
60
apps/architecture-adventure/package.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "@comfyorg/architecture-adventure",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc --noEmit && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:"
|
||||
},
|
||||
"nx": {
|
||||
"tags": [
|
||||
"scope:docs",
|
||||
"type:app"
|
||||
],
|
||||
"targets": {
|
||||
"dev": {
|
||||
"executor": "nx:run-commands",
|
||||
"continuous": true,
|
||||
"options": {
|
||||
"cwd": "apps/architecture-adventure",
|
||||
"command": "vite"
|
||||
}
|
||||
},
|
||||
"build": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"options": {
|
||||
"command": "vite build --config apps/architecture-adventure/vite.config.ts"
|
||||
},
|
||||
"outputs": [
|
||||
"{projectRoot}/dist"
|
||||
]
|
||||
},
|
||||
"preview": {
|
||||
"executor": "nx:run-commands",
|
||||
"continuous": true,
|
||||
"dependsOn": [
|
||||
"build"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "apps/architecture-adventure",
|
||||
"command": "vite preview"
|
||||
}
|
||||
},
|
||||
"typecheck": {
|
||||
"executor": "nx:run-commands",
|
||||
"cache": true,
|
||||
"options": {
|
||||
"cwd": "apps/architecture-adventure",
|
||||
"command": "tsc --noEmit"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
apps/architecture-adventure/src/main.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
function main(): void {
|
||||
const app = document.getElementById('app')
|
||||
if (!app) throw new Error('Missing #app element')
|
||||
app.textContent = 'Codebase Caverns v2 — Loading...'
|
||||
}
|
||||
|
||||
main()
|
||||
21
apps/architecture-adventure/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2023",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": ["src/**/*.ts", "vite.config.ts"]
|
||||
}
|
||||
26
apps/architecture-adventure/vite.config.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
const projectRoot = fileURLToPath(new URL('.', import.meta.url))
|
||||
|
||||
export default defineConfig({
|
||||
root: projectRoot,
|
||||
base: './',
|
||||
build: {
|
||||
target: 'es2022',
|
||||
outDir: 'dist',
|
||||
assetsInlineLimit: 1_000_000,
|
||||
cssCodeSplit: false,
|
||||
rolldownOptions: {
|
||||
output: {
|
||||
inlineDynamicImports: true
|
||||
}
|
||||
}
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(projectRoot, 'src')
|
||||
}
|
||||
}
|
||||
})
|
||||
683
docs/architecture/WALKTHROUGH.txt
Normal file
@@ -0,0 +1,683 @@
|
||||
===============================================================================
|
||||
____ _ _ ____
|
||||
/ ___|___ __| | ___| |__ __ _ ___ ___ / ___|__ ___ _____ _ __ _ __ ___
|
||||
| | / _ \ / _` |/ _ \ '_ \ / _` / __|/ _ \ | | / _` \ \ / / _ \ '__| '_ \/ __|
|
||||
| |__| (_) | (_| | __/ |_) | (_| \__ \ __/ | |__| (_| |\ V / __/ | | | | \__ \
|
||||
\____\___/ \__,_|\___|_.__/ \__,_|___/\___| \____\__,_| \_/ \___|_| |_| |_|___/
|
||||
|
||||
ComfyUI Frontend Architecture Adventure - Complete Walkthrough
|
||||
===============================================================================
|
||||
|
||||
Platform: Web Browser (any modern browser)
|
||||
Version: 1.0
|
||||
Author: An Architect Who Has Seen Things
|
||||
Last Updated: 2026-03-24
|
||||
Spoilers: YES. This guide contains ALL solutions and ALL endings.
|
||||
|
||||
===============================================================================
|
||||
TABLE OF CONTENTS
|
||||
===============================================================================
|
||||
|
||||
I. Introduction & Controls
|
||||
II. Game Mechanics
|
||||
III. Room Guide & Map
|
||||
IV. Challenge Solutions (SPOILERS)
|
||||
V. Optimal Route - "The ECS Enlightenment" Speedrun
|
||||
VI. All Four Endings
|
||||
VII. Achievements
|
||||
VIII. Artifacts Checklist
|
||||
IX. Pro Tips & Secrets
|
||||
|
||||
===============================================================================
|
||||
I. INTRODUCTION & CONTROLS
|
||||
===============================================================================
|
||||
|
||||
Codebase Caverns is an interactive choose-your-own-adventure game that
|
||||
teaches you the architecture of the ComfyUI frontend codebase. You explore
|
||||
10 rooms representing different architectural layers, face 9 real engineering
|
||||
challenges, collect artifacts, and reach one of 4 endings based on your
|
||||
decisions.
|
||||
|
||||
Every challenge in this game is based on REAL architectural problems
|
||||
documented in the ComfyUI frontend repo. The "correct" answers match the
|
||||
actual migration strategy being used in production.
|
||||
|
||||
CONTROLS:
|
||||
=========
|
||||
1, 2, 3 Navigate between rooms (press the number key)
|
||||
A, B, C Choose a challenge option (press the letter key)
|
||||
M Toggle the map overlay
|
||||
Escape Close the map / close ending preview
|
||||
|
||||
BUTTONS:
|
||||
========
|
||||
Map [M] Opens the room map overlay
|
||||
Restart Resets the current run (keeps achievements)
|
||||
Play Again After an ending, starts a new run
|
||||
|
||||
Your progress auto-saves to localStorage. Close the tab and come back
|
||||
later - you'll pick up right where you left off.
|
||||
|
||||
===============================================================================
|
||||
II. GAME MECHANICS
|
||||
===============================================================================
|
||||
|
||||
STATS
|
||||
=====
|
||||
You have four stats tracked in the HUD at the top:
|
||||
|
||||
Debt [||||||||..] 50 Technical debt. LOWER is better.
|
||||
Quality [|||.......] 30 Code quality. HIGHER is better.
|
||||
Morale [||||||....] 60 Team morale. HIGHER is better.
|
||||
ECS [.........] 0/5 Migration progress. 5 is max.
|
||||
|
||||
Each challenge choice modifies these stats. Your final stats determine
|
||||
which of the 4 endings you get.
|
||||
|
||||
CHALLENGES
|
||||
==========
|
||||
9 of the 10 rooms contain a one-time challenge - an architectural dilemma
|
||||
with 2-3 options. Each option has a rating:
|
||||
|
||||
[GOOD] Best practice. Matches the real migration strategy.
|
||||
Usually: Debt down, Quality up, +1 ECS progress.
|
||||
|
||||
[OK] Pragmatic but imperfect. Gets the job done.
|
||||
Mixed stat effects.
|
||||
|
||||
[BAD] Tempting but harmful. Short-term gain, long-term pain.
|
||||
Usually: Debt up or Morale down.
|
||||
|
||||
After choosing, you see your result, the recommended answer, and a link
|
||||
to the real architecture documentation that explains why.
|
||||
|
||||
ARTIFACTS
|
||||
=========
|
||||
Rooms contain collectible artifacts - key files and concepts from the
|
||||
codebase. These are auto-collected when you enter a room. They appear
|
||||
as icons in your Inventory sidebar.
|
||||
|
||||
ENDINGS
|
||||
=======
|
||||
After resolving all 8 challenges, you get one of 4 endings based on
|
||||
your accumulated stats. See Section VI for details.
|
||||
|
||||
ACHIEVEMENTS
|
||||
============
|
||||
Each ending you reach is permanently saved as an achievement badge.
|
||||
Achievements persist across runs - even after restarting. Click an
|
||||
unlocked badge to review that ending's screen.
|
||||
|
||||
===============================================================================
|
||||
III. ROOM GUIDE & MAP
|
||||
===============================================================================
|
||||
|
||||
+-------------------+
|
||||
| ENTRY POINT |
|
||||
| (src/main.ts) |
|
||||
+-+--------+------+-+
|
||||
| | |
|
||||
+----------+ | +-----------+
|
||||
| | |
|
||||
+---v----------+ +-----v--------+ +------v---------+
|
||||
| COMPONENT | | STORE | | SERVICE |
|
||||
| GALLERY | | VAULTS | | CORRIDORS |
|
||||
| [Challenge] | | [Challenge] | | [Challenge] |
|
||||
+--+------+----+ +--+------+----+ +--------+-------+
|
||||
| | | | |
|
||||
| | +----v---+ | +------v-------+
|
||||
| | | ECS | | | COMPOSABLES |
|
||||
| | | CHAMB. | | | WORKSHOP |
|
||||
| | | [Chal] | | | [Challenge] |
|
||||
| | +---+----+ | +--------------+
|
||||
| | | +----v------+
|
||||
| | +----v--+--+ |
|
||||
| | |SUBGRAPH| RENDERER |
|
||||
| | | DEPTHS | OVERLOOK |
|
||||
| | | [Chal] | [Chal] |
|
||||
| | +--------+----------+
|
||||
| |
|
||||
+--v------v----+
|
||||
| LITEGRAPH |
|
||||
| ENGINE |
|
||||
| [Challenge] |
|
||||
+------+-------+
|
||||
|
|
||||
+------v-------+
|
||||
| COMMAND |
|
||||
| FORGE |
|
||||
| [Challenge] |
|
||||
+--------------+
|
||||
|
||||
ROOM DETAILS:
|
||||
=============
|
||||
|
||||
1. THE ENTRY POINT [src/main.ts]
|
||||
No challenge. No artifacts. Starting room.
|
||||
Exits: Components (1), Stores (2), Services (3)
|
||||
|
||||
2. THE COMPONENT GALLERY [Presentation]
|
||||
Challenge: The Circular Dependency
|
||||
Artifacts: GraphView.vue
|
||||
Exits: Litegraph (1), Command Forge (2), Entry (3)
|
||||
|
||||
3. THE STORE VAULTS [State]
|
||||
Challenge: The Scattered Mutations
|
||||
Artifacts: widgetValueStore.ts, layoutStore.ts
|
||||
Exits: ECS (1), Renderer (2), Entry (3)
|
||||
|
||||
4. THE SERVICE CORRIDORS [Services]
|
||||
Challenge: The Migration Question
|
||||
Artifacts: litegraphService.ts, Extension Migration Guide
|
||||
Exits: Composables (1), Entry (2)
|
||||
|
||||
5. THE LITEGRAPH ENGINE ROOM [Graph Engine]
|
||||
Challenge: The God Object Dilemma
|
||||
Artifacts: LGraphCanvas.ts, LGraphNode.ts
|
||||
Exits: ECS (1), Components (2), Entry (3)
|
||||
|
||||
6. THE ECS ARCHITECT'S CHAMBER [ECS]
|
||||
Challenge: The ID Crossroads
|
||||
Artifacts: World Registry, Branded Entity IDs
|
||||
Exits: Subgraph Depths (1), Renderer (2), Entry (3)
|
||||
|
||||
7. THE SUBGRAPH DEPTHS [Graph Boundaries]
|
||||
Challenge: The Widget Promotion Decision
|
||||
Artifacts: SubgraphStructure, Typed Interface Contracts
|
||||
Exits: ECS (1), Litegraph (2), Entry (3)
|
||||
|
||||
8. THE RENDERER OVERLOOK [Renderer]
|
||||
Challenge: The Render-Time Mutation
|
||||
Artifacts: QuadTree Spatial Index, Y.js CRDT Layout
|
||||
Exits: ECS (1), Entry (2)
|
||||
|
||||
9. THE COMPOSABLES WORKSHOP [Composables]
|
||||
Challenge: The Collaboration Protocol
|
||||
Artifacts: useCoreCommands.ts
|
||||
Exits: Stores (1), Entry (2)
|
||||
|
||||
10. THE COMMAND FORGE [Commands & Intent]
|
||||
Challenge: The Mutation Gateway
|
||||
Artifacts: CommandExecutor, Command Interface
|
||||
Exits: Components (1), Stores (2), Entry (3)
|
||||
|
||||
===============================================================================
|
||||
IV. CHALLENGE SOLUTIONS (SPOILERS)
|
||||
===============================================================================
|
||||
|
||||
*** WARNING: FULL SOLUTIONS BELOW ***
|
||||
*** SCROLL PAST SECTION VI IF YOU WANT TO PLAY BLIND ***
|
||||
|
||||
.--------------------------------------------------------------------.
|
||||
| CHALLENGE 1: The Circular Dependency | Room: Components |
|
||||
|------------------------------------------------------------------ |
|
||||
| Subgraph extends LGraph, but LGraph creates Subgraph instances. |
|
||||
| Circular import forces order-dependent barrel exports. |
|
||||
|------------------------------------------------------------------ |
|
||||
| |
|
||||
| >>> A. Composition over inheritance [GOOD] <<< |
|
||||
| Debt -10, Quality +15, Morale +5, ECS +1 |
|
||||
| A subgraph IS a graph - just a node with SubgraphStructure. |
|
||||
| Under graph unification, no class inheritance at all. |
|
||||
| |
|
||||
| B. Barrel file reordering [BAD] |
|
||||
| Debt +10, Quality -5, Morale -5 |
|
||||
| Band-aid. The coupling remains and will break again. |
|
||||
| |
|
||||
| C. Factory injection [OK] |
|
||||
| Debt -5, Quality +10 |
|
||||
| Pragmatic fix but classes stay coupled at runtime. |
|
||||
'--------------------------------------------------------------------'
|
||||
|
||||
.--------------------------------------------------------------------.
|
||||
| CHALLENGE 2: The Scattered Mutations | Room: Stores |
|
||||
|------------------------------------------------------------------ |
|
||||
| graph._version++ appears in 19 locations across 7 files. |
|
||||
| One missed site = silent data loss. |
|
||||
|------------------------------------------------------------------ |
|
||||
| |
|
||||
| >>> A. Centralize into graph.incrementVersion() [GOOD] <<< |
|
||||
| Debt -15, Quality +15, ECS +1 |
|
||||
| This is Phase 0a of the real migration plan. |
|
||||
| 19 sites -> 1 method. Auditable change tracking. |
|
||||
| |
|
||||
| B. Add a JavaScript Proxy [OK] |
|
||||
| Debt +5, Quality +5, Morale -5 |
|
||||
| Catches mutations but adds opaque runtime overhead. |
|
||||
| |
|
||||
| C. Leave it as-is [BAD] |
|
||||
| Debt +10, Morale +5 |
|
||||
| "It works, don't touch it" - until it doesn't. |
|
||||
'--------------------------------------------------------------------'
|
||||
|
||||
.--------------------------------------------------------------------.
|
||||
| CHALLENGE 3: The Migration Question | Room: Services |
|
||||
|------------------------------------------------------------------ |
|
||||
| Legacy litegraph works. How to migrate to ECS without breaking |
|
||||
| production for thousands of users? |
|
||||
|------------------------------------------------------------------ |
|
||||
| |
|
||||
| >>> A. 5-phase incremental plan [GOOD] <<< |
|
||||
| Quality +15, Morale +10, ECS +1 |
|
||||
| Foundation -> Types -> Bridge -> Systems -> Legacy Removal. |
|
||||
| Each phase independently shippable. This is the real plan. |
|
||||
| |
|
||||
| B. Big bang rewrite [BAD] |
|
||||
| Debt -10, Quality +5, Morale -20 |
|
||||
| Feature freeze + scope creep + burnout = disaster. |
|
||||
| |
|
||||
| C. Strangler fig pattern [OK] |
|
||||
| Quality +10, Morale +5 |
|
||||
| Solid pattern but lacks clear milestones without a plan. |
|
||||
'--------------------------------------------------------------------'
|
||||
|
||||
.--------------------------------------------------------------------.
|
||||
| CHALLENGE 4: The God Object Dilemma | Room: Litegraph |
|
||||
|------------------------------------------------------------------ |
|
||||
| LGraphCanvas: ~9,100 lines. LGraphNode: ~4,300 lines. |
|
||||
| God objects mixing rendering, serialization, connectivity, etc. |
|
||||
|------------------------------------------------------------------ |
|
||||
| |
|
||||
| A. Rewrite from scratch [BAD] |
|
||||
| Debt -20, Quality +5, Morale -25 |
|
||||
| Heroic rewrite stalls at month three. Team burns out. |
|
||||
| |
|
||||
| >>> B. Extract incrementally [GOOD] <<< |
|
||||
| Debt -10, Quality +15, Morale +5, ECS +1 |
|
||||
| Position -> Connectivity -> Rendering. Small testable PRs. |
|
||||
| This matches the actual migration strategy. |
|
||||
| |
|
||||
| C. Add a facade layer [OK] |
|
||||
| Debt +5, Quality +5, Morale +10 |
|
||||
| Nicer API but complexity lives behind the facade. |
|
||||
| |
|
||||
| NOTE: This is the only challenge where A is NOT the best answer! |
|
||||
'--------------------------------------------------------------------'
|
||||
|
||||
.--------------------------------------------------------------------.
|
||||
| CHALLENGE 5: The ID Crossroads | Room: ECS |
|
||||
|------------------------------------------------------------------ |
|
||||
| NodeId is number | string. Nothing prevents passing a LinkId |
|
||||
| where a NodeId is expected. Six entity kinds share one ID space. |
|
||||
|------------------------------------------------------------------ |
|
||||
| |
|
||||
| >>> A. Branded types with cast helpers [GOOD] <<< |
|
||||
| Debt -15, Quality +20, ECS +1 |
|
||||
| type NodeEntityId = number & { __brand: 'NodeEntityId' } |
|
||||
| Compile-time safety, zero runtime cost. Phase 1a. |
|
||||
| |
|
||||
| B. String prefixes at runtime [OK] |
|
||||
| Debt +5, Quality +5, Morale -5 |
|
||||
| "node:42" - parsing overhead everywhere. |
|
||||
| |
|
||||
| C. Keep plain numbers [BAD] |
|
||||
| Debt +15, Quality -5 |
|
||||
| "Just be careful" - someone WILL pass the wrong ID. |
|
||||
'--------------------------------------------------------------------'
|
||||
|
||||
.--------------------------------------------------------------------.
|
||||
| CHALLENGE 6: The Widget Promotion Decision | Room: Subgraph |
|
||||
|------------------------------------------------------------------ |
|
||||
| A user promotes a widget from inside a subgraph to the parent. |
|
||||
| Today this needs PromotionStore + ViewManager + PromotedWidgetView |
|
||||
| — a parallel state system. Two ECS candidates. |
|
||||
|------------------------------------------------------------------ |
|
||||
| |
|
||||
| >>> A. Connections-only: promotion = typed input [GOOD] <<< |
|
||||
| Debt -15, Quality +15, Morale +5, ECS +1 |
|
||||
| Promotion = adding an interface input. Type->widget mapping |
|
||||
| creates the widget automatically. Eliminates PromotionStore, |
|
||||
| ViewManager, and PromotedWidgetView entirely. |
|
||||
| |
|
||||
| B. Simplified component promotion [OK] |
|
||||
| Debt -5, Quality +10, Morale +5 |
|
||||
| WidgetPromotion component on widget entities. Removes |
|
||||
| ViewManager but keeps promotion as a distinct concept. |
|
||||
| Shared subgraph instance ambiguity remains. |
|
||||
| |
|
||||
| C. Keep the current three-layer system [BAD] |
|
||||
| Debt +10, Quality -5, Morale -5 |
|
||||
| The parallel state system persists indefinitely. |
|
||||
'--------------------------------------------------------------------'
|
||||
|
||||
.--------------------------------------------------------------------.
|
||||
| CHALLENGE 7: The Render-Time Mutation | Room: Renderer |
|
||||
|------------------------------------------------------------------ |
|
||||
| drawNode() calls _setConcreteSlots() and arrange() during the |
|
||||
| render pass. Draw order affects layout. Classic mutation-in- |
|
||||
| render bug. |
|
||||
|------------------------------------------------------------------ |
|
||||
| |
|
||||
| >>> A. Separate update and render phases [GOOD] <<< |
|
||||
| Debt -15, Quality +15, ECS +1 |
|
||||
| Input -> Update (layout) -> Render (read-only). |
|
||||
| Matches the ECS system pipeline design. |
|
||||
| |
|
||||
| B. Dirty flags and deferred render [OK] |
|
||||
| Debt -5, Quality +5, Morale +5 |
|
||||
| Reduces symptoms but render pass can still mutate. |
|
||||
| |
|
||||
| NOTE: Only 2 options here. Both are reasonable; A is optimal. |
|
||||
'--------------------------------------------------------------------'
|
||||
|
||||
.--------------------------------------------------------------------.
|
||||
| CHALLENGE 8: The Collaboration Protocol | Room: Composables |
|
||||
|------------------------------------------------------------------ |
|
||||
| Multiple users want to edit the same workflow simultaneously. |
|
||||
| layoutStore already extracts position data. How to sync? |
|
||||
|------------------------------------------------------------------ |
|
||||
| |
|
||||
| >>> A. Y.js CRDTs [GOOD] <<< |
|
||||
| Debt -10, Quality +15, Morale +10 |
|
||||
| Conflict-free replicated data types. Already proven. |
|
||||
| This is what the real layoutStore uses. |
|
||||
| |
|
||||
| B. Polling-based sync [BAD] |
|
||||
| Debt +10, Quality -5, Morale -5 |
|
||||
| Flickering, lag, silent data loss. Support nightmare. |
|
||||
| |
|
||||
| C. Skip collaboration for now [OK] |
|
||||
| Morale +5 |
|
||||
| Pragmatic delay but cloud team won't be happy. |
|
||||
'--------------------------------------------------------------------'
|
||||
|
||||
.--------------------------------------------------------------------.
|
||||
| CHALLENGE 9: The Mutation Gateway | Room: Command Forge |
|
||||
|------------------------------------------------------------------ |
|
||||
| The World's imperative API (world.setComponent()) vs. the command |
|
||||
| pattern requirement from ADR 0003. How should external callers |
|
||||
| mutate the World? |
|
||||
|------------------------------------------------------------------ |
|
||||
| |
|
||||
| >>> A. Commands as intent; systems as handlers; World as store <<< |
|
||||
| Debt -10, Quality +15, Morale +5, ECS +1 [GOOD] |
|
||||
| Caller -> Command -> System -> World -> Y.js. Commands are |
|
||||
| serializable. ADR 0003 and ADR 0008 are complementary. |
|
||||
| |
|
||||
| B. Make World.setComponent() itself serializable [OK] |
|
||||
| Debt +5, Quality +5, Morale -5 |
|
||||
| Conflates store with command layer. Batch ops become noisy. |
|
||||
| |
|
||||
| C. Skip commands - let callers mutate directly [BAD] |
|
||||
| Debt +15, Quality -10 |
|
||||
| No undo/redo, no replay, no CRDT sync, no audit trail. |
|
||||
'--------------------------------------------------------------------'
|
||||
|
||||
===============================================================================
|
||||
V. OPTIMAL ROUTE - "THE ECS ENLIGHTENMENT" SPEEDRUN
|
||||
===============================================================================
|
||||
|
||||
This route hits all 8 challenges picking the GOOD answer, collecting
|
||||
all 13 artifacts, visiting all 10 rooms. Order matters for efficiency
|
||||
(fewest key presses).
|
||||
|
||||
Starting stats: Debt 50, Quality 30, Morale 60, ECS 0/5
|
||||
|
||||
ENTRY POINT
|
||||
Press 1 -> Component Gallery
|
||||
|
||||
COMPONENT GALLERY
|
||||
Challenge: The Circular Dependency -> Press A (Composition)
|
||||
[Debt 40, Quality 45, Morale 65, ECS 1/5]
|
||||
Press 2 -> Command Forge
|
||||
|
||||
THE COMMAND FORGE
|
||||
Challenge: The Mutation Gateway -> Press A (Commands as intent)
|
||||
[Debt 30, Quality 60, Morale 70, ECS 2/5]
|
||||
Press 2 -> Store Vaults
|
||||
|
||||
STORE VAULTS
|
||||
Challenge: The Scattered Mutations -> Press A (Centralize)
|
||||
[Debt 15, Quality 75, Morale 70, ECS 3/5]
|
||||
Press 1 -> ECS Chamber
|
||||
|
||||
ECS ARCHITECT'S CHAMBER
|
||||
Challenge: The ID Crossroads -> Press A (Branded types)
|
||||
[Debt 0, Quality 95, Morale 70, ECS 4/5]
|
||||
Press 1 -> Subgraph Depths
|
||||
|
||||
SUBGRAPH DEPTHS
|
||||
Challenge: The Widget Promotion Decision -> Press A (Connections-only)
|
||||
[Debt 0, Quality 100, Morale 75, ECS 5/5]
|
||||
Press 1 -> ECS Chamber
|
||||
Press 2 -> Renderer
|
||||
|
||||
RENDERER OVERLOOK
|
||||
Challenge: The Render-Time Mutation -> Press A (Separate phases)
|
||||
[Debt 0, Quality 100, Morale 75, ECS 5/5]
|
||||
Press 2 -> Entry Point
|
||||
|
||||
ENTRY POINT
|
||||
Press 3 -> Services
|
||||
|
||||
SERVICE CORRIDORS
|
||||
Challenge: The Migration Question -> Press A (5-phase plan)
|
||||
[Debt 0, Quality 100, Morale 85, ECS 5/5]
|
||||
Press 1 -> Composables
|
||||
|
||||
COMPOSABLES WORKSHOP
|
||||
Challenge: The Collaboration Protocol -> Press A (Y.js CRDTs)
|
||||
[Debt 0, Quality 100, Morale 95, ECS 5/5]
|
||||
Press 2 -> Entry Point
|
||||
|
||||
ENTRY POINT
|
||||
Press 1 -> Components
|
||||
Press 1 -> Litegraph
|
||||
|
||||
LITEGRAPH ENGINE ROOM
|
||||
Challenge: The God Object Dilemma -> Press B (Extract incrementally)
|
||||
[Debt 0, Quality 100, Morale 100, ECS 5/5]
|
||||
|
||||
FINAL STATS: Debt 0 | Quality 100 | Morale 100 | ECS 5/5
|
||||
|
||||
*** ENDING: THE ECS ENLIGHTENMENT ***
|
||||
|
||||
Total key presses: 28 (including challenge answers)
|
||||
Rooms visited: 10/10
|
||||
Artifacts: 16/16
|
||||
Challenges: 9/9 correct
|
||||
|
||||
===============================================================================
|
||||
VI. ALL FOUR ENDINGS
|
||||
===============================================================================
|
||||
|
||||
Endings are checked in order. First match wins.
|
||||
|
||||
.--------------------------------------------------------------------.
|
||||
| |
|
||||
| ENDING 1: THE ECS ENLIGHTENMENT [BEST] |
|
||||
| |
|
||||
| Requirements: Debt < 25 AND Quality >= 75 AND Morale >= 60 |
|
||||
| |
|
||||
| "The World registry hums with clean data. Node removal: |
|
||||
| 30 lines instead of 107. Serialization: one system instead |
|
||||
| of six scattered methods. Branded IDs catch bugs at compile |
|
||||
| time. Y.js CRDTs enable real-time collaboration. The team |
|
||||
| ships features faster than ever." |
|
||||
| |
|
||||
| HOW TO GET IT: Pick ALL good answers. Hard to miss if you |
|
||||
| read the hints carefully. |
|
||||
'--------------------------------------------------------------------'
|
||||
|
||||
.--------------------------------------------------------------------.
|
||||
| |
|
||||
| ENDING 2: THE CLEAN ARCHITECTURE [GOOD] |
|
||||
| |
|
||||
| Requirements: Debt < 40 AND Quality >= 50 |
|
||||
| |
|
||||
| "The migration completes on schedule. Systems hum along, |
|
||||
| the ECS World holds most entity state, and the worst god |
|
||||
| objects have been tamed." |
|
||||
| |
|
||||
| HOW TO GET IT: Pick mostly good answers, 1-2 OK answers. |
|
||||
'--------------------------------------------------------------------'
|
||||
|
||||
.--------------------------------------------------------------------.
|
||||
| |
|
||||
| ENDING 3: THE ETERNAL REFACTOR [MEH] |
|
||||
| |
|
||||
| Requirements: Debt < 70 |
|
||||
| |
|
||||
| "The migration... continues. Every sprint has a 'cleanup' |
|
||||
| ticket that never quite closes." |
|
||||
| |
|
||||
| HOW TO GET IT: Mix of OK and BAD answers. The "safe" middle. |
|
||||
'--------------------------------------------------------------------'
|
||||
|
||||
.--------------------------------------------------------------------.
|
||||
| |
|
||||
| ENDING 4: THE SPAGHETTI SINGULARITY [WORST] |
|
||||
| |
|
||||
| Requirements: Debt >= 70 (catch-all) |
|
||||
| |
|
||||
| "The god objects grew sentient. LGraphCanvas hit 12,000 lines |
|
||||
| and developed a circular dependency with itself." |
|
||||
| |
|
||||
| HOW TO GET IT: Pick all BAD answers. You have to try. |
|
||||
| Starting debt is 50, so you need +20 from bad choices. |
|
||||
'--------------------------------------------------------------------'
|
||||
|
||||
===============================================================================
|
||||
VII. ACHIEVEMENTS
|
||||
===============================================================================
|
||||
|
||||
Achievements are permanently saved across runs. You need 4 playthroughs
|
||||
(minimum) to unlock all endings, since each run can only reach one.
|
||||
|
||||
[x] The ECS Enlightenment - All good answers
|
||||
[x] The Clean Architecture - Mostly good, few OK
|
||||
[x] The Eternal Refactor - Mix of OK and bad
|
||||
[x] The Spaghetti Singularity - Maximize debt (see pro tip below)
|
||||
|
||||
Click any unlocked achievement badge in the Endings sidebar panel
|
||||
to review that ending's screen without resetting your current game.
|
||||
|
||||
PRO TIP: "The Spaghetti Singularity" requires Debt >= 70. This is
|
||||
TRICKY because some "bad" answers actually LOWER debt! Rewrites
|
||||
(Litegraph A: Debt -20) and big bang rewrites (Services B: Debt -10)
|
||||
reduce debt short-term even though they tank morale.
|
||||
|
||||
To hit Debt >= 70 you must pick options that ADD debt or leave it
|
||||
alone. Here's the proven path (starting at Debt 50):
|
||||
|
||||
Components: B (Barrel file reordering) Debt +10 -> 60
|
||||
Command Forge: C (Skip commands) Debt +15 -> 75
|
||||
Stores: C (Leave it as-is) Debt +10 -> 85
|
||||
Services: C (Strangler fig) Debt +0 -> 85
|
||||
Litegraph: C (Add a facade) Debt +5 -> 90
|
||||
ECS: C (Keep plain numbers) Debt +15 -> 100
|
||||
Subgraph: C (Keep three-layer system) Debt +10 -> 100
|
||||
Renderer: B (Dirty flags) Debt -5 -> 95
|
||||
Composables: B (Polling-based sync) Debt +10 -> 100
|
||||
|
||||
Final: Debt 100 / Quality 10 / Morale 50 -> SPAGHETTI SINGULARITY
|
||||
|
||||
WARNING: Picking "all bad-rated answers" does NOT work! The bad
|
||||
answers for Litegraph (A: Rewrite, Debt -20) and Services (B: Big
|
||||
bang, Debt -10) have negative debt effects that pull you back
|
||||
under 70.
|
||||
|
||||
===============================================================================
|
||||
VIII. ARTIFACTS CHECKLIST
|
||||
===============================================================================
|
||||
|
||||
Room | Artifact | Type
|
||||
==================|============================|==================
|
||||
Component Gallery | GraphView.vue | Component
|
||||
Store Vaults | widgetValueStore.ts | Proto-ECS Store
|
||||
Store Vaults | layoutStore.ts | Proto-ECS Store
|
||||
Service Corridors | litegraphService.ts | Service
|
||||
Service Corridors | Extension Migration Guide | Design Pattern
|
||||
Litegraph Engine | LGraphCanvas.ts | God Object
|
||||
Litegraph Engine | LGraphNode.ts | God Object
|
||||
ECS Chamber | World Registry | ECS Core
|
||||
ECS Chamber | Branded Entity IDs | Type Safety
|
||||
Subgraph Depths | SubgraphStructure | ECS Component
|
||||
Subgraph Depths | Typed Interface Contracts | Design Pattern
|
||||
Renderer Overlook | QuadTree Spatial Index | Data Structure
|
||||
Renderer Overlook | Y.js CRDT Layout | Collaboration
|
||||
Composables | useCoreCommands.ts | Composable
|
||||
Command Forge | CommandExecutor | ECS Core
|
||||
Command Forge | Command Interface | Design Pattern
|
||||
|
||||
Total: 16 artifacts across 9 rooms.
|
||||
Entry Point has no artifacts.
|
||||
|
||||
===============================================================================
|
||||
IX. PRO TIPS & SECRETS
|
||||
===============================================================================
|
||||
|
||||
* Your game auto-saves after every room change and challenge. Close
|
||||
the tab and come back anytime - you won't lose progress.
|
||||
|
||||
* The Restart button in the HUD resets your run but KEEPS your
|
||||
achievement badges. Use it to go for a different ending.
|
||||
|
||||
* Every code reference in the room descriptions is a clickable link
|
||||
to the actual file on GitHub. Open them in new tabs to read the
|
||||
real code while you play.
|
||||
|
||||
* After each challenge, the "Read more" link takes you to the
|
||||
architecture documentation that explains the real engineering
|
||||
rationale behind the recommended answer.
|
||||
|
||||
* The map overlay (press M) shows challenge badges:
|
||||
[?] = challenge available but not yet attempted
|
||||
[v] = challenge completed
|
||||
|
||||
* Room navigation preloads images for adjacent rooms, so transitions
|
||||
should be instant after the first visit.
|
||||
|
||||
* The Command Forge (formerly the Side Panel) teaches the Command
|
||||
Pattern - how commands relate to systems and the World. Its challenge
|
||||
covers the architectural layering from ADR 0003 and ADR 0008.
|
||||
|
||||
* The ECS Migration Progress stat maxes at 5, matching the 5 phases
|
||||
of the real migration plan. But 9 challenges can give +1 each
|
||||
(8 of the 9 GOOD answers grant +1 ECS). The Services challenge
|
||||
("5-phase plan") gives +1 ECS but no debt reduction - it's pure
|
||||
planning, not implementation.
|
||||
|
||||
* There are between 2-3 choices per challenge, giving
|
||||
3*3*3*3*3*3*2*3*3 = 13,122 possible playthroughs. But only 4
|
||||
distinct endings. Most paths lead to "The Clean Architecture"
|
||||
or "The Eternal Refactor."
|
||||
|
||||
* If you want to learn the ComfyUI frontend architecture for real,
|
||||
the recommended reading order matches the optimal speedrun route:
|
||||
1. src/main.ts (entry point)
|
||||
2. src/views/GraphView.vue (main canvas)
|
||||
3. src/stores/ (state management)
|
||||
4. src/ecs/ (the future)
|
||||
5. docs/architecture/ecs-world-command-api.md (command layer)
|
||||
6. src/renderer/core/ (canvas pipeline)
|
||||
7. docs/architecture/ecs-migration-plan.md (the plan)
|
||||
8. src/composables/ (Vue logic hooks)
|
||||
9. src/lib/litegraph/src/ (the legacy engine)
|
||||
|
||||
* The pixel art images were generated using the Z-Image Turbo
|
||||
pipeline on the same ComfyUI that this frontend controls.
|
||||
Meta, isn't it?
|
||||
|
||||
===============================================================================
|
||||
|
||||
This document Copyright (c) 2026 A Concerned Architect
|
||||
ComfyUI is maintained by Comfy-Org: https://github.com/Comfy-Org
|
||||
|
||||
"In a world of god objects, be an entity-component-system."
|
||||
|
||||
___
|
||||
| |
|
||||
___| |___
|
||||
| |
|
||||
| COMFY UI |
|
||||
| FRONTEND |
|
||||
|___________|
|
||||
| | | | | | |
|
||||
| | | | | | |
|
||||
_| | | | | | |_
|
||||
|_______________|
|
||||
|
||||
GG. GIT GUD.
|
||||
|
||||
===============================================================================
|
||||
26
docs/architecture/adventure-achievement-icon-prompts.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"meta": {
|
||||
"style": "Pixel art badge/medal icon, 128x128, dark background, achievement unlock style",
|
||||
"usage": "Each key matches an ending ID. Shown in achievements panel when that ending has been reached.",
|
||||
"model": "Z-Image Turbo (no LoRA)",
|
||||
"resolution": "128x128"
|
||||
},
|
||||
"achievements": {
|
||||
"great": {
|
||||
"title": "The ECS Enlightenment",
|
||||
"prompt": "Pixel art achievement badge of a radiant crystal temple with clean geometric architecture, bright green and gold triumphant glow, laurel wreath border, dark background"
|
||||
},
|
||||
"good": {
|
||||
"title": "The Clean Architecture",
|
||||
"prompt": "Pixel art achievement badge of a solid fortress with neat organized blocks, blue and silver steady glow, star emblem, dark background"
|
||||
},
|
||||
"mediocre": {
|
||||
"title": "The Eternal Refactor",
|
||||
"prompt": "Pixel art achievement badge of an hourglass with sand still flowing endlessly, amber and grey weary glow, circular border, dark background"
|
||||
},
|
||||
"disaster": {
|
||||
"title": "The Spaghetti Singularity",
|
||||
"prompt": "Pixel art achievement badge of a tangled mass of spaghetti code wires collapsing into a black hole, red and purple chaotic glow, cracked border, dark background"
|
||||
}
|
||||
}
|
||||
}
|
||||
114
docs/architecture/adventure-choice-icon-prompts.json
Normal file
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"meta": {
|
||||
"style": "Pixel art icon, 128x128, dark background, game UI button icon style, clean readable silhouette",
|
||||
"usage": "Each key is {room}-{choiceKey lowercase}. Used in challenge choice buttons in adventure.html.",
|
||||
"model": "Z-Image Turbo (no LoRA)",
|
||||
"resolution": "128x128"
|
||||
},
|
||||
"choices": {
|
||||
"components-a": {
|
||||
"label": "Composition over inheritance",
|
||||
"prompt": "Pixel art icon of puzzle pieces snapping together cleanly, green glow, dark background, game UI icon"
|
||||
},
|
||||
"components-b": {
|
||||
"label": "Barrel file reordering",
|
||||
"prompt": "Pixel art icon of a stack of files being shuffled with arrows, amber warning glow, dark background, game UI icon"
|
||||
},
|
||||
"components-c": {
|
||||
"label": "Factory injection",
|
||||
"prompt": "Pixel art icon of a factory building with a syringe injecting into it, blue mechanical glow, dark background, game UI icon"
|
||||
},
|
||||
"stores-a": {
|
||||
"label": "Centralize into graph.incrementVersion()",
|
||||
"prompt": "Pixel art icon of scattered dots converging into a single glowing funnel point, green glow, dark background, game UI icon"
|
||||
},
|
||||
"stores-b": {
|
||||
"label": "Add a JavaScript Proxy",
|
||||
"prompt": "Pixel art icon of a shield proxy intercepting arrows mid-flight, amber translucent glow, dark background, game UI icon"
|
||||
},
|
||||
"stores-c": {
|
||||
"label": "Leave it as-is",
|
||||
"prompt": "Pixel art icon of a shrug gesture with cobwebs on old machinery, grey muted glow, dark background, game UI icon"
|
||||
},
|
||||
"services-a": {
|
||||
"label": "5-phase incremental plan",
|
||||
"prompt": "Pixel art icon of five stepping stones ascending in a staircase with checkmarks, green glow, dark background, game UI icon"
|
||||
},
|
||||
"services-b": {
|
||||
"label": "Big bang rewrite",
|
||||
"prompt": "Pixel art icon of a dynamite stick with lit fuse and explosion sparks, red danger glow, dark background, game UI icon"
|
||||
},
|
||||
"services-c": {
|
||||
"label": "Strangler fig pattern",
|
||||
"prompt": "Pixel art icon of vines growing around and enveloping an old tree trunk, green and brown organic glow, dark background, game UI icon"
|
||||
},
|
||||
"litegraph-a": {
|
||||
"label": "Rewrite from scratch",
|
||||
"prompt": "Pixel art icon of a wrecking ball demolishing a building into rubble, red destructive glow, dark background, game UI icon"
|
||||
},
|
||||
"litegraph-b": {
|
||||
"label": "Extract incrementally",
|
||||
"prompt": "Pixel art icon of surgical tweezers carefully extracting a glowing module from a larger block, green precise glow, dark background, game UI icon"
|
||||
},
|
||||
"litegraph-c": {
|
||||
"label": "Add a facade layer",
|
||||
"prompt": "Pixel art icon of a decorative mask covering a cracked wall, yellow cosmetic glow, dark background, game UI icon"
|
||||
},
|
||||
"ecs-a": {
|
||||
"label": "Branded types with cast helpers",
|
||||
"prompt": "Pixel art icon of ID badges with distinct colored stamps and a compiler checkmark, green type-safe glow, dark background, game UI icon"
|
||||
},
|
||||
"ecs-b": {
|
||||
"label": "String prefixes at runtime",
|
||||
"prompt": "Pixel art icon of text labels being parsed with a magnifying glass at runtime, amber slow glow, dark background, game UI icon"
|
||||
},
|
||||
"ecs-c": {
|
||||
"label": "Keep plain numbers",
|
||||
"prompt": "Pixel art icon of bare numbers floating unprotected with a question mark, red risky glow, dark background, game UI icon"
|
||||
},
|
||||
"renderer-a": {
|
||||
"label": "Separate update and render phases",
|
||||
"prompt": "Pixel art icon of two clean pipeline stages labeled U and R with an arrow between them, green orderly glow, dark background, game UI icon"
|
||||
},
|
||||
"renderer-b": {
|
||||
"label": "Dirty flags and deferred render",
|
||||
"prompt": "Pixel art icon of a flag with a smudge mark and a clock showing delay, amber patch glow, dark background, game UI icon"
|
||||
},
|
||||
"composables-a": {
|
||||
"label": "Y.js CRDTs",
|
||||
"prompt": "Pixel art icon of two documents merging seamlessly with sync arrows and no conflicts, green collaboration glow, dark background, game UI icon"
|
||||
},
|
||||
"composables-b": {
|
||||
"label": "Polling-based sync",
|
||||
"prompt": "Pixel art icon of a clock with circular refresh arrows and flickering signal, red laggy glow, dark background, game UI icon"
|
||||
},
|
||||
"composables-c": {
|
||||
"label": "Skip collaboration for now",
|
||||
"prompt": "Pixel art icon of a single person at a desk with a pause symbol, grey neutral glow, dark background, game UI icon"
|
||||
},
|
||||
"subgraph-a": {
|
||||
"label": "Connections-only: promotion = adding a typed input",
|
||||
"prompt": "Pixel art icon of a function signature with typed input slots and a green checkmark, clean minimal glow, dark background, game UI icon"
|
||||
},
|
||||
"subgraph-b": {
|
||||
"label": "Simplified component promotion",
|
||||
"prompt": "Pixel art icon of a widget being lifted up with a promotion arrow and a component badge, amber glow, dark background, game UI icon"
|
||||
},
|
||||
"subgraph-c": {
|
||||
"label": "Keep the current three-layer system",
|
||||
"prompt": "Pixel art icon of three stacked translucent layers with proxy shadows underneath, red complex glow, dark background, game UI icon"
|
||||
},
|
||||
"sidepanel-a": {
|
||||
"label": "Commands as intent; systems as handlers; World as store",
|
||||
"prompt": "Pixel art icon of a layered architectural diagram with arrows flowing top-to-bottom through five labeled tiers, green glow, dark background, game UI icon"
|
||||
},
|
||||
"sidepanel-b": {
|
||||
"label": "Make World.setComponent() itself serializable",
|
||||
"prompt": "Pixel art icon of a database with every cell being logged into a scroll, amber overflow glow, dark background, game UI icon"
|
||||
},
|
||||
"sidepanel-c": {
|
||||
"label": "Skip commands — let callers mutate directly",
|
||||
"prompt": "Pixel art icon of multiple hands reaching into a glowing orb simultaneously causing cracks, red chaos glow, dark background, game UI icon"
|
||||
}
|
||||
}
|
||||
}
|
||||
90
docs/architecture/adventure-icon-prompts.json
Normal file
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"meta": {
|
||||
"style": "Pixel art icon on transparent black background, 128x128, clean edges, glowing accent color, game inventory item style",
|
||||
"usage": "Each key is an artifact ID used in adventure.html. Generate one icon per artifact.",
|
||||
"model": "Z-Image Turbo (no LoRA)",
|
||||
"resolution": "128x128"
|
||||
},
|
||||
"artifacts": {
|
||||
"graphview": {
|
||||
"name": "GraphView.vue",
|
||||
"type": "Component",
|
||||
"prompt": "Pixel art icon of a glowing canvas frame with connected nodes and wires inside, blue accent glow, dark background, game inventory item"
|
||||
},
|
||||
"widgetvaluestore": {
|
||||
"name": "widgetValueStore.ts",
|
||||
"type": "Proto-ECS Store",
|
||||
"prompt": "Pixel art icon of a vault door with a glowing slider widget embossed on it, purple and gold accents, dark background, game inventory item"
|
||||
},
|
||||
"layoutstore": {
|
||||
"name": "layoutStore.ts",
|
||||
"type": "Proto-ECS Store",
|
||||
"prompt": "Pixel art icon of a grid blueprint with glowing position markers, purple accent lines, dark background, game inventory item"
|
||||
},
|
||||
"litegraphservice": {
|
||||
"name": "litegraphService.ts",
|
||||
"type": "Service",
|
||||
"prompt": "Pixel art icon of a gear with a graph node symbol in the center, copper and blue metallic glow, dark background, game inventory item"
|
||||
},
|
||||
"lgraphcanvas": {
|
||||
"name": "LGraphCanvas.ts",
|
||||
"type": "God Object",
|
||||
"prompt": "Pixel art icon of a massive cracked monolith radiating red warning light, labeled 9100, ominous dark background, game inventory item"
|
||||
},
|
||||
"lgraphnode": {
|
||||
"name": "LGraphNode.ts",
|
||||
"type": "God Object",
|
||||
"prompt": "Pixel art icon of an oversized cube with tangled wires bursting from every face, red and amber glow, dark background, game inventory item"
|
||||
},
|
||||
"world-registry": {
|
||||
"name": "World Registry",
|
||||
"type": "ECS Core",
|
||||
"prompt": "Pixel art icon of a glowing crystalline orb containing tiny entity symbols, bright blue and white aura, dark background, game inventory item"
|
||||
},
|
||||
"branded-ids": {
|
||||
"name": "Branded Entity IDs",
|
||||
"type": "Type Safety",
|
||||
"prompt": "Pixel art icon of a set of ID cards with distinct colored borders and brand stamps, green checkmark glow, dark background, game inventory item"
|
||||
},
|
||||
"quadtree": {
|
||||
"name": "QuadTree Spatial Index",
|
||||
"type": "Data Structure",
|
||||
"prompt": "Pixel art icon of a square recursively divided into four quadrants with glowing dots at intersections, teal accent, dark background, game inventory item"
|
||||
},
|
||||
"yjs-crdt": {
|
||||
"name": "Y.js CRDT Layout",
|
||||
"type": "Collaboration",
|
||||
"prompt": "Pixel art icon of two overlapping document layers merging with sync arrows, purple and green glow, dark background, game inventory item"
|
||||
},
|
||||
"usecorecommands": {
|
||||
"name": "useCoreCommands.ts",
|
||||
"type": "Composable",
|
||||
"prompt": "Pixel art icon of a hook tool with keyboard key symbols orbiting it, yellow and blue glow, dark background, game inventory item"
|
||||
},
|
||||
"subgraph-structure": {
|
||||
"name": "SubgraphStructure",
|
||||
"type": "ECS Component",
|
||||
"prompt": "Pixel art icon of nested rectangular frames inside each other like Russian dolls with glowing typed connections at each boundary, purple and teal accent, dark background, game inventory item"
|
||||
},
|
||||
"typed-contracts": {
|
||||
"name": "Typed Interface Contracts",
|
||||
"type": "Architecture",
|
||||
"prompt": "Pixel art icon of a sealed scroll with a glowing typed signature stamp and interface brackets, gold and blue accent, dark background, game inventory item"
|
||||
},
|
||||
"command-executor": {
|
||||
"name": "CommandExecutor",
|
||||
"type": "ECS Core",
|
||||
"prompt": "Pixel art icon of a glowing anvil with a gear and execute arrow symbol, blue-purple forge glow, dark background, game inventory item"
|
||||
},
|
||||
"command-interface": {
|
||||
"name": "Command Interface",
|
||||
"type": "Design Pattern",
|
||||
"prompt": "Pixel art icon of a sealed scroll with a type discriminator tag and execute method seal, blue glow, dark background, game inventory item"
|
||||
},
|
||||
"extension-migration": {
|
||||
"name": "Extension Migration Guide",
|
||||
"type": "Design Pattern",
|
||||
"prompt": "Pixel art icon of a scroll with legacy code on left transforming via arrow to ECS code on right, green transition glow, dark background, game inventory item"
|
||||
}
|
||||
}
|
||||
}
|
||||
61
docs/architecture/adventure-image-prompts.json
Normal file
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"meta": {
|
||||
"style": "Pixel art, 16:9 aspect ratio, dark moody palette with glowing accent lighting",
|
||||
"usage": "Each key corresponds to a room ID in adventure.html. Generate images with generate-images.py.",
|
||||
"model": "Z-Image Turbo (no LoRA)",
|
||||
"resolution": "1152x640",
|
||||
"generated": "2026-03-24"
|
||||
},
|
||||
"rooms": {
|
||||
"entry": {
|
||||
"title": "The Entry Point",
|
||||
"prompt": "Pixel art of a glowing terminal in a vast dark server room, Vue.js and TypeScript logos floating as holographic projections, three corridors branching ahead lit by blue, green, and purple lights",
|
||||
"path": "images/entry.png"
|
||||
},
|
||||
"components": {
|
||||
"title": "The Component Gallery",
|
||||
"prompt": "Pixel art gallery hall with framed Vue component cards hung on stone walls, a massive canvas painting labeled 'GraphView' in the center, smaller panels flanking either side, warm torchlight",
|
||||
"path": "images/components.png"
|
||||
},
|
||||
"stores": {
|
||||
"title": "The Store Vaults",
|
||||
"prompt": "Pixel art underground vault with 60 glowing vault doors lining the walls, three doors in front glow brightest (labeled widget, layout, promotion), a Pinia pineapple emblem etched in stone above",
|
||||
"path": "images/stores.png"
|
||||
},
|
||||
"services": {
|
||||
"title": "The Service Corridors",
|
||||
"prompt": "Pixel art clean corridors with labeled pipes connecting rooms overhead, data flowing as glowing particles through transparent tubes, service names etched on brass plaques",
|
||||
"path": "images/services.png"
|
||||
},
|
||||
"litegraph": {
|
||||
"title": "The Litegraph Engine Room",
|
||||
"prompt": "Pixel art dark engine room with three massive monolith machines labeled 9100, 4300, and 3100 lines of code, warning lights flashing amber, tangled wires and cables everywhere",
|
||||
"path": "images/litegraph.png"
|
||||
},
|
||||
"ecs": {
|
||||
"title": "The ECS Architect's Chamber",
|
||||
"prompt": "Pixel art architect's drafting room with blueprints pinned to walls showing entity-component diagrams, a glowing World orb floating in the center, branded ID cards scattered across the desk",
|
||||
"path": "images/ecs.png"
|
||||
},
|
||||
"subgraph": {
|
||||
"title": "The Subgraph Depths",
|
||||
"prompt": "Pixel art recursive fractal chamber where identical rooms nest inside each other like Russian dolls, typed contract scrolls float at each boundary doorway, a DAG tree diagram glows on the ceiling",
|
||||
"path": "images/subgraph.png"
|
||||
},
|
||||
"renderer": {
|
||||
"title": "The Renderer Overlook",
|
||||
"prompt": "Pixel art observation deck overlooking a vast canvas being painted by precise robotic arms, Y.js CRDT symbols floating in the air, a QuadTree grid visible on the floor below",
|
||||
"path": "images/renderer.png"
|
||||
},
|
||||
"composables": {
|
||||
"title": "The Composables Workshop",
|
||||
"prompt": "Pixel art workshop with hooks hanging from a pegboard wall, each labeled (useCoreCommands, useCanvasDrop, etc.), workbenches for auth, canvas, and queue domains, cozy lantern light",
|
||||
"path": "images/composables.png"
|
||||
},
|
||||
"sidepanel": {
|
||||
"title": "The Command Forge",
|
||||
"prompt": "Pixel art anvil forge where glowing command scrolls are being hammered into structured objects, a layered diagram on the wall showing five architectural tiers connected by arrows, blue and purple forge light, dark background",
|
||||
"path": "images/sidepanel.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
3272
docs/architecture/adventure.html
Normal file
196
docs/architecture/generate-icons.py
Normal file
@@ -0,0 +1,196 @@
|
||||
"""
|
||||
Generate pixel art inventory icons for the Architecture Adventure game.
|
||||
Uses Z-Image Turbo pipeline via local ComfyUI server (no LoRA).
|
||||
Skips icons that already exist on disk.
|
||||
|
||||
Usage: python docs/architecture/generate-icons.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
COMFY_URL = "http://localhost:8188"
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ARTIFACT_PROMPTS = os.path.join(SCRIPT_DIR, "adventure-icon-prompts.json")
|
||||
CHOICE_PROMPTS = os.path.join(SCRIPT_DIR, "adventure-choice-icon-prompts.json")
|
||||
ACHIEVEMENT_PROMPTS = os.path.join(SCRIPT_DIR, "adventure-achievement-icon-prompts.json")
|
||||
OUTPUT_DIR = os.path.join(SCRIPT_DIR, "icons")
|
||||
BASE_SEED = 7777
|
||||
WIDTH = 128
|
||||
HEIGHT = 128
|
||||
|
||||
|
||||
def build_workflow(prompt_text, seed, prefix):
|
||||
return {
|
||||
"1": {
|
||||
"class_type": "UNETLoader",
|
||||
"inputs": {
|
||||
"unet_name": "ZIT\\z_image_turbo_bf16.safetensors",
|
||||
"weight_dtype": "default",
|
||||
},
|
||||
},
|
||||
"2": {
|
||||
"class_type": "CLIPLoader",
|
||||
"inputs": {
|
||||
"clip_name": "qwen_3_4b.safetensors",
|
||||
"type": "lumina2",
|
||||
"device": "default",
|
||||
},
|
||||
},
|
||||
"3": {
|
||||
"class_type": "VAELoader",
|
||||
"inputs": {"vae_name": "ae.safetensors"},
|
||||
},
|
||||
"4": {
|
||||
"class_type": "ModelSamplingAuraFlow",
|
||||
"inputs": {"shift": 3, "model": ["1", 0]},
|
||||
},
|
||||
"6": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {"text": prompt_text, "clip": ["2", 0]},
|
||||
},
|
||||
"7": {
|
||||
"class_type": "ConditioningZeroOut",
|
||||
"inputs": {"conditioning": ["6", 0]},
|
||||
},
|
||||
"8": {
|
||||
"class_type": "EmptySD3LatentImage",
|
||||
"inputs": {"width": WIDTH, "height": HEIGHT, "batch_size": 1},
|
||||
},
|
||||
"9": {
|
||||
"class_type": "KSampler",
|
||||
"inputs": {
|
||||
"seed": seed,
|
||||
"control_after_generate": "fixed",
|
||||
"steps": 8,
|
||||
"cfg": 1,
|
||||
"sampler_name": "res_multistep",
|
||||
"scheduler": "simple",
|
||||
"denoise": 1,
|
||||
"model": ["4", 0],
|
||||
"positive": ["6", 0],
|
||||
"negative": ["7", 0],
|
||||
"latent_image": ["8", 0],
|
||||
},
|
||||
},
|
||||
"10": {
|
||||
"class_type": "VAEDecode",
|
||||
"inputs": {"samples": ["9", 0], "vae": ["3", 0]},
|
||||
},
|
||||
"11": {
|
||||
"class_type": "SaveImage",
|
||||
"inputs": {"filename_prefix": prefix, "images": ["10", 0]},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def submit_prompt(workflow):
|
||||
payload = json.dumps({"prompt": workflow}).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
f"{COMFY_URL}/prompt",
|
||||
data=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
try:
|
||||
resp = urllib.request.urlopen(req)
|
||||
return json.loads(resp.read())
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode()
|
||||
raise RuntimeError(f"HTTP {e.code}: {body}")
|
||||
|
||||
|
||||
def poll_history(prompt_id, timeout=120):
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
try:
|
||||
resp = urllib.request.urlopen(f"{COMFY_URL}/history/{prompt_id}")
|
||||
data = json.loads(resp.read())
|
||||
if prompt_id in data:
|
||||
return data[prompt_id]
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(2)
|
||||
return None
|
||||
|
||||
|
||||
def download_image(filename, subfolder, dest_path):
|
||||
url = f"{COMFY_URL}/view?filename={urllib.request.quote(filename)}&subfolder={urllib.request.quote(subfolder)}&type=output"
|
||||
urllib.request.urlretrieve(url, dest_path)
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
|
||||
# Collect all icons from both prompt files
|
||||
all_icons = {}
|
||||
|
||||
with open(ARTIFACT_PROMPTS) as f:
|
||||
data = json.load(f)
|
||||
for icon_id, entry in data["artifacts"].items():
|
||||
all_icons[icon_id] = entry["prompt"]
|
||||
|
||||
if os.path.exists(CHOICE_PROMPTS):
|
||||
with open(CHOICE_PROMPTS) as f:
|
||||
data = json.load(f)
|
||||
for icon_id, entry in data["choices"].items():
|
||||
all_icons[icon_id] = entry["prompt"]
|
||||
|
||||
if os.path.exists(ACHIEVEMENT_PROMPTS):
|
||||
with open(ACHIEVEMENT_PROMPTS) as f:
|
||||
data = json.load(f)
|
||||
for icon_id, entry in data["achievements"].items():
|
||||
all_icons[f"ending-{icon_id}"] = entry["prompt"]
|
||||
|
||||
# Filter out already-generated icons
|
||||
to_generate = {}
|
||||
for icon_id, prompt in all_icons.items():
|
||||
dest = os.path.join(OUTPUT_DIR, f"{icon_id}.png")
|
||||
if os.path.exists(dest):
|
||||
print(f" Skipping {icon_id}.png (already exists)")
|
||||
else:
|
||||
to_generate[icon_id] = prompt
|
||||
|
||||
if not to_generate:
|
||||
print("All icons already generated. Nothing to do.")
|
||||
return
|
||||
|
||||
# Submit jobs
|
||||
jobs = []
|
||||
for i, (icon_id, prompt) in enumerate(to_generate.items()):
|
||||
prefix = f"adventure-icons/{icon_id}"
|
||||
wf = build_workflow(prompt, BASE_SEED + i, prefix)
|
||||
result = submit_prompt(wf)
|
||||
prompt_id = result["prompt_id"]
|
||||
jobs.append((icon_id, prompt_id))
|
||||
print(f" Submitted: {icon_id} -> {prompt_id}")
|
||||
|
||||
print(f"\n{len(jobs)} jobs queued. Polling for completion...\n")
|
||||
|
||||
# Poll for completion
|
||||
completed = set()
|
||||
while len(completed) < len(jobs):
|
||||
for icon_id, prompt_id in jobs:
|
||||
if prompt_id in completed:
|
||||
continue
|
||||
history = poll_history(prompt_id, timeout=5)
|
||||
if history:
|
||||
completed.add(prompt_id)
|
||||
outputs = history.get("outputs", {})
|
||||
for node_out in outputs.values():
|
||||
for img in node_out.get("images", []):
|
||||
src_filename = img["filename"]
|
||||
subfolder = img.get("subfolder", "")
|
||||
dest = os.path.join(OUTPUT_DIR, f"{icon_id}.png")
|
||||
download_image(src_filename, subfolder, dest)
|
||||
print(f" [{len(completed)}/{len(jobs)}] {icon_id}.png downloaded")
|
||||
if len(completed) < len(jobs):
|
||||
time.sleep(2)
|
||||
|
||||
print(f"\nDone! {len(completed)} icons saved to {OUTPUT_DIR}/")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
165
docs/architecture/generate-images.py
Normal file
@@ -0,0 +1,165 @@
|
||||
"""
|
||||
Generate pixel art room images for the Architecture Adventure game.
|
||||
Uses Z-Image Turbo pipeline via local ComfyUI server (no LoRA).
|
||||
|
||||
Usage: python docs/architecture/generate-images.py
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
COMFY_URL = "http://localhost:8188"
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
PROMPTS_FILE = os.path.join(SCRIPT_DIR, "adventure-image-prompts.json")
|
||||
OUTPUT_DIR = os.path.join(SCRIPT_DIR, "images")
|
||||
BASE_SEED = 2024
|
||||
WIDTH = 1152
|
||||
HEIGHT = 640
|
||||
|
||||
|
||||
def build_workflow(prompt_text, seed, prefix):
|
||||
return {
|
||||
"1": {
|
||||
"class_type": "UNETLoader",
|
||||
"inputs": {
|
||||
"unet_name": "ZIT\\z_image_turbo_bf16.safetensors",
|
||||
"weight_dtype": "default",
|
||||
},
|
||||
},
|
||||
"2": {
|
||||
"class_type": "CLIPLoader",
|
||||
"inputs": {
|
||||
"clip_name": "qwen_3_4b.safetensors",
|
||||
"type": "lumina2",
|
||||
"device": "default",
|
||||
},
|
||||
},
|
||||
"3": {
|
||||
"class_type": "VAELoader",
|
||||
"inputs": {"vae_name": "ae.safetensors"},
|
||||
},
|
||||
"4": {
|
||||
"class_type": "ModelSamplingAuraFlow",
|
||||
"inputs": {"shift": 3, "model": ["1", 0]},
|
||||
},
|
||||
"6": {
|
||||
"class_type": "CLIPTextEncode",
|
||||
"inputs": {"text": prompt_text, "clip": ["2", 0]},
|
||||
},
|
||||
"7": {
|
||||
"class_type": "ConditioningZeroOut",
|
||||
"inputs": {"conditioning": ["6", 0]},
|
||||
},
|
||||
"8": {
|
||||
"class_type": "EmptySD3LatentImage",
|
||||
"inputs": {"width": WIDTH, "height": HEIGHT, "batch_size": 1},
|
||||
},
|
||||
"9": {
|
||||
"class_type": "KSampler",
|
||||
"inputs": {
|
||||
"seed": seed,
|
||||
"control_after_generate": "fixed",
|
||||
"steps": 8,
|
||||
"cfg": 1,
|
||||
"sampler_name": "res_multistep",
|
||||
"scheduler": "simple",
|
||||
"denoise": 1,
|
||||
"model": ["4", 0],
|
||||
"positive": ["6", 0],
|
||||
"negative": ["7", 0],
|
||||
"latent_image": ["8", 0],
|
||||
},
|
||||
},
|
||||
"10": {
|
||||
"class_type": "VAEDecode",
|
||||
"inputs": {"samples": ["9", 0], "vae": ["3", 0]},
|
||||
},
|
||||
"11": {
|
||||
"class_type": "SaveImage",
|
||||
"inputs": {"filename_prefix": prefix, "images": ["10", 0]},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def submit_prompt(workflow):
|
||||
payload = json.dumps({"prompt": workflow}).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
f"{COMFY_URL}/prompt",
|
||||
data=payload,
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
try:
|
||||
resp = urllib.request.urlopen(req)
|
||||
return json.loads(resp.read())
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode()
|
||||
raise RuntimeError(f"HTTP {e.code}: {body}")
|
||||
|
||||
|
||||
def poll_history(prompt_id, timeout=120):
|
||||
start = time.time()
|
||||
while time.time() - start < timeout:
|
||||
try:
|
||||
resp = urllib.request.urlopen(f"{COMFY_URL}/history/{prompt_id}")
|
||||
data = json.loads(resp.read())
|
||||
if prompt_id in data:
|
||||
return data[prompt_id]
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(2)
|
||||
return None
|
||||
|
||||
|
||||
def download_image(filename, subfolder, dest_path):
|
||||
url = f"{COMFY_URL}/view?filename={urllib.request.quote(filename)}&subfolder={urllib.request.quote(subfolder)}&type=output"
|
||||
urllib.request.urlretrieve(url, dest_path)
|
||||
|
||||
|
||||
def main():
|
||||
with open(PROMPTS_FILE) as f:
|
||||
data = json.load(f)
|
||||
|
||||
rooms = data["rooms"]
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
|
||||
# Submit all jobs
|
||||
jobs = []
|
||||
for i, (room_id, room) in enumerate(rooms.items()):
|
||||
prefix = f"adventure/{room_id}"
|
||||
wf = build_workflow(room["prompt"], BASE_SEED + i, prefix)
|
||||
result = submit_prompt(wf)
|
||||
prompt_id = result["prompt_id"]
|
||||
jobs.append((room_id, prompt_id, prefix))
|
||||
print(f" Submitted: {room_id} -> {prompt_id}")
|
||||
|
||||
print(f"\n{len(jobs)} jobs queued. Polling for completion...\n")
|
||||
|
||||
# Poll for completion
|
||||
completed = set()
|
||||
while len(completed) < len(jobs):
|
||||
for room_id, prompt_id, prefix in jobs:
|
||||
if prompt_id in completed:
|
||||
continue
|
||||
history = poll_history(prompt_id, timeout=5)
|
||||
if history:
|
||||
completed.add(prompt_id)
|
||||
# Find output filename
|
||||
outputs = history.get("outputs", {})
|
||||
for node_id, node_out in outputs.items():
|
||||
images = node_out.get("images", [])
|
||||
for img in images:
|
||||
src_filename = img["filename"]
|
||||
subfolder = img.get("subfolder", "")
|
||||
dest = os.path.join(OUTPUT_DIR, f"{room_id}.png")
|
||||
download_image(src_filename, subfolder, dest)
|
||||
print(f" [{len(completed)}/{len(jobs)}] {room_id}.png downloaded")
|
||||
if len(completed) < len(jobs):
|
||||
time.sleep(2)
|
||||
|
||||
print(f"\nDone! {len(completed)} images saved to {OUTPUT_DIR}/")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
BIN
docs/architecture/icons/branded-ids.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/architecture/icons/command-executor.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
docs/architecture/icons/command-interface.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
docs/architecture/icons/components-a.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
docs/architecture/icons/components-b.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/architecture/icons/components-c.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
docs/architecture/icons/composables-a.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/architecture/icons/composables-b.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/architecture/icons/composables-c.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
docs/architecture/icons/ecs-a.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
docs/architecture/icons/ecs-b.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
docs/architecture/icons/ecs-c.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
docs/architecture/icons/ending-disaster.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/architecture/icons/ending-good.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
docs/architecture/icons/ending-great.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
docs/architecture/icons/ending-mediocre.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
docs/architecture/icons/extension-migration.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/architecture/icons/graphview.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
docs/architecture/icons/layoutstore.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/architecture/icons/lgraphcanvas.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/architecture/icons/lgraphnode.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/architecture/icons/litegraph-a.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
docs/architecture/icons/litegraph-b.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/architecture/icons/litegraph-c.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/architecture/icons/litegraphservice.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
docs/architecture/icons/quadtree.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
docs/architecture/icons/renderer-a.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/architecture/icons/renderer-b.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/architecture/icons/services-a.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
docs/architecture/icons/services-b.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
docs/architecture/icons/services-c.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
docs/architecture/icons/sidepanel-a.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/architecture/icons/sidepanel-b.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
docs/architecture/icons/sidepanel-c.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/architecture/icons/stores-a.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/architecture/icons/stores-b.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/architecture/icons/stores-c.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
docs/architecture/icons/subgraph-a.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
docs/architecture/icons/subgraph-b.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/architecture/icons/subgraph-c.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/architecture/icons/subgraph-structure.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
docs/architecture/icons/typed-contracts.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
docs/architecture/icons/usecorecommands.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/architecture/icons/widgetvaluestore.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
docs/architecture/icons/world-registry.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/architecture/icons/yjs-crdt.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/architecture/images/components.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
docs/architecture/images/composables.png
Normal file
|
After Width: | Height: | Size: 880 KiB |
BIN
docs/architecture/images/ecs.png
Normal file
|
After Width: | Height: | Size: 941 KiB |
BIN
docs/architecture/images/entry.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
docs/architecture/images/litegraph.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/architecture/images/renderer.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/architecture/images/services.png
Normal file
|
After Width: | Height: | Size: 896 KiB |
BIN
docs/architecture/images/sidepanel.png
Normal file
|
After Width: | Height: | Size: 630 KiB |
BIN
docs/architecture/images/stores.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/architecture/images/subgraph.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
@@ -28,6 +28,10 @@ const config: KnipConfig = {
|
||||
'packages/ingest-types': {
|
||||
project: ['src/**/*.{js,ts}']
|
||||
},
|
||||
'apps/architecture-adventure': {
|
||||
project: ['src/**/*.ts'],
|
||||
vite: { config: ['vite.config.ts'] }
|
||||
},
|
||||
'apps/website': {
|
||||
entry: [
|
||||
'src/pages/**/*.astro',
|
||||
@@ -61,7 +65,9 @@ const config: KnipConfig = {
|
||||
// Pending integration in stacked PR
|
||||
'src/components/sidebar/tabs/nodeLibrary/CustomNodesPanel.vue',
|
||||
// Agent review check config, not part of the build
|
||||
'.agents/checks/eslint.strict.config.js'
|
||||
'.agents/checks/eslint.strict.config.js',
|
||||
// ECS draft interfaces (ADR 0008) — not yet consumed by production code
|
||||
'src/ecs/**/*.ts'
|
||||
],
|
||||
vite: {
|
||||
config: ['vite?(.*).config.mts']
|
||||
|
||||
9
pnpm-lock.yaml
generated
@@ -829,6 +829,15 @@ importers:
|
||||
specifier: 'catalog:'
|
||||
version: 3.25.1(zod@3.25.76)
|
||||
|
||||
apps/architecture-adventure:
|
||||
devDependencies:
|
||||
typescript:
|
||||
specifier: 'catalog:'
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: ^8.0.0
|
||||
version: 8.0.0(@types/node@25.0.3)(esbuild@0.27.3)(jiti@2.6.1)(terser@5.39.2)(tsx@4.19.4)(yaml@2.8.2)
|
||||
|
||||
apps/desktop-ui:
|
||||
dependencies:
|
||||
'@comfyorg/comfyui-electron-types':
|
||||
|
||||
32
src/ecs/components/group.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Group components.
|
||||
*
|
||||
* Groups are visual containers that hold nodes and reroutes.
|
||||
* Currently no state has been extracted from LGraphGroup — these
|
||||
* components represent the full extraction target.
|
||||
*/
|
||||
|
||||
import type { NodeEntityId, RerouteEntityId } from '../entityId'
|
||||
|
||||
/** Metadata for a group. */
|
||||
export interface GroupMeta {
|
||||
title: string
|
||||
font?: string
|
||||
fontSize: number
|
||||
}
|
||||
|
||||
/** Visual properties for group rendering. */
|
||||
export interface GroupVisual {
|
||||
color?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Entities contained within a group.
|
||||
*
|
||||
* Replaces LGraphGroup._children (Set<Positionable>) and
|
||||
* LGraphGroup._nodes (LGraphNode[]).
|
||||
*/
|
||||
export interface GroupChildren {
|
||||
nodeIds: ReadonlySet<NodeEntityId>
|
||||
rerouteIds: ReadonlySet<RerouteEntityId>
|
||||
}
|
||||
51
src/ecs/components/link.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Link components.
|
||||
*
|
||||
* Decomposes LLink into endpoint topology, visual state, and
|
||||
* transient interaction state.
|
||||
*/
|
||||
|
||||
import type {
|
||||
CanvasColour,
|
||||
ISlotType,
|
||||
Point
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
|
||||
import type { NodeEntityId, RerouteEntityId } from '../entityId'
|
||||
|
||||
/**
|
||||
* The topological endpoints of a link.
|
||||
*
|
||||
* Replaces origin_id/origin_slot/target_id/target_slot/type on LLink.
|
||||
* Slot indices will migrate to SlotEntityId references once slots
|
||||
* have independent IDs.
|
||||
*/
|
||||
export interface LinkEndpoints {
|
||||
originNodeId: NodeEntityId
|
||||
originSlotIndex: number
|
||||
targetNodeId: NodeEntityId
|
||||
targetSlotIndex: number
|
||||
/** Data type flowing through this link (e.g., 'IMAGE', 'MODEL'). */
|
||||
type: ISlotType
|
||||
/** Reroute that owns this link segment, if any. */
|
||||
parentRerouteId?: RerouteEntityId
|
||||
}
|
||||
|
||||
/** Visual properties for link rendering. */
|
||||
export interface LinkVisual {
|
||||
color?: CanvasColour
|
||||
/** Cached rendered path (invalidated on position change). */
|
||||
path?: Path2D
|
||||
/** Cached center point of the link curve. */
|
||||
centerPos?: Point
|
||||
/** Cached angle at the center point. */
|
||||
centerAngle?: number
|
||||
}
|
||||
|
||||
/** Transient interaction state for a link. */
|
||||
export interface LinkState {
|
||||
/** True while the user is dragging this link. */
|
||||
dragging: boolean
|
||||
/** Arbitrary data payload flowing through the link. */
|
||||
data?: unknown
|
||||
}
|
||||
87
src/ecs/components/node.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Node-specific components.
|
||||
*
|
||||
* These decompose the ~4,300-line LGraphNode class into focused data
|
||||
* objects. Each component captures one concern; systems provide behavior.
|
||||
*
|
||||
* Reuses existing types from litegraph where possible to ease migration.
|
||||
*/
|
||||
|
||||
import type { Dictionary, INodeFlags } from '@/lib/litegraph/src/interfaces'
|
||||
import type { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type {
|
||||
LGraphEventMode,
|
||||
RenderShape
|
||||
} from '@/lib/litegraph/src/types/globalEnums'
|
||||
|
||||
import type { SlotEntityId, WidgetEntityId } from '../entityId'
|
||||
|
||||
/** Static identity and classification of a node. */
|
||||
export interface NodeType {
|
||||
/** Registered node type string (e.g., 'KSampler', 'CLIPTextEncode'). */
|
||||
type: string
|
||||
/** Display title. */
|
||||
title: string
|
||||
/** Category path for the node menu (e.g., 'sampling'). */
|
||||
category?: string
|
||||
/** Backend node definition data, if resolved. */
|
||||
nodeData?: unknown
|
||||
/** Optional description shown in tooltips/docs. */
|
||||
description?: string
|
||||
}
|
||||
|
||||
/** Visual / rendering properties of a node. */
|
||||
export interface NodeVisual {
|
||||
color?: string
|
||||
bgcolor?: string
|
||||
boxcolor?: string
|
||||
shape?: RenderShape
|
||||
}
|
||||
|
||||
/**
|
||||
* Connectivity — references to this node's slot entities.
|
||||
*
|
||||
* Replaces the `inputs[]` and `outputs[]` arrays on LGraphNode.
|
||||
* Actual slot data lives on SlotIdentity / SlotConnection components
|
||||
* keyed by SlotEntityId.
|
||||
*/
|
||||
export interface Connectivity {
|
||||
inputSlotIds: readonly SlotEntityId[]
|
||||
outputSlotIds: readonly SlotEntityId[]
|
||||
}
|
||||
|
||||
/** Execution scheduling state. */
|
||||
export interface Execution {
|
||||
/** Computed execution order (topological sort index). */
|
||||
order: number
|
||||
/** How this node participates in execution. */
|
||||
mode: LGraphEventMode
|
||||
/** Behavioral flags (pinned, collapsed, ghost, etc.). */
|
||||
flags: INodeFlags
|
||||
}
|
||||
|
||||
/** User-defined key-value properties on a node. */
|
||||
export interface Properties {
|
||||
properties: Dictionary<NodeProperty | undefined>
|
||||
propertiesInfo: readonly PropertyInfo[]
|
||||
}
|
||||
|
||||
export interface PropertyInfo {
|
||||
name?: string
|
||||
type?: string
|
||||
default_value?: NodeProperty
|
||||
widget?: string
|
||||
label?: string
|
||||
values?: unknown[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Container for widget entities owned by this node.
|
||||
*
|
||||
* Replaces the `widgets[]` array on LGraphNode.
|
||||
* Actual widget data lives on WidgetIdentity / WidgetValue components
|
||||
* keyed by WidgetEntityId.
|
||||
*/
|
||||
export interface WidgetContainer {
|
||||
widgetIds: readonly WidgetEntityId[]
|
||||
}
|
||||
24
src/ecs/components/position.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Position component — shared by Node, Reroute, and Group entities.
|
||||
*
|
||||
* Plain data object. No methods, no back-references.
|
||||
* Corresponds to the spatial data currently on LGraphNode.pos/size,
|
||||
* Reroute.pos, and LGraphGroup._bounding.
|
||||
*
|
||||
* During the bridge phase, this mirrors data from the LayoutStore
|
||||
* (Y.js CRDTs). See migration plan Phase 2a.
|
||||
*/
|
||||
|
||||
import type { Point, Size } from '@/lib/litegraph/src/interfaces'
|
||||
|
||||
export interface Position {
|
||||
/** Position in graph coordinates (top-left for nodes/groups, center for reroutes). */
|
||||
pos: Point
|
||||
/** Width and height. Undefined for point-like entities (reroutes). */
|
||||
size?: Size
|
||||
/**
|
||||
* Bounding rectangle as [x, y, width, height].
|
||||
* May extend beyond pos/size (e.g., nodes with title overhang).
|
||||
*/
|
||||
bounding: readonly [x: number, y: number, w: number, h: number]
|
||||
}
|
||||
35
src/ecs/components/reroute.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Reroute components.
|
||||
*
|
||||
* Reroutes are waypoints on link paths. Position is shared via the
|
||||
* Position component. These components capture the link topology
|
||||
* and visual state specific to reroutes.
|
||||
*/
|
||||
|
||||
import type { CanvasColour } from '@/lib/litegraph/src/interfaces'
|
||||
|
||||
import type { LinkEntityId, RerouteEntityId } from '../entityId'
|
||||
|
||||
/**
|
||||
* Link topology for a reroute.
|
||||
*
|
||||
* A reroute can be chained (parentId) and carries a set of links
|
||||
* that pass through it.
|
||||
*/
|
||||
export interface RerouteLinks {
|
||||
/** Parent reroute in the chain, if any. */
|
||||
parentId?: RerouteEntityId
|
||||
/** Links that pass through this reroute. */
|
||||
linkIds: ReadonlySet<LinkEntityId>
|
||||
/** Floating (in-progress) links passing through this reroute. */
|
||||
floatingLinkIds: ReadonlySet<LinkEntityId>
|
||||
}
|
||||
|
||||
/** Visual state specific to reroute rendering. */
|
||||
export interface RerouteVisual {
|
||||
color?: CanvasColour
|
||||
/** Cached path for the link segment. */
|
||||
path?: Path2D
|
||||
/** Angle at the reroute center (for directional rendering). */
|
||||
centerAngle?: number
|
||||
}
|
||||
76
src/ecs/components/slot.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Slot components.
|
||||
*
|
||||
* Slots currently lack independent IDs — they're identified by their
|
||||
* index on a parent node's inputs/outputs array. The ECS assigns each
|
||||
* slot a synthetic SlotEntityId, making them first-class entities.
|
||||
*
|
||||
* Decomposes SlotBase / INodeInputSlot / INodeOutputSlot into identity,
|
||||
* connection topology, and visual state.
|
||||
*/
|
||||
|
||||
import type {
|
||||
CanvasColour,
|
||||
ISlotType,
|
||||
Point
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import type {
|
||||
LinkDirection,
|
||||
RenderShape
|
||||
} from '@/lib/litegraph/src/types/globalEnums'
|
||||
|
||||
import type { LinkEntityId, NodeEntityId } from '../entityId'
|
||||
|
||||
/** Immutable identity of a slot. */
|
||||
export interface SlotIdentity {
|
||||
/** Display name (e.g., 'model', 'positive'). */
|
||||
name: string
|
||||
/** Localized display name, if available. */
|
||||
localizedName?: string
|
||||
/** Optional label override. */
|
||||
label?: string
|
||||
/** Data type accepted/produced by this slot. */
|
||||
type: ISlotType
|
||||
/** Whether this is an input or output slot. */
|
||||
direction: 'input' | 'output'
|
||||
/** The node that owns this slot. */
|
||||
parentNodeId: NodeEntityId
|
||||
/** Position index on the parent node (0-based). */
|
||||
index: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection state of a slot.
|
||||
*
|
||||
* Input slots have at most one link. Output slots can have many.
|
||||
*/
|
||||
export interface SlotConnection {
|
||||
/**
|
||||
* For input slots: the single connected link, or null.
|
||||
* For output slots: all connected links.
|
||||
*/
|
||||
linkIds: readonly LinkEntityId[]
|
||||
/** Widget locator, if this slot backs a promoted widget. */
|
||||
widgetLocator?: SlotWidgetLocator
|
||||
}
|
||||
|
||||
export interface SlotWidgetLocator {
|
||||
name: string
|
||||
nodeId: NodeEntityId
|
||||
}
|
||||
|
||||
/** Visual / rendering properties of a slot. */
|
||||
export interface SlotVisual {
|
||||
/** Computed position relative to the node. */
|
||||
pos?: Point
|
||||
/** Bounding rectangle for hit testing. */
|
||||
boundingRect: readonly [x: number, y: number, w: number, h: number]
|
||||
/** Color when connected. */
|
||||
colorOn?: CanvasColour
|
||||
/** Color when disconnected. */
|
||||
colorOff?: CanvasColour
|
||||
/** Render shape (circle, arrow, grid, etc.). */
|
||||
shape?: RenderShape
|
||||
/** Flow direction for link rendering. */
|
||||
dir?: LinkDirection
|
||||
}
|
||||
34
src/ecs/components/subgraph.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Subgraph components.
|
||||
*
|
||||
* A subgraph is a graph that lives inside a SubgraphNode. It inherits
|
||||
* all node components (via its SubgraphNode entity) and adds structural
|
||||
* and metadata components for its interior.
|
||||
*
|
||||
* This is the most complex entity kind — it depends on Node and Link
|
||||
* extraction being complete first. See migration plan Phase 2.
|
||||
*/
|
||||
|
||||
import type { LinkEntityId, NodeEntityId, RerouteEntityId } from '../entityId'
|
||||
|
||||
/**
|
||||
* The interior structure of a subgraph.
|
||||
*
|
||||
* Replaces the recursive LGraph container that Subgraph inherits.
|
||||
* Entity IDs reference entities that live in the World — not in a
|
||||
* nested graph instance.
|
||||
*/
|
||||
export interface SubgraphStructure {
|
||||
/** Nodes contained within the subgraph. */
|
||||
nodeIds: readonly NodeEntityId[]
|
||||
/** Internal links (both endpoints inside the subgraph). */
|
||||
linkIds: readonly LinkEntityId[]
|
||||
/** Internal reroutes. */
|
||||
rerouteIds: readonly RerouteEntityId[]
|
||||
}
|
||||
|
||||
/** Descriptive metadata for a subgraph definition. */
|
||||
export interface SubgraphMeta {
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
75
src/ecs/components/widget.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Widget components.
|
||||
*
|
||||
* Widget value extraction is already complete (WidgetValueStore).
|
||||
* These interfaces formalize the target shape and add the layout
|
||||
* component that remains on the BaseWidget class.
|
||||
*
|
||||
* The 23+ widget subclasses (NumberWidget, ComboWidget, etc.) become
|
||||
* configuration data here. Widget-type-specific rendering behavior
|
||||
* will live in the RenderSystem.
|
||||
*/
|
||||
|
||||
import type {
|
||||
IWidgetOptions,
|
||||
TWidgetValue
|
||||
} from '@/lib/litegraph/src/types/widgets'
|
||||
|
||||
import type { NodeEntityId } from '../entityId'
|
||||
|
||||
/** Immutable identity of a widget within its parent node. */
|
||||
export interface WidgetIdentity {
|
||||
/** Widget name (unique within a node). */
|
||||
name: string
|
||||
/**
|
||||
* Widget type string (e.g., 'number', 'combo', 'toggle', 'text').
|
||||
* Determines which system handles rendering and interaction.
|
||||
*/
|
||||
widgetType: string
|
||||
/** The node that owns this widget. */
|
||||
parentNodeId: NodeEntityId
|
||||
}
|
||||
|
||||
/**
|
||||
* Widget value and configuration.
|
||||
*
|
||||
* Structurally equivalent to the existing WidgetState in
|
||||
* WidgetValueStore — the bridge layer can share the same objects.
|
||||
*/
|
||||
export interface WidgetValue {
|
||||
/** Current value (type depends on widgetType). */
|
||||
value: TWidgetValue
|
||||
/** Configuration options (min, max, step, values, etc.). */
|
||||
options: IWidgetOptions
|
||||
/** Display label override. */
|
||||
label?: string
|
||||
/** Whether the widget is disabled. */
|
||||
disabled?: boolean
|
||||
/** Whether to include this widget's value in serialized workflow JSON. */
|
||||
serialize?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout metrics computed during the arrange phase.
|
||||
*
|
||||
* Currently lives as mutable properties on BaseWidget (y,
|
||||
* computedHeight, width). The LayoutSystem will own these writes;
|
||||
* the RenderSystem reads them.
|
||||
*/
|
||||
export interface WidgetLayout {
|
||||
/** Vertical position relative to the node body. */
|
||||
y: number
|
||||
/** Computed height after layout distribution. */
|
||||
computedHeight: number
|
||||
/** Width override (undefined = use node width). */
|
||||
width?: number
|
||||
/** Layout size constraints from computeLayoutSize(). */
|
||||
constraints?: WidgetLayoutConstraints
|
||||
}
|
||||
|
||||
export interface WidgetLayoutConstraints {
|
||||
minHeight: number
|
||||
maxHeight?: number
|
||||
minWidth?: number
|
||||
maxWidth?: number
|
||||
}
|
||||
64
src/ecs/entityId.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Branded entity ID types for compile-time cross-kind safety.
|
||||
*
|
||||
* Each entity kind gets a nominal type wrapping its underlying primitive.
|
||||
* The brand prevents accidentally passing a LinkEntityId where a
|
||||
* NodeEntityId is expected — a class of bugs that plain `number` allows.
|
||||
*
|
||||
* At runtime these are just numbers (or strings for subgraphs). The brand
|
||||
* is erased by TypeScript and has zero runtime cost.
|
||||
*
|
||||
* @see {@link ../../../docs/adr/0008-entity-component-system.md}
|
||||
*/
|
||||
|
||||
// -- Branded ID types -------------------------------------------------------
|
||||
|
||||
type Brand<T, B extends string> = T & { readonly __brand: B }
|
||||
|
||||
export type NodeEntityId = Brand<number, 'NodeEntityId'>
|
||||
export type LinkEntityId = Brand<number, 'LinkEntityId'>
|
||||
export type SubgraphEntityId = Brand<string, 'SubgraphEntityId'>
|
||||
export type WidgetEntityId = Brand<number, 'WidgetEntityId'>
|
||||
export type SlotEntityId = Brand<number, 'SlotEntityId'>
|
||||
export type RerouteEntityId = Brand<number, 'RerouteEntityId'>
|
||||
export type GroupEntityId = Brand<number, 'GroupEntityId'>
|
||||
|
||||
/** Union of all entity ID types. */
|
||||
export type EntityId =
|
||||
| NodeEntityId
|
||||
| LinkEntityId
|
||||
| SubgraphEntityId
|
||||
| WidgetEntityId
|
||||
| SlotEntityId
|
||||
| RerouteEntityId
|
||||
| GroupEntityId
|
||||
|
||||
// -- Cast helpers (for use at system boundaries) ----------------------------
|
||||
|
||||
export function asNodeEntityId(id: number): NodeEntityId {
|
||||
return id as NodeEntityId
|
||||
}
|
||||
|
||||
export function asLinkEntityId(id: number): LinkEntityId {
|
||||
return id as LinkEntityId
|
||||
}
|
||||
|
||||
export function asSubgraphEntityId(id: string): SubgraphEntityId {
|
||||
return id as SubgraphEntityId
|
||||
}
|
||||
|
||||
export function asWidgetEntityId(id: number): WidgetEntityId {
|
||||
return id as WidgetEntityId
|
||||
}
|
||||
|
||||
export function asSlotEntityId(id: number): SlotEntityId {
|
||||
return id as SlotEntityId
|
||||
}
|
||||
|
||||
export function asRerouteEntityId(id: number): RerouteEntityId {
|
||||
return id as RerouteEntityId
|
||||
}
|
||||
|
||||
export function asGroupEntityId(id: number): GroupEntityId {
|
||||
return id as GroupEntityId
|
||||
}
|
||||
242
src/ecs/world.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* World — the central registry for all entity state.
|
||||
*
|
||||
* The World is a typed container mapping branded entity IDs to their
|
||||
* component sets. Systems query the World to read/write components;
|
||||
* entities never reference each other directly.
|
||||
*
|
||||
* This is the initial type definition (Phase 1c of the migration plan).
|
||||
* The implementation starts as plain Maps. CRDT backing, transactions,
|
||||
* and reactivity are future concerns.
|
||||
*/
|
||||
|
||||
import type {
|
||||
Connectivity,
|
||||
Execution,
|
||||
NodeType,
|
||||
NodeVisual,
|
||||
Properties,
|
||||
WidgetContainer
|
||||
} from './components/node'
|
||||
import type { GroupChildren, GroupMeta, GroupVisual } from './components/group'
|
||||
import type { LinkEndpoints, LinkState, LinkVisual } from './components/link'
|
||||
import type { Position } from './components/position'
|
||||
import type { RerouteLinks, RerouteVisual } from './components/reroute'
|
||||
import type {
|
||||
SlotConnection,
|
||||
SlotIdentity,
|
||||
SlotVisual
|
||||
} from './components/slot'
|
||||
import type { SubgraphMeta, SubgraphStructure } from './components/subgraph'
|
||||
import type {
|
||||
WidgetIdentity,
|
||||
WidgetLayout,
|
||||
WidgetValue
|
||||
} from './components/widget'
|
||||
import type {
|
||||
GroupEntityId,
|
||||
LinkEntityId,
|
||||
NodeEntityId,
|
||||
RerouteEntityId,
|
||||
SlotEntityId,
|
||||
SubgraphEntityId,
|
||||
WidgetEntityId
|
||||
} from './entityId'
|
||||
|
||||
// -- Component bundles per entity kind --------------------------------------
|
||||
|
||||
export interface NodeComponents {
|
||||
position: Position
|
||||
nodeType: NodeType
|
||||
visual: NodeVisual
|
||||
connectivity: Connectivity
|
||||
execution: Execution
|
||||
properties: Properties
|
||||
widgetContainer: WidgetContainer
|
||||
}
|
||||
|
||||
export interface LinkComponents {
|
||||
endpoints: LinkEndpoints
|
||||
visual: LinkVisual
|
||||
state: LinkState
|
||||
}
|
||||
|
||||
export interface SlotComponents {
|
||||
identity: SlotIdentity
|
||||
connection: SlotConnection
|
||||
visual: SlotVisual
|
||||
}
|
||||
|
||||
export interface WidgetComponents {
|
||||
identity: WidgetIdentity
|
||||
value: WidgetValue
|
||||
layout: WidgetLayout
|
||||
}
|
||||
|
||||
export interface RerouteComponents {
|
||||
position: Position
|
||||
links: RerouteLinks
|
||||
visual: RerouteVisual
|
||||
}
|
||||
|
||||
export interface GroupComponents {
|
||||
position: Position
|
||||
meta: GroupMeta
|
||||
visual: GroupVisual
|
||||
children: GroupChildren
|
||||
}
|
||||
|
||||
export interface SubgraphComponents {
|
||||
structure: SubgraphStructure
|
||||
meta: SubgraphMeta
|
||||
}
|
||||
|
||||
// -- Entity kind registry ---------------------------------------------------
|
||||
|
||||
export interface EntityKindMap {
|
||||
node: { id: NodeEntityId; components: NodeComponents }
|
||||
link: { id: LinkEntityId; components: LinkComponents }
|
||||
slot: { id: SlotEntityId; components: SlotComponents }
|
||||
widget: { id: WidgetEntityId; components: WidgetComponents }
|
||||
reroute: { id: RerouteEntityId; components: RerouteComponents }
|
||||
group: { id: GroupEntityId; components: GroupComponents }
|
||||
subgraph: { id: SubgraphEntityId; components: SubgraphComponents }
|
||||
}
|
||||
|
||||
export type EntityKind = keyof EntityKindMap
|
||||
|
||||
// -- World interface --------------------------------------------------------
|
||||
|
||||
export interface World {
|
||||
/** Per-kind entity stores. */
|
||||
nodes: Map<NodeEntityId, NodeComponents>
|
||||
links: Map<LinkEntityId, LinkComponents>
|
||||
slots: Map<SlotEntityId, SlotComponents>
|
||||
widgets: Map<WidgetEntityId, WidgetComponents>
|
||||
reroutes: Map<RerouteEntityId, RerouteComponents>
|
||||
groups: Map<GroupEntityId, GroupComponents>
|
||||
subgraphs: Map<SubgraphEntityId, SubgraphComponents>
|
||||
|
||||
/**
|
||||
* Create a new entity of the given kind, returning its branded ID.
|
||||
* The entity starts with no components — call setComponent() to populate.
|
||||
*/
|
||||
createEntity<K extends EntityKind>(kind: K): EntityKindMap[K]['id']
|
||||
|
||||
/**
|
||||
* Remove an entity and all its components.
|
||||
* Returns true if the entity existed, false otherwise.
|
||||
*/
|
||||
deleteEntity<K extends EntityKind>(
|
||||
kind: K,
|
||||
id: EntityKindMap[K]['id']
|
||||
): boolean
|
||||
|
||||
/**
|
||||
* Get a single component from an entity.
|
||||
* Returns undefined if the entity or component doesn't exist.
|
||||
*/
|
||||
getComponent<
|
||||
K extends EntityKind,
|
||||
C extends keyof EntityKindMap[K]['components']
|
||||
>(
|
||||
kind: K,
|
||||
id: EntityKindMap[K]['id'],
|
||||
component: C
|
||||
): EntityKindMap[K]['components'][C] | undefined
|
||||
|
||||
/**
|
||||
* Set a single component on an entity.
|
||||
* Creates the component if it doesn't exist, overwrites if it does.
|
||||
*/
|
||||
setComponent<
|
||||
K extends EntityKind,
|
||||
C extends keyof EntityKindMap[K]['components']
|
||||
>(
|
||||
kind: K,
|
||||
id: EntityKindMap[K]['id'],
|
||||
component: C,
|
||||
data: EntityKindMap[K]['components'][C]
|
||||
): void
|
||||
}
|
||||
|
||||
// -- Factory ----------------------------------------------------------------
|
||||
|
||||
export function createWorld(): World {
|
||||
const counters = {
|
||||
node: 0,
|
||||
link: 0,
|
||||
slot: 0,
|
||||
widget: 0,
|
||||
reroute: 0,
|
||||
group: 0,
|
||||
subgraph: 0
|
||||
}
|
||||
|
||||
const stores = {
|
||||
nodes: new Map<NodeEntityId, NodeComponents>(),
|
||||
links: new Map<LinkEntityId, LinkComponents>(),
|
||||
slots: new Map<SlotEntityId, SlotComponents>(),
|
||||
widgets: new Map<WidgetEntityId, WidgetComponents>(),
|
||||
reroutes: new Map<RerouteEntityId, RerouteComponents>(),
|
||||
groups: new Map<GroupEntityId, GroupComponents>(),
|
||||
subgraphs: new Map<SubgraphEntityId, SubgraphComponents>()
|
||||
}
|
||||
|
||||
const storeForKind: Record<EntityKind, Map<unknown, unknown>> = {
|
||||
node: stores.nodes,
|
||||
link: stores.links,
|
||||
slot: stores.slots,
|
||||
widget: stores.widgets,
|
||||
reroute: stores.reroutes,
|
||||
group: stores.groups,
|
||||
subgraph: stores.subgraphs
|
||||
}
|
||||
|
||||
return {
|
||||
...stores,
|
||||
|
||||
createEntity<K extends EntityKind>(kind: K): EntityKindMap[K]['id'] {
|
||||
const id = ++counters[kind]
|
||||
const store = storeForKind[kind]
|
||||
store.set(id, {} as never)
|
||||
return id as EntityKindMap[K]['id']
|
||||
},
|
||||
|
||||
deleteEntity<K extends EntityKind>(
|
||||
kind: K,
|
||||
id: EntityKindMap[K]['id']
|
||||
): boolean {
|
||||
return storeForKind[kind].delete(id)
|
||||
},
|
||||
|
||||
getComponent<
|
||||
K extends EntityKind,
|
||||
C extends keyof EntityKindMap[K]['components']
|
||||
>(
|
||||
kind: K,
|
||||
id: EntityKindMap[K]['id'],
|
||||
component: C
|
||||
): EntityKindMap[K]['components'][C] | undefined {
|
||||
const entity = storeForKind[kind].get(id) as
|
||||
| EntityKindMap[K]['components']
|
||||
| undefined
|
||||
return entity?.[component]
|
||||
},
|
||||
|
||||
setComponent<
|
||||
K extends EntityKind,
|
||||
C extends keyof EntityKindMap[K]['components']
|
||||
>(
|
||||
kind: K,
|
||||
id: EntityKindMap[K]['id'],
|
||||
component: C,
|
||||
data: EntityKindMap[K]['components'][C]
|
||||
): void {
|
||||
const entity = storeForKind[kind].get(id)
|
||||
if (entity) {
|
||||
Object.assign(entity, { [component]: data })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||