world-view

The Babylon view of a [[world-store]]. The store is the authoritative, serializable truth; the view is a disposable projection of it. WorldView watches the store and reconciles one mesh per entity every frame: entities that appeared get a mesh, entities that moved get their mesh repositioned, entities that were forgotten get their mesh disposed. Data flows one way — store → meshes — so the rendering layer can never desync the simulation.

This keeps the architecture honest: the view imports Babylon, the store does not. You can run the store headlessly (tests, a server, a driver developing against the contract) and attach a view only where there's something to draw.

The default factory draws primitives (capsule for characters, box for objects) so a scene is visible with zero assets; pass your own factory to swap in b3dBiped, library instances, or GLB meshes per entity kind.

import {
  b3d, b3dSun, b3dSkybox, b3dGround, WorldStore, WorldView,
} from 'tosijs-3d'

// A stand-in "director" sets up a scene by writing to the store.
const store = new WorldStore()
const witness = store.spawn({
  kind: 'npc',
  position: { x: 4, y: 0.8, z: 3 },
  components: { interactable: { promptId: 'talk', locked: false } },
})
store.spawn({ kind: 'item', position: { x: -2, y: 0.25, z: 1 } })
store.spawn({ kind: 'container', position: { x: 0, y: 0.4, z: -4 } })

// The driver only ever observes events + queries; it never blocks the sim.
store.subscribe((event) => console.log('event:', event))
const FORGET_AFTER = 15 // seconds; a real driver would use minutes/hours

const keys = new Set()
window.addEventListener('keydown', (e) => keys.add(e.key.toLowerCase()))
window.addEventListener('keyup', (e) => keys.delete(e.key.toLowerCase()))

preview.append(
  b3d(
    {
      sceneCreated(el) {
        // Bridge the headless store to the live scene.
        new WorldView(el.scene, store)
      },
      update(el) {
        const dt = el.scene.getEngine().getDeltaTime() / 1000
        store.tick(dt)
        // WASD writes the player's position back into the store.
        const p = { ...store.getState().entities.player.position }
        const speed = 4 * dt
        if (keys.has('w')) p.z += speed
        if (keys.has('s')) p.z -= speed
        if (keys.has('a')) p.x -= speed
        if (keys.has('d')) p.x += speed
        store.moveEntity('player', p)
        // E engages the nearest interactable — a commitment, not a fly-by.
        if (keys.has('e')) {
          const near = store
            .query((entity) => !!entity.components.interactable)
            .find((entity) => {
              const d = entity.position
              return (d.x - p.x) ** 2 + (d.z - p.z) ** 2 < 4
            })
          if (near) store.interact('player', near.id)
        }
        // Driver work: drop a witness the player has ignored for too long.
        const w = store.getEntity(witness)
        if (w && w.lastInteractedAt === undefined && store.getState().now > FORGET_AFTER) {
          store.forget(witness)
        }
      },
    },
    b3dSun(),
    b3dSkybox({ timeOfDay: 11 }),
    b3dGround({ width: 24, height: 24, color: '#4a7a4a' })
  )
)