From d9888816d00cd454ab1932abfd4d8baba015ab16 Mon Sep 17 00:00:00 2001 From: David-Alexandre Chanel Date: Fri, 2 Oct 2026 12:29:06 +0200 Subject: [PATCH 01/14] fix: restore 50 ms zone stale timeout --- src/zone-state.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zone-state.js b/src/zone-state.js index 57d9fad..9d69499 100644 --- a/src/zone-state.js +++ b/src/zone-state.js @@ -35,7 +35,7 @@ export function collectZoneAddresses(container, result = new Set()) { } -export const ZONE_STREAM_STALE_MS = 250; +export const ZONE_STREAM_STALE_MS = 50; export function isZoneStreamFresh(lastSeenAt, now, staleMs = ZONE_STREAM_STALE_MS) { return Number.isFinite(lastSeenAt) From bde1561bef227d5e3e161720e5e12bbb3aca890c Mon Sep 17 00:00:00 2001 From: David-Alexandre Chanel Date: Fri, 2 Oct 2026 12:29:09 +0200 Subject: [PATCH 02/14] test: lock zone stale timeout to 50 ms --- tests/zone-state.test.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/zone-state.test.mjs b/tests/zone-state.test.mjs index 80b5d54..964a458 100644 --- a/tests/zone-state.test.mjs +++ b/tests/zone-state.test.mjs @@ -71,8 +71,8 @@ test('zone address collection follows the full setup hierarchy', () => { }); -test('zone stream stale grace period stays at 250 ms', () => { - assert.equal(ZONE_STREAM_STALE_MS, 250); +test('zone stream stale grace period stays at 50 ms', () => { + assert.equal(ZONE_STREAM_STALE_MS, 50); }); test('zone stream stays visible only within the stale grace period', () => { From 9a152d54a35c5569cccc9f2b2d7ae72c158f8027 Mon Sep 17 00:00:00 2001 From: David-Alexandre Chanel Date: Fri, 2 Oct 2026 12:29:12 +0200 Subject: [PATCH 03/14] refactor: clarify zone rendering constants --- src/zones.js | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/zones.js b/src/zones.js index 37d262e..6f8f5cb 100644 --- a/src/zones.js +++ b/src/zones.js @@ -33,6 +33,9 @@ const ZONE_SLIDER_FILL_OPACITY = 0.22; const ZONE_SLIDER_IDLE_FILL_OPACITY = 0.08; const ZONE_VALUE_IDLE_COLOR = new THREE.Color(0xb0b5bd); const ROUND_OUTLINE_SEGMENTS = 32; +const SPHERE_WIDTH_SEGMENTS = 24; +const SPHERE_HEIGHT_SEGMENTS = 16; +const MIN_GEOMETRY_SIZE = 0.001; export function createZoneRenderer() { const views = new Map(); @@ -128,7 +131,7 @@ export function createZoneRenderer() { visualGroup.visible = isZoneStreamFresh(lastSeenAt, performance.now()); views.set(address, view); - setPresence(view, presenceByAddress.get(address) ?? 0); + setPresence(view, presenceByAddress.get(address) ?? 0, false); const xyPadValue = xyPadByAddress.get(address); if (xyPadValue) updateBoxXYPad(view.xyPad, xyPadValue.x, xyPadValue.y); @@ -387,18 +390,18 @@ function zoneGeometry(params) { case ShapeType.Cylinder: { const { radius, height } = params.getCylinderShapeParameters(); return new THREE.CylinderGeometry( - Math.max(Math.abs(radius), 0.001), - Math.max(Math.abs(radius), 0.001), - Math.max(Math.abs(height), 0.001), - 32 + Math.max(Math.abs(radius), MIN_GEOMETRY_SIZE), + Math.max(Math.abs(radius), MIN_GEOMETRY_SIZE), + Math.max(Math.abs(height), MIN_GEOMETRY_SIZE), + ROUND_OUTLINE_SEGMENTS ); } case ShapeType.Sphere: return new THREE.SphereGeometry( Math.max(Math.abs(params.getSphereShapeParameters().radius), 0.001), - 24, - 16 + SPHERE_WIDTH_SEGMENTS, + SPHERE_HEIGHT_SEGMENTS ); default: @@ -444,8 +447,8 @@ function createZoneOutline(params, sourceGeometry, color) { if (params.isCylinder()) { const { radius, height } = params.getCylinderShapeParameters(); guidePositions = cylinderRingPositions( - Math.max(Math.abs(radius), 0.001), - Math.max(Math.abs(height), 0.001) + Math.max(Math.abs(radius), MIN_GEOMETRY_SIZE), + Math.max(Math.abs(height), MIN_GEOMETRY_SIZE) ); } else if (params.isSphere()) { guidePositions = sphereEquatorPositions( @@ -654,8 +657,8 @@ function createRoundZoneSlider(params, sourceGeometry) { let max = 0.5; if (params.isCylinder()) { const { radius, height } = params.getCylinderShapeParameters(); - const r = Math.max(Math.abs(radius), 0.001); - const h = Math.max(Math.abs(height), 0.001); + const r = Math.max(Math.abs(radius), MIN_GEOMETRY_SIZE); + const h = Math.max(Math.abs(height), MIN_GEOMETRY_SIZE); min = axisName === 'y' ? -h / 2 : -r; max = axisName === 'y' ? h / 2 : r; } else if (params.isSphere()) { @@ -827,5 +830,5 @@ function roundedRect(ctx, x, y, width, height, radius) { } function positiveSize(size) { - return size.map((v) => Math.max(Math.abs(v), 0.001)); + return size.map((v) => Math.max(Math.abs(v), MIN_GEOMETRY_SIZE)); } From 0ae9df37cf7b7faf84dd3cb01d3b408ad76fc5ac Mon Sep 17 00:00:00 2001 From: David-Alexandre Chanel Date: Fri, 2 Oct 2026 12:29:15 +0200 Subject: [PATCH 04/14] refactor: rely on module initialization for QR setup --- src/qr.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/qr.js b/src/qr.js index 0fa3714..3f7293e 100644 --- a/src/qr.js +++ b/src/qr.js @@ -16,7 +16,6 @@ const ui = { let qrCopyFeedbackTimer; let qrHovering = false; -let initialized = false; function getConnectionSettings() { return { @@ -118,8 +117,7 @@ async function handleConnectionQrClick(event) { } function initializeConnectionQr() { - if (initialized || !ui.connectionQr) return; - initialized = true; + if (!ui.connectionQr) return; ui.connectionQr.addEventListener('click', handleConnectionQrClick); ui.connectionQr.addEventListener('mouseenter', () => { From 4dc34597814257933bccbaec5f859d173077607a Mon Sep 17 00:00:00 2001 From: David-Alexandre Chanel Date: Fri, 2 Oct 2026 12:29:18 +0200 Subject: [PATCH 05/14] refactor: remove unused setup store clear API --- src/setup-store.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/setup-store.js b/src/setup-store.js index 9fa7ed9..ce8c8ca 100644 --- a/src/setup-store.js +++ b/src/setup-store.js @@ -32,14 +32,7 @@ export function createSetupStore() { return root; } - function clear() { - root = undefined; - scenes = []; - byAddress = new Map(); - } - return { - clear, setRoot, applyUpdate, getRoot: () => root, From 0eb4e84357ab06020d3f0e8f33dd348bd44682a9 Mon Sep 17 00:00:00 2001 From: David-Alexandre Chanel Date: Fri, 2 Oct 2026 12:29:45 +0200 Subject: [PATCH 06/14] refactor: name UI timing and resize constants --- src/main.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main.js b/src/main.js index ee17f35..48050b3 100644 --- a/src/main.js +++ b/src/main.js @@ -14,9 +14,13 @@ import { refreshConnectionQr } from './qr.js'; const DISCONNECT_CLEANUP_DELAY_MS = 500; const CAMERA_PREFERENCE_SAVE_DELAY_MS = 250; const SIDEBAR_HANDLE_IDLE_DELAY_MS = 3000; +const FPS_WINDOW_MS = 1000; +const DEMO_FRAME_INTERVAL_MS = 33; const SIDEBAR_MIN_WIDTH = 320; const SIDEBAR_MAX_WIDTH = 450; const SIDEBAR_VIEWPORT_MARGIN = 160; +const SIDEBAR_RESIZE_STEP_PX = 16; +const SIDEBAR_RESIZE_LARGE_STEP_PX = 40; const MOBILE_MEDIA_QUERY = '(max-width: 900px), (pointer: coarse) and (max-width: 1100px)'; const SETTINGS_STORAGE_KEY = 'augmenta-threejs-settings:v1'; @@ -285,7 +289,7 @@ function trackFrame(frame) { lastFrame = frame; const now = performance.now(); frameTimes.push(now); - frameTimes = frameTimes.filter((time) => time >= now - 1000); + frameTimes = frameTimes.filter((time) => time >= now - FPS_WINDOW_MS); viewer.renderFrame(frame); renderDebug(); } @@ -487,7 +491,7 @@ function startSimulation() { const start = performance.now(); const tick = () => trackFrame(makeDemoFrame((performance.now() - start) / 1000)); tick(); - demoTimer = window.setInterval(tick, 33); + demoTimer = window.setInterval(tick, DEMO_FRAME_INTERVAL_MS); updateSimulationButton(); } @@ -548,7 +552,7 @@ function syncPanelCamera(animate = false) { } function isMobileLayout() { - return window.matchMedia(MOBILE_MEDIA_QUERY).matches; + return isMobileLayout(); } function updateSidebarTogglePresentation() { @@ -619,12 +623,12 @@ function setSidebarWidth(width) { } function resizeSidebar(event) { - if (window.matchMedia(MOBILE_MEDIA_QUERY).matches) return; + if (isMobileLayout()) return; setSidebarWidth(window.innerWidth - event.clientX); } ui.sidebarResizer.addEventListener('pointerdown', (event) => { - if (window.matchMedia(MOBILE_MEDIA_QUERY).matches || ui.app.classList.contains('sidebar-hidden')) return; + if (isMobileLayout() || ui.app.classList.contains('sidebar-hidden')) return; event.preventDefault(); ui.sidebarResizer.setPointerCapture(event.pointerId); ui.app.classList.add('sidebar-resizing'); @@ -647,7 +651,7 @@ ui.sidebarResizer.addEventListener('pointercancel', stopSidebarResize); ui.sidebarResizer.addEventListener('keydown', (event) => { if (!['ArrowLeft', 'ArrowRight'].includes(event.key)) return; event.preventDefault(); - const step = event.shiftKey ? 40 : 16; + const step = event.shiftKey ? SIDEBAR_RESIZE_LARGE_STEP_PX : SIDEBAR_RESIZE_STEP_PX; const current = ui.sidebar.getBoundingClientRect().width; setSidebarWidth(current + (event.key === 'ArrowLeft' ? step : -step)); savePreferences(); From 31a355678db3b29a34211d1891c90445791f00a2 Mon Sep 17 00:00:00 2001 From: David-Alexandre Chanel Date: Fri, 2 Oct 2026 12:29:48 +0200 Subject: [PATCH 07/14] refactor: name viewer behavior constants --- src/viewer.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/viewer.js b/src/viewer.js index 8da36b9..4cb92b8 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -6,6 +6,10 @@ import { createZoneRenderer } from './zones.js'; import { collectZoneAddresses } from './zone-state.js'; const FLOOR_Y = 0; +const PANEL_INSET_ANIMATION_DURATION_MS = 220; +const MIN_GEOMETRY_SIZE = 0.001; +const MIN_VISIBLE_SPEED_MPS = 0.001; +const CAMERA_FLOOR_CLEARANCE_M = 0.02; const GHOST_COLOR = new THREE.Color(0x8a909b); const LOOK_AT_MARKER_OPACITY = 0.58; const LOOK_AT_MARKER_GHOST_OPACITY = 0.24; @@ -172,7 +176,7 @@ export function createViewer(host) { const start = rightInset; const startedAt = performance.now(); - const duration = 220; + const duration = PANEL_INSET_ANIMATION_DURATION_MS; const tick = (now) => { const t = Math.min((now - startedAt) / duration, 1); @@ -325,9 +329,9 @@ export function createViewer(host) { view.box.visible = true; view.box.position.fromArray(center); view.box.scale.set( - Math.max(Math.abs(size[0]), 0.001), - Math.max(Math.abs(size[1]), 0.001), - Math.max(Math.abs(size[2]), 0.001) + Math.max(Math.abs(size[0]), MIN_GEOMETRY_SIZE), + Math.max(Math.abs(size[1]), MIN_GEOMETRY_SIZE), + Math.max(Math.abs(size[2]), MIN_GEOMETRY_SIZE) ); // Pleiades sends its native Y-up/left-handed quaternion. Reflect it across // Z to express the exact same orientation in Three.js' right-handed space. @@ -359,7 +363,7 @@ export function createViewer(host) { pointBounds.center.fromArray(center); pointBounds.radius = Math.max( Math.hypot(Math.abs(size[0]), Math.abs(size[1]), Math.abs(size[2])) * 0.5, - 0.001 + MIN_GEOMETRY_SIZE ); view.points.geometry.boundingSphere = pointBounds; view.points.frustumCulled = true; @@ -592,8 +596,8 @@ export function createViewer(host) { // Orbiting stays above the world floor. With screenSpacePanning=false, // right-drag panning also remains parallel to the floor plane. - if (camera.position.y < FLOOR_Y + 0.02) { - camera.position.y = FLOOR_Y + 0.02; + if (camera.position.y < FLOOR_Y + CAMERA_FLOOR_CLEARANCE_M) { + camera.position.y = FLOOR_Y + CAMERA_FLOOR_CLEARANCE_M; } renderer.render(scene, camera); @@ -676,7 +680,7 @@ function configureArrow(arrow) { function updateVelocity(arrow, origin, velocity, color, direction) { const speed = speedFromVelocity(velocity); - if (!Number.isFinite(speed) || speed < 0.001) { + if (!Number.isFinite(speed) || speed < MIN_VISIBLE_SPEED_MPS) { arrow.visible = false; return; } @@ -785,7 +789,7 @@ function hashString(value) { // Scene dimensions are magnitudes. Placement above preserves the requested // Y-up/right-handed direction while this helper keeps geometry sizes valid. function positiveSize(size) { - return size.map((v) => Math.max(Math.abs(v), 0.001)); + return size.map((v) => Math.max(Math.abs(v), MIN_GEOMETRY_SIZE)); } function clearGroup(group) { From fa8a2b736f96b03cabf5498e5699c979d75119af Mon Sep 17 00:00:00 2001 From: David-Alexandre Chanel Date: Fri, 2 Oct 2026 12:29:51 +0200 Subject: [PATCH 08/14] refactor: remove dead markup and duplicate QR module load --- index.html | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/index.html b/index.html index 7b80bde..2ec30d1 100644 --- a/index.html +++ b/index.html @@ -74,7 +74,7 @@

Augmenta × Three.js

> - From 7d05f5095c7071aa7641b794907f2cb6095b64ca Mon Sep 17 00:00:00 2001 From: David-Alexandre Chanel Date: Fri, 2 Oct 2026 12:29:54 +0200 Subject: [PATCH 09/14] build: fail on missing Pages rewrite tokens --- scripts/assemble-site.mjs | 47 +++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/scripts/assemble-site.mjs b/scripts/assemble-site.mjs index 63762a2..15d83ae 100644 --- a/scripts/assemble-site.mjs +++ b/scripts/assemble-site.mjs @@ -22,6 +22,13 @@ function requirePath(path, label) { } } +function replaceRequired(source, search, replacement) { + if (!source.includes(search)) { + throw new Error(`Missing expected build token: ${search}`); + } + return source.replaceAll(search, replacement); +} + requirePath(join(root, 'vendor', 'AugmentaClientSDK-JS', 'dist', 'esm'), 'built Augmenta SDK'); requirePath(join(root, 'vendor', 'qrcode-generator', 'qrcode.js'), 'vendored QR generator'); requirePath(join(threeRoot, 'build', 'three.module.js'), 'Three.js runtime'); @@ -71,23 +78,29 @@ cpSync( const indexPath = join(out, 'index.html'); let html = readFileSync(indexPath, 'utf8'); -html = html - .replace(' \n', '') - .replace( - 'https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js', - `./vendor/three/${rev}/build/three.module.js` - ) - .replace( - 'https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/', - `./vendor/three/${rev}/examples/jsm/` - ) - .replace( - './vendor/AugmentaClientSDK-JS/dist/esm/index.js', - `./vendor/AugmentaClientSDK-JS/${rev}/dist/esm/index.js` - ) - .replaceAll('./src/styles.css', `./src/styles.css?v=${rev}`) - .replaceAll('./src/qr.js', `./src/qr.js?v=${rev}`) - .replaceAll('./src/main.js', `./src/main.js?v=${rev}`); +html = replaceRequired( + html, + ' \n', + '' +); +html = replaceRequired( + html, + 'https://cdn.jsdelivr.net/npm/three@0.186.1/build/three.module.js', + `./vendor/three/${rev}/build/three.module.js` +); +html = replaceRequired( + html, + 'https://cdn.jsdelivr.net/npm/three@0.186.1/examples/jsm/', + `./vendor/three/${rev}/examples/jsm/` +); +html = replaceRequired( + html, + './vendor/AugmentaClientSDK-JS/dist/esm/index.js', + `./vendor/AugmentaClientSDK-JS/${rev}/dist/esm/index.js` +); +html = replaceRequired(html, './src/styles.css', `./src/styles.css?v=${rev}`); +html = replaceRequired(html, './src/qr.js', `./src/qr.js?v=${rev}`); +html = replaceRequired(html, './src/main.js', `./src/main.js?v=${rev}`); if (html.includes('cdn.jsdelivr.net')) { throw new Error('Built Pages index still contains a runtime CDN dependency.'); From dabd52aea00f72604f17c1b8f7e8d02d841d442c Mon Sep 17 00:00:00 2001 From: David-Alexandre Chanel Date: Fri, 2 Oct 2026 12:29:58 +0200 Subject: [PATCH 10/14] docs: align README with current viewer behavior --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c50b87d..f8028e1 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,11 @@ Three.js renders: - scene bounds and box/cylinder/sphere zones, with live presence labels shown below occupied zones and slider-volume fill on round zones; - a Pleiades-style orbit camera, 1 × 1 m floor grid and axes. -Mouse navigation follows the Pleiades viewer philosophy: left-drag orbits, right-drag pans parallel to the floor, middle-drag/wheel zooms, and the orbit is constrained above the floor plane. The camera view is persisted after navigation and restored on refresh; setup updates do not overwrite a restored/user-controlled view. **Reset camera** explicitly returns the viewer to automatic framing, so a later full setup can frame its new bounds normally. Without a saved/user-controlled view, a successful connection setup initializes the same centered framing used by **Reset camera**. **Simulate data**, **Reset camera**, or a left-button double-click reframes the displayed setup. The sidebar uses two lightweight levels of disclosure: each main section can be folded to a compact status row, and each section can expose a denser **Advanced** subsection for less-used controls. In **Connection**, the server address, Connect/Simulate controls, and live connection log stay primary while Port, Protocol and Point downsample live under Advanced. **Display** keeps the Scene plus common visibility toggles primary, with a lightweight **Reset camera** action above them and Velocity vectors under Advanced. **Live debug data** stays readable as the main content with a lightweight **Clear data** action above the inspector. While the Live debug data section is folded, only its compact summary is refreshed; the detailed debug HTML is not rebuilt until the section is opened again. While interacting with Frame / Objects / Zones / Control disclosures, debug DOM refreshes briefly pause so a live 4 Hz update cannot replace the clicked disclosure between pointer-down and click. The Display panel exposes **All scenes** plus every Scene received in the current World, with independent **Scenes** and **Zones** visibility toggles. On desktop, the translucent panel overlays the 3D view: use the edge arrow to hide/show it, or drag its left edge to resize it. On phone/touch layouts, the same control becomes a small horizontal handle at the viewer/menu boundary; folding the menu expands the viewer to the full viewport while keeping the handle available to reopen it. **Escape** folds the panel. **H** toggles the viewer title/subtitle as a transient display shortcut; unlike the other UI/display preferences, this title visibility is intentionally not persisted. After 3 seconds without mouse movement or keyboard input, the handle fades whether the panel is unfolded or folded; activity brings it back quickly. Folded sidebar/QR content is removed from keyboard navigation while hidden. The camera projection follows the panel width so the orbit target remains centered in the unobscured part of the view. +Mouse navigation follows the Pleiades viewer philosophy: left-drag orbits, right-drag pans parallel to the floor, middle-drag/wheel zooms, and the orbit is constrained above the floor plane. The camera view is persisted after navigation and restored on refresh; setup updates do not overwrite a restored/user-controlled view. **Reset camera** explicitly returns the viewer to automatic framing, so a later full setup can frame its new bounds normally. Without a saved/user-controlled view, a successful connection setup initializes the same centered framing used by **Reset camera**. **Simulate data**, **Reset camera**, or a left-button double-click reframes the displayed setup. The sidebar uses two lightweight levels of disclosure: each main section can be folded to a compact status row, and each section can expose a denser **Advanced** subsection for less-used controls. In **Connection**, the server address, Connect/Simulate controls, and live connection log stay primary while Port, Protocol and Point downsample live under Advanced. **Display** keeps the Scene plus common visibility toggles primary, with **Reset camera** beside the Scene label and Velocity vectors under Advanced. **Live debug data** stays readable as the main content with a lightweight **Clear data** action above the inspector. While the Live debug data section is folded, only its compact summary is refreshed; the detailed debug HTML is not rebuilt until the section is opened again. While interacting with Frame / Objects / Zones / Control disclosures, debug DOM refreshes briefly pause so a live 4 Hz update cannot replace the clicked disclosure between pointer-down and click. The Display panel exposes **All scenes** plus every Scene received in the current World, with independent **Scenes** and **Zones** visibility toggles. On desktop, the translucent panel overlays the 3D view: use the edge arrow to hide/show it, or drag its left edge to resize it. On phone/touch layouts, the same control becomes a small horizontal handle at the viewer/menu boundary; folding the menu expands the viewer to the full viewport while keeping the handle available to reopen it. **Escape** folds the panel. **H** toggles the viewer title/subtitle as a transient display shortcut; unlike the other UI/display preferences, this title visibility is intentionally not persisted. On desktop, after 3 seconds without mouse movement or keyboard input, the handle fades whether the panel is unfolded or folded; activity brings it back quickly. On phone/touch layouts, the handle stays visible so the folded menu can always be reopened. When folded, the sidebar is removed from keyboard navigation; on desktop the hidden QR is inert as well, while on phone/touch layouts the visible QR remains usable. The camera projection follows the panel width so the orbit target remains centered in the unobscured part of the view. -Partial setup updates are merged into the cached hierarchy before rendering, so a Scene update that omits its Zone children does not erase them. Updates that race ahead of the initial full setup are ignored rather than promoted to an incomplete root. When a specific Scene is selected, live setup updates still render from the World root so ancestor transforms remain intact. Zone-event caches are pruned against the latest setup hierarchy so renamed/removed addresses do not accumulate stale visual state. Scene selection and scene-size debug information therefore always use the latest valid merged setup. +Partial setup updates are merged into the cached hierarchy before rendering, so a Scene update that omits its Zone children does not erase them. Updates that race ahead of the initial full setup are ignored rather than promoted to an incomplete root. When a specific Scene is selected, live setup updates still render from the World root so ancestor transforms remain intact. Zone-event caches are pruned against the latest setup hierarchy so renamed/removed addresses do not accumulate stale visual state. A zone stops rendering 50 ms after its live event heartbeat disappears, so disabled zones vanish quickly while their setup definition remains available. Scene selection and scene-size debug information therefore always use the latest valid merged setup. -The live debug inspector is throttled and preserves the user's open/closed section state across refreshes. Cluster debug values include the received OBB quaternion and `lookAt` vector so the rendered direction marker can be checked against the wire data. Using **Clear** preserves the inspector layout and sidebar scroll position while the live stream repopulates, so clearing data does not jump the panel. The JavaScript SDK is included as a **Git submodule** in `vendor/AugmentaClientSDK-JS` and pinned to SDK `1.0.0-beta.1` commit `115b77c1` for reproducible builds. The SDK lockfile is committed, so CI and local reproducible builds use `npm ci`. The pinned SDK includes bulk point-cloud parsing, packet-boundary validation, protocol V2/V3 guards and stale-WebSocket-event protection. The inspector exposes the main fields useful for this example; raw point arrays are rendered in full in Three.js while the text panel is throttled and point-intensity statistics are bounded to a representative sample to stay responsive with large clouds. +The live debug inspector is throttled and preserves the user's open/closed section state across refreshes. Cluster debug values include the received OBB quaternion and `lookAt` vector so the rendered direction marker can be checked against the wire data. Using **Clear data** preserves the inspector layout and sidebar scroll position while the live stream repopulates, so clearing data does not jump the panel. The JavaScript SDK is included as a **Git submodule** in `vendor/AugmentaClientSDK-JS` and pinned to SDK `1.0.0-beta.1` commit `115b77c1` for reproducible builds. The SDK lockfile is committed, so CI and local reproducible builds use `npm ci`. The pinned SDK includes bulk point-cloud parsing, packet-boundary validation, protocol V2/V3 guards and stale-WebSocket-event protection. The inspector exposes the main fields useful for this example; raw point arrays are rendered in full in Three.js while the text panel is throttled and point-intensity statistics are bounded to a representative sample to stay responsive with large clouds. ## Clone and run locally @@ -230,4 +230,4 @@ Augmenta-authored example code is distributed under the Augmenta SDK license in ## Live viewer behavior -If the server-side WebSocket connection disappears unexpectedly, live clusters, point clouds, velocity vectors and debug data are cleared after 500 ms while the application keeps retrying. The static scene/zones remain visible. Clicking **Connected** to disconnect manually stops retries and immediately removes only live tracking/debug data; the scene/zones remain visible. The **Clear** button only clears the debug inspector. +If the server-side WebSocket connection disappears unexpectedly, live clusters, point clouds, velocity vectors and debug data are cleared after 500 ms while the application keeps retrying. The static scene/zones remain visible. Clicking **Connected** to disconnect manually stops retries and immediately removes only live tracking/debug data; the scene/zones remain visible. The **Clear data** button only clears the debug inspector. From d59788cfe683ca7d9f28bc9ff7a55116d8f8d6e0 Mon Sep 17 00:00:00 2001 From: David-Alexandre Chanel Date: Fri, 2 Oct 2026 12:30:40 +0200 Subject: [PATCH 11/14] fix: keep mobile layout helper grounded in media query --- src/main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index 48050b3..a4b88b0 100644 --- a/src/main.js +++ b/src/main.js @@ -552,7 +552,7 @@ function syncPanelCamera(animate = false) { } function isMobileLayout() { - return isMobileLayout(); + return window.matchMedia(MOBILE_MEDIA_QUERY).matches; } function updateSidebarTogglePresentation() { From b6561e8700aac0387310b516dc3729bce74cd4cc Mon Sep 17 00:00:00 2001 From: David-Alexandre Chanel Date: Fri, 2 Oct 2026 12:30:43 +0200 Subject: [PATCH 12/14] refactor: name initial velocity arrow length --- src/viewer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/viewer.js b/src/viewer.js index 4cb92b8..d52b3cc 100644 --- a/src/viewer.js +++ b/src/viewer.js @@ -8,6 +8,7 @@ import { collectZoneAddresses } from './zone-state.js'; const FLOOR_Y = 0; const PANEL_INSET_ANIMATION_DURATION_MS = 220; const MIN_GEOMETRY_SIZE = 0.001; +const MIN_ARROW_LENGTH_M = 0.001; const MIN_VISIBLE_SPEED_MPS = 0.001; const CAMERA_FLOOR_CLEARANCE_M = 0.02; const GHOST_COLOR = new THREE.Color(0x8a909b); @@ -263,7 +264,7 @@ export function createViewer(host) { const velocity = new THREE.ArrowHelper( new THREE.Vector3(0, 0, 1), new THREE.Vector3(), - 0.001, + MIN_ARROW_LENGTH_M, color.getHex(), 0.12, 0.07 From e743cbd909def0e47b3d5e1910bf525181c93696 Mon Sep 17 00:00:00 2001 From: David-Alexandre Chanel Date: Fri, 2 Oct 2026 12:30:47 +0200 Subject: [PATCH 13/14] refactor: reuse minimum geometry size for sphere zones --- src/zones.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/zones.js b/src/zones.js index 6f8f5cb..ad78692 100644 --- a/src/zones.js +++ b/src/zones.js @@ -399,7 +399,7 @@ function zoneGeometry(params) { case ShapeType.Sphere: return new THREE.SphereGeometry( - Math.max(Math.abs(params.getSphereShapeParameters().radius), 0.001), + Math.max(Math.abs(params.getSphereShapeParameters().radius), MIN_GEOMETRY_SIZE), SPHERE_WIDTH_SEGMENTS, SPHERE_HEIGHT_SEGMENTS ); @@ -452,7 +452,7 @@ function createZoneOutline(params, sourceGeometry, color) { ); } else if (params.isSphere()) { guidePositions = sphereEquatorPositions( - Math.max(Math.abs(params.getSphereShapeParameters().radius), 0.001) + Math.max(Math.abs(params.getSphereShapeParameters().radius), MIN_GEOMETRY_SIZE) ); } @@ -662,7 +662,7 @@ function createRoundZoneSlider(params, sourceGeometry) { min = axisName === 'y' ? -h / 2 : -r; max = axisName === 'y' ? h / 2 : r; } else if (params.isSphere()) { - const radius = Math.max(Math.abs(params.getSphereShapeParameters().radius), 0.001); + const radius = Math.max(Math.abs(params.getSphereShapeParameters().radius), MIN_GEOMETRY_SIZE); min = -radius; max = radius; } From 3d9f3becfc07eec54fcaae8e3b49d23e34639e92 Mon Sep 17 00:00:00 2001 From: David-Alexandre Chanel Date: Fri, 2 Oct 2026 12:30:50 +0200 Subject: [PATCH 14/14] style: clean connection field markup --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 2ec30d1..8e0b69b 100644 --- a/index.html +++ b/index.html @@ -85,7 +85,7 @@

Augmenta × Three.js

-
+

IP address or hostname