Skip to content

Roadmap #12 playground & polish (19 phases) - #29

Merged
AlexZ005 merged 21 commits into
mainfrom
feature/playground-polish
Jul 20, 2026
Merged

Roadmap #12 playground & polish (19 phases)#29
AlexZ005 merged 21 commits into
mainfrom
feature/playground-polish

Conversation

@AlexZ005

Copy link
Copy Markdown
Collaborator

Roadmap #12 — playground & polish (all 19 phases)

Ships feature/playground-polish (stacked on the AI branch, now on top of main after #28). One commit per phase; each shipped its own e2e suite. svelte-check ended 501/77 (under the 502/77 baseline); npm run build green.

Viewport first impression (V)

  • V-1 shadows by default (objectsGroup sweep sets cast/receiveShadow; sun casts + scene-fit frustum; env shadow-catcher disc; shadowQuality gains off).
  • V-3 curated deterministic 8-hex palette (paletteColorFor(uuid), kills 0x00ff00) + look tune.
  • V-2 N8AO into the Outline composer + LOCAL view modes (Shaded / Shaded+AO / Wireframe = scene.overrideMaterial).

Terrain (T)

  • T-1 Add ▸ Terrain (48-seg plane under the meshgeo float cap).
  • T-2 brush sculpt over the meshgeo channel (weld by quantized (x,z) columns, smooth normals, one snapshot+undo per stroke, SculptToolbar).

Physics playground (P)

  • P-A physics reworked as a flowRuntime post-tick hook; flow-animated objects become KINEMATIC bodies (spin pushes boxes — root cause of "spin removes physics" was two competing rAF loops); fixed-timestep accumulator; deviation-detected external holds; hull colliders; Inspector Physics section; SimControls + P.
  • P-B joints — replicated sceneJoints (weld / revolute + motors), object-local anchors, jointcreate/delete + getjoints handshake, joint history kind, sessions persist.

SDK input + core modules (K)

  • K-C store-only inputRuntime + claims (keys/locomotion) + api.physics.
  • K-D possess + avatar controller module (tank drive + chase camera).
  • K-E VR essentials pack (6 interactables; kind derived from replicated NAME).
  • K-F drivable car (click-claim + drive-op forwarding; initiator applies motors).

Music (M) + VR/UX (R/U)

  • M-1/M-2 scene-music singleton (latest-wins + synced-clock offset) + CC0 starter audio pack with sound-node rolloff.
  • R-1 VR pointer beam + reticle + hover shell; R-2 VR snap-angle unify + live labels; R-3 hand models + broadcast-identity custom hands.
  • U-1 ping v2 (uuid object-highlight + radial Ping); U-2 multi-select context menu (Group selection, multi prefab/delete, desktop Ungroup); U-3 toast dedupe/cap + Settings search.

Deep pre-existing bugs fixed along the way

  • Joiner-cannot-send-to-host: host closes the joiner's conn pre-approval and the close never signals over real WebRTC → adopt an open inbound conn as the send channel + wire the data dispatcher on outgoing conns + retrying restore.
  • meshgeo big-array stack overflow: positions now travel as raw Float32 bytes — a big plain-number array blew binarypack's recursion and broadcast()'s catch swallowed it, so large face-edits silently never replicated.
  • Explorer thumbnail hang: addItemFromBytes time-boxes the decorative thumbnail (a wedged GLB parse used to block storing shared bytes on receivers).

Verification

  • Per-phase e2e suites committed; npm run build green; svelte-check 501/77. Plan docs (docs/plan, docs/sdk) intentionally uncommitted / gitignored.

🤖 Generated with Claude Code

AlexZ005 and others added 21 commits July 19, 2026 15:05
- shadowDefaults.js: one objectsGroup-keyed sweep sets castShadow/
  receiveShadow on every mesh (WeakSet-guarded, no userData writes) so
  all creation paths + loaded scenes get shadows without touching each
  call site; opt-out rides userData.shadow=false
- environment.js: rig sun castShadow=true with tuned bias, shadow-camera
  frustum fit to sceneRadius each apply, and an env-shadow-catcher
  ShadowMaterial disc under ENV_ROOT (the infinite Grid shader can't
  receive shadows); catcher hidden when no sun / shadows off / passthrough
- createLight: new Directional/Spot cast by default (Point opt-in)
- lightParams: shadowQuality gains 'off' (disables renderer.shadowMap);
  applyShadowQualityCap now walks the env rig too and toggles the renderer
- Inspector Cast toggle stamps userData.shadow so opt-out survives GLTF
- e2e shadows.test.cjs (10 checks), build green, svelte-check 502/77

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- palette.js: 8-color muted palette + paletteColorFor(uuid) deterministic
  char-sum hash; creator and every receiver compute the SAME color from
  the create message's uuid, so the look is shared with zero wire bytes
  and objects cycle instead of all being bright green
- geometries.createGeometry: material color from paletteColorFor(uuid)
  (set after uuid assignment) + roughness 0.85, replacing 0x00ff00
- look tune: studio bg #363b43, hemi ground #4c525c; grid fadeDistance
  100 + lighter section color; removed the inert ignoreOverrideMaterial
  userData from the Grid (no per-object override opt-out exists in three)
- e2e palette.test.cjs (two-peer, deterministic color match), build
  green, svelte-check 502/77

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- customGeometries.terrain(size=24, segments=48): PlaneGeometry rotated
  flat (up = +Y, resting at y=0); segments clamped to 48 so a sculpted
  terrain's non-indexed meshgeo snapshot (18*seg^2 = 41,472 floats) stays
  under the 45,000 cap that terrainSculpt/T-2 will reuse
- primitivesCatalog: new Ground group with Terrain — appears in the Add
  menu, search, and spawn-at-click automatically (buildAddChildren maps
  groups generically)
- createGeometry: Terrain gets a sage material + userData.terrain flag
  (deterministic on both peers, survives toJSON + GLTF extras) that the
  T-2 Sculpt menu will key off
- e2e terrain.test.cjs (two-peer: tri count, verts, flag, size, sage,
  segment clamp), build green, svelte-check 502/77

Note: add-menu.test.cjs "search Enter" check is a pre-existing failure
(reproduces on the clean base without this change) — not T-1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- objectMenu.buildObjectMenuItems: when the clicked object is part of a
  2+ selection, set-oriented items act on the whole set with counted
  labels (Delete (3), Duplicate (3), ...); object-specific items
  (rename/edit mesh/add note/request control) stay on the clicked one.
  Falls back to selectionUuids() so both callers work unchanged; neither
  right-click path re-selects, so the multi-selection survives menu open
- new "Group selection": objectActions.groupSelection creates one empty
  group at the selection centroid and moves every member in, wrapped in
  a history batch so undo restores the flat layout in ONE step; all
  replicated via the existing group/create/move messages
- Ungroup exposed on the desktop menu for Group objects (was VR/AI only)
- Delete no longer clobbers the selection when the clicked object is in
  it; savePrefabSelection captures all members as one group prefab;
  ping.pingObjects pings the union-bounds top for a set
- e2e multiselect-menu.test.cjs (two-peer: group replicates + one-step
  undo, multi delete, multi prefab); object-delete/multi-select/prefabs
  suites still green; build green, svelte-check 502/77

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- ping messages gain an optional uuid: pingObject sends it, and receivers
  flash a highlight box around that object for the ping TTL via a new
  PingHighlights overlay (Box3Helper, emissive-independent, tracks the
  object per frame, scene-root so it never touches replicated materials)
- positional pings (Alt+click / new "Ping here" viewport-menu item) carry
  no uuid and stay point-only
- VR: pingFromController carries the hit object's uuid when the ray lands
  on one; new radial Tools > Ping entry fires an immediate ping from the
  pointer hand (partner to the existing right-stick-click ping)
- applyPing passes uuid through unchanged (no peerHandler change)
- e2e ping-highlights.test.cjs (two-peer: object ping highlights + clears,
  positional stays uuid-less, radial entry present); existing ping/ping-v2
  suites green; build green, svelte-check 502/77

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diagnose-first: vrControls already reads vrTeleportEnabled/vrSnapAngle
LIVE each frame and the VR settings panel labels are reactive, so the
behaviour did apply — but the radial snap-turn control diverged from the
panel/desktop:
- the radial cycled [15,30,45] (no Off) while the panel + desktop use
  [0,15,30,45]; unified SNAP_ANGLES to [0,15,30,45] so the radial can also
  turn snap-turn off and cycling is consistent everywhere
- the radial's static "Turn deg" label never showed the value, so a change
  read as "nothing happened"; made it a live label ("Turn: 45 / Off") that
  re-derives on the store (VRMenu now renders function labels)
- widened the menu-entry label type to string | (() => string)
- also updated vr-radial-menu.test.cjs for the U-1 Tools > Ping entry
- e2e vr-settings-immediacy.test.cjs; vr-radial-menu / vr-settings-panel
  green; build green, svelte-check 502/77

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The VR ray was a 1px THREE.Line (0x8ab4ff) that all but vanishes on-device,
and hover feedback was an emissive tint that shows nothing on
MeshBasicMaterial objects. Now:
- the ray is a tapered additive-blended cylinder beam that trims to the hit
  distance and shifts idle->hover color (light blue -> cyan)
- a hit reticle disc sits at the beam tip, scaled with distance for a
  constant angular size, shown only when the ray lands on something
- the pointed object gets a scene-root Box3Helper shell (emissive-
  INDEPENDENT, tracks world bounds per frame, never parented into
  objectsGroup so it can't leak into GLTF sync); the emissive tint stays as
  a secondary cue
- e2e vr-ray-hover.test.cjs verifies the shell headlessly (appears, tracks
  bounds, scene-root, no objectsGroup leak, hides on clear); on-device beam/
  reticle feel is the user's check. build green, svelte-check 499/77

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Sound node gains a rolloff param (0.5-4, default 1) wired to
  panner.rolloffFactor and the restart key; radius slider max 30 -> 60
  for larger ambiences
- packs: a default-list entry may carry a `zip` field (a self-describing
  .zip pack) — installDefaultPackZip fetches it and runs the existing
  kind-agnostic import path, so audio/SFX packs work; the Explorer pack
  row gains an "Install pack" action for such entries
- PACKS.md documents the audio/.zip pack format + CC0 sourcing note (the
  actual starter-audio .zip content drops at static/library/starter-audio/)
- e2e audio-pack.test.cjs: a generated audio .zip imports as an 'audio'
  Explorer item, install helper present, rolloff default = 1
- build green, svelte-check 498/77 (under baseline)

Note: sound-node.test.cjs has a pre-existing flake (flow-palette empty-hint
overlaps the Play button at the node's x:20 position) — fails identically
on the base without this change, so left as-is.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- sceneMusic.js: ONE shared background track synced latest-wins via a
  SEPARATE `music` message (not piggybacked on environment, which would
  leak music into saved presets). State {hash,name,volume,playing,
  startedAt,changedAt}; non-spatial WebAudio Source->Gain chain; playback
  phase = ((now-startedAt)/1000) % duration off the synced clock so late
  joiners converge mid-track; a 500ms reconciler decodes-by-hash (pulls via
  assetShare when missing) and recovers from a suspended autoplay context
- per-device overlay: musicLocalVolume + musicMuted (local, persisted);
  effective gain = shared * local, so muting is per-viewer
- peerHandler: `music` message case + musicState() in sendHandshake (late
  joiners adopt the current track)
- Inspector Configure Scene gains a Music section: track picker over
  Explorer audio items (pushes bytes on assign), play/stop, shared volume,
  and a "this device" local volume + mute; autoplay-blocked hint
- e2e scene-music.test.cjs (two-peer: state + phase anchor replicate, byte
  pull + decode, local mute stays local, stop replicates)
- build green, svelte-check 498/77 (under baseline)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- add n8ao (N8AOPostPass) into the existing Outline EffectComposer, before
  the outline passes; grounds objects with soft contact AO. Enabled by the
  local viewMode 'shaded-ao'; quality (halfRes + mode) follows shadowQuality
  so there's one perf knob. Desktop-only for free (the composer unmounts in
  VR/play and postprocessing doesn't run in WebXR)
- viewMode store (sceneStore, LOCAL, localStorage, default 'shaded-ao') +
  viewMode.js: 'wireframe' sets scene.overrideMaterial (local-only — a
  per-material sweep would leak into replicated materials); the infinite grid
  and shadow catcher hide in wireframe (grid gated in Scene, catcher via
  environment respecting wireframeActive)
- Inspector Configure Scene > View gains a "Viewport - this device" 3-chip
  switch (Shaded / Shaded + AO / Wireframe)
- e2e view-mode.test.cjs (default AO, wireframe override is local + hides the
  catcher, clears on exit, persists); shadows/environment/tint suites green;
  build green with the new dep, svelte-check 498/77 (under baseline)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Concrete, testable slice of the Settings/Toasts pass (the broader VISUAL
restructure is screenshot-driven and left to the design loop):
- showToast collapses duplicate plain-string toasts so a repeated message
  can't spam the stack (action toasts stay distinct); the Toasts container
  caps generic toasts at 4 with a "+N more" line
- Settings modal gains a search box that filters rows: each row carries a
  .setting-row class and a legacy-safe use: action toggles row visibility by
  text match (inner controls live in <p>, never hidden by a shown row)
- e2e settings-toasts-ux.test.cjs (dedupe vs action toasts, search
  hides/restores rows); settings-layout/labels still green; build green,
  svelte-check 498/77

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…map 12 P-A)

- physics steps as a flowRuntime POST-TICK hook (setPostTick), not its own
  rAF: deterministic per-frame order flow-pose -> kinematic feed -> step ->
  write-back; fixed-timestep accumulator (1/60, max 8 substeps) so sim time
  tracks REAL time even under throttled rAF (the old per-frame 1/30 clamp
  ran slow-motion below 30fps and made the physics suite flaky)
- flow-ANIMATED objects become KINEMATIC bodies: targets slerp-interpolated
  across substeps (a single end-pose feed gave velocity on substep 1 and
  zero after - drag/brake alternation netted no fling); a spinning slab now
  flings resting boxes; ZERO extra traffic (peers run flow deterministically)
- dynamic+animated -> dynamic wins (suspendAnimation for the run, resume +
  notifyExternalMove at stop); sleep disabled for dynamics (a kinematic
  platform never wakes a sleeping rider) with movement-gated broadcasts
- drag-during-sim: gizmo/multi-pivot holds flip the body kinematic and
  release with a clamped velocity estimate (throw); EXTERNAL writes (peer
  move streams, undo, AI edits) are detected by write-back DEVIATION
  (component-wise compare - quaternion dot reads |q|^2 and rapier's f32
  norm error pinned resting bodies) and hold the body until 250ms silence
- colliders: userData.physics.collider 'hull' opt-in (ConvexHull, scale
  baked, single-Mesh <=5k verts, box fallback); trimesh/heightfield noted
- Inspector "Physics" section on userData.physics {mode,mass,restitution,
  friction,collider} (replicates via objectParameters 'physics' branch +
  props history); flow nodes still OVERRIDE userData at sim start
- lifecycle: SimControls HUD transport (play/pause/stop/reset), ViewportMenu
  pause/reset entries, P shortcut; pause rides {type:'simulate',paused};
  reset restores beforeStates with NO history entry
- peerHandler: fixed the JOINER-CANNOT-SEND-TO-HOST bug (pre-existing) -
  the host closes the joiner's original conn pre-approval, the close is
  never signaled and reopens wedge mid-ICE; an OPEN inbound conn is now
  ADOPTED as the send channel (DataConnections are bidirectional), outgoing
  conns wire the data dispatcher (extracted handleData), and the restore
  path retries with backoff + closes stale conns first
- history exports undoStack read-only (tests)
- e2e physics-kinematic.test.cjs (14 checks, two-peer + prewarm); old
  physics.test.cjs now FULLY passes (the accumulator fixed its pre-existing
  "cube fell" flake); build green, svelte-check 502/77 baseline held

Note: scene-music byte-push + environment late-joiner failures reproduce
IDENTICALLY on the pre-P-A base (environmental drift today, not this change).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- NEW src/lib/inputRuntime.js (store-only, peerApproval pattern - imports
  nothing from peerHandler/vrControls so no cycle): held key codes with the
  shortcuts text-entry guard, getInput() {codes, axes, vrButtons} snapshot,
  onInput down/up events, window-blur clears held keys (stuck-key guard),
  registerBindings(moduleId, ...) lists display-only entries in Settings >
  Shortcuts under a "Module: <id>" group
- input CLAIMS pause the host's own consumers while a module drives:
  'keys' gates PointerLockControls WASD + the editor fly-navigation;
  'locomotion' gates VR left-stick locomotion (VRControls.svelte)
- vrControls publishes each hand's stick axes + trigger/squeeze into
  inputRuntime per frame (the safe import direction)
- moduleSDK: api.registerBindings/input/onInput/claimInput/releaseInput +
  api.physics {simulating, isInitiator, applyImpulse} via PRIMED dynamic
  imports (static edges would close flowRuntime->moduleSDK / physics->
  flowRuntime cycles - the vite-dev TDZ trap); physics gains isInitiator()
  + applyImpulse(uuid, [x,y,z]) (initiator-only)
- MODULES.md documents the input API + the forward-inputs-to-initiator
  recipe as the blessed authoritative pattern
- __stores hook gains inputRuntime + shortcutsRegistry
- e2e sdk-input.test.cjs (8 checks incl. behavioral claim gating of the
  editor fly); build green, svelte-check 502/77 held

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- NEW src/lib/joints.js: replicated sceneJoints defs {id, a, b, kind
  fixed|revolute, anchorA/B (OBJECT-local at attach), axisA, motor} - the
  annotations pattern (a joint references two uuids, so it can't ride
  either object's userData). jointcreate/jointdelete apply-local + send;
  late joiners pull via getjoints -> joints (retry-until-open reply);
  history kind 'joint' (presence-style, one-step undo/redo that also
  replicates); sender-side cascade when objects are deleted; sessions
  serialize/restore the defs (restore re-broadcasts each jointcreate)
- physics.js builds rapier impulse joints at startSimulation: object-local
  anchors -> world -> BODY-local (box bodies start identity-rotated so a
  world axis is valid in both frames; hull bodies use the object frame);
  fixed bodies are joinable too (weld a box to scenery pins it);
  setJointMotor(jointId, vel, maxForce) drives a revolute (initiator-only)
- object context menu: exactly two selected -> Physics > Weld together /
  Hinge (X/Y/Z axis of the FIRST object, anchored at the second) ; Detach
  joints appears when any joint touches the selection
- api.physics gains setJointMotor + joints() (module SDK)
- e2e physics-joints.test.cjs (14 checks: replication, undo/redo, menu,
  welded pair keeps its 1.5 offset through a fall on BOTH peers, motorized
  hinge spins its wheel, detach); physics-kinematic + physics suites still
  green; build green, svelte-check 502/77 held

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- NEW src/lib/possess.js: possess(uuid, {camera chase|orbit|none, speed,
  turnSpeed}) drives any object with tank controls (W/S along its facing,
  A/D turn, arrows too, VR left stick via the K-C axes) on a dedicated rAF.
  Possessing SELECTS the object (selection = lock, peers see the usual
  highlight and refusal applies for peer-locked objects), suspends its flow
  effects, claims the 'keys' + 'locomotion' input scopes (editor fly, play
  WASD and VR stick locomotion pause), and broadcasts throttled moves at
  the multiTransform/physics 10Hz rate. Release (Esc / release()) restores
  the camera, records ONE transform undo entry, sends a final move and
  re-bases the animation (notifyExternalMove). Mid-sim the move stream
  rides P-A's external-hold, so a possessed dynamic body follows
  kinematically. Entering VR auto-releases.
- chase camera: eased third-person follow behind the facing (OrbitControls
  disabled for the ride, restored + retargeted on release); orbit mode just
  retargets the controls each frame
- NEW src/modules/avatar/module.js (core set): registerBindings entries,
  a "Possess selected object" menu, VR Edit-ring Possess entry
- moduleSDK: api.possess/releasePossess/selectedUuid (primed dynamic import
  - a static edge would cycle via objectActions -> flowRuntime -> moduleSDK);
  MODULES.md documents it
- e2e possess.test.cjs (11 checks, two-peer: claims, lock on B, drive/turn,
  replication, Esc + one-step undo); build green, svelte-check 501/77

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- NEW src/modules/essentials/module.js (core set, SDK dogfood - no host
  changes): six clickable interactables spawned as REPLICATED primitives
  through the normal /create path (they persist, sync and undo like any
  object); the interactable kind derives from the object NAME the create
  command assigns, deterministic on every peer
- Button + Target: click fires the wired flow graph via the already-
  replicated nodetrigger (fireObjectClick) + a deterministic press-squash
  tween broadcast as a tiny module op
- Lever: toggles with a base-pivot tilt, module op + late-joiner state
  (registerStateSync {levers}); also pulses the flow graph
- Spawner pad: 1s-cooldown replicated cube spawn 1m above the pad
- Teleport pad: strictly LOCAL editor-camera jump (userData.essentialLink
  twin honored); Sound tile: spatial synth chime on every peer (op)
- "Spawn essentials demo row" menu lines the set up for a tour
- works with desktop clicks AND the VR trigger (same moduleClickHandlers
  dispatch); on-device VR feel is the user's manual check
- e2e essentials.test.cjs (9 checks, two-peer: create replication, counter
  pulse on both, lever tilt on both, spawner replication); build green,
  svelte-check 501/77

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- peerHandStyle gains 'model': rounded CAPSULE hands from the same 24
  broadcast bones with per-bone radii (thick palm metacarpals, thin
  fingertips - handModelSegments in vrControls); desktop select + the VR
  settings row cycle Model/Hands/Spheres
- NEW src/lib/handModels.js: a user's chosen hand GLB is IDENTITY (the
  avatar-photo precedent) - the content hash rides a tiny `handmodel`
  message + the handshake, peers pull the bytes via assetShare and parse
  them into a render cache; Player renders the model RIGIDLY at the
  broadcast wrist pose (the hand group's pos/rot IS the wrist - no
  retargeting needed for v1; articulated retarget stays in the backlog),
  left hand mirrored; fallback chain model -> viewer's style while bytes
  load or on parse failure; peer entry dropped on disconnect
- Settings VR gains "My hand model" (Explorer object items; bytes push on
  choose); explorer.addItemFromBytes now TIME-BOXES the decorative
  thumbnail (Promise.race 4s) - a slow/wedged GLB parse used to silently
  block storing SHARED bytes on the receiving peer
- e2e hand-models.test.cjs (8 checks: capsule segments/radii, style
  option, hash replication, bytes arrive, pipeline parses to cache,
  clearing falls back). On-device look is the user's manual check.
- build green, svelte-check 501/77 (under baseline)

Note: this machine's headless pages run ~4fps with ~1.8x-slow timers under
load (both pages equally - not tab throttling; flags already set). The
scene-music/environment suite flakes earlier trace to the same saturation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- NEW src/lib/terrainSculpt.js: raise/lower/smooth/flatten brush on the
  Terrain primitive through the EXISTING meshgeo channel - live strokes
  stream throttled previews (~5/s) and commit ONE snapshot + ONE undo
  entry per stroke (commitMeshGeoSnapshot). The non-indexed-split trap is
  dissolved by welding on QUANTIZED (x,z) COLUMNS (sculpt only moves Y, so
  the map stays valid across strokes/undo/remote swaps and is REBUILT in
  the applyMeshGeo hook - golden rule 6's stale-cache class); smooth uses
  precomputed 1.5-cell neighbor lists; entering sculpt selects (= locks)
  the terrain so a second sculptor is refused; first entry converts the
  indexed plane to non-indexed and syncs the representation (no history)
- applyMeshGeo: terrain normals become WELDED-SMOOTH (average across
  position-welded vertices - deterministic per peer, nothing on the wire)
- WIRE-FORMAT FIX for every meshgeo sender: positions now travel as raw
  Float32 BYTES (ArrayBuffer). A 48-seg terrain snapshot as a plain number
  array (41k elements) blew binarypack's recursion ("Maximum call stack
  size exceeded") and broadcast()'s catch swallowed it - big face-edit
  results silently never replicated. applyMeshGeo normalizes plain array /
  ArrayBuffer / typed-array VIEW (slice exact bytes); history replays and
  all existing suites stay compatible (~half the wire size as a bonus)
- Scene pointer wiring (drawMode pattern): drag brushes, cursor ring
  (scene-root) tracks the hit; SculptToolbar pill (ops + radius/strength,
  Esc/Done exits); objectMenu gains "Sculpt terrain" on Terrain objects
- e2e terrain-sculpt.test.cjs (10 checks: lock refusal, hill, NO tearing,
  byte-identical replication, smooth normals, one-step undo on both);
  desktop-face-gizmo / faces-toolbar / vr-face-polygon / vr-mesh-undo all
  still green; build green, svelte-check 501/77

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- NEW src/modules/car/module.js (core set): "Car: spawn demo car" builds a
  REPLICATED assembly - Carbody primitive (mass 20) + 4 hull-collider
  cylinder wheels (mass 2, friction 1.4) hinged to the body's local X axle
  via P-B's motorized revolute joints; everything persists, syncs and
  undoes like normal objects
- AUTHORITATIVE drive (golden rule 8, pong's paddle pattern): click the
  body to CLAIM it (kind derives from the replicated NAME, deterministic
  per peer; a second driver is refused; claims sync to late joiners and
  free on disconnect), the driver forwards {op:'drive', throttle, steer}
  at ~20Hz from WASD / the VR stick (K-C input), and ONLY the physics
  initiator applies wheel motors - differential/tank steering v1 (side
  from the attach-time body-local anchor's X sign); driver != initiator
  adds ~150-250ms input latency, accepted + documented
- MODULES.md: setJointMotor/joints() in the api.physics reference + the
  car as the worked authoritative-recipe example
- e2e car-module.test.cjs (8 checks, two-peer + prewarm: spawn + 4 hinges
  replicate, claim + refusal, holding W drives the car ~14m with a running
  sim, motion replicates); physics-kinematic + physics-joints still green;
  build green, svelte-check 501/77

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- CLAUDE.md: architecture map entries for physics/joints/inputRuntime/
  possess/handModels/terrainSculpt/sceneMusic/shadowDefaults/palette/
  viewMode + new core modules (avatar/essentials/car) and HUD overlays;
  raw-bytes meshgeo wire format + binarypack trap in golden rule 6;
  adopted-inbound-conn note in rule 9; new gotchas (connect-dance fix,
  rapier quaternion-compare/kinematic-wake/slerp-substep/accumulator
  traps, thumbnail time-box, import-cycle map); Module SDK section gains
  the 12 api additions (input/physics/possess, kind-from-NAME rule);
  status: roadmap 12 SHIPPED, baseline 501/77
- e2e-verify skill: __stores list refreshed (+viewModeCtl-vs-viewMode
  naming trap); B-to-A messaging works + freshReload id-reread pattern;
  real-time physics note; machine-saturation symptoms and cures;
  binarypack large-array trap; pre-existing flakes list + git-stash
  proof technique; baseline 501/77
- peer-feature skill: raw-bytes rule for large numeric payloads;
  joints/sceneMusic/handModels reference patterns (own singleton
  message, annotations-style def list, identity-hash + assetShare);
  car = blessed input-forwarding authoritative recipe; input claims
  are LOCAL; SDK api surface additions with worked examples

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@AlexZ005
AlexZ005 merged commit f0fae02 into main Jul 20, 2026
@AlexZ005
AlexZ005 deleted the feature/playground-polish branch August 5, 2026 20:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant