Architecture
Packages
| Package | What it does | Depends on |
|---|---|---|
@isora/core | Scene model and validation, projection and depth sorting, placement and stacking, sitting, pathfinding, behavior planning, motion, computing parts to draw | Nothing (pure TS, no DOM) |
@isora/react | <IsoScene>, drawing layers, playback hooks (clock, behavior, timelines, scene changes) | core, React 18/19 |
@isora/assets | 10 official asset packs (common, office, factory, city, medical, military, school, retail, transit, people; CC0) | core |
@isora/pack | Pack manifest, validation and build, SVG sanitizing, static SVG export | core |
@isora/cli | isora init, studio (local server), pack, export | core, pack |
apps/studio | GUI editor (MUI) | react, assets |
example/* | Examples that use the framework like a real app (claude-office, nullus-login, showcase: ten large scenes built in code) | react, assets |
Core modules and flow
types · asset(defineAsset) · scene(validation·parse)
│
projection(iso·rotation·projectPoint) ── geometry(convex hull·SAT) ── shape(part boxes)
│ │
placement(stacking·floor height·floorPointAt) ── seating(spots·SITTING·sit path check·describeRig)
│ │
navigation(grid·A*·time reservations) │
│ │
activities(activities·routines) ── behavior(planning·commands·frameAt) ── motion(gait·pose blending·transitionClear)
│ │
depth(front/back graph) ── render(prepareRender·composeRender·buildRenderList)
timeline(paths·keyframes) ──────────────────────────────────────────────────┘
builder(build scenes in code) · lint(layout checks) · motionLint(motion physics checks) ─ tools that only read the layers above- Dependencies only go data → plan → pose → picture. Lower layers do not know about upper ones.
- The plan (
behavior) is a function of time. Random numbers come from person id and seed, and the plan only grows in order of decision time. - The pose (
motion) turns the values the plan gives for one moment (position, pose, transition progress, distance walked) into part boxes. It has no state. - The picture (
render) turns boxes into screen faces and decides front and back. It splits static and moving things to keep per-frame cost low. - Text shown to people (lint messages,
describeRigwarnings, asset, state, activity and routine names) is English by default with Korean built in (Languages).
react
| Layer | What |
|---|---|
Clock createTimelineClock | Tick (80 ms) notifications + per-frame time timeAt. Behavior and timelines can share it |
| Playback hooks | useBehaviorPlayback (routines, commands) · useTimelinePlayback (cargo, keyframes) · useObjectMotion (scene data changes) |
| Per-frame source | LiveSource { ids, sample(wall), until? } — the hooks provide these, and apps can build their own |
| Layers | LiveSceneLayer (prepared + sources → every frame) · SceneLayer (parts → SVG) · GroundGrid · HullOutline · ActivityBubbles |
| Viewer | <IsoScene> — all of the above, assembled. Apps hook in through behavior, clock, renderOverlay and onSceneClick |
Ticks and frames
| Tick (80 ms) | Frame (rAF) | |
|---|---|---|
| What changes | Activities and phases (people), device and door states, time of day, timeline cargo | Parts of walking and sitting people |
| Where | clock.subscribe → hooks → React re-render | LiveSceneLayer's store → only that layer re-renders |
| Why split | Full re-renders and rebuilding the static picture are expensive | Walking chopped into ticks would jitter |
Extension points
| You want to | Use |
|---|---|
| Move people on app events | useBehaviorPlayback().command / BehaviorWorld.command |
| Turn constantly changing data into people's movement | useActorCommands |
| Build scenes in code | createSceneBuilder |
| Define routines and schedules | Scene routines, clock |
Add a new activity (e.g. charge) | Write the name in furniture seats[].activity or spots[].activity and use it in a routine |
| Move non-people every frame (live forklift or AGV positions) | Build a LiveSource and put it in LiveSceneLayer |
| Marks above heads, pins, path lines | <IsoScene renderOverlay> or LiveSceneLayer children |
| Pick a location by clicking | <IsoScene onSceneClick>, floorPointAt |
| A new person asset | Custom people, describeRig |
| Another renderer (Canvas, WebGL, server images) | Faces from prepareRender and composeRender, boxes from actorPartBoxes and objectPartBoxes |
| Bind equipment states | <IsoScene states> — the asset's states names |
| Show names and messages in another language | names / labels next to the English text, assetName · stateLabel · localize, { locale } on lint (Languages) |
Checklist: is it usable as a framework?
| Area | Status | Notes |
|---|---|---|
| Scene and asset data model | ✅ | Schema validation (parseScene, validateScene, defineAsset), pack id rules, deterministic JSON |
| Coordinates, projection, click coordinates | ✅ | projectPoint, unprojectPoint, floorPointAt, onSceneClick |
| Placement, stacking, collisions | ✅ | placeAt, surfaceHeightAt, findCollisions |
| Autonomous people behavior | ✅ | Routines, schedules, making way, coming and going; deterministic |
| Commanding people | ✅ | go, do, idle, leave, routine; fallback commands (orElse); standing activities; gathering around; interrupting, releasing spots, reporting results; replaying the log |
| Data → movement | ✅ | Declare what each person should do with useActorCommands (example/claude-office) |
| Building scenes in code | ✅ | createSceneBuilder (add, sit, floor) |
| Reading positions and activities | ✅ | people, overrides, frameAt(t), per-frame renderOverlay |
| Playback control | ✅ | Shared clock, seek, setSpeed, reduced motion |
| Motion quality | ✅ | Every frame in world space, sit-path collision checks |
| Extending person assets | ✅ | Part naming conventions + describeRig |
| Swapping the renderer | ✅ | The core provides faces and boxes without the DOM |
| Languages | ✅ | English by default, Korean built in; translations sit next to the English text |
| Command-finished events | △ | Compare people every tick instead of a callback (Moving people) |
| Pathfinding commands for non-people | △ | Forklifts and AGVs get positions from the app through a LiveSource. Only people pathfind and make way |
| Floors and stairs | ✗ | One floor only. Moving between floors is not supported yet |
| Continuous rotation | ✗ | Facing turns instantly in 90° steps |
| Renaming the rig | ✗ | Part naming rules (leg-l etc.) are fixed. To use other names, rename the parts to match |
| Long jumps | △ | Seeking an hour ahead takes a few hundred ms of planning |
What the tests guard
packages/core/src/behavior.test.ts— planning, making way, schedules, commands (interrupting, releasing spots, coming and going, failures), determinism, no overlaps when commands and routines mixpackages/assets/src/motion.test.ts— sitting in desk chairs and sofas facing four directions never passes through furniture for any person, transition continuity, gait, depth order, rig conventions, the doc example assetapps/studio/src/editor/samples.test.ts— every desk, sofa and bench seat in the sample scenes passes the sit-path checkpackages/react/src/IsoScene.test.tsx— app-owned behavior playback, overlaysexample/claude-office/src/office.test.ts— sessions → desk assignment; per-mood commands (code, coffee, Zz) succeed without overlaps even when spots run short