diff --git a/packages/viewer-sandbox/src/main.ts b/packages/viewer-sandbox/src/main.ts index 1973bd327..8c927a387 100644 --- a/packages/viewer-sandbox/src/main.ts +++ b/packages/viewer-sandbox/src/main.ts @@ -39,6 +39,7 @@ const createViewer = async (containerName: string, _stream: string) => { const params = DefaultViewerParams params.showStats = true params.verbose = true + // params.restrictInputToCanvas = true const multiSelectList: SelectionEvent[] = [] const viewer: Viewer = new Viewer(container, params) diff --git a/packages/viewer/src/IViewer.ts b/packages/viewer/src/IViewer.ts index 61687c414..6ab7732bd 100644 --- a/packages/viewer/src/IViewer.ts +++ b/packages/viewer/src/IViewer.ts @@ -39,7 +39,9 @@ export interface ViewerParams { showStats: boolean environmentSrc: Asset verbose: boolean + restrictInputToCanvas: boolean } + export enum AssetType { TEXTURE_8BPP = 'png', // For now TEXTURE_HDR = 'hdr', @@ -71,7 +73,8 @@ export const DefaultViewerParams: ViewerParams = { id: 'defaultHDRI', src: defaultHdri, type: AssetType.TEXTURE_EXR - } + }, + restrictInputToCanvas: false } export enum ViewerEvent { @@ -205,6 +208,7 @@ export interface IViewer { getRenderer(): SpeckleRenderer getContainer(): HTMLElement + getCanvas(): HTMLCanvasElement createExtension(type: Constructor): T getExtension(type: Constructor): T diff --git a/packages/viewer/src/modules/SpeckleRenderer.ts b/packages/viewer/src/modules/SpeckleRenderer.ts index 66591530f..0371126f9 100644 --- a/packages/viewer/src/modules/SpeckleRenderer.ts +++ b/packages/viewer/src/modules/SpeckleRenderer.ts @@ -436,7 +436,10 @@ export default class SpeckleRenderer { this._pipeline = new DefaultPipeline(this) - this.input = new Input(this._renderer.domElement) + this.input = new Input( + this._renderer.domElement, + this.viewer.params.restrictInputToCanvas + ) this.input.on(InputEvent.Click, this.onClick.bind(this)) this.input.on(InputEvent.DoubleClick, this.onDoubleClick.bind(this)) diff --git a/packages/viewer/src/modules/Viewer.ts b/packages/viewer/src/modules/Viewer.ts index b05cb34b6..8fe0cd08e 100644 --- a/packages/viewer/src/modules/Viewer.ts +++ b/packages/viewer/src/modules/Viewer.ts @@ -75,6 +75,10 @@ export class Viewer extends EventEmitter implements IViewer { return this.speckleRenderer.input } + public get params(): ViewerParams { + return this.startupParams + } + private getConstructorChain(obj: object) { const cs = [] let pt = obj @@ -166,6 +170,10 @@ export class Viewer extends EventEmitter implements IViewer { return this.container } + public getCanvas() { + return this.speckleRenderer.renderer.domElement + } + public getRenderer() { return this.speckleRenderer } diff --git a/packages/viewer/src/modules/extensions/CameraController.ts b/packages/viewer/src/modules/extensions/CameraController.ts index 9ea068aee..fa6095f64 100644 --- a/packages/viewer/src/modules/extensions/CameraController.ts +++ b/packages/viewer/src/modules/extensions/CameraController.ts @@ -182,7 +182,7 @@ export class CameraController extends Extension implements SpeckleCamera { this._flyControls = new FlyControls( this._renderingCamera, - this.viewer.getContainer(), + this.viewer.getRenderer().input, this.viewer.World, this._options ) diff --git a/packages/viewer/src/modules/extensions/HybridCameraController.ts b/packages/viewer/src/modules/extensions/HybridCameraController.ts index 043cabc0d..4ef5ec6ed 100644 --- a/packages/viewer/src/modules/extensions/HybridCameraController.ts +++ b/packages/viewer/src/modules/extensions/HybridCameraController.ts @@ -1,6 +1,7 @@ import { PerspectiveCamera } from 'three' import { IViewer } from '../../IViewer.js' import { CameraController } from './CameraController.js' +import { InputEvent } from '../input/Input.js' type MoveType = 'forward' | 'back' | 'left' | 'right' | 'up' | 'down' export class HybridCameraController extends CameraController { @@ -17,9 +18,9 @@ export class HybridCameraController extends CameraController { public constructor(viewer: IViewer) { super(viewer) - document.addEventListener('keydown', this.onKeyDown.bind(this)) - document.addEventListener('keyup', this.onKeyUp.bind(this)) - document.addEventListener('contextmenu', this.onContextMenu.bind(this)) + viewer.getRenderer().input.on(InputEvent.KeyUp, this.onKeyUp.bind(this)) + viewer.getRenderer().input.on(InputEvent.KeyDown, this.onKeyDown.bind(this)) + viewer.getRenderer().input.on(InputEvent.ContextMenu, this.onContextMenu.bind(this)) } public onEarlyUpdate(_delta?: number): void { diff --git a/packages/viewer/src/modules/extensions/controls/FlyControls.ts b/packages/viewer/src/modules/extensions/controls/FlyControls.ts index e2efc0884..ce7c439fe 100644 --- a/packages/viewer/src/modules/extensions/controls/FlyControls.ts +++ b/packages/viewer/src/modules/extensions/controls/FlyControls.ts @@ -13,6 +13,7 @@ import { SpeckleControls } from './SpeckleControls.js' import { World } from '../../World.js' import { AngleDamper } from '../../utils/AngleDamper.js' import { TIME_MS } from '@speckle/shared' +import Input, { InputEvent } from '../../input/Input.js' const _vectorBuff0 = new Vector3() @@ -33,9 +34,9 @@ export interface FlyControlsOptions { } class FlyControls extends SpeckleControls { + protected input: Input protected _options: Required protected _targetCamera: PerspectiveCamera | OrthographicCamera - protected container: HTMLElement protected velocity = new Vector3() protected euler = new Euler(0, 0, 0, 'YXZ') protected position = new Vector3() @@ -109,14 +110,14 @@ class FlyControls extends SpeckleControls { constructor( camera: PerspectiveCamera | OrthographicCamera, - container: HTMLElement, + input: Input, world: World, options: Required ) { super() this._targetCamera = camera - this.container = container + this.input = input this.world = world this._options = Object.assign({}, options) @@ -324,20 +325,20 @@ class FlyControls extends SpeckleControls { protected connect() { if (this._enabled) return - - this.container.addEventListener('pointermove', this.onMouseMove) - document.addEventListener('keydown', this.onKeyDown) - document.addEventListener('keyup', this.onKeyUp) - document.addEventListener('contextmenu', this.onContextMenu) + this.input.on(InputEvent.KeyUp, this.onKeyUp) + this.input.on(InputEvent.KeyDown, this.onKeyDown) + this.input.on(InputEvent.PointerMove, this.onMouseMove) + this.input.on(InputEvent.ContextMenu, this.onContextMenu) } protected disconnect() { if (!this._enabled) return - this.container.removeEventListener('pointermove', this.onMouseMove) - document.removeEventListener('keydown', this.onKeyDown) - document.removeEventListener('keyup', this.onKeyUp) - document.removeEventListener('contextmenu', this.onContextMenu) + this.input.removeListener(InputEvent.KeyUp, this.onKeyUp) + this.input.removeListener(InputEvent.KeyDown, this.onKeyDown) + this.input.removeListener(InputEvent.PointerMove, this.onMouseMove) + this.input.removeListener(InputEvent.ContextMenu, this.onContextMenu) + for (const k in this.keyMap) this.keyMap[k as MoveType] = false } @@ -355,11 +356,11 @@ class FlyControls extends SpeckleControls { } // event listeners - protected onMouseMove = (event: PointerEvent) => { - if (event.buttons !== 1 || !this._enabled) return + protected onMouseMove = (arg: Vector2 & { event: PointerEvent }) => { + if (arg.event.buttons !== 1 || !this._enabled) return - const movementX = event.movementX || 0 - const movementY = event.movementY || 0 + const movementX = arg.event.movementX || 0 + const movementY = arg.event.movementY || 0 const amount = new Vector2() amount.y = movementX * 0.005 * this._options.lookSpeed amount.x = movementY * 0.005 * this._options.lookSpeed diff --git a/packages/viewer/src/modules/extensions/sections/SectionTool.ts b/packages/viewer/src/modules/extensions/sections/SectionTool.ts index 0358d5b9a..17be7dd1f 100644 --- a/packages/viewer/src/modules/extensions/sections/SectionTool.ts +++ b/packages/viewer/src/modules/extensions/sections/SectionTool.ts @@ -618,9 +618,8 @@ export class SectionTool extends Extension { } } - /** Event listeners */ - document.addEventListener('keydown', this.keydownHandler) - document.addEventListener('keyup', this.keyupHandler) + this.viewer.getRenderer().input.on(InputEvent.KeyDown, this.keydownHandler) + this.viewer.getRenderer().input.on(InputEvent.KeyUp, this.keyupHandler) } /** @@ -1135,10 +1134,14 @@ export class SectionTool extends Extension { */ public dispose() { if (this.keydownHandler) { - document.removeEventListener('keydown', this.keydownHandler) + this.viewer + .getRenderer() + .input.removeListener(InputEvent.KeyDown, this.keydownHandler) } if (this.keyupHandler) { - document.removeEventListener('keyup', this.keyupHandler) + this.viewer + .getRenderer() + .input.removeListener(InputEvent.KeyUp, this.keyupHandler) } } } diff --git a/packages/viewer/src/modules/input/Input.ts b/packages/viewer/src/modules/input/Input.ts index 13232b119..a30d9c2be 100644 --- a/packages/viewer/src/modules/input/Input.ts +++ b/packages/viewer/src/modules/input/Input.ts @@ -9,7 +9,9 @@ export enum InputEvent { Wheel = 'wheel', Click = 'click', DoubleClick = 'double-click', - KeyUp = 'key-up' + KeyUp = 'key-up', + KeyDown = 'key-down', + ContextMenu = 'context-menu' } export interface InputEventPayload { @@ -21,6 +23,8 @@ export interface InputEventPayload { [InputEvent.Click]: Vector2 & { event: PointerEvent; multiSelect: boolean } [InputEvent.DoubleClick]: Vector2 & { event: PointerEvent; multiSelect: boolean } [InputEvent.KeyUp]: KeyboardEvent + [InputEvent.KeyDown]: KeyboardEvent + [InputEvent.ContextMenu]: PointerEvent } //TO DO: Define proper interface for InputEvent data @@ -33,14 +37,20 @@ export default class Input extends EventEmitter { private touchLocation: Touch | undefined private container - constructor(container: HTMLElement) { + constructor(container: HTMLElement, restrictKeyInput: boolean = false) { super() this.container = container - + if (restrictKeyInput) { + // Make canvas focusable for scoped keyboard events + this.container.tabIndex = -1 + // Remove default focus outline + this.container.style.outline = 'none' + } // Handle mouseclicks let mdTime: number this.container.addEventListener('pointerdown', (e) => { e.preventDefault() + this.container.focus() // preventDefault blocks default focus const loc = this._getNormalisedClickPosition(e) ;(loc as unknown as Record).event = e mdTime = new Date().getTime() @@ -106,11 +116,15 @@ export default class Input extends EventEmitter { this.emit(InputEvent.PointerMove, data) }) - document.addEventListener('keyup', (e) => { + const keySource = restrictKeyInput ? this.container : document + keySource.addEventListener('keyup', (e) => { this.emit(InputEvent.KeyUp, e) }) + keySource.addEventListener('keydown', (e) => { + this.emit(InputEvent.KeyDown, e) + }) - document.addEventListener('wheel', (e) => { + this.container.addEventListener('wheel', (e) => { this.emit(InputEvent.Wheel, e) }) @@ -121,6 +135,10 @@ export default class Input extends EventEmitter { this.emit(InputEvent.PointerUp, loc) this.emit(InputEvent.PointerCancel, loc) }) + + this.container.addEventListener('contextmenu', (e) => { + this.emit(InputEvent.ContextMenu, e) + }) } public on(