From d62a86fbe72827577e5f29cd19243ef609bb8dbd Mon Sep 17 00:00:00 2001 From: PozzettiAndrea Date: Sun, 7 Jun 2026 09:00:00 +0200 Subject: [PATCH] better controls --- web/js/gaussian_preview.js | 91 +++++++++++++++++++++++++++++++++-- web/viewer_gaussian.html | 24 +++++++-- web/viewer_gaussian_dual.html | 15 ++++++ workflows/spectate.json | 2 +- 4 files changed, 125 insertions(+), 7 deletions(-) diff --git a/web/js/gaussian_preview.js b/web/js/gaussian_preview.js index 514f446..95f14ea 100644 --- a/web/js/gaussian_preview.js +++ b/web/js/gaussian_preview.js @@ -15,6 +15,63 @@ const EXTENSION_FOLDER = (() => { console.log("[GaussianPack] Loading extension..."); +// Build a small WASD / arrows / Shift+Space+C keypad that lights up as keys +// are pressed (driven by KEY_STATE messages the viewer iframe forwards). Handy +// for screen-recording navigation. Returns { el, set(code,on), reset() }. +function makeKeyPad() { + const codes = {}; + const wrap = document.createElement("div"); + Object.assign(wrap.style, { + display: "flex", gap: "10px", alignItems: "center", + padding: "4px 10px", flexShrink: "0", background: "#1a1a1a", + borderTop: "1px solid #444", borderLeft: "1px solid #333", + }); + const keyEl = (label, code, w = 18) => { + const k = document.createElement("div"); + k.textContent = label; + Object.assign(k.style, { + minWidth: w + "px", height: "18px", lineHeight: "16px", textAlign: "center", + fontSize: "10px", fontFamily: "monospace", color: "#aaa", + background: "#2a2a2a", border: "1px solid #444", borderRadius: "3px", + padding: "0 3px", boxSizing: "border-box", transition: "all 0.04s", + }); + codes[code] = k; + return k; + }; + const grid = (rows) => { + const g = document.createElement("div"); + Object.assign(g.style, { display: "grid", gridTemplateColumns: "repeat(3, 18px)", gap: "2px" }); + rows.forEach(r => r.forEach(cell => g.appendChild(cell || document.createElement("div")))); + return g; + }; + const wasd = grid([ + [null, keyEl("W", "KeyW"), null], + [keyEl("A", "KeyA"), keyEl("S", "KeyS"), keyEl("D", "KeyD")], + ]); + const arrows = grid([ + [null, keyEl("↑", "ArrowUp"), null], + [keyEl("←", "ArrowLeft"), keyEl("↓", "ArrowDown"), keyEl("→", "ArrowRight")], + ]); + const extra = document.createElement("div"); + Object.assign(extra.style, { display: "flex", gap: "2px", alignItems: "center" }); + const shift = keyEl("⇧", "ShiftLeft", 24); + codes["ShiftRight"] = shift; // both shifts light the same key + extra.appendChild(shift); + extra.appendChild(keyEl("Spc", "Space", 26)); + extra.appendChild(keyEl("C", "KeyC")); + wrap.append(wasd, arrows, extra); + + const set = (code, on) => { + const el = codes[code]; + if (!el) return; + el.style.background = on ? "#00e0ff" : "#2a2a2a"; + el.style.color = on ? "#002b30" : "#aaa"; + el.style.borderColor = on ? "#00e0ff" : "#444"; + }; + const reset = () => Object.keys(codes).forEach(c => set(c, false)); + return { el: wrap, set, reset }; +} + app.registerExtension({ name: "gaussianpack.previewgaussians", @@ -302,10 +359,19 @@ app.registerExtension({ infoPanel.style.overflow = "hidden"; infoPanel.innerHTML = 'Gaussian splat info will appear here after execution'; - // Add iframe + info panel + loading-bar overlay to container. + // Footer = info panel (left, grows) + key-press indicator (right). + const keyPad = makeKeyPad(); + infoPanel.style.flex = "1 1 auto"; + const footer = document.createElement("div"); + Object.assign(footer.style, { display: "flex", alignItems: "stretch", flexShrink: "0" }); + footer.appendChild(infoPanel); + footer.appendChild(keyPad.el); + this.gaussianKeyPad = keyPad; + + // Add iframe + footer + loading-bar overlay to container. // loadBar last so its overlay sits above the iframe. container.appendChild(iframe); - container.appendChild(infoPanel); + container.appendChild(footer); container.appendChild(loadBar); // --- Persistent camera-state plumbing --------------------- @@ -397,6 +463,12 @@ app.registerExtension({ // node's state. if (event.source !== iframe.contentWindow) return; + // Key-press indicator: light up the footer keypad. + if (event.data?.type === "KEY_STATE") { + this.gaussianKeyPad?.set(event.data.code, !!event.data.down); + return; + } + // Camera-state pushes from iframe -> persist on // node.properties["Camera Config"] (Load3D pattern). if (event.data?.type === "CAMERA_STATE" && event.data.state) { @@ -752,8 +824,17 @@ app.registerExtension({ infoPanel.style.cssText = "background:#1a1a1a;border-top:1px solid #444;padding:6px 12px;font:10px monospace;color:#ccc;line-height:1.3;flex-shrink:0;overflow:hidden;"; infoPanel.innerHTML = 'Dual view info will appear after execution'; + // Footer = info panel (left) + key-press indicator (right). + const keyPad = makeKeyPad(); + infoPanel.style.flex = "1 1 auto"; + const footer = document.createElement("div"); + Object.assign(footer.style, { display: "flex", alignItems: "stretch", flexShrink: "0" }); + footer.appendChild(infoPanel); + footer.appendChild(keyPad.el); + this.gaussianKeyPad = keyPad; + container.appendChild(iframe); - container.appendChild(infoPanel); + container.appendChild(footer); container.appendChild(lb1.bar); container.appendChild(lb2.bar); @@ -775,6 +856,10 @@ app.registerExtension({ this.properties = this.properties || {}; window.addEventListener('message', (event) => { if (event.source !== iframe.contentWindow) return; + if (event.data?.type === "KEY_STATE") { + this.gaussianKeyPad?.set(event.data.code, !!event.data.down); + return; + } if (event.data?.type === "CAMERA_STATE" && event.data.state) { node.properties["Camera Config"] = { cameraType: "perspective", diff --git a/web/viewer_gaussian.html b/web/viewer_gaussian.html index aa1e9e9..c2e513f 100644 --- a/web/viewer_gaussian.html +++ b/web/viewer_gaussian.html @@ -257,6 +257,18 @@ const VIEWER_MODE = _urlParams.get("mode") === "spectate" ? "spectate" : "trackball"; const SPECTATE_INITIAL_SPEED = parseFloat(_urlParams.get("speed") || "2.0"); + // Forward navigation key state to the parent node so it can light up its + // on-screen keypad indicator (useful for screen recordings). + (() => { + const _down = new Set(); + const fwd = (code, down) => { + try { window.parent.postMessage({ type: "KEY_STATE", code, down }, "*"); } catch (_) {} + }; + window.addEventListener("keydown", (e) => { if (!_down.has(e.code)) { _down.add(e.code); fwd(e.code, true); } }); + window.addEventListener("keyup", (e) => { if (_down.delete(e.code)) fwd(e.code, false); }); + window.addEventListener("blur", () => { _down.forEach(c => fwd(c, false)); _down.clear(); }); + })(); + // ============================================================ // SpectateControls — TrackballControls-compatible API for fly-cam. // @@ -390,6 +402,9 @@ this._scratchFwd.set(0, 0, -1).applyQuaternion(this.camera.quaternion); this._scratchRight.set(1, 0, 0).applyQuaternion(this.camera.quaternion); + // Up/down is relative to the camera's CURRENT orientation, not a + // fixed world axis, so Space/C follow where you're looking now. + this._scratchUp.set(0, 1, 0).applyQuaternion(this.camera.quaternion); this._scratchMove.set(0, 0, 0); if (this._keys.has("KeyW")) this._scratchMove.add(this._scratchFwd); @@ -948,18 +963,21 @@ const fwd = new pc.Vec3(); const right = new pc.Vec3(); + const up = new pc.Vec3(); cam.getWorldTransform().getZ(fwd); cam.getWorldTransform().getX(right); + cam.getWorldTransform().getY(up); // camera's CURRENT up fwd.mulScalar(-1); - // WASD: move camera + // WASD: move camera. Space/C use the camera's current up axis, so + // up/down is relative to where you're looking now, not world Y. let mx = 0, my = 0, mz = 0; if (this._keys.has("KeyW")) { mx += fwd.x; my += fwd.y; mz += fwd.z; } if (this._keys.has("KeyS")) { mx -= fwd.x; my -= fwd.y; mz -= fwd.z; } if (this._keys.has("KeyD")) { mx += right.x; my += right.y; mz += right.z; } if (this._keys.has("KeyA")) { mx -= right.x; my -= right.y; mz -= right.z; } - if (this._keys.has("Space")) { my += 1; } - if (this._keys.has("KeyC")) { my -= 1; } + if (this._keys.has("Space")) { mx += up.x; my += up.y; mz += up.z; } + if (this._keys.has("KeyC")) { mx -= up.x; my -= up.y; mz -= up.z; } const lenSq = mx*mx + my*my + mz*mz; if (lenSq > 0) { diff --git a/web/viewer_gaussian_dual.html b/web/viewer_gaussian_dual.html index 54a2c6a..3999092 100644 --- a/web/viewer_gaussian_dual.html +++ b/web/viewer_gaussian_dual.html @@ -111,6 +111,18 @@ const LAYOUT = params.get("layout") || "side_by_side"; const MODE = params.get("mode") === "spectate" ? "spectate" : "orbit"; const SPECTATE_INITIAL_SPEED = parseFloat(params.get("speed") || "2.0"); +// Forward navigation key state to the parent node so it can light up its +// on-screen keypad indicator (useful for screen recordings). +(() => { + const _down = new Set(); + const fwd = (code, down) => { + try { window.parent.postMessage({ type: "KEY_STATE", code, down }, "*"); } catch (_) {} + }; + window.addEventListener("keydown", (e) => { if (!_down.has(e.code)) { _down.add(e.code); fwd(e.code, true); } }); + window.addEventListener("keyup", (e) => { if (_down.delete(e.code)) fwd(e.code, false); }); + window.addEventListener("blur", () => { _down.forEach(c => fwd(c, false)); _down.clear(); }); +})(); + const container = document.getElementById('viewerContainer'); const vp1 = document.getElementById('vp1'); const vp2 = document.getElementById('vp2'); @@ -420,6 +432,9 @@ class SpectateControls { this._fwd.set(0, 0, -1).applyQuaternion(this.camera.quaternion); this._right.set(1, 0, 0).applyQuaternion(this.camera.quaternion); + // Up/down relative to the camera's CURRENT orientation, not a fixed + // world axis, so Space/C follow where you're looking now. + this._up.set(0, 1, 0).applyQuaternion(this.camera.quaternion); this._move.set(0, 0, 0); if (this._keys.has("KeyW")) this._move.add(this._fwd); if (this._keys.has("KeyS")) this._move.sub(this._fwd); diff --git a/workflows/spectate.json b/workflows/spectate.json index d23d5b3..509c865 100644 --- a/workflows/spectate.json +++ b/workflows/spectate.json @@ -1 +1 @@ -{"id":"6662828f-82cb-4faa-8977-cd10a1b9d52b","revision":0,"last_node_id":2,"last_link_id":1,"nodes":[{"id":1,"type":"LoadPLY","pos":[-143.84952498754797,190.94188881558176],"size":[270,58],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"ply_file","name":"ply_file","type":"COMBO","widget":{"name":"ply_file"},"link":null}],"outputs":[{"localized_name":"ply_path","name":"ply_path","type":"STRING","links":[1]}],"properties":{"cnr_id":"ComfyUI-GaussianPack","ver":"aba5d58066450088421d87a680e57ae599d92a1b","Node name for S&R":"LoadPLY"},"widgets_values":["compressed_room.ply"]},{"id":2,"type":"PreviewGaussians","pos":[164.8117698509801,184.28127124793232],"size":[512,714],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"ply_path","name":"ply_path","type":"STRING","link":1},{"localized_name":"camera_mode","name":"camera_mode","type":"COMBO","widget":{"name":"camera_mode"},"link":null},{"localized_name":"fov_degrees","name":"fov_degrees","type":"FLOAT","widget":{"name":"fov_degrees"},"link":null},{"localized_name":"renderer","name":"renderer","type":"COMBO","widget":{"name":"renderer"},"link":null},{"localized_name":"transport_format","name":"transport_format","type":"COMBO","widget":{"name":"transport_format"},"link":null}],"outputs":[],"properties":{"cnr_id":"ComfyUI-GaussianPack","ver":"aba5d58066450088421d87a680e57ae599d92a1b","Node name for S&R":"PreviewGaussians","Widget Values":{"camera_mode":"spectate","fov_degrees":70,"renderer":"playcanvas","transport_format":"ply"},"Camera Config":{"cameraType":"perspective","fov":50,"state":"{\"v\":1,\"pos\":[0.5147809982299805,-0.3441455066204071,-0.2638316750526428],\"target\":[0,0,1],\"fov\":50,\"scale\":20,\"opacity\":1}"}},"widgets_values":["spectate",70,"playcanvas","ply",""]}],"links":[[1,1,0,2,0,"STRING"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.7970122433967322,"offset":[526.1427860582855,186.55364205821778]}},"version":0.4} \ No newline at end of file +{"id":"6662828f-82cb-4faa-8977-cd10a1b9d52b","revision":0,"last_node_id":2,"last_link_id":1,"nodes":[{"id":1,"type":"LoadPLY","pos":[-143.84952498754797,190.94188881558176],"size":[270,58],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"ply_file","name":"ply_file","type":"COMBO","widget":{"name":"ply_file"},"link":null}],"outputs":[{"localized_name":"ply_path","name":"ply_path","type":"STRING","links":[1]}],"properties":{"cnr_id":"ComfyUI-GaussianPack","ver":"aba5d58066450088421d87a680e57ae599d92a1b","Node name for S&R":"LoadPLY"},"widgets_values":["compressed_room.ply"]},{"id":2,"type":"PreviewGaussians","pos":[164.8117698509801,184.28127124793232],"size":[807.3472077244259,714],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"ply_path","name":"ply_path","type":"STRING","link":1},{"localized_name":"camera_mode","name":"camera_mode","type":"COMBO","widget":{"name":"camera_mode"},"link":null},{"localized_name":"fov_degrees","name":"fov_degrees","type":"FLOAT","widget":{"name":"fov_degrees"},"link":null},{"localized_name":"renderer","name":"renderer","type":"COMBO","widget":{"name":"renderer"},"link":null},{"localized_name":"transport_format","name":"transport_format","type":"COMBO","widget":{"name":"transport_format"},"link":null}],"outputs":[],"properties":{"cnr_id":"ComfyUI-GaussianPack","ver":"aba5d58066450088421d87a680e57ae599d92a1b","Node name for S&R":"PreviewGaussians","Widget Values":{"camera_mode":"spectate","fov_degrees":70,"renderer":"playcanvas","transport_format":"ply"},"Camera Config":{"cameraType":"perspective","fov":50,"state":"{\"v\":1,\"pos\":[-2.3301117420196533,0.8196620941162109,1.912135362625122],\"target\":[0,0,1],\"fov\":50,\"scale\":20,\"opacity\":1}"}},"widgets_values":["spectate",70,"playcanvas","ply",""]}],"links":[[1,1,0,2,0,"STRING"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.9404450261780105,"offset":[314.09923160447045,0.2480432962569592]}},"version":0.4} \ No newline at end of file