From 896407fdc3b5594efa496b5b89261e60cb4b472b Mon Sep 17 00:00:00 2001 From: Donovan Hutchence Date: Wed, 17 Jul 2024 15:10:28 +0100 Subject: [PATCH 1/2] latest --- package-lock.json | 12 +- package.json | 4 +- src/camera.ts | 4 +- src/controllers.ts | 6 - src/file-handler.ts | 4 +- src/infinite-grid.ts | 209 ++++++++++++++++++++++++++++++++++ src/main.ts | 6 +- src/scene.ts | 3 +- src/splat.ts | 67 ++++++++++- src/style.scss | 19 +--- src/tools/brush-selection.ts | 21 ++-- src/tools/picker-selection.ts | 12 +- src/tools/rect-selection.ts | 24 ++-- src/ui/data-panel.ts | 10 +- src/ui/editor.ts | 18 ++- src/ui/histogram.ts | 8 +- src/ui/popup.ts | 6 +- 17 files changed, 348 insertions(+), 85 deletions(-) create mode 100644 src/infinite-grid.ts diff --git a/package-lock.json b/package-lock.json index 498e0a06..05b3d872 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "supersplat", - "version": "0.22.2", + "version": "0.23.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "supersplat", - "version": "0.22.2", + "version": "0.23.0", "license": "MIT", "devDependencies": { "@playcanvas/eslint-config": "^1.7.1", @@ -26,7 +26,7 @@ "cross-env": "^7.0.3", "eslint": "^8.56.0", "jest": "^29.7.0", - "playcanvas": "^1.72.1", + "playcanvas": "^1.73.0", "rollup": "^4.18.0", "rollup-plugin-sass": "^1.13.0", "rollup-plugin-visualizer": "^5.12.0", @@ -6299,9 +6299,9 @@ } }, "node_modules/playcanvas": { - "version": "1.72.1", - "resolved": "https://registry.npmjs.org/playcanvas/-/playcanvas-1.72.1.tgz", - "integrity": "sha512-I6mx9wzi5yTCU+YLPI0S876R0Kf9K/t554/u7r2syRd4+Or/QjBqu6YJYtTMcSFtp2s0gtVjwVZKHcsg//NtvQ==", + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/playcanvas/-/playcanvas-1.73.0.tgz", + "integrity": "sha512-XYGu9IRHJyg38bwMwSgEMDXqfmXuZPLGBBv6P1udj9V7JzFL0kmT+2VEtejg+oecYNyvCEtie0LkG4bsrJSXrA==", "dev": true, "dependencies": { "@types/webxr": "^0.5.16", diff --git a/package.json b/package.json index 07435e53..baee9e12 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "supersplat", - "version": "0.22.2", + "version": "0.23.0", "author": "PlayCanvas", "homepage": "https://playcanvas.com/supersplat/editor", "description": "3D Gaussian Splat Editor", @@ -68,7 +68,7 @@ "cross-env": "^7.0.3", "eslint": "^8.56.0", "jest": "^29.7.0", - "playcanvas": "^1.72.1", + "playcanvas": "^1.73.0", "rollup": "^4.18.0", "rollup-plugin-sass": "^1.13.0", "rollup-plugin-visualizer": "^5.12.0", diff --git a/src/camera.ts b/src/camera.ts index ee1ceb7d..08ece0a6 100644 --- a/src/camera.ts +++ b/src/camera.ts @@ -179,7 +179,9 @@ class Camera extends Element { this.entity.camera.setShaderPass(`debug_${this.scene.config.camera.debug_render}`); } - this.controller = new PointerController(this, this.scene.canvas); + const target = document.getElementById('canvas-container'); + + this.controller = new PointerController(this, target); // apply scene config const config = this.scene.config; diff --git a/src/controllers.ts b/src/controllers.ts index 87ba0818..281475b5 100644 --- a/src/controllers.ts +++ b/src/controllers.ts @@ -145,10 +145,6 @@ class PointerController { camera.pickFocalPoint(event.offsetX, event.offsetY); }; - const contextmenu = (event: globalThis.MouseEvent) => { - event.preventDefault(); - }; - // key state const keys: any = { ArrowUp: 0, @@ -192,7 +188,6 @@ class PointerController { target.addEventListener('pointermove', pointermove); target.addEventListener('wheel', wheel); target.addEventListener('dblclick', dblclick); - target.addEventListener('contextmenu', contextmenu); document.addEventListener('keydown', keydown); document.addEventListener('keyup', keyup); @@ -202,7 +197,6 @@ class PointerController { target.removeEventListener('pointermove', pointermove); target.removeEventListener('wheel', wheel); target.removeEventListener('dblclick', dblclick); - target.removeEventListener('contextmenu', contextmenu); document.removeEventListener('keydown', keydown); document.removeEventListener('keyup', keyup); }; diff --git a/src/file-handler.ts b/src/file-handler.ts index e7c5f803..db957679 100644 --- a/src/file-handler.ts +++ b/src/file-handler.ts @@ -87,7 +87,7 @@ const writeToFile = async (stream: FileSystemWritableFileStream, data: ArrayBuff }; // initialize file handler events -const initFileHandler = async (scene: Scene, events: Events, canvas: HTMLCanvasElement, remoteStorageDetails: RemoteStorageDetails) => { +const initFileHandler = async (scene: Scene, events: Events, dropTarget: HTMLElement, remoteStorageDetails: RemoteStorageDetails) => { // create a file selector element as fallback when showOpenFilePicker isn't available let fileSelector: HTMLInputElement; @@ -109,7 +109,7 @@ const initFileHandler = async (scene: Scene, events: Events, canvas: HTMLCanvasE } // create the file drag & drop handler - CreateDropHandler(document.body, async (entries) => { + CreateDropHandler(dropTarget, async (entries) => { const modelExtensions = ['.ply']; for (let i = 0; i < entries.length; i++) { const entry = entries[i]; diff --git a/src/infinite-grid.ts b/src/infinite-grid.ts new file mode 100644 index 00000000..b1905342 --- /dev/null +++ b/src/infinite-grid.ts @@ -0,0 +1,209 @@ +import { + CULLFACE_NONE, + FUNC_ALWAYS, + SEMANTIC_POSITION, + BlendState, + DepthState, + Mat4, + QuadRender, + Shader, + Vec3, + createShaderFromCode +} from 'playcanvas'; +import { Element, ElementType } from './element'; +import { Serializer } from './serializer'; + +const vsCode = /*glsl*/ ` + uniform mat4 matrix_viewProjectionInverse; + + attribute vec2 vertex_position; + + varying vec3 worldFar; + + void main(void) { + gl_Position = vec4(vertex_position, 0.0, 1.0); + + vec4 v = matrix_viewProjectionInverse * vec4(vertex_position, 1.0, 1.0); + + worldFar = v.xyz / v.w; + } +`; + +const fsCode = /*glsl*/ ` + uniform mat4 matrix_viewProjection; + uniform vec3 view_position; + uniform sampler2D blueNoiseTex32; + + varying vec3 worldFar; + + bool intersectPlane(inout float t, vec3 pos, vec3 dir, vec4 plane) { + float d = dot(dir, plane.xyz); + if (abs(d) < 1e-06) { + return false; + } + + float n = -(dot(pos, plane.xyz) + plane.w) / d; + if (n < 0.0) { + return false; + } + + t = n; + + return true; + } + + // https://bgolus.medium.com/the-best-darn-grid-shader-yet-727f9278b9d8#1e7c + float pristineGrid( in vec2 uv, in vec2 ddx, in vec2 ddy, vec2 lineWidth) + { + vec2 uvDeriv = vec2(length(vec2(ddx.x, ddy.x)), length(vec2(ddx.y, ddy.y))); + bvec2 invertLine = bvec2(lineWidth.x > 0.5, lineWidth.y > 0.5); + vec2 targetWidth = vec2( + invertLine.x ? 1.0 - lineWidth.x : lineWidth.x, + invertLine.y ? 1.0 - lineWidth.y : lineWidth.y + ); + vec2 drawWidth = clamp(targetWidth, uvDeriv, vec2(0.5)); + vec2 lineAA = uvDeriv * 1.5; + vec2 gridUV = abs(fract(uv) * 2.0 - 1.0); + gridUV.x = invertLine.x ? gridUV.x : 1.0 - gridUV.x; + gridUV.y = invertLine.y ? gridUV.y : 1.0 - gridUV.y; + vec2 grid2 = smoothstep(drawWidth + lineAA, drawWidth - lineAA, gridUV); + + grid2 *= clamp(targetWidth / drawWidth, 0.0, 1.0); + grid2 = mix(grid2, targetWidth, clamp(uvDeriv * 2.0 - 1.0, 0.0, 1.0)); + grid2.x = invertLine.x ? 1.0 - grid2.x : grid2.x; + grid2.y = invertLine.y ? 1.0 - grid2.y : grid2.y; + + return mix(grid2.x, 1.0, grid2.y); + } + + float calcDepth(vec3 p) { + vec4 v = matrix_viewProjection * vec4(p, 1.0); + return (v.z / v.w) * 0.5 + 0.5; + } + + void main(void) { + vec3 p = view_position; + vec3 v = normalize(worldFar - view_position); + + // intersect ray with the world xz plane + float t; + if (!intersectPlane(t, p, v, vec4(0, 1, 0, 0))) { + discard; + } + + // calculate grid intersection + vec3 pos = p + v * t; + + // discard distant pixels + float dist = length(pos.xz - view_position.xz); + if (dist > 200.0) { + discard; + } + + // evaluate the grid function + float grid = pristineGrid(pos.xz, dFdx(pos.xz), dFdy(pos.xz), vec2(1.0 / 50.0)); + + // smooth fade into distance + float a = grid * (1.0 - sin(dist / 200.0 * 3.14159 * 0.5)); + + // early discard semitrans pixels + if (a < 0.1) { + discard; + } + + if (a < 0.9) { + // apply dithered discard for semitrans pixels + vec2 uv = fract(gl_FragCoord.xy / 32.0); + float noise = texture2DLodEXT(blueNoiseTex32, uv, 0.0).y; + if (a < noise) { + discard; + } + } + + // calculate color + vec3 color; + + vec3 apos = abs(pos); + if (apos.x < 0.05) { + if (apos.z < 0.05) { + color = vec3(1.0); + } else { + color = vec3(0.2, 0.2, 1.0); + } + } else if (apos.z < 0.05) { + color = vec3(1.0, 0.2, 0.2); + } else { + color = vec3(0.6); + } + + gl_FragColor = vec4(color, 1.0); + gl_FragDepth = calcDepth(pos); + } +`; + +const attributes = { + vertex_position: SEMANTIC_POSITION +}; + +class InfiniteGrid extends Element { + shader: Shader; + quadRender: QuadRender; + blendState = new BlendState(false); + depthState = new DepthState(FUNC_ALWAYS, true); + + visible = true; + + constructor() { + super(ElementType.debug); + } + + add() { + const device = this.scene.app.graphicsDevice; + + this.shader = createShaderFromCode(device, vsCode, fsCode, 'infinite-grid', attributes); + this.quadRender = new QuadRender(this.shader); + + const viewPosition = device.scope.resolve('view_position'); + const viewProjection = device.scope.resolve('matrix_viewProjection'); + const viewProjectionInverse = device.scope.resolve('matrix_viewProjectionInverse'); + + this.scene.debugLayer.onPreRenderOpaque = () => { + if (this.visible) { + device.setBlendState(BlendState.ALPHABLEND); + device.setCullMode(CULLFACE_NONE); + device.setDepthState(DepthState.WRITEDEPTH); + device.setStencilState(null, null); + + // update viewProjectionInverse matrix + const projectionMatrix = this.scene.camera.entity.camera.projectionMatrix; + const cameraMatrix = this.scene.camera.entity.getWorldTransform(); + + const mat = new Mat4(); + mat.invert(projectionMatrix); + mat.mul2(cameraMatrix, mat); + + const mat2 = new Mat4(); + mat2.invert(mat); + + const viewPos = new Vec3(); + cameraMatrix.getTranslation(viewPos); + + viewPosition.setValue([viewPos.x, viewPos.y, viewPos.z]); + viewProjection.setValue(mat2.data); + viewProjectionInverse.setValue(mat.data); + + this.quadRender.render(); + } + }; + } + + remove() { + this.scene.debugLayer.onPreRenderOpaque = null; + } + + serialize(serializer: Serializer): void { + serializer.pack(this.visible); + } +} + +export { InfiniteGrid }; diff --git a/src/main.ts b/src/main.ts index bd8b64fd..bfdeec10 100644 --- a/src/main.ts +++ b/src/main.ts @@ -154,9 +154,9 @@ const main = async () => { toolManager.register('move', new MoveTool(events, editHistory, scene)); toolManager.register('rotate', new RotateTool(events, editHistory, scene)); toolManager.register('scale', new ScaleTool(events, editHistory, scene)); - toolManager.register('rectSelection', new RectSelection(events, editorUI.toolsContainer.dom, editorUI.canvas)); - toolManager.register('brushSelection', new BrushSelection(events, editorUI.toolsContainer.dom, editorUI.canvas)); - toolManager.register('pickerSelection', new PickerSelection(events, editorUI.toolsContainer.dom, editorUI.canvas)); + toolManager.register('rectSelection', new RectSelection(events, editorUI.toolsContainer.dom)); + toolManager.register('brushSelection', new BrushSelection(events, editorUI.toolsContainer.dom)); + toolManager.register('pickerSelection', new PickerSelection(events, editorUI.toolsContainer.dom)); window.scene = scene; diff --git a/src/scene.ts b/src/scene.ts index 02277bed..ef5922b3 100644 --- a/src/scene.ts +++ b/src/scene.ts @@ -17,7 +17,8 @@ import { Model } from './model'; import { Splat } from './splat'; import { Camera } from './camera'; import { CustomShadow as Shadow } from './custom-shadow'; -import { Grid } from './grid'; +// import { Grid } from './grid'; +import { InfiniteGrid as Grid } from './infinite-grid'; class Scene { events: Events; diff --git a/src/splat.ts b/src/splat.ts index 31d375eb..00da087c 100644 --- a/src/splat.ts +++ b/src/splat.ts @@ -4,9 +4,11 @@ import { PIXELFORMAT_L8, Asset, BoundingBox, + Color, Entity, GSplatData, GSplatResource, + Mat4, Quat, Texture, Vec3 @@ -132,6 +134,21 @@ void main(void) `; const vec = new Vec3(); +const veca = new Vec3(); +const vecb = new Vec3(); + +const boundingPoints = + [-1, 1].map((x) => { + return [-1, 1].map((y) => { + return [-1, 1].map((z) => { + return [ + new Vec3(x, y, z), new Vec3(x * 0.75, y, z), + new Vec3(x, y, z), new Vec3(x, y * 0.75, z), + new Vec3(x, y, z), new Vec3(x, y, z * 0.75) + ]; + }); + }); + }).flat(3); class Splat extends Element { asset: Asset; @@ -205,16 +222,44 @@ class Splat extends Element { updateState(recalcBound = false) { const state = this.splatData.getProp('state') as Uint8Array; + + // write state data to gpu texture const data = this.stateTexture.lock(); data.set(state); this.stateTexture.unlock(); + // update splat debug visual this.splatDebug.update(); + // handle splats being added or removed if (recalcBound) { this.localBoundDirty = true; this.worldBoundDirty = true; this.scene.boundDirty = true; + + // count number of still-visible splats + let numSplats = 0; + for (let i = 0; i < state.length; ++i) { + if ((state[i] & State.deleted) === 0) { + numSplats++; + } + } + + let mapping; + + // create a sorter mapping to remove deleted splats + if (numSplats !== state.length) { + mapping = new Uint32Array(numSplats); + let idx = 0; + for (let i = 0; i < state.length; ++i) { + if ((state[i] & State.deleted) === 0) { + mapping[idx++] = i; + } + } + } + + // update sorting instance + this.root.gsplat.instance.sorter.setMapping(mapping); } this.scene.forceRender = true; @@ -286,9 +331,25 @@ class Splat extends Element { material.setParameter('ringSize', (selected && cameraMode === 'rings' && splatSize > 0) ? 0.04 : 0); // render splat centers - if (this.visible && selected && cameraMode === 'centers' && splatSize > 0) { - this.splatDebug.splatSize = splatSize; - this.scene.app.drawMeshInstance(this.splatDebug.meshInstance); + if (this.visible && selected) { + if (cameraMode === 'centers' && splatSize > 0) { + this.splatDebug.splatSize = splatSize; + this.scene.app.drawMeshInstance(this.splatDebug.meshInstance); + } + + // render bounding box + const bound = this.localBound; + const scale = new Mat4().setTRS(bound.center, Quat.IDENTITY, bound.halfExtents); + scale.mul2(this.root.getWorldTransform(), scale); + + for (let i = 0; i < boundingPoints.length / 2; i++) { + const a = boundingPoints[i * 2]; + const b = boundingPoints[i * 2 + 1]; + scale.transformPoint(a, veca); + scale.transformPoint(b, vecb); + + this.scene.app.drawLine(veca, vecb, Color.WHITE, true, this.scene.debugLayer); + } } this.entity.enabled = this.visible; diff --git a/src/style.scss b/src/style.scss index e24c08c3..ad0de289 100644 --- a/src/style.scss +++ b/src/style.scss @@ -15,6 +15,7 @@ body { max-height: 100%; background-color: $bcg-primary; overflow: hidden; + touch-action: none; } #app-container { @@ -70,8 +71,6 @@ body { flex-grow: 0; flex-shrink: 0; overflow-y: auto; - // display: flex; - // flex-direction: column; } #data-controls { @@ -84,7 +83,7 @@ body { } #histogram-canvas { - + image-rendering: pixelated; } #histogram-svg { @@ -106,7 +105,6 @@ body { position: absolute; left: 50px; top: 50px; - pointer-events: none; } #data-panel-popup-label { @@ -199,7 +197,6 @@ body { .scene-panel-splat-item-text { flex-grow: 1; flex-shrink: 1; - pointer-events: none; .selected & { color: $text-primary; @@ -382,14 +379,12 @@ body { position: absolute; width: 100%; height: 100%; - pointer-events: none; } #brush-select-canvas { display: none; position: absolute; opacity: 0.4; - pointer-events: none; } #canvas-container { @@ -403,18 +398,10 @@ body { } #tools-container { + display: none; position: absolute; width: 100%; height: 100%; - pointer-events: none; -} - -#focus-capture { - position: absolute; - width: 100%; - height: 100%; - pointer-events: none; - opacity: 0; } #canvas { diff --git a/src/tools/brush-selection.ts b/src/tools/brush-selection.ts index 7e60f0f7..2f726c91 100644 --- a/src/tools/brush-selection.ts +++ b/src/tools/brush-selection.ts @@ -4,7 +4,7 @@ class BrushSelection { activate: () => void; deactivate: () => void; - constructor(events: Events, parent: HTMLElement, canvas: HTMLCanvasElement) { + constructor(events: Events, parent: HTMLElement) { let radius = 40; // create svg @@ -57,7 +57,7 @@ class BrushSelection { e.stopPropagation(); dragId = e.pointerId; - canvas.setPointerCapture(dragId); + parent.setPointerCapture(dragId); // initialize canvas if (selectCanvas.width !== parent.clientWidth || selectCanvas.height !== parent.clientHeight) { @@ -92,7 +92,7 @@ class BrushSelection { e.preventDefault(); e.stopPropagation(); - canvas.releasePointerCapture(dragId); + parent.releasePointerCapture(dragId); dragId = undefined; selectCanvas.style.display = 'none'; @@ -107,17 +107,18 @@ class BrushSelection { this.activate = () => { svg.style.display = 'inline'; - canvas.addEventListener('pointerdown', pointerdown, true); - canvas.addEventListener('pointermove', pointermove, true); - canvas.addEventListener('pointerup', pointerup, true); - + parent.style.display = 'block'; + parent.addEventListener('pointerdown', pointerdown); + parent.addEventListener('pointermove', pointermove); + parent.addEventListener('pointerup', pointerup); }; this.deactivate = () => { svg.style.display = 'none'; - canvas.removeEventListener('pointerdown', pointerdown, true); - canvas.removeEventListener('pointermove', pointermove, true); - canvas.removeEventListener('pointerup', pointerup, true); + parent.style.display = 'none'; + parent.removeEventListener('pointerdown', pointerdown); + parent.removeEventListener('pointermove', pointermove); + parent.removeEventListener('pointerup', pointerup); }; events.on('tool.brushSelection.smaller', () => { diff --git a/src/tools/picker-selection.ts b/src/tools/picker-selection.ts index 560559b7..6de0a2cc 100644 --- a/src/tools/picker-selection.ts +++ b/src/tools/picker-selection.ts @@ -4,7 +4,7 @@ class PickerSelection { activate: () => void; deactivate: () => void; - constructor(events: Events, parent: HTMLElement, canvas: HTMLCanvasElement) { + constructor(events: Events, parent: HTMLElement) { const pointerdown = (e: PointerEvent) => { if (e.pointerType === 'mouse' ? e.button === 0 : e.isPrimary) { e.preventDefault(); @@ -13,17 +13,19 @@ class PickerSelection { events.fire( 'select.point', e.shiftKey ? 'add' : (e.ctrlKey ? 'remove' : 'set'), - { x: e.offsetX / canvas.clientWidth, y: e.offsetY / canvas.clientHeight } + { x: e.offsetX / parent.clientWidth, y: e.offsetY / parent.clientHeight } ); } }; this.activate = () => { - canvas.addEventListener('pointerdown', pointerdown, true); + parent.style.display = 'block'; + parent.addEventListener('pointerdown', pointerdown); } - + this.deactivate = () => { - canvas.removeEventListener('pointerdown', pointerdown, true); + parent.style.display = 'none'; + parent.removeEventListener('pointerdown', pointerdown); } } } diff --git a/src/tools/rect-selection.ts b/src/tools/rect-selection.ts index 43e4646b..b7a5fe5c 100644 --- a/src/tools/rect-selection.ts +++ b/src/tools/rect-selection.ts @@ -5,7 +5,7 @@ class RectSelection { activate: () => void; deactivate: () => void; - constructor(events: Events, parent: HTMLElement, canvas: HTMLCanvasElement) { + constructor(events: Events, parent: HTMLElement) { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.id = 'rect-select-svg'; svg.classList.add('select-svg'); @@ -39,7 +39,7 @@ class RectSelection { e.stopPropagation(); dragId = e.pointerId; - canvas.setPointerCapture(dragId); + parent.setPointerCapture(dragId); start.x = end.x = e.offsetX; start.y = end.y = e.offsetY; @@ -67,10 +67,10 @@ class RectSelection { e.preventDefault(); e.stopPropagation(); - const w = canvas.clientWidth; - const h = canvas.clientHeight; + const w = parent.clientWidth; + const h = parent.clientHeight; - canvas.releasePointerCapture(dragId); + parent.releasePointerCapture(dragId); dragId = undefined; svg.style.display = 'none'; @@ -83,15 +83,17 @@ class RectSelection { }; this.activate = () => { - canvas.addEventListener('pointerdown', pointerdown, true); - canvas.addEventListener('pointermove', pointermove, true); - canvas.addEventListener('pointerup', pointerup, true); + parent.style.display = 'block'; + parent.addEventListener('pointerdown', pointerdown); + parent.addEventListener('pointermove', pointermove); + parent.addEventListener('pointerup', pointerup); }; this.deactivate = () => { - canvas.removeEventListener('pointerdown', pointerdown, true); - canvas.removeEventListener('pointermove', pointermove, true); - canvas.removeEventListener('pointerup', pointerup, true); + parent.style.display = 'none'; + parent.removeEventListener('pointerdown', pointerdown); + parent.removeEventListener('pointermove', pointermove); + parent.removeEventListener('pointerup', pointerup); }; parent.appendChild(svg); diff --git a/src/ui/data-panel.ts b/src/ui/data-panel.ts index 757fbb11..29231566 100644 --- a/src/ui/data-panel.ts +++ b/src/ui/data-panel.ts @@ -109,6 +109,7 @@ class DataPanel extends Panel { { v: 'x', t: 'X' }, { v: 'y', t: 'Y' }, { v: 'z', t: 'Z' }, + { v: 'distance', t: 'Distance' }, { v: 'volume', t: 'Volume' }, { v: 'surface-area', t: 'Surface Area' }, { v: 'scale_0', t: 'Scale X' }, @@ -193,6 +194,13 @@ class DataPanel extends Panel { func = (i) => scaleFunc(sx[i]) * scaleFunc(sy[i]) * scaleFunc(sz[i]); break; } + case 'distance': { + const x = splat.splatData.getProp('x'); + const y = splat.splatData.getProp('y'); + const z = splat.splatData.getProp('z'); + func = (i) => Math.sqrt(x[i] ** 2 + y[i] ** 2 + z[i] ** 2); + break; + } case 'surface-area': { const sx = splat.splatData.getProp('scale_0'); const sy = splat.splatData.getProp('scale_1'); @@ -249,7 +257,7 @@ class DataPanel extends Panel { } } - splatsValue.text = state.length.toString(); + splatsValue.text = (state.length - deleted).toString(); selectedValue.text = selected.toString(); hiddenValue.text = hidden.toString(); deletedValue.text = deleted.toString(); diff --git a/src/ui/editor.ts b/src/ui/editor.ts index 7487d72f..c6a34561 100644 --- a/src/ui/editor.ts +++ b/src/ui/editor.ts @@ -58,7 +58,6 @@ class EditorUI { // canvas const canvas = document.createElement('canvas'); canvas.id = 'canvas'; - canvas.style.touchAction = 'none'; // filename label const filenameLabel = new Label({ @@ -75,18 +74,9 @@ class EditorUI { id: 'tools-container' }); - // focus capture - const focusCapture = new Element({ - id: 'focus-capture' - }); - focusCapture.dom.addEventListener('pointerdown', (event: PointerEvent) => { - document.body.focus(); - }, true); - canvasContainer.dom.appendChild(canvas); canvasContainer.append(filenameLabel); canvasContainer.append(toolsContainer); - canvasContainer.append(focusCapture); // control panel const controlPanel = new ControlPanel(events, remoteStorageMode); @@ -139,6 +129,14 @@ class EditorUI { const pixelRatio = window.devicePixelRatio; canvas.width = Math.ceil(canvasContainer.dom.offsetWidth * pixelRatio); canvas.height = Math.ceil(canvasContainer.dom.offsetHeight * pixelRatio); + + // disable context menu globally + document.addEventListener('contextmenu', event => event.preventDefault()); + + // whenever the canvas container is clicked, set keyboard focus on the body + canvasContainer.dom.addEventListener('pointerdown', (event: PointerEvent) => { + document.body.focus(); + }, true); } setFilename(filename: string) { diff --git a/src/ui/histogram.ts b/src/ui/histogram.ts index 1717d090..55748bb6 100644 --- a/src/ui/histogram.ts +++ b/src/ui/histogram.ts @@ -90,12 +90,11 @@ class Histogram { constructor(numBins: number, height: number) { const canvas = document.createElement('canvas'); - canvas.classList.add('histogram-canvas'); + canvas.setAttribute('id', 'histogram-canvas'); canvas.width = numBins; canvas.height = height; canvas.style.width = `100%`; canvas.style.height = `100%`; - canvas.style.imageRendering = 'pixelated'; const context = canvas.getContext('2d'); context.globalCompositeOperation = 'copy'; @@ -199,11 +198,6 @@ class Histogram { this.canvas.addEventListener('pointerleave', (e: PointerEvent) => { this.events.fire('hideOverlay'); }); - - this.canvas.addEventListener('contextmenu', (e: MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - }, true); } update(options: UpdateOptions) { diff --git a/src/ui/popup.ts b/src/ui/popup.ts index f6a8bb4d..ca7676e9 100644 --- a/src/ui/popup.ts +++ b/src/ui/popup.ts @@ -54,7 +54,8 @@ class Popup { const container = new Container({ id: 'popup', - hidden: true + hidden: true, + tabIndex: -1 }); container.append(background); @@ -120,6 +121,9 @@ class Popup { inputValue.focus(); } + // take keyboard focus so shortcuts stop working + container.dom.focus(); + return new Promise<{action: string, value?: string}>((resolve) => { okFn = () => { this.hide(); From 4d33aad168a32b9fdf8f458db10514ff53608b27 Mon Sep 17 00:00:00 2001 From: Donovan Hutchence Date: Thu, 18 Jul 2024 09:38:31 +0100 Subject: [PATCH 2/2] small --- src/splat.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/splat.ts b/src/splat.ts index 00da087c..15b82b4f 100644 --- a/src/splat.ts +++ b/src/splat.ts @@ -330,8 +330,8 @@ class Splat extends Element { const material = this.root.gsplat.instance.material; material.setParameter('ringSize', (selected && cameraMode === 'rings' && splatSize > 0) ? 0.04 : 0); - // render splat centers if (this.visible && selected) { + // render splat centers if (cameraMode === 'centers' && splatSize > 0) { this.splatDebug.splatSize = splatSize; this.scene.app.drawMeshInstance(this.splatDebug.meshInstance);