diff --git a/README.md b/README.md index c040644..c50b87d 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ 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. 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. **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 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. 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. diff --git a/index.html b/index.html index eccfa69..aa2a10d 100644 --- a/index.html +++ b/index.html @@ -137,12 +137,11 @@

Augmenta × Three.js

-
- -
-
- +
+ + +
diff --git a/src/debug.js b/src/debug.js index 2fb244d..8eed4ab 100644 --- a/src/debug.js +++ b/src/debug.js @@ -2,6 +2,7 @@ import { ClusterState, ShapeType, ZonePropertyType } from 'augmenta-client-sdk'; import { speedFromVelocity } from './motion.js'; const DEBUG_RENDER_INTERVAL_MS = 250; +const DEBUG_INTERACTION_SETTLE_MS = DEBUG_RENDER_INTERVAL_MS * 2; const INTENSITY_SAMPLE_LIMIT = 2048; export function createDebugPanel(summary, content) { @@ -10,6 +11,17 @@ export function createDebugPanel(summary, content) { let cachedControlHtml = ''; let pendingOpenSections; let pendingScrollTop; + let interactionHoldUntil = 0; + + function holdInteraction() { + interactionHoldUntil = performance.now() + DEBUG_INTERACTION_SETTLE_MS; + } + + content.addEventListener('pointerdown', holdInteraction, { passive: true }); + content.addEventListener('click', holdInteraction); + content.addEventListener('keydown', (event) => { + if (event.key === 'Enter' || event.key === ' ') holdInteraction(); + }); function render( frame, @@ -30,7 +42,7 @@ export function createDebugPanel(summary, content) { ? `${frame.getObjectCount()} objects · ${frame.getZoneEventCount()} zones · ${fps} fps` : 'Waiting for tracking data'; - if (!renderContent) return; + if (!renderContent || (!force && now < interactionHoldUntil)) return; const blocks = []; if (frame) { diff --git a/src/main.js b/src/main.js index 0d7d634..6a8f77a 100644 --- a/src/main.js +++ b/src/main.js @@ -539,35 +539,45 @@ ui.debugSection.addEventListener('toggle', () => { // The panel overlays the renderer. Shift the camera projection by the visible // panel width so the orbit target stays centered in the unobscured viewport. function syncPanelCamera(animate = false) { - const isMobile = window.matchMedia(MOBILE_MEDIA_QUERY).matches; const hidden = ui.app.classList.contains('sidebar-hidden'); - const inset = isMobile || hidden ? 0 : ui.sidebar.getBoundingClientRect().width; + const inset = isMobileLayout() || hidden ? 0 : ui.sidebar.getBoundingClientRect().width; viewer.setRightInset(inset, animate); } +function isMobileLayout() { + return window.matchMedia(MOBILE_MEDIA_QUERY).matches; +} + +function updateSidebarTogglePresentation() { + const hidden = ui.app.classList.contains('sidebar-hidden'); + const mobile = isMobileLayout(); + const label = hidden ? (mobile ? 'Show menu' : 'Show panel') : (mobile ? 'Hide menu' : 'Hide panel'); + + ui.sidebarToggle.textContent = mobile + ? (hidden ? '⌃' : '⌄') + : (hidden ? '<' : '>'); + ui.sidebarToggle.title = label; + ui.sidebarToggle.setAttribute('aria-label', label); + ui.sidebarToggle.setAttribute('aria-expanded', String(!hidden)); +} + function syncSidebarAccessibility() { - const foldedOnDesktop = ui.app.classList.contains('sidebar-hidden') - && !window.matchMedia(MOBILE_MEDIA_QUERY).matches; + const hidden = ui.app.classList.contains('sidebar-hidden'); + const mobile = isMobileLayout(); + const hideQr = hidden && !mobile; - ui.sidebar.inert = foldedOnDesktop; - ui.connectionQrVisibility.inert = foldedOnDesktop; + ui.sidebar.inert = hidden; + ui.sidebar.toggleAttribute('aria-hidden', hidden); - if (foldedOnDesktop) { - ui.sidebar.setAttribute('aria-hidden', 'true'); - ui.connectionQrVisibility.setAttribute('aria-hidden', 'true'); - } else { - ui.sidebar.removeAttribute('aria-hidden'); - ui.connectionQrVisibility.removeAttribute('aria-hidden'); - } + ui.connectionQrVisibility.inert = hideQr; + ui.connectionQrVisibility.toggleAttribute('aria-hidden', hideQr); } function setSidebarHidden(hidden, animate = true) { if (!animate) ui.app.classList.add('sidebar-no-transition'); ui.app.classList.toggle('sidebar-hidden', hidden); - ui.sidebarToggle.textContent = hidden ? '<' : '>'; - ui.sidebarToggle.title = hidden ? 'Show panel' : 'Hide panel'; - ui.sidebarToggle.setAttribute('aria-expanded', String(!hidden)); + updateSidebarTogglePresentation(); syncSidebarAccessibility(); syncPanelCamera(animate); @@ -643,8 +653,9 @@ ui.sidebarResizer.addEventListener('keydown', (event) => { window.addEventListener('pagehide', savePreferences); window.addEventListener('resize', () => { + updateSidebarTogglePresentation(); syncSidebarAccessibility(); - if (!window.matchMedia(MOBILE_MEDIA_QUERY).matches) { + if (!isMobileLayout()) { setSidebarWidth(ui.sidebar.getBoundingClientRect().width); } else { syncPanelCamera(false); diff --git a/src/styles.css b/src/styles.css index 6794194..926708c 100644 --- a/src/styles.css +++ b/src/styles.css @@ -255,7 +255,7 @@ button, input, select { font: inherit; } } label { display: block; margin: 0 0 6px; color: #9da7b9; font-size: 11px; font-weight: 600; } -input, select { +input:not([type="checkbox"]), select { width: 100%; min-height: 38px; padding: 8px 10px; @@ -265,7 +265,7 @@ input, select { color: #eef2f8; background: #171b23; } -input:focus, select:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-focus); } +input:not([type="checkbox"]):focus, select:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-focus); } .button-row { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 9px; margin-top: 13px; } .button-row button { width: 100%; min-width: 0; } @@ -321,11 +321,29 @@ button.subtle-button.active:hover { } .display-scenes { margin-bottom: var(--section-space-relaxed); } +.field-heading { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: var(--section-space); + margin-bottom: 6px; +} +.field-heading label { margin: 0; } .display-scenes select { min-height: 34px; } .toggles { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .toggle-column { display: grid; align-content: start; gap: var(--section-space); } .toggles label { margin: 0; display: flex; align-items: center; gap: 7px; font-weight: 500; color: #c5ccd8; } -.toggles input { width: 14px; min-height: 14px; margin: 0; accent-color: var(--accent); } +.toggles input { + width: 14px; + height: 14px; + min-height: 14px; + margin: 0; + padding: 0; + outline: none; + box-shadow: none; + accent-color: var(--accent); + -webkit-tap-highlight-color: transparent; +} .debug-content { display: grid; gap: 9px; } .empty-state { padding: 22px 14px; border: 1px dashed #303746; border-radius: 8px; color: #778194; text-align: center; font-size: 11px; line-height: 1.5; } @@ -599,7 +617,7 @@ button.subtle-button.active:hover { font-size: 10px; } -.advanced-panel input, +.advanced-panel input:not([type="checkbox"]), .advanced-panel select { height: 32px; min-height: 32px; @@ -624,9 +642,20 @@ button.subtle-button.active:hover { .advanced-toggle input { width: 14px; + height: 14px; min-height: 14px; margin: 0; + padding: 0; + outline: none; + box-shadow: none; accent-color: var(--accent); + -webkit-tap-highlight-color: transparent; +} + +.toggles input:focus-visible, +.advanced-toggle input:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; } .panel-section.debug-panel { @@ -668,16 +697,28 @@ button.subtle-button.active:hover { @media (max-width: 900px), (pointer: coarse) and (max-width: 1100px) { body { overflow: auto; } - #app, - #app.sidebar-hidden { + + #app { height: auto; - min-height: 100%; + min-height: 100vh; + min-height: 100dvh; grid-template-columns: 1fr; grid-template-rows: 58vh auto; + grid-template-rows: 58dvh auto; overflow: visible; } - #sidebar, - #app.sidebar-hidden #sidebar { + + #app.sidebar-hidden { + height: 100vh; + height: 100dvh; + min-height: 100vh; + min-height: 100dvh; + grid-template-rows: 100vh 0; + grid-template-rows: 100dvh 0; + overflow: hidden; + } + + #sidebar { position: static; width: auto; height: auto; @@ -687,12 +728,48 @@ button.subtle-button.active:hover { transform: none; pointer-events: auto; } - .sidebar-toggle, + + #app.sidebar-hidden #sidebar { + display: none; + } + .sidebar-resizer { display: none; } + + .sidebar-toggle { + display: block; + top: 58vh; + top: 58dvh; + right: 12px; + bottom: auto; + width: 42px; + height: 28px; + min-height: 28px; + transform: translateY(-50%); + border: 1px solid #343b49; + border-radius: 999px; + background: rgba(23, 27, 35, .94); + font-size: 14px; + opacity: 1; + pointer-events: auto; + } + + #app.sidebar-hidden .sidebar-toggle { + top: auto; + right: 12px; + bottom: max(12px, env(safe-area-inset-bottom)); + transform: none; + } + + #app.sidebar-handle-idle .sidebar-toggle { + opacity: 1; + pointer-events: auto; + } + #app.sidebar-hidden .connection-qr-visibility { opacity: 1; pointer-events: auto; } + .viewer-overlay { padding: 16px; } .connection-qr-visibility { left: 12px;