From a9458477d17a99ba6ae28716c9dd10494e88d057 Mon Sep 17 00:00:00 2001 From: Donovan Hutchence Date: Mon, 8 Jul 2024 17:19:24 +0100 Subject: [PATCH 1/5] simplify tools ui --- src/editor.ts | 2 +- src/main.ts | 6 +- src/style.scss | 16 +--- src/tools/brush-selection.ts | 134 ++++++++++++++-------------------- src/tools/picker-selection.ts | 38 ++++------ src/tools/rect-selection.ts | 99 ++++++++++++------------- 6 files changed, 123 insertions(+), 172 deletions(-) diff --git a/src/editor.ts b/src/editor.ts index ba4d546..2034a19 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -27,7 +27,7 @@ const registerEditorEvents = (events: Events, editHistory: EditHistory, scene: S // get the list of selected splats (currently limited to just a single one) const selectedSplats = () => { const selected = events.invoke('selection') as Splat; - return [selected]; + return selected ? [selected] : []; }; const debugSphereCenter = new Vec3(); diff --git a/src/main.ts b/src/main.ts index a3a6746..5a12233 100644 --- a/src/main.ts +++ b/src/main.ts @@ -153,9 +153,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.canvasContainer.dom)); - toolManager.register('brushSelection', new BrushSelection(events, editorUI.canvasContainer.dom)); - toolManager.register('pickerSelection', new PickerSelection(events, editorUI.canvasContainer.dom)); + toolManager.register('rectSelection', new RectSelection(events, editorUI.canvasContainer.dom, editorUI.canvas)); + toolManager.register('brushSelection', new BrushSelection(events, editorUI.canvasContainer.dom, editorUI.canvas)); + toolManager.register('pickerSelection', new PickerSelection(events, editorUI.canvasContainer.dom, editorUI.canvas)); window.scene = scene; diff --git a/src/style.scss b/src/style.scss index 142f4f9..a4d601f 100644 --- a/src/style.scss +++ b/src/style.scss @@ -201,27 +201,19 @@ body { background-color: #f60 !important; } -#select-root { +.select-svg { display: none; position: absolute; width: 100%; height: 100%; + pointer-events: none; } -#select-svg { - display: none; - width: 100%; - height: 100%; -} - -#select-canvas { +#brush-select-canvas { display: none; position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; opacity: 0.4; + pointer-events: none; } #canvas-container { diff --git a/src/tools/brush-selection.ts b/src/tools/brush-selection.ts index 48ba0c0..7e60f0f 100644 --- a/src/tools/brush-selection.ts +++ b/src/tools/brush-selection.ts @@ -1,41 +1,33 @@ import { Events } from "../events"; class BrushSelection { - events: Events; - root: HTMLElement; - canvas: HTMLCanvasElement; - context: CanvasRenderingContext2D; - svg: SVGElement; - circle: SVGCircleElement; - radius = 40; - prev = { x: 0, y: 0 }; - - constructor(events: Events, parent: HTMLElement) { - // create input dom - const root = document.createElement('div'); - root.id = 'select-root'; + activate: () => void; + deactivate: () => void; + + constructor(events: Events, parent: HTMLElement, canvas: HTMLCanvasElement) { + let radius = 40; // create svg const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - svg.id = 'select-svg'; - svg.style.display = 'inline'; - root.style.touchAction = 'none'; + svg.id = 'brush-select-svg'; + svg.classList.add('select-svg'); // create circle element const circle = document.createElementNS(svg.namespaceURI, 'circle') as SVGCircleElement; - circle.setAttribute('r', this.radius.toString()); + circle.setAttribute('r', radius.toString()); circle.setAttribute('fill', 'rgba(255, 102, 0, 0.2)'); circle.setAttribute('stroke', '#f60'); circle.setAttribute('stroke-width', '1'); circle.setAttribute('stroke-dasharray', '5, 5'); // create canvas - const canvas = document.createElement('canvas'); - canvas.id = 'select-canvas'; + const selectCanvas = document.createElement('canvas'); + selectCanvas.id = 'brush-select-canvas'; - const context = canvas.getContext('2d'); + const context = selectCanvas.getContext('2d'); context.globalCompositeOperation = 'copy'; + const prev = { x: 0, y: 0 }; let dragId: number | undefined; const update = (e: PointerEvent) => { @@ -49,114 +41,98 @@ class BrushSelection { context.beginPath(); context.strokeStyle = '#f60'; context.lineCap = 'round'; - context.lineWidth = this.radius * 2; - context.moveTo(this.prev.x, this.prev.y); + context.lineWidth = radius * 2; + context.moveTo(prev.x, prev.y); context.lineTo(x, y); context.stroke(); - this.prev.x = x; - this.prev.y = y; + prev.x = x; + prev.y = y; } }; - root.addEventListener('contextmenu', (e) => { - e.preventDefault(); - }); - - root.addEventListener('pointerdown', (e) => { + const pointerdown = (e: PointerEvent) => { if (dragId === undefined && (e.pointerType === 'mouse' ? e.button === 0 : e.isPrimary)) { e.preventDefault(); e.stopPropagation(); dragId = e.pointerId; - root.setPointerCapture(dragId); + canvas.setPointerCapture(dragId); // initialize canvas - if (canvas.width !== parent.clientWidth || canvas.height !== parent.clientHeight) { - canvas.width = parent.clientWidth; - canvas.height = parent.clientHeight; + if (selectCanvas.width !== parent.clientWidth || selectCanvas.height !== parent.clientHeight) { + selectCanvas.width = parent.clientWidth; + selectCanvas.height = parent.clientHeight; } // clear canvas - context.clearRect(0, 0, canvas.width, canvas.height); + context.clearRect(0, 0, selectCanvas.width, selectCanvas.height); // display it - canvas.style.display = 'inline'; + selectCanvas.style.display = 'inline'; - this.prev.x = e.offsetX; - this.prev.y = e.offsetY; + prev.x = e.offsetX; + prev.y = e.offsetY; update(e); } - }); + }; - root.addEventListener('pointermove', (e) => { + const pointermove = (e: PointerEvent) => { if (dragId !== undefined) { e.preventDefault(); e.stopPropagation(); } update(e); - }); + }; - root.addEventListener('pointerup', (e) => { + const pointerup = (e: PointerEvent) => { if (e.pointerId === dragId) { e.preventDefault(); e.stopPropagation(); - root.releasePointerCapture(dragId); + canvas.releasePointerCapture(dragId); dragId = undefined; - canvas.style.display = 'none'; + selectCanvas.style.display = 'none'; - this.events.fire( + events.fire( 'select.byMask', e.shiftKey ? 'add' : (e.ctrlKey ? 'remove' : 'set'), - context.getImageData(0, 0, canvas.width, canvas.height) + context.getImageData(0, 0, selectCanvas.width, selectCanvas.height) ); } - }); + }; - parent.appendChild(root); - root.appendChild(svg); - svg.appendChild(circle); - root.appendChild(canvas); + this.activate = () => { + svg.style.display = 'inline'; + canvas.addEventListener('pointerdown', pointerdown, true); + canvas.addEventListener('pointermove', pointermove, true); + canvas.addEventListener('pointerup', pointerup, true); + + }; + + this.deactivate = () => { + svg.style.display = 'none'; + canvas.removeEventListener('pointerdown', pointerdown, true); + canvas.removeEventListener('pointermove', pointermove, true); + canvas.removeEventListener('pointerup', pointerup, true); + }; events.on('tool.brushSelection.smaller', () => { - this.smaller(); + radius = Math.max(1, radius / 1.05); + circle.setAttribute('r', radius.toString()); }); events.on('tool.brushSelection.bigger', () => { - this.bigger(); + radius = Math.min(500, radius * 1.05); + circle.setAttribute('r', radius.toString()); }); - this.events = events; - this.root = root; - this.svg = svg; - this.circle = circle; - this.canvas = canvas; - this.context = context; - - canvas.width = parent.clientWidth; - canvas.height = parent.clientHeight; - } - - activate() { - this.root.style.display = 'block'; - } - - deactivate() { - this.root.style.display = 'none'; - } - - smaller() { - this.radius = Math.max(1, this.radius / 1.05); - this.circle.setAttribute('r', this.radius.toString()); - } - - bigger() { - this.radius = Math.min(500, this.radius * 1.05); - this.circle.setAttribute('r', this.radius.toString()); + svg.appendChild(circle); + parent.appendChild(svg); + parent.appendChild(selectCanvas); } } diff --git a/src/tools/picker-selection.ts b/src/tools/picker-selection.ts index 25f7348..560559b 100644 --- a/src/tools/picker-selection.ts +++ b/src/tools/picker-selection.ts @@ -1,15 +1,11 @@ import { Events } from "../events"; class PickerSelection { - events: Events; - root: HTMLElement; + activate: () => void; + deactivate: () => void; - 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) => { + constructor(events: Events, parent: HTMLElement, canvas: HTMLCanvasElement) { + const pointerdown = (e: PointerEvent) => { if (e.pointerType === 'mouse' ? e.button === 0 : e.isPrimary) { e.preventDefault(); e.stopPropagation(); @@ -17,24 +13,18 @@ class PickerSelection { events.fire( 'select.point', e.shiftKey ? 'add' : (e.ctrlKey ? 'remove' : 'set'), - { x: e.offsetX / this.root.clientWidth, y: e.offsetY / this.root.clientHeight } + { x: e.offsetX / canvas.clientWidth, y: e.offsetY / canvas.clientHeight } ); } - }); - - parent.appendChild(this.root); - - this.root.addEventListener('contextmenu', (e) => { - e.preventDefault(); - }); - } - - activate() { - this.root.style.display = 'block'; - } - - deactivate() { - this.root.style.display = 'none'; + }; + + this.activate = () => { + canvas.addEventListener('pointerdown', pointerdown, true); + } + + this.deactivate = () => { + canvas.removeEventListener('pointerdown', pointerdown, true); + } } } diff --git a/src/tools/rect-selection.ts b/src/tools/rect-selection.ts index 95e2f5d..43e4646 100644 --- a/src/tools/rect-selection.ts +++ b/src/tools/rect-selection.ts @@ -1,16 +1,14 @@ import { Events } from '../events'; class RectSelection { - events: Events; - root: HTMLElement; - svg: SVGElement; - rect: SVGRectElement; - start = { x: 0, y: 0 }; - end = { x: 0, y: 0 }; - - constructor(events: Events, parent: HTMLElement) { + + activate: () => void; + deactivate: () => void; + + constructor(events: Events, parent: HTMLElement, canvas: HTMLCanvasElement) { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - svg.id = 'select-svg'; + svg.id = 'rect-select-svg'; + svg.classList.add('select-svg'); // create rect element const rect = document.createElementNS(svg.namespaceURI, 'rect') as SVGRectElement; @@ -19,18 +17,15 @@ class RectSelection { rect.setAttribute('stroke-width', '1'); rect.setAttribute('stroke-dasharray', '5, 5'); - // create input dom - const root = document.createElement('div'); - root.id = 'select-root'; - root.style.touchAction = 'none'; - + const start = { x: 0, y: 0 }; + const end = { x: 0, y: 0 }; let dragId: number | undefined; const updateRect = () => { - const x = Math.min(this.start.x, this.end.x); - const y = Math.min(this.start.y, this.end.y); - const width = Math.abs(this.start.x - this.end.x); - const height = Math.abs(this.start.y - this.end.y); + const x = Math.min(start.x, end.x); + const y = Math.min(start.y, end.y); + const width = Math.abs(start.x - end.x); + const height = Math.abs(start.y - end.y); rect.setAttribute('x', x.toString()); rect.setAttribute('y', y.toString()); @@ -38,75 +33,73 @@ class RectSelection { rect.setAttribute('height', height.toString()); }; - root.addEventListener('contextmenu', (e) => { - e.preventDefault(); - }); - - root.addEventListener('pointerdown', (e) => { + const pointerdown = (e: PointerEvent) => { if (dragId === undefined && (e.pointerType === 'mouse' ? e.button === 0 : e.isPrimary)) { e.preventDefault(); e.stopPropagation(); dragId = e.pointerId; - root.setPointerCapture(dragId); + canvas.setPointerCapture(dragId); - this.start.x = this.end.x = e.offsetX; - this.start.y = this.end.y = e.offsetY; + start.x = end.x = e.offsetX; + start.y = end.y = e.offsetY; updateRect(); - this.svg.style.display = 'inline'; + svg.style.display = 'inline'; } - }); + }; - root.addEventListener('pointermove', (e) => { + const pointermove = (e: PointerEvent) => { if (e.pointerId === dragId) { e.preventDefault(); e.stopPropagation(); - this.end.x = e.offsetX; - this.end.y = e.offsetY; + end.x = e.offsetX; + end.y = e.offsetY; updateRect(); } - }); + }; - root.addEventListener('pointerup', (e) => { + const pointerup = (e: PointerEvent) => { if (e.pointerId === dragId) { e.preventDefault(); e.stopPropagation(); - const w = root.clientWidth; - const h = root.clientHeight; + const w = canvas.clientWidth; + const h = canvas.clientHeight; - root.releasePointerCapture(dragId); + canvas.releasePointerCapture(dragId); dragId = undefined; - this.svg.style.display = 'none'; + svg.style.display = 'none'; - this.events.fire('select.rect', e.shiftKey ? 'add' : (e.ctrlKey ? 'remove' : 'set'), { - start: { x: Math.min(this.start.x, this.end.x) / w, y: Math.min(this.start.y, this.end.y) / h }, - end: { x: Math.max(this.start.x, this.end.x) / w, y: Math.max(this.start.y, this.end.y) / h }, + events.fire('select.rect', e.shiftKey ? 'add' : (e.ctrlKey ? 'remove' : 'set'), { + start: { x: Math.min(start.x, end.x) / w, y: Math.min(start.y, end.y) / h }, + end: { x: Math.max(start.x, end.x) / w, y: Math.max(start.y, end.y) / h }, }); } - }); + }; - parent.appendChild(root); - root.appendChild(svg); - svg.appendChild(rect); + this.activate = () => { + canvas.addEventListener('pointerdown', pointerdown, true); + canvas.addEventListener('pointermove', pointermove, true); + canvas.addEventListener('pointerup', pointerup, true); + }; - this.events = events; - this.root = root; - this.svg = svg; - this.rect = rect; - } + this.deactivate = () => { + canvas.removeEventListener('pointerdown', pointerdown, true); + canvas.removeEventListener('pointermove', pointermove, true); + canvas.removeEventListener('pointerup', pointerup, true); + }; - activate() { - this.root.style.display = 'block'; + parent.appendChild(svg); + svg.appendChild(rect); } - deactivate() { - this.root.style.display = 'none'; + destroy() { + } } From 7736a20e43c1de3396ae2c4338e501824203f86b Mon Sep 17 00:00:00 2001 From: Donovan Hutchence Date: Tue, 9 Jul 2024 09:19:10 +0100 Subject: [PATCH 2/5] add camera modifier on right click --- src/controllers.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/controllers.ts b/src/controllers.ts index 9915901..a44be45 100644 --- a/src/controllers.ts +++ b/src/controllers.ts @@ -94,11 +94,17 @@ class PointerController { x = event.offsetX; y = event.offsetY; - if (buttons[0]) { + // right button can be used to orbit with ctrl key and to zoom with alt | meta key + const mod = buttons[2] ? + (event.shiftKey ? 'orbit' : + (event.altKey || event.metaKey ? 'zoom' : null)) : + null; + + if (mod === 'orbit' || (mod === null && buttons[0])) { orbit(dx, dy); - } else if (buttons[1]) { + } else if (mod === 'zoom' || (mod === null && buttons[1])) { zoom(dy * -0.02); - } else if (buttons[2]) { + } else if (mod === 'pan' || (mod === null && buttons[2])) { pan(x, y, dx, dy); } } else { From 07f0841d7e1bf52177e467f4a55c3ecf2e80510f Mon Sep 17 00:00:00 2001 From: Donovan Hutchence Date: Tue, 9 Jul 2024 09:33:04 +0100 Subject: [PATCH 3/5] small --- src/controllers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers.ts b/src/controllers.ts index a44be45..87ba081 100644 --- a/src/controllers.ts +++ b/src/controllers.ts @@ -96,7 +96,7 @@ class PointerController { // right button can be used to orbit with ctrl key and to zoom with alt | meta key const mod = buttons[2] ? - (event.shiftKey ? 'orbit' : + (event.shiftKey || event.ctrlKey ? 'orbit' : (event.altKey || event.metaKey ? 'zoom' : null)) : null; From e6470a68a93d9861915dd817b0bfc5f6926c9841 Mon Sep 17 00:00:00 2001 From: Donovan Hutchence Date: Tue, 9 Jul 2024 10:00:43 +0100 Subject: [PATCH 4/5] make tools toggleable --- src/main.ts | 12 ++++++------ src/shortcuts.ts | 42 +++++++++++++++++++++++++++++++++++------- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/main.ts b/src/main.ts index 5a12233..0e57e04 100644 --- a/src/main.ts +++ b/src/main.ts @@ -62,15 +62,15 @@ const initShortcuts = (events: Events) => { shortcuts.register(['Delete', 'Backspace'], { event: 'select.delete' }); shortcuts.register(['Escape'], { event: 'tool.deactivate' }); shortcuts.register(['Tab'], { event: 'selection.next' }); - shortcuts.register(['1'], { event: 'tool.move' }); - shortcuts.register(['2'], { event: 'tool.rotate' }); - shortcuts.register(['3'], { event: 'tool.scale' }); + shortcuts.register(['1'], { event: 'tool.move', toggle: true }); + shortcuts.register(['2'], { event: 'tool.rotate', toggle: true }); + shortcuts.register(['3'], { event: 'tool.scale', toggle: true }); shortcuts.register(['G', 'g'], { event: 'show.gridToggle' }); shortcuts.register(['C', 'c'], { event: 'tool.toggleCoordSpace' }); shortcuts.register(['F', 'f'], { event: 'camera.focus' }); - shortcuts.register(['B', 'b'], { event: 'tool.brushSelection' }); - shortcuts.register(['R', 'r'], { event: 'tool.rectSelection' }); - shortcuts.register(['P', 'p'], { event: 'tool.pickerSelection' }); + shortcuts.register(['B', 'b'], { event: 'tool.brushSelection', toggle: true }); + shortcuts.register(['R', 'r'], { event: 'tool.rectSelection', toggle: true }); + shortcuts.register(['P', 'p'], { event: 'tool.pickerSelection', toggle: true }); shortcuts.register(['A', 'a'], { event: 'select.all' }); shortcuts.register(['A', 'a'], { event: 'select.none', shift: true }); shortcuts.register(['I', 'i'], { event: 'select.invert' }); diff --git a/src/shortcuts.ts b/src/shortcuts.ts index e17c8ed..6d62d21 100644 --- a/src/shortcuts.ts +++ b/src/shortcuts.ts @@ -3,38 +3,66 @@ import { Events } from "./events"; interface ShortcutOptions { ctrl?: boolean; shift?: boolean; + toggle?: boolean; func?: () => void; event?: string; } class Shortcuts { - shortcuts: { keys: string[], options: ShortcutOptions }[] = []; + shortcuts: { keys: string[], options: ShortcutOptions, toggled: boolean }[] = []; constructor(events: Events) { const shortcuts = this.shortcuts; - // register keyboard handler - document.addEventListener('keydown', (e) => { + const handleEvent = (e: KeyboardEvent, down: boolean) => { // skip keys in input fields if (e.target !== document.body) return; for (let i = 0; i < shortcuts.length; i++) { - if (shortcuts[i].keys.includes(e.key) && - !!shortcuts[i].options.ctrl === !!(e.ctrlKey || e.metaKey) && - !!shortcuts[i].options.shift === !!e.shiftKey) { + const shortcut = shortcuts[i]; + const options = shortcut.options; + + if (shortcut.keys.includes(e.key) && + !!options.ctrl === !!(e.ctrlKey || e.metaKey) && + !!options.shift === !!e.shiftKey) { + + // handle toggle shortcuts + if (options.toggle) { + if (down) { + shortcut.toggled = e.repeat; + } + + if (down === shortcut.toggled) { + return; + } + } else { + // ignore up events on non-toggled shortcuts + if (!down) return; + } + if (shortcuts[i].options.event) { events.fire(shortcuts[i].options.event); } else { shortcuts[i].options.func(); } + break; } } + }; + + // register keyboard handler + document.addEventListener('keydown', (e) => { + handleEvent(e, true); + }); + + document.addEventListener('keyup', (e) => { + handleEvent(e, false); }); } register(keys: string[], options: ShortcutOptions) { - this.shortcuts.push({ keys, options }); + this.shortcuts.push({ keys, options, toggled: false }); } } From 0059d7afcb858652ff9cdf47852a067243f6b244 Mon Sep 17 00:00:00 2001 From: Donovan Hutchence Date: Tue, 9 Jul 2024 11:24:31 +0100 Subject: [PATCH 5/5] rename --- src/main.ts | 12 ++++++------ src/shortcuts.ts | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main.ts b/src/main.ts index 0e57e04..f8c0353 100644 --- a/src/main.ts +++ b/src/main.ts @@ -62,15 +62,15 @@ const initShortcuts = (events: Events) => { shortcuts.register(['Delete', 'Backspace'], { event: 'select.delete' }); shortcuts.register(['Escape'], { event: 'tool.deactivate' }); shortcuts.register(['Tab'], { event: 'selection.next' }); - shortcuts.register(['1'], { event: 'tool.move', toggle: true }); - shortcuts.register(['2'], { event: 'tool.rotate', toggle: true }); - shortcuts.register(['3'], { event: 'tool.scale', toggle: true }); + shortcuts.register(['1'], { event: 'tool.move', sticky: true }); + shortcuts.register(['2'], { event: 'tool.rotate', sticky: true }); + shortcuts.register(['3'], { event: 'tool.scale', sticky: true }); shortcuts.register(['G', 'g'], { event: 'show.gridToggle' }); shortcuts.register(['C', 'c'], { event: 'tool.toggleCoordSpace' }); shortcuts.register(['F', 'f'], { event: 'camera.focus' }); - shortcuts.register(['B', 'b'], { event: 'tool.brushSelection', toggle: true }); - shortcuts.register(['R', 'r'], { event: 'tool.rectSelection', toggle: true }); - shortcuts.register(['P', 'p'], { event: 'tool.pickerSelection', toggle: true }); + shortcuts.register(['B', 'b'], { event: 'tool.brushSelection', sticky: true }); + shortcuts.register(['R', 'r'], { event: 'tool.rectSelection', sticky: true }); + shortcuts.register(['P', 'p'], { event: 'tool.pickerSelection', sticky: true }); shortcuts.register(['A', 'a'], { event: 'select.all' }); shortcuts.register(['A', 'a'], { event: 'select.none', shift: true }); shortcuts.register(['I', 'i'], { event: 'select.invert' }); diff --git a/src/shortcuts.ts b/src/shortcuts.ts index 6d62d21..57b8168 100644 --- a/src/shortcuts.ts +++ b/src/shortcuts.ts @@ -3,7 +3,7 @@ import { Events } from "./events"; interface ShortcutOptions { ctrl?: boolean; shift?: boolean; - toggle?: boolean; + sticky?: boolean; func?: () => void; event?: string; } @@ -26,8 +26,8 @@ class Shortcuts { !!options.ctrl === !!(e.ctrlKey || e.metaKey) && !!options.shift === !!e.shiftKey) { - // handle toggle shortcuts - if (options.toggle) { + // handle sticky shortcuts + if (options.sticky) { if (down) { shortcut.toggled = e.repeat; } @@ -36,7 +36,7 @@ class Shortcuts { return; } } else { - // ignore up events on non-toggled shortcuts + // ignore up events on non-sticky shortcuts if (!down) return; }