engine v4.4.0
Solid
May 15, 2026
Things stack right, worlds load clean, and clicks land where you point.
what's new
- Stacking just works. A mug on a chair on a house keeps its size. Ask Savi to put something on top of something else and she'll land it first try — huge for any game where things sit on shelves, stack into towers, or get carried around.
- Worlds load clean. Things appear at the right size right away. No more pop-in where stuff suddenly grows or shrinks as the level comes in.
- Clicks land where you point. The sky doesn't steal clicks anymore, and big crowds of repeating things (trees, rocks, props) can be clicked one by one. Shooters, click-to-place, and pick-up-anything games all feel sharper.
- Effects stick to surfaces the right way. Bullet holes, splats, dust puffs and decals face the wall they hit instead of floating sideways.
- Camera stays still when you open the menu — no more drift while you're trying to read.
- Heads up: a few old scenes might look a little different where you'd attached one thing to another. Take a peek and tweak if anything looks off.
›technical notes
Scene graph + layout (breaking)
- BREAKING: scene graph rewrite (#6480). Replaces
Scale+LayoutScale+RenderScale+LayoutScaleFeature+RenderScaleFeature+ the multi-stage hierarchy composition with three explicit components, each with one job:LocalScale/LocalRotation/LocalFeetPosition/LocalPivot— authored inputs.WorldScale/WorldRotation/WorldFeetPosition— solved hierarchy outputs (World* = parent.World* ⊗ Local*). Pure tree math, no asset awareness.GeometryScale— per-asset fit factor derived from(DrawModel.rawBounds, TomeLayout). Computed for every entity, leaf-only at consumption, never inherited.
- BREAKING: fixed layout cascade bug — a root's
layout.maxExtentsno longer shrinks every descendant. Renderer / physics now readWorldScale × GeometryScaleper entity instead of a composedRenderScalethat mixed asset-fit into the hierarchy. Regression test:tome/__tests__/mug-on-chair-on-house.test.ts. - BREAKING: fixed parented-clamp drop bug — a parented entity's own
layout.maxExtentsnow applies.GeometryScaleis written regardless of parenthood. Regression test:tome/__tests__/parented-layout-clamp.test.ts. - BREAKING:
maxExtentsis a ceiling (factor = min(1, maxExtents / rawBounds)),minExtentsis a floor (factor = max(1, minExtents / rawBounds)),minExtents === maxExtents= exact fit. - Added
api.getWorldBoundsBox(id): { min, max, size, center } | null. Backed bytome/api/world-bounds.ts;WorldBoundsBoxtype surfaces in Savi's prompt via the shared-schemas section. - Renderer reads
WorldScale × GeometryScaleuniformly. Model matrix =T(WorldFeetPosition) × R(WorldRotation) × S(WorldScale × GeometryScale). Feet alignment uses the combined factor. - Rapier reads
WorldScale × GeometryScaleso collider geometry matches visible geometry on every entity. Pose stays in pure graph-transform space; feet-to-body-center offset =(rawHeight/2) × WorldScale × GeometryScale.
Asset bounds prefetch
- New server-side
BoundsPrefetchFeature(#6490) discovers entities withDrawModel, submits Range-fetch jobs that parse GLB headers (~64KB per model) off the main thread, and writes results toGameSpecResource.assets.metadata. SupportsKHR_mesh_quantizationdequantization. - Bounds persist to Supabase via
patchAssets, so subsequent room starts find them cached and avoid the refetch. - Hooks-driven, not per-tick (#6491):
onComponentAdd/onComponentSet(DrawModel)+ a one-time seed of existing entities. Steady-state cost is zero. CatchesapplySpec,api.spawnObject, and runtimesetProperty("model", …)swaps without scanning script source. - Removed the now-unused
collectModelUrls/MODEL_URL_INLINE_REspec walker.
Raycast
- Raycast results now include per-hit surface normals (packed + decoded) for both CPU and GPU paths (#6504).
- CPU/GPU raycast behavior aligned for first-hit queries; both use snapped ray directions so results are consistent across paths.
- Sky meshes are excluded from raycasts — click queries no longer hit the skybox before the world.
- Raycast reset and distance handling hardened against invalid (NaN/Infinity) values.
- Fixes raycast against
IndirectBatchedMeshin the compute raycast path.
Input
handleMouseMoveinengine/input/raw-capture.tsnow gates ongetInputMode() === "overlay"like the other input handlers (#6501). Pointer-lock mouse deltas no longer rotate the camera under an open overlay.
Skills (Savi-facing)
- Removed six phantom API method names from the engine skill files (#6500). All were taught to Savi but didn't exist on
ObjectAPI/TomeCameraAPI, causing tool-call failures or silent[Tome] patchTerrain() ignored…rejections:world-composition.md:api.setSpec("...decorations")→api.patch("terrain", { decorations: … })voxel-terrain.md:api.patchTerrain({…})→api.patch("terrain", { addMaterials, addMarks, … })voxel-terrain.md:objectApi.getPointerRay(input)→objectApi.getInputRay(input)pointer-raycasting.md:getPointerRay/getAimOrigin→getInputRay/getProperty("feetPosition")+ offsetpointer-raycasting.md:objectApi.raycastPhysics(…)→objectApi.raycast(…)3d-billboard-sprites.md:objectApi.getPosition()→objectApi.getProperty("feetPosition")turrets.md:objectApi.damage(id, n)→getObjectState+patchObjectState({ health: … - n })