Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
963ee43
[feat] 52 V1: measure in VR - laser points with the 47 snapping, chai…
AlexZ005 Oct 22, 2026
483e403
[feat] 52 V2: align / distribute / drop to ground from the VR radial …
AlexZ005 Oct 22, 2026
d10042d
[feat] 52-browser P1: the drag-to-place ghost on the VR laser (absorb…
AlexZ005 Oct 22, 2026
87b1b80
[fix] 52 V1: the Measure ring's Done uses circle-check (log-out is no…
AlexZ005 Oct 22, 2026
22d9cca
[test] 52: vr-measure-iwer writes headset eye shots of the labels and…
AlexZ005 Oct 22, 2026
282b467
[feat] 52-browser P2: the Asset Browser on the VR wrist (packs, free …
AlexZ005 Oct 22, 2026
8c75f6a
[feat] 52-browser P3: light hand tracking - pinch browses and places,…
AlexZ005 Oct 22, 2026
f3ce2f1
[perf] 52-browser P4: the wrist panel repaints only on change; frame …
AlexZ005 Oct 22, 2026
74d2073
[feat] 52 V5: universal two-grip world grab (miniature), detents to 1…
AlexZ005 Oct 23, 2026
22e92b1
[feat] 52 V3: terrain paint + scatter brushes on the controller, in m…
AlexZ005 Oct 23, 2026
118a4ca
[test] 52 V3 perf: time XR callbacks summed per frame, not one by one
AlexZ005 Oct 23, 2026
23b7430
Merge tag v1.42.0 into the 52 union
AlexZ005 Oct 23, 2026
389ce9f
Merge remote-tracking branch 'origin/feat/52-measure' into feat/52-union
AlexZ005 Oct 23, 2026
7514dfd
Merge remote-tracking branch 'origin/feat/52-browser' into feat/52-union
AlexZ005 Oct 23, 2026
6ebc630
Merge remote-tracking branch 'origin/feat/52-paint' into feat/52-union
AlexZ005 Oct 23, 2026
8fbad0e
Merge remote-tracking branch 'origin/deps' into feat/52-union
AlexZ005 Oct 23, 2026
4cd3daf
[test] 52 union: vr-radial-menu counts Add ▸ Assets (52 V4) in the Ad…
AlexZ005 Oct 23, 2026
b727168
[physics] #14 On Impact fires on a hit from any side, not only on a l…
AlexZ005 Oct 24, 2026
257e8f6
[a11y] #23 profile menu opens on Enter/Space only; compact headers at…
AlexZ005 Oct 24, 2026
677d369
[flow] draw the sockets the runtime reads: Set Game State, Set Variab…
AlexZ005 Oct 24, 2026
10111ec
[flow] Rotate / Motor spin-up and HUD Timer autostart count from thei…
AlexZ005 Oct 25, 2026
f42422d
[docs] correct thirteen one-line node descriptions (docs-1)
AlexZ005 Oct 25, 2026
d90b960
[flow] Sequence card: the wait fields show their values at the standa…
AlexZ005 Oct 25, 2026
0b5a1b1
[test] Rotate / Motor unit tests follow switch-on time (10111ec3)
AlexZ005 Oct 25, 2026
0522e70
[flow] rotor / autostart timer: re-base only on a real clock jump (> …
AlexZ005 Oct 25, 2026
caad21a
Merge main (v1.44.0) into deps
AlexZ005 Oct 25, 2026
dad9d73
Merge tag 'v1.44.0' into feat/52-int
AlexZ005 Oct 26, 2026
8cccf49
Merge remote-tracking branch 'origin/feat/52-fix-batch-1' into feat/5…
AlexZ005 Oct 26, 2026
cedac33
Merge remote-tracking branch 'origin/deps' into feat/52-int
AlexZ005 Oct 26, 2026
729ee67
[docs] CHANGELOG 1.45.0 + battery: the 52 and fix-batch-1 suites
AlexZ005 Oct 26, 2026
4fb3dfc
net-drop: wire + app trace for deployed builds, B->A premise before a…
AlexZ005 Oct 26, 2026
8a2148b
net: our peer id outlives a signaling drop (deployed net-drop B->A loss)
AlexZ005 Oct 26, 2026
2974199
[test] lock-search follows 52 V5: the Settings ▸ VR 'Both grips scale…
AlexZ005 Oct 26, 2026
51630e4
peerDrop test: type the started list (svelte-check ratchet back to 28…
AlexZ005 Oct 26, 2026
64292d4
CLAUDE.md: peer.id survives a signaling drop; run two-peer suites wit…
AlexZ005 Oct 26, 2026
964049e
net-drop: the cut ends the signaling socket at once; HARD tolerates a…
AlexZ005 Oct 26, 2026
7cbfd6d
net-drop: own id sampled through the whole drop; trace on an A->B mis…
AlexZ005 Oct 26, 2026
1065f8b
merge #478 fix/net-drop-heal into the 52 union (1.45.0): our peer id …
AlexZ005 Oct 26, 2026
fb2c9a0
[docs] CHANGELOG 1.45.0: the net-drop heal fix (#478)
AlexZ005 Oct 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,58 @@
per release, newest first. HTML comments like this one are stripped before
rendering, so maintainer notes stay out of the user-facing window. -->

## 1.45.0 — Build in the headset: measure, arrange, paint, scatter, browse assets and shrink the world 🥽

Everything you need to build a scene now works with the headset on: measure and line things up, paint terrain and
scatter, pull models off your wrist and drop them in, and scale the whole world down to a miniature to work from above.

### 📏 Measure and arrange

- **Tools ▸ Measure**: tap points with the laser — they snap to corners, edges and faces (and to the grid when grid
snapping is on). Chains of points, labels that turn to face you and stay readable, a double tap (or **Keep**) makes it
a measurement item like on the desktop; **Selected ▸ Edit measurement** moves its points.
- **Selected ▸ Arrange** for two or more objects: pick the axis, then Align min / centre / max, Even centres / gaps,
Drop to ground or Align to — the same as the desktop, one Undo, everyone sees it.

### 🌍 Shrink the world

- In Edit, **both grips always grab the world**, wherever you point: pull your hands together to shrink it down to a
**1:50 miniature**, apart to grow it, twist to turn it. It clicks (with a buzz) at 1:50 · 1:10 · 1:5 · 1:2 · 1:1 · 2× · 5×
· 10×, a readout shows the scale, and **Back to 1:1** sits on the quick menu while the world is off life size. The
comfort vignette closes in while it scales or turns. Only you see it.
- To scale or turn an **object** with two hands, grab it and move it a little first, then add the second grip. Grips
never pick up the terrain, locked objects, things bigger than about 2 m or a room you are standing in.
- Settings ▸ VR ▸ **Both grips scale the world**: Always (new) or Only in empty air (the old way).

### 🖌️ Brushes on the controller

- Quick menu ▸ Sculpt ▸ **Paint** (terrain layers 1-4) and **Scatter** (paint, erase, rotate, scale, relax, choose a
source). The trigger paints; the stick sets radius and strength (or density); a ring shows the brush on the ground
and a buzz starts each stroke. Works in a miniature too.

### 📦 Asset Browser on your wrist

- **Add ▸ Assets** opens a browser on your wrist: packs, free CC0 models and your library, searchable with the VR
keyboard. Trigger-drag a card into the scene, or click it and place copies with the laser (it snaps, the stick turns
it, grip stops). Grip the panel off your wrist to pin it in the air; grip it back to return it.
- **Hand tracking**: no controllers needed to browse and place — a pinch is the trigger, pinch-drag drops a model. Tools
that need precision ask you to pick up a controller.

### 🔧 Fixes

- Someone who joined your room no longer comes back from a network or signaling blip as View-only — their edits kept
staying on their own screen while you still saw them as an Editor (hosted site; seen on 1.43.2 and 1.44.0).
- **On Impact** now fires when an object hits something from any side — a ball thrown at a wall or a target — not only
when it lands (new **From** option: any side / landing only; On Impact nodes in existing scenes keep landing only).
- Rotate and Motor nodes spin up from rest when they switch on, and an auto-starting HUD Timer counts down from its full
time (both used to start as if they had been running since midnight).
- Set Game State, Set Variable, HUD Rows, HUD Set Input, Device Param and Note Trigger show the sockets they read, so
you can wire them by hand; a HUD Timer's **start** socket starts it.
- The Sequence card shows its wait times at the normal card width; thirteen node descriptions say what the node really
does.
- The profile button opens with Enter or Space instead of on focus, so Tab moves past it, and Esc returns to it. At
large UI scales on a narrow phone, window headers stay on one row with the close button beside the title.

## 1.44.0 — Quick wins: play from where you stand, trace over a picture, share files in, embed anywhere ⚡

Small things you feel in every session: try the corner you are building without walking there, trace a plan, send a
Expand Down
539 changes: 270 additions & 269 deletions CLAUDE-index.md

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4263,6 +4263,20 @@ sub-entry with its line: grep the index for a word, then read this file from tha
roster-length check shows a phantom peer while pending. `$peers` ticks
(`peers.update`) on every open/close, so derive from `openedPeers` (#14 CN; the
Users peers-trigger had this bug too).
- **`peer.id` does NOT go null on a signaling drop any more — and "is signaling up" is
`peer.open`, never `peer.id`** (net-drop-1). peerjs's `Peer.disconnect()`, which runs on every
drop of the signaling socket, sets `id` to null until the socket re-registers; everything
that asked "who am I" during the gap read null. The cloud roles plugin
(`getPeers().peer.id`) counted itself ALONE and wiped its role map, so a joiner came back
from a healed drop a VIEWER whose edits never left it (net-drop B -> A red on every
deployed build and green on a dev server, which loads no plugin). Core's own readers
(own avatar, own locks, shared-library owner, chat, `atscene`) were wrong for the same
seconds. `peerDrop.keepPeerId` puts an own getter on the one Peer core builds (live id,
else the id it was created with); peerjs's `_id`/`_lastServerId`/`open`/`disconnected`
are untouched, and `reconnectDial` checks `peer.disconnected` itself. Suite
`net-drop` (own id at the cut; roles unchanged by the heal under
`E2E_CLOUD_PLUGIN=https://theprototype.pages.dev/cloud-plugin.js`, the way to run any
two-peer suite in the deployed condition on a dev server).
- A fixed overlay anchored under the Connect pill can't use a `position:fixed`
click-catcher sized to the viewport — `.connect-wrap`'s `translateX(-50%)` makes it
the containing block for fixed descendants (the transform gotcha). Close the
Expand Down
2 changes: 1 addition & 1 deletion NODES.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Verdicts: OK · FIX(ed this batch) · DOC(umented quirk).
| visibility | Logic | effect | on | OK |
| onclick | Triggers | event | — | FIX: event→effect is now a legal drag (the typed sockets rejected wiring On Click into the Object Selector its own feature requires). Pulse window OK. |
| counter | Triggers | number | pulse reset | FIX (21-E4): a wired `reset` EVENT zeroes it, which is what a round-2 score needs — the counting lives inside `applyNodeTrigger`, so the reset rides that same path and is handle-aware there rather than being a second code path. The `op` param is unchanged and still governs a counter with nothing wired into reset. |
| onimpact | Triggers | event | minStrength | NEW (PFX-C): pulses when the physics INITIATOR lands the object on the ground/another object (rapier contact-start + pre-step downward velocity ≥ 1.2 m/s + 300ms per-body cooldown). Targeting walks THROUGH intermediate nodes to the Object Selector (the fireObjectClick BFS) and honors the implicit-owner rule. Replicated via nodetrigger stamps; `minStrength` gates on impact speed (wirable). |
| onimpact | Triggers | event | minStrength, direction | NEW (PFX-C): pulses when the physics INITIATOR registers a contact-start for the object. fix-batch-1 #14: `direction: 'any'` (new nodes) = the pre-step CLOSING speed along the contact normal ≥ 1.2 m/s, from any side (walls, targets, a kinematic paddle); `'down'` = pre-step downward velocity ≥ 1.2 m/s, the only rule up to 1.43 and what a node saved WITHOUT `direction` keeps (`legacy: 'down'` on the param). Each channel has its own 300ms per-body cooldown; emitters' "On impact" stay on the landing channel. Targeting walks THROUGH intermediate nodes to the Object Selector (the fireObjectClick BFS) and honors the implicit-owner rule. Replicated via nodetrigger stamps; `minStrength` gates on impact speed (wirable). |
| shake/spin/bounce/orbit | Animation | effect | per-param | OK (generic AnimationNode; params get ⓘ editors where sensible) |
| rotor (Rotate / Motor) | Animation | effect | on (boolean) + per-param | NEW (36-fb F25): turns about a LOCAL axis and the object's origin, rpm + spin-up as a pure function of the shared clock (deterministic, kinematic for physics — pushes bodies, carries Fluid-emitter water through the measured collider motion); on a DYNAMIC body during a run it is a torque-limited motor on the initiator instead (POSE_FREE). |
| flowfloat (Float Along Flow) | Animation | effect | path (object) + per-param | NEW (36-fb F24): rides a Flow path from the base pose at the path speed × speed, bob, faces the current, wraps; nearest path within 4 m when unwired. Pure function of (base, path, time) — nothing sent. |
Expand Down
41 changes: 38 additions & 3 deletions scripts/graph-audit.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const path = require('node:path');
const ROOT = path.join(__dirname, '..');
const read = (/** @type {string} */ p) => fs.readFileSync(path.join(ROOT, p), 'utf8');

/** @returns {{type: string, group: string, label: string, inputs: string[], params: string[], note: boolean}[]} */
/** @returns {{type: string, group: string, label: string, inputs: string[], hasInputs: boolean, params: string[], ranges: string[], note: boolean}[]} */
function catalogTypes() {
const src = read('src/lib/nodeCatalog.js');
const out = [];
Expand Down Expand Up @@ -53,7 +53,8 @@ function catalogTypes() {
.map((s) => s.trim().replace(/'/g, ''))
.filter(Boolean);
const params = [...body.matchAll(/key: '([^']+)'/g)].map((m) => m[1]);
out.push({ type: t[1], group, label, inputs, params, note: /\bnote:/.test(body) });
const ranges = [...body.matchAll(/key: '([^']+)', kind: 'range'/g)].map((m) => m[1]);
out.push({ type: t[1], group, label, inputs, hasInputs: /\binputs: \[/.test(body), params, ranges, note: /\bnote:/.test(body) });
}
// a type spelled twice (inside an options literal etc.) keeps its first row
const seen = new Set();
Expand Down Expand Up @@ -82,6 +83,34 @@ function outputTable() {
return Object.fromEntries([...block.matchAll(/([a-z0-9_-]+): '([a-z0-9]+)'/g)].map((m) => [m[1], m[2]]));
}

/** flowSockets' INPUT table: type -> the handles the runtime types (and so reads) */
function inputTable() {
const src = read('src/lib/flowSockets.js');
const a = src.indexOf('const INPUT = {');
const block = src.slice(a, src.indexOf('\n};', a));
return Object.fromEntries([...block.matchAll(/^\t'?([a-z0-9_-]+)'?: \{([^}]*)\}/gm)].map((m) => [m[1], [...m[2].matchAll(/'?([A-Za-z0-9_]+)'?:/g)].map((k) => k[1])]));
}

/** HudNode's own INPUTS map: type -> the sockets that card draws */
function hudCardInputs() {
const src = read('src/components/editors/nodes/HudNode.svelte');
const a = src.indexOf('const INPUTS');
const block = src.slice(a, src.indexOf('};', a));
return Object.fromEntries([...block.matchAll(/^\t\t([a-z0-9_-]+): \[([^\]]*)\]/gm)].map((m) => [m[1], [...m[2].matchAll(/'([^']+)'/g)].map((k) => k[1])]));
}

/**
* fix-batch-1 (docs-1 Q1): the sockets a GENERIC card draws — AnimationNode draws the spec's
* `inputs` (+ a socket per range param when it has them) or, with no `inputs`, one per range
* param; HudNode draws its INPUTS map. null = a card of its own (it draws what it draws).
* @param {any} t @param {string} card @param {Record<string, string[]>} hud
*/
function drawnInputs(t, card, hud) {
if (card === 'AnimationNode') return t.hasInputs ? [...t.inputs, ...t.ranges] : t.ranges;
if (card === 'HudNode') return hud[t.type] ?? [];
return null;
}

function docKeys() {
const src = read('src/lib/nodeDocs.js');
return new Set([...src.matchAll(/^\t'?([a-z0-9_-]+)'?:/gm)].map((m) => m[1]));
Expand All @@ -93,8 +122,10 @@ function catalogReport() {
const { cases, valueTypes } = evaluatorInfo();
const outs = outputTable();
const docs = docKeys();
const ins = inputTable();
const hud = hudCardInputs();
const rows = [];
const flags = { noCard: [], valueNoEval: [], evalNotListed: [], noDoc: [], noOutType: [] };
const flags = { noCard: [], valueNoEval: [], evalNotListed: [], noDoc: [], noOutType: [], undrawnInput: [] };
for (const t of types) {
const f = [];
if (!cards[t.type]) (f.push('NO CARD'), flags.noCard.push(t.type));
Expand All @@ -103,6 +134,10 @@ function catalogReport() {
(f.push('evaluated but not a listed value source'), flags.evalNotListed.push(t.type));
if (!docs.has(t.type)) (f.push('no doc'), flags.noDoc.push(t.type));
if (!outs[t.type]) flags.noOutType.push(t.type);
// a handle the runtime types (and reads) that the card never draws: unwirable by hand
const drawn = drawnInputs(t, cards[t.type], hud);
const missing = drawn ? (ins[t.type] ?? []).filter((h) => !drawn.includes(h)) : [];
if (missing.length) (f.push('UNDRAWN INPUT ' + missing.join(',')), flags.undrawnInput.push(t.type + ':' + missing.join(',')));
rows.push(
`| ${t.type} | ${t.group} | ${cards[t.type] ?? '—'} | ${outs[t.type] ?? '—'} | ${t.inputs.join(' ') || '—'} | ${t.params.join(' ') || '—'} | ${cases.has(t.type) ? 'yes' : '—'} | ${f.join('; ') || 'OK'} |`
);
Expand Down
9 changes: 7 additions & 2 deletions src/components/MeasureOverlay.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
import { chainLabels, layoutLabels } from '$lib/measureCore'
import { isMeasurement, worldPointsOf } from '$lib/measureItems'
import { isVRMode, isLocked, objectsGroup, selectedObjects } from '../stores/sceneStore'
import VRMeasureLabels from './VRMeasureLabels.svelte'

// 47 M1 + 48 B2: the measure chains in the 3D view — the one being drawn (with a dashed rubber
// band to where the next click would land) and every measurement ITEM in the scene (48 B2:
// they are objects in objectsGroup now, so this draws them from their records and follows
// their transforms). Lines and dots only: the labels are DOM (MeasureLabels.svelte), placed
// from the screen positions this component publishes in the RENDER stage, after OrbitControls
// has moved the camera for the frame. Scene-root, never in objectsGroup. Hidden in Play (a
// measurement is an editing aid) and in a headset (the labels are DOM).
// measurement is an editing aid). 52 V1: in a headset the lines and dots draw as well and the labels
// are troika text in the scene instead (VRMeasureLabels) — DOM is invisible there.

const { camera, renderer } = useThrelte()
const { renderStage } = useScheduler()
Expand Down Expand Up @@ -105,7 +107,7 @@

useTask(
() => {
const next = $isVRMode ? [] : collectItems()
const next = collectItems()
const key = next.map((c) => c.id + (c.selected ? 's' : '') + (c.editing ? 'e' : '') + c.points.map((p) => p.map((n) => n.toFixed(4)).join(',')).join(';')).join('|')
if (key !== itemsKey) {
itemsKey = key
Expand Down Expand Up @@ -159,6 +161,9 @@
/>
</T.Points>
{/each}
{#if $isVRMode}
<VRMeasureLabels {chains} />
{/if}
{#if preview}
<T.Line geometry={preview} renderOrder={997}>
<T.LineDashedMaterial color="#ff6a1a" dashSize={0.08} gapSize={0.06} depthTest={false} transparent opacity={0.85} />
Expand Down
4 changes: 3 additions & 1 deletion src/components/Scene.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
import VRSelectionShell from './play/VRSelectionShell.svelte';
// 36 U3b: the VR welcome tour's world-space panel
import TourVRPanel from './tours/TourVRPanel.svelte';
import VRAssetBrowser from './play/VRAssetBrowser.svelte'; // 52-browser: the wrist Asset Browser
// 36 A2: the headset browser's own "Enter VR" offer — decided by us, not threlte's default
import { xrOfferMode } from '$lib/xrOffer';
import MeasureOverlay from './MeasureOverlay.svelte';
Expand Down Expand Up @@ -1515,7 +1516,7 @@
if (Date.now() - pinchMenuToggledAt < 250) return;
// K1: a sleeve ghost pick/place just handled this trigger — the release
// must not fall through to raycastSelect
if (vrModuleSelectSwallowed()) return;
if (vrModuleSelectSwallowed(xrControllers.indexOf(controller))) return; // 52-browser: per controller
// the keyboard is modal on top of any panel (116)
if ($vrKeyboardTarget) {
const key = raycastKeyboard(xrControllers.indexOf(controller));
Expand Down Expand Up @@ -1929,6 +1930,7 @@ position={[0, 2, 3]}
<VRKeyboard />
<VRSelectionShell />
<TourVRPanel />
<VRAssetBrowser />

<XR
offerSession={$xrOfferMode}
Expand Down
Loading
Loading