Skip to content

Release 1.45.0 — Build in the headset (roadmap 52) - #482

Merged
AlexZ005 merged 39 commits into
mainfrom
feat/52-int
Oct 26, 2026
Merged

AlexZ005 merged 39 commits into
mainfrom
feat/52-int

Conversation

@AlexZ005

Copy link
Copy Markdown
Collaborator

Release 1.45.0 — Build in the headset: measure, arrange, paint, scatter, browse assets and shrink the world 🥽 (roadmap 52).

The 52 union (feat/52): #422 52-measure (VR measure + Arrange), #432 52-paint (controller Paint/Scatter brushes),
#423 52-browser (wrist Asset Browser, place-drag, hand tracking, universal world grab + detents), plus fix-batch-1 #445
(On Impact any side, sockets, clock origin, Sequence card) and the orchestrator add #478 (net-drop heal: our peer id
outlives a signaling drop, so a joiner no longer comes back View-only). Merged on v1.44.0 + origin/deps. Integration on
top (52-int):

Proof: gates (build, vitest 2842, svelte-check ratchet 287/39, tokens/storage/zindex, scene-lint 16 scenes 0 errors),
battery vs the v1.44.0 baseline (18 new reds, each A/B'd against pristine v1.44.0: lock-search was ours and is fixed;
the rest are red on both sides or load flakes), preview-1-45 proof (162 rows + L3 scene-load) and #478 on
preview-1-45b: net-drop vs production 1.44.0 alternating (production shows the View-only signature, the fix does
not), 25/0, 25/0, 24/1 on a quiet slot. Known and pre-existing on production 1.44.0 (not fixed here): the CSP blocks the
basis transcoder's eval (ktx2-twins, export-presets); export-zip's grep matches the CSP URL inside the bundled env
object; net-handshake 'C received the scene' under load.

🤖 Generated with Claude Code

AlexZ005 and others added 30 commits October 23, 2026 00:05
…ns, readable troika labels, kept as 48 B2 items

- Tools > Measure (radial) arms a trigger tool: every press puts a point down, snapped through the SAME snap-engine
  search as the desktop (vertex / edge / face, the measure flags) inside a 3 deg cone around the laser - a virtual
  camera down the ray turns the engine's px radius into an angle; the desktop bias is scaled to the cone
  (unscaled, an edge seen edge-on beat a corner 1.2 deg off - measured on IWER). A raw point lands on the grid
  when grid snapping is on.
- chains with lengths / angles / total; a double tap (or the Measure ring's Keep) keeps the chain as a 48 B2
  measurement ITEM (replicated, undoable, saved); the radial opens on the Measure ring while the tool is up:
  Keep / Undo point / Discard / Snap / Done (the sculpt ring's rule)
- Selected > Edit measurement: a press near a point (by angle) picks it up, the next puts it down snapped, a press
  elsewhere adds one - measure.js's own edit step (measureEditStep, shared with the desktop click): one undo entry
  and one message per placed point. Edit mesh is no longer offered for a measurement item.
- labels: VRMeasureLabels = troika text on a plate, facing the viewer, a constant ANGLE tall (0.03 rad) so they
  read the same near and far; measureCore.layoutLabels keeps them apart (every label is the same px tall).
  MeasureOverlay now draws items in a headset too.
- hands: a pinch while measuring says "pick up a controller" and adds nothing (52 input rule)
- Interact / leaving the headset / another tool ends it (a chain of 2+ points is kept, the desktop check rule)
- tests: vitest vrMeasureCore (14); IWER suite vr-measure-iwer (T M L K N G E H F rows, two peers);
  vr-radial-menu's Tools list gains Measure. Frame cost on IWER: 0.16 ms/frame (hover search 0.24 ms at 30 Hz,
  labels 0.06 ms), 20 draw calls for a 4-point chain.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…- the desktop's results, one undo, peers see it

- Selected > Arrange (2+ objects): ONE ring, the radial's two-level rule - Axis (cycles X / Y / Z, value line),
  Align min / centre / max, Even centres / Even gaps (3+, greyed below), Drop to ground, Align to (Selection /
  Active, the Inspector's switch). Labels carry the axis ("Align X min").
- every sector is DATA (vr/arrangeCore.js, a leaf): sectorOp + arrangeCall name the SAME align.js call, with the
  same arguments, that the multi-select Inspector's Arrange buttons and the object menu's Transform rows make -
  so the move, the ONE transformSet undo entry and the replication (the move path) are the desktop's own.
- unit-level parity (tests/unit/vrArrangeParity, 23): the table per desktop control, and the REAL align.js run on
  two identical scenes, one through the desktop call and one through the ring sector, lands every object in the
  same place with one undo entry each (counterfactual: mapping gaps -> centres goes red on 4 checks).
- IWER row A: Selected > Arrange > Align X min picked with the stick (two peers), ONE Undo restores the set on
  both, X even gaps, Drop to ground on A and B.
- icons: align-start/center/end-vertical, align-horizontal-space-around, arrow-down-to-line in the VR icon table.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s f-vr-place)

- placeDrag RAY mode: beginRayPlace / updateRayPlace / commitRayPlace lay the ghost out in the
  objectsGroup's frame, so a moved or shrunk world rig (miniature, dollhouse) places at the
  scene-unit spot under the laser; desktop pointer path unchanged (frame = identity)
- placeGhost.setGhostFrame + a frame for the pending drop boxes; explorerDrop.rayHit +
  placeFromGhost(items, placements, {frame})
- prefabs get measured dims (their ghost was the 1 m unknown box and they landed half sunk)
- vr/placeRay.js: the VR glue — pointer laser per frame, stick flick turns 15 deg, the trigger
  places (stays armed, keeps the turn), a trigger-drag drops on release, a grip cancels, no ghost
  while the laser is on a panel
- the VR prefabs window's private ghost moved onto the shared one
- hooks: registerGripHooks (a feature may take a squeeze before anything grabs); the select
  swallow learns which controller (IWER fires select BEFORE selectstart on press)
- suites: vr-asset-browser-iwer row G (new, IWER), vr-prefabs taught the shared ghost

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t in the desktop icon map - iconLint)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… the rings with SHOTS=<dir> (vrEyes)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…CC0, your library)

- vr/assetBrowser.js + assetBrowserDraw.js: one canvas on one plane (one draw call) on the
  OFF hand's wrist, facing the head; Add > Assets toggles it; steps aside while the radial
  is open on that hand; one-handed it opens pinned in front
- tabs Packs (drill into a pack) / Free CC0 (Poly Haven models: a card downloads into the
  Library through the Explorer import, the card shows the progress, then it rides the laser)
  / My library (models + prefabs); search via the VR keyboard, filtering as you type;
  pages by the buttons or the stick while the laser is on the panel
- a trigger-DRAG from a card drops the item where you let go (once); a CLICK arms it (the
  next trigger places, stays armed); the status line says what the trigger / grip do now
- a GRIP lifts the panel off the wrist: let go in the air = pinned 1.6x, near the wrist =
  back on it; the Pin / To wrist button does both
- a trigger on the panel is the panel's: never a select of what is behind it, never a
  placement (also under IWER's select-before-selectstart order)
- suite vr-asset-browser-iwer row B (55 checks with G), evidence panel shots

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… precise tools ask for a controller

- a pinch IS the trigger: the panel and the laser ghost read the session's select events,
  so a pinch presses a card, a pinch held while the hand moves drops the item where it lets
  go, a pinch-click arms it and the next pinch places; the panel speaks hands ("pinch to
  place", "let go over the world")
- vr/handInput.js: isTrackedHand + controllerRequired(tool, slot) - a tracked hand gets
  "Pick up a controller to <tool>" on a small chip at that hand (one draw call) and the
  tool stands down; wired into the VR sculpt brush, face edit and vertex edit; exported
  from the vrControls facade for 52-measure / 52-paint
- the placing hint outranks a passing info line on the panel
- suite vr-asset-browser-iwer row H (IWER tracked hands; 70 checks with G + B)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cost on the Quest 3 profile

- the panel compares a few inputs per frame and builds its card list only when one moved
  (the Free tab is 400 rows; it used to be walked every frame); one laser raycast per slot
- frame-hook and repaint counters on the panel and the laser ghost
- suite row P (IWER metaQuest3, CPU x4): panel 0.59 ms/frame, a repaint 1.5 ms (only when
  the view changes), the ghost 1.17 ms/frame, +1 draw call for the panel, +2 for a box ghost

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…:50, Back to 1:1

- both grips = the world in Edit whatever the rays hit or the hands are inside (Open Brush / Gravity
  Sketch / ShapesXR); a first-hand grab is TENTATIVE (no selection, nothing sent) until it moves > 2 cm
  or 250 ms pass; an unmoved first grab joined by the second grip goes back to its start pose with no
  undo entry and the world grab starts
- two-hand object scale (+ rotate with the hands' axis) only when the first hand already MOVED the
  object; a second hand INSIDE another small object takes that one instead (33 G4 by touch)
- grip-by-contact skips objects that enclose the headset, are larger than 2 m, are terrain or are
  locked (vrGrip.contactGrabAllowed, smallest containing object wins); terrain never moves by grip
- Settings > VR "Both grips scale the world: Always / Only in empty air" (vrBothGripsWorld)
- detents 1:50 1:10 1:5 1:2 1:1 2x 5x 10x (WORLD_SCALE_MIN 0.05 -> 0.02), readout "1:10" below life size,
  the 53 comfort vignette hears the world gesture's scale/yaw rate, radial root "Back to 1:1" while the
  world is off life size
- suites: vr-universal-world-grab (new, IWER; red on the 51 union in 1/3/4/5), vr-grips-by-mode,
  vr-two-hand-grab, vr-world-grab adapted; unit vrGrip / vrWorldSnap / gameShell

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…iniature too

- Sculpt ring gains Paint ▸ (layers 1-4, terrain only) and Scatter ▸ (paint/erase/rotate/scale/relax + Source ▸)
- Source ▸ wheel: current + in-use sources + primitives; stick Y = density while scatter-painting
- haptic pulse on stroke start; tracked hands are asked to pick up a controller
- brush label reads layer/tool, radius and strength or density; ring scales with the world (miniature)
- wheel picks apply synchronously (primed sculpt module), no late dynamic import
- e2e: vr-brushes-iwer (wheels, strokes, undo, miniature 1:10 placement), vr-brushes-perf (Quest profile, 2k instances)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- several callbacks share one XR frame and most are empty, so per-callback medians read 0 ms
- lane run (--local, Quest profile, 2k instances): paint +0.2 ms, scatter +0.3 ms, rotate +3.0 ms median over idle

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	src/lib/vr/sculpt.js
#	src/lib/vrRadialMenu.js
…d ring (9 → 10)

52-browser registers Add ▸ Assets from its mount; the suite was not in that lane's battery.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…anding

The impact rule was "pre-step downward velocity >= 1.2 m/s", so a ball thrown
sideways at a wall or a target never fired On Impact. A contact now also counts
when the two bodies close along the contact normal (rapier's manifold) at
>= 1.2 m/s. The downward test stays as its own channel with its own cooldown:
On Impact gains `direction: any | down` — new nodes are seeded 'any', a node
saved without the key reads 'down' (param `legacy`, honoured by the Flow panel),
so old scenes fire exactly as before; emitters' "On impact" keep the landing rule.

impactSpeed.js is the leaf (closingSpeed / impactStrengthFor), unit-tested;
e2e flow-impact-any (red on 4cd3daf: the 'any' nodes never fire on the
wall/target hit).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… 150 % on the folded N6

Profile circle (53-int Q5): it opened on focus, so Tab walked the whole panel.
Now the ARIA menu-button pattern: Tab passes it, Enter / Space / Down open it with
focus on the first row, Escape closes and returns focus to the circle.

Compact header (53-int Q4): at a large UI scale a WindowChrome header wrapped
before it ellipsized (flex lines break on hypothetical sizes). The title enters
line-breaking at its minimum, header buttons stop growing at 120 %, the panel
icon and the back label's text step aside (still the accessible name). The
object list header's shed thresholds follow the scale and its search narrows.
Settings / Inspector / object list headers are one row at 150 % folded.

Rows: a11y-axe (profile menu keys + axe on the open panel), a11y-scale (three
one-row header checks on the folded N6 at 150 %).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…le, HUD Rows, HUD Set Input; HUD Timer honours start

Orchestrator add (docs-1 Q1). flowSockets typed these inputs and the runtime
read them, but the cards drew no socket, so they could not be wired by hand
(hudActions' own 'trigger' wires had nowhere to land): Set Game State
(trigger) and Set Variable (trigger, value) gain catalog `inputs`; HudNode
draws HUD Rows (trigger, text) and HUD Set Input (trigger, value, options).
HUD Timer drew `start` but triggerStampFor only counted an unnamed/`trigger`
wire — it now takes `start` as an alias.

The audit (scripts/graph-audit.cjs) gains an undrawn-input guard for the two
generic cards; it found Device Param (value) and Note Trigger (trigger, note,
velocity) with the same gap, fixed here too. Unit guard red on 257e8f6 with
exactly those six; e2e flow-sockets-wire wires each by mouse and checks the
effect (11 red on 257e8f6).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…r start, not the clock's origin

docs-1 CORE BUGS (verified): the rotor's ramp and an autostart HUD Timer both
measured from t = 0 of the clock they are handed, and the synced clock is
seconds since UTC midnight — every wheel was past its spin-up from the first
frame (full speed at once) and an autostart timer read 0 the moment it
appeared. The rotor now counts from the moment this peer saw it switch on
(off -> rests and spins up again from rest); an autostart timer from when
this peer first saw it. Both re-base when the clock goes back (midnight, a
session clock adopted on join) instead of freezing.

e2e flow-clock-origin: 4 red on 4cd3daf (rotor 4.70 of 5.22 rad/s in its
first half-second; timer 0), branch: 0.55 vs 4.67 rad/s, timer 59 -> 55.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each checked against the node's catalog entry and runtime before rewriting:
Sequence fires all four steps from one pulse (not one per pulse, no wrap);
Move Input is WASD x/y only (no stick, no jump); All Players is a group gate,
not an iterator; Character Controller sets the player's own movement model;
Move Speed writes on `set`; Camera Follow has no distance/height; Gamepad Axis
is the two sticks (L2/R2 are buttons); HUD Screen acts per pulse (show / hide
/ toggle); HUD List names the element, title and row cap; HUD Rows and
Leaderboard write a HUD List element; HUD Timer a HUD Timer element; HUD Input
also reads sliders, toggles, dropdowns and tabs.

These lines feed the palette tooltip, the info pane and the docs-site node
pages. Unit guard tests/unit/nodeDocsClaims.test.js: 6/6 red on 4cd3daf.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rd card width

docs-1: at the 150 px card a "wait" word on every row plus a 48 px step label
left each field's input 12 px wide — the values read as empty boxes. "wait (s)"
is now said once as the column heading (each field keeps an accessible name,
"wait before step N (seconds)"), the step label is a fixed 36 px so the field
does not jump when the ● lights, and the field takes the rest: 68 px, "10.50"
fits.

e2e sequence-card: base 4cd3daf 12 px fields, all four values clipped.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The two pose tests called applyRotor once at t = 1 and expected the angle from
the clock's origin; they now switch the wheel on at t = 0 first. New: a wheel
switched on at t = 51 300 (mid-day on the synced clock) spins up from rest,
off -> on spins up again, and a clock that goes back re-bases — red on
4cd3daf's motionNodes.js.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…1 s back)

Follow-up to 10111ec: a frame of jitter before the switch-on time no longer
moves the start; only a jump back of more than a second (midnight, a session
clock adopted on join) re-bases. Unit test covers the jitter case.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1.44.0

# Conflicts:
#	src/lib/vrRadialMenu.js
#	tests/e2e/vr-radial-menu.test.cjs
- CHANGELOG 1.45.0 "Build in the headset" (52 V1-V5 + hands, fix-batch-1 #445 fixes). 1.44.0 went to
  roadmap 60, so roadmap 52 releases as the next minor.
- tests/e2e/battery.json (written with battery.cjs's own writer, round-trip checked): suites
  vr-measure-iwer, vr-asset-browser-iwer, vr-universal-world-grab, vr-brushes-iwer (2x the 52-union
  desk times), flow-impact-any, flow-sockets-wire, flow-clock-origin, sequence-card (120 s, unmeasured);
  vr-brushes-perf excluded as a frame-rate measurement (--exclusive by hand). The batteryDesk coverage
  test was red on the union for exactly these nine.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
AlexZ005 and others added 9 commits October 26, 2026 09:35
…ny drop

The suite's shim now records, per page: every RTCPeerConnection's real ICE /
connection state, every data channel's open/close with sent / delivered / held
counts (a peerjs channel's label is its conn id), the selected ICE pair's
candidate types, each DataConnection's sends and 'data' events by message type,
and what the send / receive / role gates answer about the other peer. Printed
with NETDROP_TRACE=1, or when B's edit never reaches A. A new premise checks that
B's edit reaches A before any drop (the lock alone is a protocol-floor message).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
peerjs Peer.disconnect() - every drop of the signaling socket - sets peer.id to
null until the socket re-registers. The cloud roles plugin reads its own id from
getPeers().peer.id, so for the length of a drop it counted itself alone, wiped its
role map and the host it joined; after the heal a joiner read itself as a VIEWER
(the host still had it as an editor and, its open set unchanged, never re-sent the
map), and core's creation gate kept every edit local. Only deployed builds load
the plugin, which is why net-drop was green on a dev server and red on production
1.43.2 / 1.44.0 and every preview.

peerDrop.keepPeerId gives the Peer an own id getter: the live id, else the id it
was created with. peerjs bookkeeping (_id, _lastServerId, open, disconnected) is
untouched. reconnectDial keeps its old no-dial-while-disconnected behaviour.

net-drop: own id readable at the cut (red on v1.44.0), roles unchanged by the heal
(with a plugin). Unit: keepPeerId against the real peerjs Peer.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… the world' row

Re-recorded on the union (729ee67 snapshot) and checked to differ by exactly that row in 5 queries
(Scale, xr, zoom, vr, ai) plus the VR section on 'zoom'; verify 11/0. Base v1.44.0 is green with
the old lock, the union was red 6 (A/B ab1).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…7/39)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…h E2E_CLOUD_PLUGIN for the deployed condition

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… conn already closed

A graceful ws.close() waits for the server's close frame, which offline emulation
held for 3 s+ on a built app: B's socket sat CLOSING with peer.open still true, so
the QUIET premise 'signaling down at the cut' was red on production and on any
build, and a 3 s cut window let A's conn close before HARD reached it (script
error). The shim now hands peerjs the 1006 close a dropped socket fires.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s too (room labels, recent toasts)

A single sample at the cut raced peerjs's own retries, which put the id back
between failed reconnects (v1.44.0 read null at 4.7 s / 5.3 s when sampled).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…outlives a signaling drop

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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