[add] src/geometry/model-instance-collider-generation.ts [add] src/runtime-three/rapier-collision-world.ts [change] AGENTS.md [change] CHAT_CONTEXT.md [change] architecture.md [change] package.json [change] prompts-lite.txt [change] prompts.txt [change] roadmap.md [change] src/assets/model-instances.ts [change] src/document/migrate-scene-document.ts [change] src/document/scene-document-validation.ts [change] src/document/scene-document.ts [change] src/runtime-three/first-person-navigation-controller.ts [change] src/runtime-three/navigation-controller.ts [change] src/runtime-three/runtime-host.ts [change] src/runtime-three/runtime-scene-build.ts [change] src/runtime-three/runtime-scene-validation.ts [change] src/viewport-three/viewport-host.ts [change] testing.md
17 KiB
AGENTS.md
Project identity
This repository contains a browser-based brush/solid editor for creating lightweight interactive 3D scenes, plus a built-in browser runner for playing those scenes.
The product goal is:
- Hammer / TrenchBroom style spatial authoring
- modern browser delivery
- glTF asset import, with optional later interchange export
- fast edit -> run iteration
- lightweight interactive runtime with spatial audio, navigation modes, and simple entity-driven logic
This is not a general-purpose DCC. This is not a full game engine. This is a focused authoring + runtime tool for browser-delivered 3D spaces.
Product pillars
-
Brushes are sacred
- Layout authoring must remain faster than Blender.
- Brush editing is a first-class workflow, not a legacy compatibility mode.
- The editor must feel immediate, precise, and grid-friendly.
-
Imported assets are first-class
- glTF / GLB import must feel native.
- Imported meshes, materials, textures, and animations must coexist cleanly with brush-authored worlds.
- Imported assets complement brushes; they do not replace them.
-
The runner is built in
- Every meaningful authoring step should be testable in-browser.
- Switching from edit mode to play mode should be nearly instant.
- The runner is part of the product, not a demo app.
-
Web-native sharing matters
- Scenes should be easy to load, embed, preview, and eventually share by URL.
- The browser is a target platform, not a secondary export target.
Architectural stance
When making design decisions, prefer:
- plain three.js over unnecessary abstraction
- explicit data models over implicit scene graph state
- deterministic rebuilds over hidden mutations
- command-based editing over ad hoc state changes
- typed scene entities over free-form JSON blobs
- vertical slices over speculative infrastructure
- boring, maintainable code over cleverness
The project uses:
- React for application shell and editor UI
- three.js for viewport and runtime rendering
- a canonical editor document model independent of three.js
- command pattern for undo/redo
- runtime entity systems for navigation, triggers, audio, and interaction
- glTF / GLB as the main imported 3D asset format
- JSON as the canonical authoring format
Do not collapse editor state into raw three.js objects. Do not make the three.js scene the source of truth. Do not make glTF the canonical editor save format.
Early binding decisions
These defaults are intentionally fixed for the early slices unless a later slice explicitly changes them.
Coordinate system
- world space is right-handed and Y-up
+Xis right,+Yis up- scene units are meter-like and should be used consistently for movement, collision, and audio distances
Early repo shape
- start as a single Vite app
- keep domain folders under
src/ - do not introduce
/apps+/packagesor a monorepo split until the current code actually needs it
State ownership
- do not use the React tree as the canonical state container
- keep canonical editor state in a thin external editor store/service
- React renders and dispatches commands; it does not own the document
Persistence
- the canonical scene document is versioned from day one
- M0-M2 may use local draft persistence plus explicit JSON import/export
- once binary assets matter, user-facing save/load must become a portable project package containing canonical scene JSON plus referenced assets
- canonical scene JSON remains the source document format, but by itself is no longer a portable project once external assets exist
- runner/deployment output is a separate downstream package, not the editable project format
- when binary assets arrive, they must survive reloads via embedded data or project-scoped packaged storage
- never rely on ephemeral Blob URLs as the only persisted asset reference
Early brush defaults
- Slice 1.1 box brushes are axis-aligned only
- arbitrary brush rotation is explicitly deferred
- canonical box face IDs are fixed and stable:
posXnegXposYnegYposZnegZ
posYis the top face andnegYis the bottom face
Model placement
- placed imported models are model instances, not typed entities
- keep model instances in a document collection separate from
entities
Imported model collision scope
- collision authoring for imported models belongs on
modelInstances, not on asset records - the canonical source of truth is authored collision settings, not cooked collider bytes
- generated collider data may be cached or rebuilt, but it is derived from:
- imported model asset geometry
- model instance transform
- authored collision settings
- for imported-model collider support beyond simple boxes, prefer integrating a real collision/query library such as Rapier over inventing custom broad-phase/narrow-phase code in-house
- let the collision/query layer own broad-phase and narrow-phase pruning instead of re-implementing that manually in app code
- do not turn this slice into a full physics sandbox or general rigidbody architecture rewrite unless the roadmap explicitly asks for that
- near-term slices may adapt or replace the current handcrafted runner collision path where necessary so brush and imported-model colliders can participate in one coherent collision/query system
Runtime interaction scope
- keep trigger/action/target links explicit and typed
- do not activate actions for systems that do not exist yet
- add sound and animation actions only when those runtime systems are implemented
Early clipping scope
- until a dedicated convex-brush slice exists, clipping must be constrained to results representable by currently supported brush kinds
- unsupported clip cases must fail clearly instead of inventing new hidden geometry rules
Non-goals
Unless explicitly added to the roadmap, do not turn this project into:
- a general CAD package
- a Blender replacement
- a multiplayer MMO editor
- a full node-based visual scripting environment
- a full physics sandbox
- a photoreal AAA renderer
- a React Three Fiber showcase
- an ECS research project
We may add optional scripting, plugins, collaboration, or advanced baking later. They are not v1 priorities.
Core product vocabulary
Use these terms consistently:
- Document: canonical editor state
- Brush: author-authored solid/primitive in canonical brush form
- Face: one editable surface of a brush
- Material: logical authoring material definition
- Texture: image resource backing material channels
- Asset: imported external resource, usually GLB/GLTF or audio and related media
- Model Instance: placed scene instance of an imported asset
- Collider: runtime collision representation derived from brushes or imported models
- Prefab: reusable asset/entity package placeable in scenes
- Entity: typed scene object with runtime/editor semantics
- Project Package: portable editable bundle containing canonical scene JSON plus referenced assets
- Runner Package: deployable playable output for the built-in runner
- Runner: browser runtime that loads and plays scenes
- Viewport: editor rendering surface
- Command: undoable state transition
- Tool: editor interaction mode such as select, create, transform, or face-edit
- Build: deterministic transformation from document -> runtime scene data
- Export: downstream transformation to deployable or interchange deliverables such as runner packages or optional later GLB
Avoid vague terms like “object”, “thing”, “item”, or “component” when a more precise domain term exists.
Repo expectations for agents
When working in this repo:
-
Read:
AGENTS.mdCHAT_CONTEXT.md- then inspect the relevant sections of
architecture.md,roadmap.md, andtesting.mdfor the active slice - if a slice touches persistence, runtime, or testing boundaries in a non-obvious way, read the relevant full doc sections before changing code
-
Respect the current vertical slice.
- Do not “prepare for future flexibility” by adding unnecessary systems.
- Implement the smallest coherent version that preserves the architecture.
-
Preserve layering.
documentowns canonical state.commandsapply valid state changes.geometryowns derived brush and collider generation.viewport-threerenders editor state.runtime-threeplays runtime state.entitiesowns typed non-brush scene objects.assetsadapts external asset/audio/media formats.serializationpersists canonical state.
-
Do not bypass command infrastructure for editor mutations.
- If the user can do it in the editor, it should usually be represented as a command.
-
If persisted schema changes, update compatibility explicitly.
- bump the document schema version when required
- add or update migrations
- add at least one migration or compatibility test
-
Prefer explicit typing and explicit invariants.
- Avoid permissive
any, loose maps, or magic strings when a discriminated union or typed schema is appropriate.
- Avoid permissive
-
Keep systems testable.
- Geometry generation should be testable outside the browser UI.
- Serialization should be round-trip tested.
- Runtime interactions should be testable through deterministic fixtures where possible.
-
Keep browser concerns in mind.
- Gracefully handle pointer lock failure, audio unlock requirements, missing gamepads, and asset load failures.
- Avoid architecture that assumes native desktop privileges.
Code quality rules
General
- TypeScript only
- strict typing enabled
- prefer pure functions for transforms/build steps
- isolate impure browser/three.js side effects
- no silent catch-and-ignore
- no dead feature flags without roadmap justification
- no hidden singleton globals unless explicitly part of infrastructure
Naming
- use descriptive names
- prefer domain names over generic utility names
- avoid abbreviations unless they are standard and obvious
- function names should describe intent, not implementation detail
Good:
buildRuntimeSceneFromDocumentapplyMaterialToSelectedFacescreateBoxBrushCommand
Bad:
handleThingupdateDatadoBuild
File organization
- small files where it helps clarity
- split by domain, not by arbitrary technical categories
- avoid giant “misc” or “utils” dumping grounds
- every folder should have a clear reason to exist
Comments
- explain why, not what the code obviously does
- use comments to document invariants, constraints, and tricky browser behavior
- remove stale comments promptly
Error handling
- fail loudly in development
- surface usable diagnostics in the editor UI
- never corrupt the document silently
- preserve previous valid state on failed builds where possible
Data model rules
The canonical authoring state must remain independent from three.js scene graph objects.
Required separation
Maintain these layers:
-
Authoring model
- JSON-serializable
- versioned
- typed
- stable across runtime/editor rebuilds
-
Editor view model
- three.js meshes, helpers, overlays, gizmos
- disposable and rebuildable
-
Runtime model
- play-mode scene graph
- controllers, triggers, emitters, colliders, animation mixers
Do not let editor-only helpers leak into the canonical document. Do not store raw three.js objects inside canonical document state.
Brush rules
- brushes are not stored as triangle soup
- face material assignments are per-face
- UV transforms are canonical editor data
- runtime mesh generation is derived data
- collision generation is derived data
- early boxes are axis-aligned and use fixed face IDs
Entity rules
- entities must be typed
- entity schemas must be explicit
- entity defaults must be centralized
- entity validation must happen at document/build boundaries
- model instances remain separate from entities
Imported model collision rules
- collision settings for imported models live on
modelInstances - supported collision modes must be explicit and typed
- generated collision geometry is derived data, not the canonical source document
- collision debug visibility is editor/runtime UI state driven by authored settings, not a hidden renderer-only toggle
- avoid implicit “always collide with render mesh” behavior
- if broad-phase/narrow-phase pruning or non-box collider support is needed, prefer Rapier over ad hoc custom collision math
- collision modes mean:
none= no colliderterrain= heightfield collider, static onlystatic= triangle mesh collider, fixed onlydynamic= convex decomposition into compound collider, dynamic/kinematic capablesimple= one cheap primitive or one convex hull
Performance rules
Performance matters, but premature micro-optimization does not.
Priorities:
- editor responsiveness during common operations
- deterministic rebuild behavior
- predictable memory usage
- runtime smoothness for modest scenes
- export/build correctness
When optimizing:
- measure first
- optimize hotspots, not aesthetics
- document assumptions
- prefer algorithmic improvements over clever hacks
Expected hotspots:
- picking/raycasting
- brush rebuilds / CSG
- face highlighting
- large texture browser lists
- imported asset previews
- runtime trigger scanning if implemented naively
UX rules
The editor should feel like a real authoring tool, not a tech demo.
Prioritize:
- fast selection
- robust snapping
- visible grid and transform feedback
- obvious active tool state
- low-friction material application
- quick play testing
- understandable errors
Every new feature should answer:
- What does the user see?
- What does the user click/drag/type?
- How is failure communicated?
- How is the action undone?
- How is the result tested?
Vertical slice policy
We build in vertical slices. Each slice must deliver a complete, usable capability across all relevant layers.
A good slice includes:
- document changes
- commands
- viewport behavior
- UI panel updates
- runner behavior if relevant
- persistence
- tests
- manual QA notes
A bad slice is “just backend structure” or “just a partial UI”.
Do not land architectural scaffolding that has no immediate use in the current slice.
If a roadmap item is too large for one pass, split it into smaller end-to-end sub-slices instead of landing half-systems.
Typical slice shape
For each slice, agents should aim to deliver:
- domain model changes
- command(s)
- viewport interaction/tooling
- UI affordance
- serialization support
- runtime/build support if needed
- tests
- docs update if behavior changed materially
Decision heuristics
When uncertain:
Prefer plain three.js over extra abstraction
Unless abstraction clearly simplifies repeated patterns.
Prefer canonical JSON over reusing runtime/export data structures
The editor’s needs are different from export/runtime needs.
Prefer typed entity schemas over generic script bags
Especially in early versions.
Prefer constrained capabilities that feel good over flexible capabilities that feel vague
Example:
- better to have one excellent box-brush tool than five half-working primitive tools
Prefer immediate usability over speculative extensibility
But preserve clean seams for future extensions.
What agents must not do
Do not:
- rewrite broad project structure without a strong reason
- introduce new framework dependencies casually
- add R3F because “we might want it later”
- introduce ECS because “games use ECS”
- over-generalize the entity system
- replace canonical brush data with raw mesh editing
- implement hidden magic behaviors without schema support
- remove tests to get green CI
- make visual changes without noting them in the slice summary
- ignore browser restrictions around input/audio
Required deliverables in implementation responses
When making meaningful changes, include:
- what changed
- why it changed
- which files were added/updated
- how to run/test it
- known limitations
- follow-up suggestions only if directly relevant
If the environment prevents verification, state exactly what was and was not verified.
Definition of done for a slice
A slice is done when:
- the feature can be used end-to-end
- the feature is represented in canonical document data
- the feature can be saved and loaded
- the feature is test-covered appropriately
- the feature has manual verification notes
- the feature does not violate the architecture
- obvious failures produce usable diagnostics
- undo/redo works if the feature is editor-authored
Preferred stack unless changed deliberately
- TypeScript
- React
- Vite
- three.js
- Vitest
- Playwright
- ESLint
- Prettier
- a small state store if needed
- minimal dependencies overall
Add dependencies only when they clearly save time and complexity over building in-house.
Final instruction to agents
Build the smallest coherent thing that feels real.
The product should always trend toward:
- spatial immediacy
- authoring clarity
- browser-native practicality
- fast iteration
- strong foundations for brushes, assets, entities, and runner behavior
If forced to choose, preserve the integrity of:
- the canonical document model
- the brush workflow
- the edit -> run loop