diff --git a/package-lock.json b/package-lock.json index f4ed19a6..983847e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "supersplat", - "version": "0.19.3", + "version": "0.20.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "supersplat", - "version": "0.19.3", + "version": "0.20.0", "license": "MIT", "devDependencies": { "@playcanvas/eslint-config": "^1.7.1", diff --git a/package.json b/package.json index d9c7730d..be2a3513 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "supersplat", - "version": "0.19.3", + "version": "0.20.0", "author": "PlayCanvas", "homepage": "https://playcanvas.com/supersplat/editor", "description": "3D Gaussian Splat Editor", diff --git a/src/camera.ts b/src/camera.ts index 9b6f2e57..63653a5d 100644 --- a/src/camera.ts +++ b/src/camera.ts @@ -9,6 +9,8 @@ import { Entity, EventHandler, Picker, + Plane, + Ray, RenderTarget, Texture, Vec3, @@ -22,7 +24,7 @@ import { import { Element, ElementType } from './element'; import { TweenValue } from './tween-value'; import { Serializer } from './serializer'; -import { MouseController, TouchController } from './controllers'; +import { PointerController } from './controllers'; import { Splat } from './splat'; // calculate the forward vector given azimuth and elevation @@ -39,14 +41,16 @@ const calcForwardVec = (result: Vec3, azim: number, elev: number) => { // work globals const forwardVec = new Vec3(); const cameraPosition = new Vec3(); +const plane = new Plane(); +const ray = new Ray(); const vec = new Vec3(); +const vecb = new Vec3(); // modulo dealing with negative numbers const mod = (n: number, m: number) => ((n % m) + m) % m; class Camera extends Element { - mouseController: MouseController; - touchController: TouchController; + controller: PointerController; entity: Entity; focalPointTween = new TweenValue({x: 0, y: 0.5, z: 0}); azimElevTween = new TweenValue({azim: 30, elev: -15}); @@ -175,17 +179,12 @@ class Camera extends Element { this.entity.camera.setShaderPass(`debug_${this.scene.config.camera.debug_render}`); } - this.mouseController = new MouseController(this); - this.touchController = new TouchController(this); + this.controller = new PointerController(this, this.scene.canvas); // apply scene config const config = this.scene.config; const controls = config.controls; - this.mouseController.enableOrbit = this.touchController.enableOrbit = controls.enableRotate; - this.mouseController.enablePan = this.touchController.enablePan = controls.enablePan; - this.mouseController.enableZoom = this.touchController.enableZoom = controls.enableZoom; - // configure background const clr = config.backgroundColor; this.entity.camera.clearColor.set(clr.r, clr.g, clr.b, clr.a); @@ -219,11 +218,8 @@ class Camera extends Element { } remove() { - this.mouseController.destroy(); - this.mouseController = null; - - this.touchController.destroy(); - this.touchController = null; + this.controller.destroy(); + this.controller = null; this.entity.camera.layers = this.entity.camera.layers.filter(layer => layer !== this.scene.shadowLayer.id); this.scene.cameraRoot.removeChild(this.entity); @@ -398,6 +394,62 @@ class Camera extends Element { this.autoRotateDelayValue = config.controls.autoRotateDelay; } + // interesect the scene at the given screen coordinate and focus the camera on this location + pickFocalPoint(screenX: number, screenY: number) { + const scene = this.scene; + const cameraPos = this.entity.getPosition(); + + // @ts-ignore + const target = scene.canvas; + const sx = screenX / target.clientWidth * scene.targetSize.width; + const sy = screenY / target.clientHeight * scene.targetSize.height; + + const splats = scene.getElementsByType(ElementType.splat); + + const dist = (a: Vec3, b: Vec3) => { + return vecb.sub2(a, b).length(); + }; + + let closestD = 0; + const closestP = new Vec3(); + let closestSplat = null; + + for (let i = 0; i < splats.length; ++i) { + const splat = splats[i] as Splat; + + this.pickPrep(splat); + const pickId = this.pick(sx, sy); + + if (pickId !== -1) { + splat.getSplatWorldPosition(pickId, vec); + + // create a plane at the world position facing perpendicular to the camera + plane.setFromPointNormal(vec, this.entity.forward); + + // create the pick ray in world space + const res = this.entity.camera.screenToWorld(screenX, screenY, 1.0, vec); + vec.sub2(res, cameraPos); + vec.normalize(); + ray.set(cameraPos, vec); + + // find intersection + if (plane.intersectsRay(ray, vec)) { + const distance = dist(vec, cameraPos); + if (!closestSplat || distance < closestD) { + closestD = distance; + closestP.copy(vec); + closestSplat = splat; + } + } + } + } + + if (closestSplat) { + this.setFocalPoint(closestP); + scene.events.fire('selection', closestSplat); + } + } + // pick mode // render picker contents diff --git a/src/controllers.ts b/src/controllers.ts index 001abf15..5b52ccd4 100644 --- a/src/controllers.ts +++ b/src/controllers.ts @@ -1,379 +1,163 @@ import { Camera } from './camera'; -import { ElementType } from './element'; -import { Splat } from './splat'; -import { - EVENT_MOUSEDOWN, - EVENT_MOUSEUP, - EVENT_MOUSEMOVE, - EVENT_MOUSEWHEEL, - MOUSEBUTTON_LEFT, - MOUSEBUTTON_MIDDLE, - MOUSEBUTTON_RIGHT, - EVENT_TOUCHSTART, - EVENT_TOUCHEND, - EVENT_TOUCHCANCEL, - EVENT_TOUCHMOVE, - MouseEvent, - Plane, - Ray, - Touch, - TouchDevice, - TouchEvent, - Vec2, - Vec3, -} from 'playcanvas'; +import { Vec3 } from 'playcanvas'; -const plane = new Plane(); -const ray = new Ray(); -const vec = new Vec3(); -const vecb = new Vec3(); const fromWorldPoint = new Vec3(); const toWorldPoint = new Vec3(); const worldDiff = new Vec3(); -// MouseController +// calculate the distance between two 2d points +const dist = (x0: number, y0: number, x1: number, y1: number) => Math.sqrt((x1 - x0) ** 2 + (y1 - y0) ** 2); -class MouseController { - camera: Camera; - leftButton = false; - middleButton = false; - rightButton = false; - lastPoint = new Vec2(); +class PointerController { + destroy: () => void; - enableOrbit = true; - enablePan = true; - enableZoom = true; - zoomedIn = 1; - xMouse = 0; - yMouse = 0; + constructor(camera: Camera, target: HTMLElement) { - onMouseOutFunc = () => { - this.onMouseOut(); - }; - - onDblClick = (event: globalThis.MouseEvent) => { - const scene = this.camera.scene; - const cameraPos = this.camera.entity.getPosition(); + const orbit = (dx: number, dy: number) => { + const azim = camera.azim - dx * camera.scene.config.controls.orbitSensitivity; + const elev = camera.elevation - dy * camera.scene.config.controls.orbitSensitivity; + camera.setAzimElev(azim, elev); + } - // @ts-ignore - const target = scene.app.mouse._target; - const sx = event.offsetX / target.clientWidth * scene.targetSize.width; - const sy = event.offsetY / target.clientHeight * scene.targetSize.height; + const pan = (x: number, y: number, dx: number, dy: number) => { + // For panning to work at any zoom level, we use screen point to world projection + // to work out how far we need to pan the pivotEntity in world space + const c = camera.entity.camera; + const distance = camera.focusDistance * camera.distanceTween.value.distance; - const splats = scene.getElementsByType(ElementType.splat); + c.screenToWorld(x, y, distance, fromWorldPoint); + c.screenToWorld(x - dx, y - dy, distance, toWorldPoint); - const dist = (a: Vec3, b: Vec3) => { - return vecb.sub2(a, b).length(); + worldDiff.sub2(toWorldPoint, fromWorldPoint); + worldDiff.add(camera.focalPoint); + + camera.setFocalPoint(worldDiff); }; - let closestD = 0; - const closestP = new Vec3(); - let closestSplat = null; - - for (let i = 0; i < splats.length; ++i) { - const splat = splats[i] as Splat; - - this.camera.pickPrep(splat); - const pickId = this.camera.pick(sx, sy); - - if (pickId !== -1) { - splat.getSplatWorldPosition(pickId, vec); + const zoom = (amount: number) => { + camera.setDistance(camera.distance * (1 - amount * camera.scene.config.controls.zoomSensitivity), 2); + }; - // create a plane at the world position facing perpendicular to the camera - plane.setFromPointNormal(vec, this.camera.entity.forward); + // mouse state + const buttons = [false, false, false]; + let x: number, y: number; - // create the pick ray in world space - const res = this.camera.entity.camera.screenToWorld(event.offsetX, event.offsetY, 1.0, vec); - vec.sub2(res, cameraPos); - vec.normalize(); - ray.set(cameraPos, vec); + // touch state + let touches: { id: number, x: number, y: number}[] = []; + let midx: number, midy: number, midlen: number; - // find intersection - if (plane.intersectsRay(ray, vec)) { - const distance = dist(vec, cameraPos); - if (!closestSplat || distance < closestD) { - closestD = distance; - closestP.copy(vec); - closestSplat = splat; - } + const pointerdown = (event: PointerEvent) => { + if (event.pointerType === 'mouse') { + if (buttons.every(b => !b)) { + target.setPointerCapture(event.pointerId); + } + buttons[event.button] = true; + x = event.offsetX; + y = event.offsetY; + } else if (event.pointerType === 'touch') { + if (touches.length === 0) { + target.setPointerCapture(event.pointerId); + } + touches.push({ + x: event.offsetX, + y: event.offsetY, + id: event.pointerId + }); + + if (touches.length === 2) { + midx = (touches[0].x + touches[1].x) * 0.5; + midy = (touches[0].y + touches[1].y) * 0.5; + midlen = dist(touches[0].x, touches[0].y, touches[1].x, touches[1].y); } } - } - - if (closestSplat) { - this.camera.setFocalPoint(closestP); - scene.events.fire('selection', closestSplat); - } - }; - - constructor(camera: Camera) { - this.camera = camera; - const mouse = camera.scene.app.mouse; - mouse.on(EVENT_MOUSEDOWN, this.onMouseDown, this); - mouse.on(EVENT_MOUSEUP, this.onMouseUp, this); - mouse.on(EVENT_MOUSEMOVE, this.onMouseMove, this); - mouse.on(EVENT_MOUSEWHEEL, this.onMouseWheel, this); - - // Listen to when the mouse travels out of the window - // window.addEventListener('mouseout', this.onMouseOutFunc, false); - - // @ts-ignore - mouse._target.addEventListener('dblclick', this.onDblClick.bind(this)); - - // Disabling the context menu stops the browser displaying a menu when - // you right-click the page - mouse.disableContextMenu(); - } - - destroy() { - const mouse = this.camera.scene.app.mouse; - mouse.off(EVENT_MOUSEDOWN, this.onMouseDown, this); - mouse.off(EVENT_MOUSEUP, this.onMouseUp, this); - mouse.off(EVENT_MOUSEMOVE, this.onMouseMove, this); - mouse.off(EVENT_MOUSEWHEEL, this.onMouseWheel, this); - - // window.removeEventListener('mouseout', this.onMouseOutFunc, false); - - // @ts-ignore - mouse._target.removeEventListener('dblclick', this.onDblClick); - } - - orbit(dx: number, dy: number) { - if (!this.enableOrbit) { - return; - } - const azim = this.camera.azim - dx * this.camera.scene.config.controls.orbitSensitivity; - const elev = this.camera.elevation - dy * this.camera.scene.config.controls.orbitSensitivity; - this.camera.setAzimElev(azim, elev); - } - - pan(x: number, y: number) { - if (!this.enablePan) { - return; - } - // For panning to work at any zoom level, we use screen point to world projection - // to work out how far we need to pan the pivotEntity in world space - const camera = this.camera.entity.camera; - const distance = this.camera.focusDistance * this.camera.distanceTween.value.distance; - - camera.screenToWorld(x, y, distance, fromWorldPoint); - camera.screenToWorld(this.lastPoint.x, this.lastPoint.y, distance, toWorldPoint); - - worldDiff.sub2(toWorldPoint, fromWorldPoint); - worldDiff.add(this.camera.focalPoint); - - this.camera.setFocalPoint(worldDiff); - } - - zoom(amount: number) { - if (!this.enableZoom) { - return; - } - this.camera.setDistance(this.camera.distance * (1 - amount), 2); - } - - hasDragged(event: MouseEvent): boolean { - return this.xMouse !== event.x || this.yMouse !== event.y; - } - - onMouseDown(event: MouseEvent) { - this.xMouse = event.x; - this.yMouse = event.y; - switch (event.button) { - case MOUSEBUTTON_LEFT: - this.leftButton = true; - this.camera.notify('mouseStart'); - break; - case MOUSEBUTTON_MIDDLE: - this.middleButton = true; - this.camera.notify('mouseStart'); - break; - case MOUSEBUTTON_RIGHT: - this.rightButton = true; - this.camera.notify('mouseStart'); - break; - } - } - - onMouseUp(event: MouseEvent) { - switch (event.button) { - case MOUSEBUTTON_LEFT: - this.leftButton = false; - this.camera.notify('mouseEnd'); - break; - case MOUSEBUTTON_MIDDLE: - this.middleButton = false; - this.camera.notify('mouseEnd'); - break; - case MOUSEBUTTON_RIGHT: - this.rightButton = false; - this.camera.notify('mouseEnd'); - break; - } - - if (this.hasDragged(event)) { - this.xMouse = event.x; - this.yMouse = event.y; - } - } + }; - onMouseMove(event: MouseEvent) { - if (this.leftButton) { - if (event.ctrlKey) { - this.zoom(event.dx * -0.02); - } else if (event.shiftKey) { - this.pan(event.x, event.y); + const pointerup = (event: PointerEvent) => { + if (event.pointerType === 'mouse') { + buttons[event.button] = false; + if (buttons.every(b => !b)) { + target.releasePointerCapture(event.pointerId); + } } else { - this.orbit(event.dx, event.dy); + touches = touches.filter((touch) => touch.id !== event.pointerId); + if (touches.length === 0) { + target.setPointerCapture(event.pointerId); + } } - } else if (this.rightButton) { - this.pan(event.x, event.y); - } else if (this.middleButton) { - this.zoom(event.dx * -0.02); - } - - this.lastPoint.set(event.x, event.y); - } - - onMouseWheel(event: MouseEvent) { - this.zoom(event.wheelDelta * -0.2 * this.camera.scene.config.controls.zoomSensitivity); - this.camera.notify('mouseZoom'); - event.event.preventDefault(); - if (event.wheelDelta !== this.zoomedIn) { - this.zoomedIn = event.wheelDelta; - } - } - - onMouseOut() { - this.leftButton = this.middleButton = this.rightButton = false; - } -} - -// TouchController - -class TouchController { - touch: TouchDevice; - camera: Camera; - lastTouchPoint = new Vec2(); - lastPinchMidPoint = new Vec2(); - lastPinchDistance = 0; - pinchMidPoint = new Vec2(); - - enableOrbit = true; - enablePan = true; - enableZoom = true; - xTouch: number; - yTouch: number; - - constructor(camera: Camera) { - this.camera = camera; - this.xTouch = 0; - this.yTouch = 0; - // Use the same callback for the touchStart, touchEnd and touchCancel events as they - // all do the same thing which is to deal the possible multiple touches to the screen - const touch = this.camera.scene.app.touch; - touch.on(EVENT_TOUCHSTART, this.onTouchStartEndCancel, this); - touch.on(EVENT_TOUCHEND, this.onTouchStartEndCancel, this); - touch.on(EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this); - touch.on(EVENT_TOUCHMOVE, this.onTouchMove, this); - } - - destroy() { - const touch = this.camera.scene.app.touch; - touch.off(EVENT_TOUCHSTART, this.onTouchStartEndCancel, this); - touch.off(EVENT_TOUCHEND, this.onTouchStartEndCancel, this); - touch.off(EVENT_TOUCHCANCEL, this.onTouchStartEndCancel, this); - touch.off(EVENT_TOUCHMOVE, this.onTouchMove, this); - } - - getPinchDistance(pointA: Touch, pointB: Touch) { - // Return the distance between the two points - const dx = pointA.x - pointB.x; - const dy = pointA.y - pointB.y; - return Math.sqrt(dx * dx + dy * dy); - } - - calcMidPoint(pointA: Touch, pointB: Touch, result: Vec2) { - result.set(pointB.x - pointA.x, pointB.y - pointA.y); - result.mulScalar(0.5); - result.x += pointA.x; - result.y += pointA.y; - } - - onTouchStartEndCancel(event: TouchEvent) { - // We only care about the first touch for camera rotation. As the user touches the screen, - // we stored the current touch position - const touches = event.touches; - if (touches.length === 1) { - this.lastTouchPoint.set(touches[0].x, touches[0].y); - this.camera.notify('touchStart'); - this.xTouch = touches[0].x; - this.yTouch = touches[0].y; - } else if (touches.length === 2) { - // If there are 2 touches on the screen, then set the pinch distance - this.lastPinchDistance = this.getPinchDistance(touches[0], touches[1]); - this.calcMidPoint(touches[0], touches[1], this.lastPinchMidPoint); - this.camera.notify('touchStart'); - } else { - this.camera.notify('touchEnd'); - } - } - - pan(midPoint: Vec2) { - if (!this.enablePan) { - return; - } - - // For panning to work at any zoom level, we use screen point to world projection - // to work out how far we need to pan the pivotEntity in world space - const camera = this.camera.entity.camera; - const distance = this.camera.distance; - - camera.screenToWorld(midPoint.x, midPoint.y, distance, fromWorldPoint); - camera.screenToWorld(this.lastPinchMidPoint.x, this.lastPinchMidPoint.y, distance, toWorldPoint); - - worldDiff.sub2(toWorldPoint, fromWorldPoint); - worldDiff.add(this.camera.focalPoint); + }; - this.camera.setFocalPoint(worldDiff); - } + const pointermove = (event: PointerEvent) => { + if (event.pointerType === 'mouse') { + const dx = event.offsetX - x; + const dy = event.offsetY - y; + x = event.offsetX; + y = event.offsetY; + + if (buttons[0]) { + orbit(dx, dy); + } else if (buttons[1]) { + zoom(dy * -0.02); + } else if (buttons[2]) { + pan(x, y, dx, dy); + } + } else { + if (touches.length === 1) { + const touch = touches[0]; + const dx = event.offsetX - touch.x; + const dy = event.offsetY - touch.y; + touch.x = event.offsetX; + touch.y = event.offsetY; + orbit(dx, dy); + } else if (touches.length === 2) { + const touch = touches[touches.map(t => t.id).indexOf(event.pointerId)]; + touch.x = event.offsetX; + touch.y = event.offsetY; + + const mx = (touches[0].x + touches[1].x) * 0.5; + const my = (touches[0].y + touches[1].y) * 0.5; + const ml = dist(touches[0].x, touches[0].y, touches[1].x, touches[1].y); + + pan(mx, my, (mx - midx), (my - midy)); + zoom((ml - midlen) * 0.01); + + midx = mx; + midy = my; + midlen = ml; + } + } + }; - onTouchMove(event: TouchEvent) { - const pinchMidPoint = this.pinchMidPoint; + const wheel = (event: WheelEvent) => { + event.preventDefault(); + const sign = (v: number) => v > 0 ? 1 : v < 0 ? -1 : 0; + zoom(sign(event.deltaY) * -0.2); + orbit(sign(event.deltaX) * 2.0, 0); + }; - // We only care about the first touch for camera rotation. Work out the difference moved since the last event - // and use that to update the camera target position - const touches = event.touches; - if (touches.length === 1 && this.enableOrbit) { - const touch = touches[0]; - const elev = - this.camera.elevation - - (touch.y - this.lastTouchPoint.y) * this.camera.scene.config.controls.orbitSensitivity; - const azim = - this.camera.azim - - (touch.x - this.lastTouchPoint.x) * this.camera.scene.config.controls.orbitSensitivity; - this.camera.setAzimElev(azim, elev); - this.lastTouchPoint.set(touch.x, touch.y); - } else if (touches.length === 2 && this.enableZoom) { - // Calculate the difference in pinch distance since the last event - const currentPinchDistance = this.getPinchDistance(touches[0], touches[1]); - const diffInPinchDistance = currentPinchDistance - this.lastPinchDistance; - this.lastPinchDistance = currentPinchDistance; + const dblclick = (event: globalThis.MouseEvent) => { + camera.pickFocalPoint(event.offsetX, event.offsetY); + }; - const distance = - this.camera.distance - - diffInPinchDistance * - this.camera.scene.config.controls.zoomSensitivity * - 0.1 * - (this.camera.distance * 0.1); - this.camera.setDistance(distance); + const contextmenu = (event: globalThis.MouseEvent) => { + event.preventDefault(); + }; - // Calculate pan difference - this.calcMidPoint(touches[0], touches[1], pinchMidPoint); - this.pan(pinchMidPoint); - this.lastPinchMidPoint.copy(pinchMidPoint); - } + target.addEventListener('pointerdown', pointerdown); + target.addEventListener('pointerup', pointerup); + target.addEventListener('pointermove', pointermove); + target.addEventListener('wheel', wheel); + target.addEventListener('dblclick', dblclick); + target.addEventListener('contextmenu', contextmenu); + + this.destroy = () => { + target.removeEventListener('pointerdown', pointerdown); + target.removeEventListener('pointerup', pointerup); + target.removeEventListener('pointermove', pointermove); + target.removeEventListener('wheel', wheel); + target.removeEventListener('dblclick', dblclick); + target.removeEventListener('contextmenu', contextmenu); + }; } } -export {MouseController, TouchController}; +export { PointerController }; diff --git a/src/scene.ts b/src/scene.ts index 61c0d82c..02277bed 100644 --- a/src/scene.ts +++ b/src/scene.ts @@ -5,8 +5,6 @@ import { Color, Entity, Layer, - Mouse, - TouchDevice, GraphicsDevice } from 'playcanvas'; import { PCApp } from './pc-app'; @@ -62,11 +60,7 @@ class Scene { // configure the playcanvas application. we render to an offscreen buffer so require // only the simplest of backbuffers. - this.app = new PCApp(canvas, { - mouse: new Mouse(canvas), - touch: new TouchDevice(canvas), - graphicsDevice: graphicsDevice - }); + this.app = new PCApp(canvas, { graphicsDevice }); // only render the scene when instructed this.app.autoRender = false; diff --git a/src/tools/brush-selection.ts b/src/tools/brush-selection.ts index 89937e20..48ba0c07 100644 --- a/src/tools/brush-selection.ts +++ b/src/tools/brush-selection.ts @@ -19,6 +19,7 @@ class BrushSelection { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.id = 'select-svg'; svg.style.display = 'inline'; + root.style.touchAction = 'none'; // create circle element const circle = document.createElementNS(svg.namespaceURI, 'circle') as SVGCircleElement; diff --git a/src/tools/picker-selection.ts b/src/tools/picker-selection.ts index f0a29b89..25f7348c 100644 --- a/src/tools/picker-selection.ts +++ b/src/tools/picker-selection.ts @@ -7,6 +7,7 @@ class PickerSelection { constructor(events: Events, parent: HTMLElement) { this.root = document.createElement('div'); this.root.id = 'select-root'; + this.root.style.touchAction = 'none'; this.root.addEventListener('pointerdown', (e) => { if (e.pointerType === 'mouse' ? e.button === 0 : e.isPrimary) { diff --git a/src/tools/rect-selection.ts b/src/tools/rect-selection.ts index 5b35f2c3..95e2f5dd 100644 --- a/src/tools/rect-selection.ts +++ b/src/tools/rect-selection.ts @@ -22,6 +22,7 @@ class RectSelection { // create input dom const root = document.createElement('div'); root.id = 'select-root'; + root.style.touchAction = 'none'; let dragId: number | undefined; diff --git a/src/ui/control-panel.ts b/src/ui/control-panel.ts index f1513c03..f2757dae 100644 --- a/src/ui/control-panel.ts +++ b/src/ui/control-panel.ts @@ -70,6 +70,10 @@ class ControlPanel extends Panel { const item = items.get(selection); if (item) { item.selected = true; + + // Temporary workaround for shortcuts not working after load - remove focus from tree element + // @ts-ignore + document.activeElement?.blur(); } }); diff --git a/src/ui/editor.ts b/src/ui/editor.ts index c27dc6d5..7957998c 100644 --- a/src/ui/editor.ts +++ b/src/ui/editor.ts @@ -56,6 +56,7 @@ class EditorUI { // canvas const canvas = document.createElement('canvas'); canvas.id = 'canvas'; + canvas.style.touchAction = 'none'; // filename label const filenameLabel = new Label({