Roadmap #12 playground & polish (19 phases) - #29
Merged
Conversation
- 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Roadmap #12 — playground & polish (all 19 phases)
Ships
feature/playground-polish(stacked on the AI branch, now on top ofmainafter #28). One commit per phase; each shipped its own e2e suite. svelte-check ended 501/77 (under the 502/77 baseline);npm run buildgreen.Viewport first impression (V)
shadowQualitygainsoff).paletteColorFor(uuid), kills0x00ff00) + look tune.scene.overrideMaterial).Terrain (T)
Physics playground (P)
P.sceneJoints(weld / revolute + motors), object-local anchors,jointcreate/delete+getjointshandshake,jointhistory kind, sessions persist.SDK input + core modules (K)
inputRuntime+ claims (keys/locomotion) +api.physics.Music (M) + VR/UX (R/U)
Deep pre-existing bugs fixed along the way
broadcast()'s catch swallowed it, so large face-edits silently never replicated.addItemFromBytestime-boxes the decorative thumbnail (a wedged GLB parse used to block storing shared bytes on receivers).Verification
npm run buildgreen; svelte-check 501/77. Plan docs (docs/plan,docs/sdk) intentionally uncommitted / gitignored.🤖 Generated with Claude Code