From 84ae47f997469290b325d345d07f84ec575a1e67 Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:04:07 +0100 Subject: [PATCH 1/2] Add show-hide button for gizmo hud --- accessibility/keyboardui.js | 5 ++ locale/de.js | 3 + locale/en.js | 3 + locale/es.js | 3 + locale/fr.js | 3 + locale/it.js | 3 + locale/pl.js | 3 + locale/pt.js | 3 + locale/sv.js | 3 + tests/gizmo-mobile-hud.test.js | 160 +++++++++++++++++++++++++++++---- ui/gizmo-mobile-hud.js | 118 ++++++++++++++++++++++++ ui/gizmos.js | 15 ++++ 12 files changed, 306 insertions(+), 16 deletions(-) diff --git a/accessibility/keyboardui.js b/accessibility/keyboardui.js index ada5170c..4b99c7d3 100644 --- a/accessibility/keyboardui.js +++ b/accessibility/keyboardui.js @@ -626,6 +626,11 @@ function getShortcuts() { keys: `F`, category: translate('shortcut_category_gizmos'), }, + { + label: translate('shortcut_toggle_hud'), + keys: `O`, + category: translate('shortcut_category_gizmos'), + }, { label: translate('shortcut_quick_colour'), keys: `C`, diff --git a/locale/de.js b/locale/de.js index 76bf3adc..7987ebb5 100644 --- a/locale/de.js +++ b/locale/de.js @@ -1274,6 +1274,9 @@ export default { shortcut_lock_transform: 'Transformation auf Achse einschränken', shortcut_transform_3d: 'In 3D transformieren', shortcut_focus_camera: 'Kamera auf Objekt ausrichten', + shortcut_toggle_hud: 'Bildschirmsteuerung ein-/ausblenden', // machine + hud_hidden: 'Bildschirmsteuerung ausgeblendet', // machine + hud_shown: 'Bildschirmsteuerung eingeblendet', // machine shortcut_quick_colour: 'Schnellfarbauswahl im Farbwähler', shortcut_delete_object: 'Objekt löschen', diff --git a/locale/en.js b/locale/en.js index 44731f0b..32d11924 100644 --- a/locale/en.js +++ b/locale/en.js @@ -1397,6 +1397,9 @@ export default { shortcut_lock_transform: 'Lock transform to axis', shortcut_transform_3d: 'Transform in 3D', shortcut_focus_camera: 'Focus camera on object', + shortcut_toggle_hud: 'Show/hide on-screen controls', + hud_hidden: 'On-screen controls hidden', + hud_shown: 'On-screen controls shown', shortcut_quick_colour: 'Quick use colour in colour picker', shortcut_delete_object: 'Delete object', diff --git a/locale/es.js b/locale/es.js index e0ea4fe0..a123152c 100644 --- a/locale/es.js +++ b/locale/es.js @@ -1342,6 +1342,9 @@ export default { shortcut_lock_transform: 'Bloquear transformación en eje', shortcut_transform_3d: 'Transformar en 3D', shortcut_focus_camera: 'Enfocar cámara en objeto', + shortcut_toggle_hud: 'Mostrar/ocultar controles en pantalla', // machine + hud_hidden: 'Controles en pantalla ocultos', // machine + hud_shown: 'Controles en pantalla visibles', // machine shortcut_quick_colour: 'Usar color rápido en el selector de color', shortcut_delete_object: 'Eliminar objeto', diff --git a/locale/fr.js b/locale/fr.js index b691905e..0da02cda 100644 --- a/locale/fr.js +++ b/locale/fr.js @@ -1281,6 +1281,9 @@ export default { shortcut_lock_transform: 'Verrouiller la transformation sur un axe', shortcut_transform_3d: 'Transformer en 3D', shortcut_focus_camera: "Centrer la caméra sur l'objet", + shortcut_toggle_hud: "Afficher/masquer les commandes à l'écran", // machine + hud_hidden: "Commandes à l'écran masquées", // machine + hud_shown: "Commandes à l'écran affichées", // machine shortcut_quick_colour: 'Utiliser rapidement une couleur dans le sélecteur', shortcut_delete_object: "Supprimer l'objet", diff --git a/locale/it.js b/locale/it.js index 662a301d..077afae8 100644 --- a/locale/it.js +++ b/locale/it.js @@ -1283,6 +1283,9 @@ export default { shortcut_lock_transform: "Blocca trasformazione sull'asse", shortcut_transform_3d: 'Trasforma in 3D', shortcut_focus_camera: "Centra la telecamera sull'oggetto", + shortcut_toggle_hud: 'Mostra/nascondi i comandi a schermo', // machine + hud_hidden: 'Comandi a schermo nascosti', // machine + hud_shown: 'Comandi a schermo visibili', // machine shortcut_quick_colour: 'Uso rapido del colore nel selettore colori', shortcut_delete_object: 'Elimina oggetto', diff --git a/locale/pl.js b/locale/pl.js index f8c388ed..cb4b4d42 100644 --- a/locale/pl.js +++ b/locale/pl.js @@ -1274,6 +1274,9 @@ export default { shortcut_lock_transform: 'Zablokuj transformację na osi', shortcut_transform_3d: 'Transformuj w 3D', shortcut_focus_camera: 'Skieruj kamerę na obiekt', + shortcut_toggle_hud: 'Pokaż/ukryj sterowanie ekranowe', // machine + hud_hidden: 'Sterowanie ekranowe ukryte', // machine + hud_shown: 'Sterowanie ekranowe widoczne', // machine shortcut_quick_colour: 'Szybkie użycie koloru w selektorze kolorów', shortcut_delete_object: 'Usuń obiekt', diff --git a/locale/pt.js b/locale/pt.js index a551fdf1..98b45336 100644 --- a/locale/pt.js +++ b/locale/pt.js @@ -1285,6 +1285,9 @@ export default { shortcut_lock_transform: 'Bloquear transformação no eixo', shortcut_transform_3d: 'Transformar em 3D', shortcut_focus_camera: 'Focar câmara no objeto', + shortcut_toggle_hud: 'Mostrar/ocultar controlos no ecrã', // machine + hud_hidden: 'Controlos no ecrã ocultos', // machine + hud_shown: 'Controlos no ecrã visíveis', // machine shortcut_quick_colour: 'Uso rápido de cor no seletor de cores', shortcut_delete_object: 'Eliminar objeto', diff --git a/locale/sv.js b/locale/sv.js index 756459be..a2420911 100644 --- a/locale/sv.js +++ b/locale/sv.js @@ -1260,6 +1260,9 @@ export default { shortcut_lock_transform: 'Lås transformering till axel', shortcut_transform_3d: 'Transformera i 3D', shortcut_focus_camera: 'Rikta kameran mot objekt', + shortcut_toggle_hud: 'Visa/dölj skärmkontroller', // machine + hud_hidden: 'Skärmkontroller dolda', // machine + hud_shown: 'Skärmkontroller visas', // machine shortcut_quick_colour: 'Snabb färganvändning i färgväljaren', shortcut_delete_object: 'Ta bort objekt', diff --git a/tests/gizmo-mobile-hud.test.js b/tests/gizmo-mobile-hud.test.js index 560d322c..e6bfb0fe 100644 --- a/tests/gizmo-mobile-hud.test.js +++ b/tests/gizmo-mobile-hud.test.js @@ -10,12 +10,34 @@ function findControl(flock, name) { return hud?.getDescendants(false, (c) => c.name === name)[0] ?? null; } +const COLLAPSED_KEY = 'flock-gizmo-hud-collapsed'; + +// A dispatched PointerEvent has no active pointer behind it, so the slider's +// canvas.setPointerCapture() throws NotFoundError here even though it is fine +// in a real browser. Stub it for the dispatch — it is incidental to what these +// tests assert. +function withoutPointerCapture(canvas, fn) { + const real = canvas.setPointerCapture; + canvas.setPointerCapture = () => {}; + try { + fn(); + } finally { + canvas.setPointerCapture = real; + } +} + export function runGizmoMobileHudTests(flock) { describe('ui/gizmo-mobile-hud @gizmomobilehud', function () { let stop; let moves; let axisChanges; + // The collapsed preference persists in localStorage, so without this a + // single collapse test would silently invalidate every later assertion. + beforeEach(function () { + localStorage.removeItem(COLLAPSED_KEY); + }); + function make(overrides = {}) { moves = []; axisChanges = []; @@ -34,6 +56,7 @@ export function runGizmoMobileHudTests(flock) { stop = null; flock.controlsTexture = undefined; flock._joystickSource = undefined; + localStorage.removeItem(COLLAPSED_KEY); }); describe('guard clause', function () { @@ -207,14 +230,16 @@ export function runGizmoMobileHudTests(flock) { make({ initialAxis: 'x', getValues: () => ({ x: 0, y: 0, z: 0 }) }); const canvas = flock.canvas; const rect = canvas.getBoundingClientRect(); - canvas.dispatchEvent( - new PointerEvent('pointerdown', { - pointerId: 2, - clientX: rect.left + rect.width / 4 + 30, - clientY: rect.bottom - 10, - }) - ); - canvas.dispatchEvent(new PointerEvent('pointerup', { pointerId: 2 })); + withoutPointerCapture(canvas, () => { + canvas.dispatchEvent( + new PointerEvent('pointerdown', { + pointerId: 2, + clientX: rect.left + rect.width / 4 + 30, + clientY: rect.bottom - 10, + }) + ); + canvas.dispatchEvent(new PointerEvent('pointerup', { pointerId: 2 })); + }); expect(moves.length).to.equal(1); expect(moves[0][0]).to.be.above(0); }); @@ -223,14 +248,16 @@ export function runGizmoMobileHudTests(flock) { make({ initialAxis: 'x', getValues: () => ({ x: 0, y: 0, z: 0 }) }); const canvas = flock.canvas; const rect = canvas.getBoundingClientRect(); - canvas.dispatchEvent( - new PointerEvent('pointerdown', { - pointerId: 3, - clientX: rect.left + rect.width / 4 - 30, - clientY: rect.bottom - 10, - }) - ); - canvas.dispatchEvent(new PointerEvent('pointerup', { pointerId: 3 })); + withoutPointerCapture(canvas, () => { + canvas.dispatchEvent( + new PointerEvent('pointerdown', { + pointerId: 3, + clientX: rect.left + rect.width / 4 - 30, + clientY: rect.bottom - 10, + }) + ); + canvas.dispatchEvent(new PointerEvent('pointerup', { pointerId: 3 })); + }); expect(moves.length).to.equal(1); expect(moves[0][0]).to.be.below(0); }); @@ -249,5 +276,106 @@ export function runGizmoMobileHudTests(flock) { expect(moves).to.deep.equal([]); }); }); + + describe('collapse handle', function () { + it('creates the handle in slider mode', function () { + make(); + expect(findControl(flock, 'gizmo-hud-toggle')).to.exist; + }); + + it('creates the handle in arrows mode', function () { + make({ mode: 'arrows' }); + expect(findControl(flock, 'gizmo-hud-toggle')).to.exist; + }); + + it('starts expanded by default', function () { + make(); + expect(stop.isCollapsed()).to.equal(false); + expect(findControl(flock, 'gizmoHudContainer').isVisible).to.equal(true); + }); + + it('tapping the handle hides the strip but keeps the handle visible', function () { + make(); + const handle = findControl(flock, 'gizmo-hud-toggle'); + handle.onPointerUpObservable.notifyObservers(); + expect(findControl(flock, 'gizmoHudContainer').isVisible).to.equal(false); + expect(handle.isVisible).to.equal(true); + }); + + it('tapping the handle again restores the strip', function () { + make(); + const handle = findControl(flock, 'gizmo-hud-toggle'); + handle.onPointerUpObservable.notifyObservers(); + handle.onPointerUpObservable.notifyObservers(); + expect(findControl(flock, 'gizmoHudContainer').isVisible).to.equal(true); + }); + + it('stop.toggleCollapsed() reports and applies the new state', function () { + make(); + expect(stop.toggleCollapsed()).to.equal(true); + expect(stop.isCollapsed()).to.equal(true); + expect(stop.toggleCollapsed()).to.equal(false); + }); + + it('persists the collapsed state to localStorage', function () { + make(); + stop.toggleCollapsed(); + expect(localStorage.getItem(COLLAPSED_KEY)).to.equal('1'); + }); + + it('a HUD built while the preference is set starts collapsed', function () { + localStorage.setItem(COLLAPSED_KEY, '1'); + make(); + expect(stop.isCollapsed()).to.equal(true); + expect(findControl(flock, 'gizmoHudContainer').isVisible).to.equal(false); + }); + + it('does not claim slider touches while collapsed', function () { + // The slider hit-tests canvas geometry rather than using GUI + // hit-testing, so hiding the strip alone would still swallow the + // camera drag in that corner. + make({ initialAxis: 'x', getValues: () => ({ x: 0, y: 0, z: 0 }) }); + stop.toggleCollapsed(); + const canvas = flock.canvas; + const rect = canvas.getBoundingClientRect(); + withoutPointerCapture(canvas, () => { + canvas.dispatchEvent( + new PointerEvent('pointerdown', { + pointerId: 5, + clientX: rect.left + rect.width / 4 + 30, + clientY: rect.bottom - 10, + }) + ); + canvas.dispatchEvent(new PointerEvent('pointerup', { pointerId: 5 })); + }); + expect(moves).to.deep.equal([]); + }); + + it('claims slider touches again after expanding', function () { + make({ initialAxis: 'x', getValues: () => ({ x: 0, y: 0, z: 0 }) }); + stop.toggleCollapsed(); + stop.toggleCollapsed(); + const canvas = flock.canvas; + const rect = canvas.getBoundingClientRect(); + withoutPointerCapture(canvas, () => { + canvas.dispatchEvent( + new PointerEvent('pointerdown', { + pointerId: 6, + clientX: rect.left + rect.width / 4 + 30, + clientY: rect.bottom - 10, + }) + ); + canvas.dispatchEvent(new PointerEvent('pointerup', { pointerId: 6 })); + }); + expect(moves.length).to.equal(1); + }); + + it('toggleCollapsed after stop() does not throw', function () { + make(); + stop(); + expect(() => stop.toggleCollapsed()).to.not.throw(); + stop = null; + }); + }); }); } diff --git a/ui/gizmo-mobile-hud.js b/ui/gizmo-mobile-hud.js index 3fcfbebd..4d207487 100644 --- a/ui/gizmo-mobile-hud.js +++ b/ui/gizmo-mobile-hud.js @@ -2,6 +2,42 @@ import { flock } from '../flock.js'; const fontFamily = 'Atkinson Hyperlegible Next'; +// Collapsed/expanded survives tool switches, canvas rebuilds and reloads — +// re-hiding on every tool switch would make the toggle pointless. +// Font Awesome Free 6.7.2 "gears" (solid) by @fontawesome — https://fontawesome.com +// License: https://fontawesome.com/license/free — Copyright 2025 Fonticons, Inc. +// Inlined rather than imported: @fortawesome is only a transitive dependency here, +// and index.html embeds its icons the same way. +const GEARS_PATH = + 'M308.5 135.3c7.1-6.3 9.9-16.2 6.2-25c-2.3-5.3-4.8-10.5-7.6-15.5L304 89.4c-3-5-6.3-9.9-9.8-14.6c-5.7-7.6-15.7-10.1-24.7-7.1l-28.2 9.3c-10.7-8.8-23-16-36.2-20.9L199 27.1c-1.9-9.3-9.1-16.7-18.5-17.8C173.9 8.4 167.2 8 160.4 8l-.7 0c-6.8 0-13.5 .4-20.1 1.2c-9.4 1.1-16.6 8.6-18.5 17.8L115 56.1c-13.3 5-25.5 12.1-36.2 20.9L50.5 67.8c-9-3-19-.5-24.7 7.1c-3.5 4.7-6.8 9.6-9.9 14.6l-3 5.3c-2.8 5-5.3 10.2-7.6 15.6c-3.7 8.7-.9 18.6 6.2 25l22.2 19.8C32.6 161.9 32 168.9 32 176s.6 14.1 1.7 20.9L11.5 216.7c-7.1 6.3-9.9 16.2-6.2 25c2.3 5.3 4.8 10.5 7.6 15.6l3 5.2c3 5.1 6.3 9.9 9.9 14.6c5.7 7.6 15.7 10.1 24.7 7.1l28.2-9.3c10.7 8.8 23 16 36.2 20.9l6.1 29.1c1.9 9.3 9.1 16.7 18.5 17.8c6.7 .8 13.5 1.2 20.4 1.2s13.7-.4 20.4-1.2c9.4-1.1 16.6-8.6 18.5-17.8l6.1-29.1c13.3-5 25.5-12.1 36.2-20.9l28.2 9.3c9 3 19 .5 24.7-7.1c3.5-4.7 6.8-9.5 9.8-14.6l3.1-5.4c2.8-5 5.3-10.2 7.6-15.5c3.7-8.7 .9-18.6-6.2-25l-22.2-19.8c1.1-6.8 1.7-13.8 1.7-20.9s-.6-14.1-1.7-20.9l22.2-19.8zM112 176a48 48 0 1 1 96 0 48 48 0 1 1 -96 0zM504.7 500.5c6.3 7.1 16.2 9.9 25 6.2c5.3-2.3 10.5-4.8 15.5-7.6l5.4-3.1c5-3 9.9-6.3 14.6-9.8c7.6-5.7 10.1-15.7 7.1-24.7l-9.3-28.2c8.8-10.7 16-23 20.9-36.2l29.1-6.1c9.3-1.9 16.7-9.1 17.8-18.5c.8-6.7 1.2-13.5 1.2-20.4s-.4-13.7-1.2-20.4c-1.1-9.4-8.6-16.6-17.8-18.5L583.9 307c-5-13.3-12.1-25.5-20.9-36.2l9.3-28.2c3-9 .5-19-7.1-24.7c-4.7-3.5-9.6-6.8-14.6-9.9l-5.3-3c-5-2.8-10.2-5.3-15.6-7.6c-8.7-3.7-18.6-.9-25 6.2l-19.8 22.2c-6.8-1.1-13.8-1.7-20.9-1.7s-14.1 .6-20.9 1.7l-19.8-22.2c-6.3-7.1-16.2-9.9-25-6.2c-5.3 2.3-10.5 4.8-15.6 7.6l-5.2 3c-5.1 3-9.9 6.3-14.6 9.9c-7.6 5.7-10.1 15.7-7.1 24.7l9.3 28.2c-8.8 10.7-16 23-20.9 36.2L315.1 313c-9.3 1.9-16.7 9.1-17.8 18.5c-.8 6.7-1.2 13.5-1.2 20.4s.4 13.7 1.2 20.4c1.1 9.4 8.6 16.6 17.8 18.5l29.1 6.1c5 13.3 12.1 25.5 20.9 36.2l-9.3 28.2c-3 9-.5 19 7.1 24.7c4.7 3.5 9.5 6.8 14.6 9.8l5.4 3.1c5 2.8 10.2 5.3 15.5 7.6c8.7 3.7 18.6 .9 25-6.2l19.8-22.2c6.8 1.1 13.8 1.7 20.9 1.7s14.1-.6 20.9-1.7l19.8 22.2zM464 304a48 48 0 1 1 0 96 48 48 0 1 1 0-96z'; +const GEARS_ASPECT = 512 / 640; +// Babylon GUI images can't be tinted, so bake one data URI per fill. +const gearsIcon = (fill) => + 'data:image/svg+xml;charset=utf-8,' + + encodeURIComponent( + // width/height are required as well as viewBox: without an intrinsic size + // the browser hands Babylon a zero-sized image and the icon renders cropped. + `` + ); +const GEARS_DARK = gearsIcon('#1a1a1a'); +const GEARS_LIGHT = gearsIcon('#ffffff'); + +const COLLAPSED_KEY = 'flock-gizmo-hud-collapsed'; +const readCollapsed = () => { + try { + return localStorage.getItem(COLLAPSED_KEY) === '1'; + } catch { + return false; + } +}; +const writeCollapsed = (value) => { + try { + localStorage.setItem(COLLAPSED_KEY, value ? '1' : '0'); + } catch { + /* private mode / storage disabled — the session-local flag still works */ + } +}; + export function createGizmoMobileHud({ onMove, stepNormal, @@ -47,6 +83,10 @@ export function createGizmoMobileHud({ // Cap at the same size as the existing on-screen controls (70 * s), // but shrink if there isn't room for all axis buttons in the right half. const BTN_SIZE = Math.min(70 * s, (HALF - (numAxes + 1) * GAP) / numAxes); + // Sized against the buttons so it reads as part of the same set, with a floor + // that keeps it a usable touch target on the narrowest canvases. It lives in + // the opposite corner from the strip, so it costs the row no width. + const TOGGLE = Math.max(44 * s, Math.min(56 * s, BTN_SIZE)); const TOTAL_H = BTN_SIZE + 2 * GAP; // ── Transparent container ───────────────────────────────────────────────── @@ -61,6 +101,62 @@ export function createGizmoMobileHud({ container.isPointerBlocker = false; hudTexture.addControl(container); + // ── Collapse handle ─────────────────────────────────────────────────────── + // A sibling of the strip rather than a child, so collapsing is just + // container.isVisible = false — Babylon GUI skips hit-testing hidden + // controls — while the handle itself stays visible and tappable. + // Same on/off language as the gizmo toolbar right above the canvas: the icon + // never changes, the button does. Amber is --color-focus, which is what + // .gizmo-button.active uses. + const TOGGLE_ON = '#ffcc33'; + const TOGGLE_OFF = 'rgba(0,0,0,0.4)'; + + const handle = new flock.GUI.Rectangle('gizmo-hud-toggle'); + handle.width = `${TOGGLE}px`; + handle.height = `${TOGGLE}px`; + handle.cornerRadius = 8 * s; + handle.color = 'white'; + handle.thickness = 3 * s; + handle.isPointerBlocker = true; + handle.horizontalAlignment = flock.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; + handle.verticalAlignment = flock.GUI.Control.VERTICAL_ALIGNMENT_TOP; + // Top-left, clear of the strip entirely, so it holds the same spot whether + // the controls are showing or not. + handle.left = `${GAP}px`; + handle.top = `${GAP}px`; + hudTexture.addControl(handle); + + const gears = new flock.GUI.Image('gizmo-hud-toggle-icon', GEARS_DARK); + gears.width = `${TOGGLE * 0.62}px`; + gears.height = `${TOGGLE * 0.62 * GEARS_ASPECT}px`; + gears.stretch = flock.GUI.Image.STRETCH_UNIFORM; + gears.isPointerBlocker = false; + handle.addControl(gears); + + let collapsed = readCollapsed(); + // Cancel anything mid-gesture that would otherwise keep firing against + // controls the user can no longer see. + const collapseHooks = []; + + function applyCollapsed() { + container.isVisible = !collapsed; + // Lit means the controls are showing. + handle.background = collapsed ? TOGGLE_OFF : TOGGLE_ON; + gears.source = collapsed ? GEARS_LIGHT : GEARS_DARK; + if (collapsed) collapseHooks.forEach((fn) => fn()); + // The mouse gizmo can rotate the mesh while the strip is hidden, so the + // thumb has to be re-read from the mesh on the way back. + else refreshThumb?.(); + } + + function setCollapsed(next) { + collapsed = next; + writeCollapsed(collapsed); + applyCollapsed(); + } + + handle.onPointerUpObservable.add(() => setCollapsed(!collapsed)); + // ── Axis buttons (right half, single horizontal row) ────────────────────── const axisButtons = {}; AXIS_DEFS.forEach(({ key, label }, i) => { @@ -200,6 +296,7 @@ export function createGizmoMobileHud({ btn.onPointerOutObservable.add(stopRepeat); cleanups.push(stopRepeat); + collapseHooks.push(stopRepeat); return btn; } @@ -336,6 +433,10 @@ export function createGizmoMobileHud({ } function onPointerDown(e) { + // The slider hit-tests geometry on the canvas rather than using GUI + // hit-testing, so hiding the strip isn't enough to stop it claiming + // touches in that corner — it has to be switched off explicitly. + if (collapsed) return; if (activePointer !== null) return; const b = sliderBounds(); if (e.clientY < b.top || e.clientY > b.bottom) return; @@ -389,8 +490,17 @@ export function createGizmoMobileHud({ canvas.addEventListener('pointerup', onPointerUp); canvas.addEventListener('pointercancel', onPointerUp); + // Collapsing mid-drag has to release the pointer, or activePointer stays + // set and the slider ignores the first touch after expanding again. + collapseHooks.push(() => { + activePointer = null; + lastSnappedGUI = null; + thumb.background = 'rgba(255,255,255,0.85)'; + }); + // Block camera/scene for slider touches — including the initial pointerdown const prePointerObserver = flock.scene.onPrePointerObservable.add((info) => { + if (collapsed) return; const e = info.event; if (activePointer !== null && e.pointerId === activePointer) { info.skipOnPointerObservable = true; @@ -418,6 +528,8 @@ export function createGizmoMobileHud({ }); } + applyCollapsed(); + // ── Stop / cleanup ──────────────────────────────────────────────────────── let stopped = false; function stop() { @@ -428,6 +540,12 @@ export function createGizmoMobileHud({ if (savedControls) savedControls.rootContainer.isVisible = true; if (flock._joystickSource) flock._joystickSource.resume(); } + stop.toggleCollapsed = () => { + if (stopped) return collapsed; + setCollapsed(!collapsed); + return collapsed; + }; + stop.isCollapsed = () => collapsed; stop.setAxis = (newAxis) => { if (stopped) return; const def = AXIS_DEFS.find((d) => d.key === newAxis); diff --git a/ui/gizmos.js b/ui/gizmos.js index 1197eca4..ea38c130 100644 --- a/ui/gizmos.js +++ b/ui/gizmos.js @@ -189,6 +189,7 @@ function createAdaptiveInput({ onAxisChange?.(axis); }; stop.getAxis = () => keyboard?.getAxis?.() ?? null; + stop.toggleHud = () => hud?.toggleCollapsed?.(); return stop; } @@ -220,6 +221,20 @@ function registerBindings() { toggleGizmo('eye'); }) ); + // Show/hide the on-screen transform controls with O + KeyboardDispatcher.on( + 'GIZMO', + 'KeyO', + noMod((e) => { + if (!stopAxisKeyboard?.toggleHud) return; + e.preventDefault(); + const collapsed = stopAxisKeyboard.toggleHud(); + showStatus(translate(collapsed ? 'hud_hidden' : 'hud_shown'), { + duration: 3, + owner: 'hud', + }); + }) + ); // Delete selected mesh with Del key KeyboardDispatcher.on('GIZMO', 'Delete', (e) => { if (!gizmoManager?.attachedMesh) return; From 1ceb15114e50f614358e17d7f4a4548224305ee7 Mon Sep 17 00:00:00 2001 From: Laura Sach <5183697+lawsie@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:41:56 +0100 Subject: [PATCH 2/2] Remove Claude comment mess --- ui/gizmo-mobile-hud.js | 28 +++------------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/ui/gizmo-mobile-hud.js b/ui/gizmo-mobile-hud.js index 4d207487..2612122b 100644 --- a/ui/gizmo-mobile-hud.js +++ b/ui/gizmo-mobile-hud.js @@ -2,21 +2,14 @@ import { flock } from '../flock.js'; const fontFamily = 'Atkinson Hyperlegible Next'; -// Collapsed/expanded survives tool switches, canvas rebuilds and reloads — -// re-hiding on every tool switch would make the toggle pointless. // Font Awesome Free 6.7.2 "gears" (solid) by @fontawesome — https://fontawesome.com // License: https://fontawesome.com/license/free — Copyright 2025 Fonticons, Inc. -// Inlined rather than imported: @fortawesome is only a transitive dependency here, -// and index.html embeds its icons the same way. const GEARS_PATH = 'M308.5 135.3c7.1-6.3 9.9-16.2 6.2-25c-2.3-5.3-4.8-10.5-7.6-15.5L304 89.4c-3-5-6.3-9.9-9.8-14.6c-5.7-7.6-15.7-10.1-24.7-7.1l-28.2 9.3c-10.7-8.8-23-16-36.2-20.9L199 27.1c-1.9-9.3-9.1-16.7-18.5-17.8C173.9 8.4 167.2 8 160.4 8l-.7 0c-6.8 0-13.5 .4-20.1 1.2c-9.4 1.1-16.6 8.6-18.5 17.8L115 56.1c-13.3 5-25.5 12.1-36.2 20.9L50.5 67.8c-9-3-19-.5-24.7 7.1c-3.5 4.7-6.8 9.6-9.9 14.6l-3 5.3c-2.8 5-5.3 10.2-7.6 15.6c-3.7 8.7-.9 18.6 6.2 25l22.2 19.8C32.6 161.9 32 168.9 32 176s.6 14.1 1.7 20.9L11.5 216.7c-7.1 6.3-9.9 16.2-6.2 25c2.3 5.3 4.8 10.5 7.6 15.6l3 5.2c3 5.1 6.3 9.9 9.9 14.6c5.7 7.6 15.7 10.1 24.7 7.1l28.2-9.3c10.7 8.8 23 16 36.2 20.9l6.1 29.1c1.9 9.3 9.1 16.7 18.5 17.8c6.7 .8 13.5 1.2 20.4 1.2s13.7-.4 20.4-1.2c9.4-1.1 16.6-8.6 18.5-17.8l6.1-29.1c13.3-5 25.5-12.1 36.2-20.9l28.2 9.3c9 3 19 .5 24.7-7.1c3.5-4.7 6.8-9.5 9.8-14.6l3.1-5.4c2.8-5 5.3-10.2 7.6-15.5c3.7-8.7 .9-18.6-6.2-25l-22.2-19.8c1.1-6.8 1.7-13.8 1.7-20.9s-.6-14.1-1.7-20.9l22.2-19.8zM112 176a48 48 0 1 1 96 0 48 48 0 1 1 -96 0zM504.7 500.5c6.3 7.1 16.2 9.9 25 6.2c5.3-2.3 10.5-4.8 15.5-7.6l5.4-3.1c5-3 9.9-6.3 14.6-9.8c7.6-5.7 10.1-15.7 7.1-24.7l-9.3-28.2c8.8-10.7 16-23 20.9-36.2l29.1-6.1c9.3-1.9 16.7-9.1 17.8-18.5c.8-6.7 1.2-13.5 1.2-20.4s-.4-13.7-1.2-20.4c-1.1-9.4-8.6-16.6-17.8-18.5L583.9 307c-5-13.3-12.1-25.5-20.9-36.2l9.3-28.2c3-9 .5-19-7.1-24.7c-4.7-3.5-9.6-6.8-14.6-9.9l-5.3-3c-5-2.8-10.2-5.3-15.6-7.6c-8.7-3.7-18.6-.9-25 6.2l-19.8 22.2c-6.8-1.1-13.8-1.7-20.9-1.7s-14.1 .6-20.9 1.7l-19.8-22.2c-6.3-7.1-16.2-9.9-25-6.2c-5.3 2.3-10.5 4.8-15.6 7.6l-5.2 3c-5.1 3-9.9 6.3-14.6 9.9c-7.6 5.7-10.1 15.7-7.1 24.7l9.3 28.2c-8.8 10.7-16 23-20.9 36.2L315.1 313c-9.3 1.9-16.7 9.1-17.8 18.5c-.8 6.7-1.2 13.5-1.2 20.4s.4 13.7 1.2 20.4c1.1 9.4 8.6 16.6 17.8 18.5l29.1 6.1c5 13.3 12.1 25.5 20.9 36.2l-9.3 28.2c-3 9-.5 19 7.1 24.7c4.7 3.5 9.5 6.8 14.6 9.8l5.4 3.1c5 2.8 10.2 5.3 15.5 7.6c8.7 3.7 18.6 .9 25-6.2l19.8-22.2c6.8 1.1 13.8 1.7 20.9 1.7s14.1-.6 20.9-1.7l19.8 22.2zM464 304a48 48 0 1 1 0 96 48 48 0 1 1 0-96z'; const GEARS_ASPECT = 512 / 640; -// Babylon GUI images can't be tinted, so bake one data URI per fill. const gearsIcon = (fill) => 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent( - // width/height are required as well as viewBox: without an intrinsic size - // the browser hands Babylon a zero-sized image and the icon renders cropped. `` ); const GEARS_DARK = gearsIcon('#1a1a1a'); @@ -81,11 +74,7 @@ export function createGizmoMobileHud({ const HALF = canvas.width / 2; const BOTTOM_PADDING = 24 * s; // Cap at the same size as the existing on-screen controls (70 * s), - // but shrink if there isn't room for all axis buttons in the right half. const BTN_SIZE = Math.min(70 * s, (HALF - (numAxes + 1) * GAP) / numAxes); - // Sized against the buttons so it reads as part of the same set, with a floor - // that keeps it a usable touch target on the narrowest canvases. It lives in - // the opposite corner from the strip, so it costs the row no width. const TOGGLE = Math.max(44 * s, Math.min(56 * s, BTN_SIZE)); const TOTAL_H = BTN_SIZE + 2 * GAP; @@ -101,13 +90,7 @@ export function createGizmoMobileHud({ container.isPointerBlocker = false; hudTexture.addControl(container); - // ── Collapse handle ─────────────────────────────────────────────────────── - // A sibling of the strip rather than a child, so collapsing is just - // container.isVisible = false — Babylon GUI skips hit-testing hidden - // controls — while the handle itself stays visible and tappable. - // Same on/off language as the gizmo toolbar right above the canvas: the icon - // never changes, the button does. Amber is --color-focus, which is what - // .gizmo-button.active uses. + // Same as gizmo highlight color const TOGGLE_ON = '#ffcc33'; const TOGGLE_OFF = 'rgba(0,0,0,0.4)'; @@ -120,9 +103,7 @@ export function createGizmoMobileHud({ handle.isPointerBlocker = true; handle.horizontalAlignment = flock.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; handle.verticalAlignment = flock.GUI.Control.VERTICAL_ALIGNMENT_TOP; - // Top-left, clear of the strip entirely, so it holds the same spot whether - // the controls are showing or not. - handle.left = `${GAP}px`; + handle.left = `${GAP}px`; // Top-left to save space handle.top = `${GAP}px`; hudTexture.addControl(handle); @@ -140,7 +121,6 @@ export function createGizmoMobileHud({ function applyCollapsed() { container.isVisible = !collapsed; - // Lit means the controls are showing. handle.background = collapsed ? TOGGLE_OFF : TOGGLE_ON; gears.source = collapsed ? GEARS_LIGHT : GEARS_DARK; if (collapsed) collapseHooks.forEach((fn) => fn()); @@ -433,9 +413,7 @@ export function createGizmoMobileHud({ } function onPointerDown(e) { - // The slider hit-tests geometry on the canvas rather than using GUI - // hit-testing, so hiding the strip isn't enough to stop it claiming - // touches in that corner — it has to be switched off explicitly. + // Turn off slider explicitly when collapsed if (collapsed) return; if (activePointer !== null) return; const b = sliderBounds();