From 5d9e6ec1ec760a62cf65d900d79f8e0b12996fd6 Mon Sep 17 00:00:00 2001 From: Rodri Date: Wed, 11 Dec 2024 16:29:42 +0000 Subject: [PATCH] fix: controls event dispatcher types (#388) --- src/controls/ArcballControls.ts | 5 +- src/controls/DeviceOrientationControls.ts | 6 +- src/controls/DragControls.ts | 32 ++++- src/controls/EventDispatcher.ts | 139 ++++++++++++++++++++ src/controls/FirstPersonControls.ts | 6 +- src/controls/FlyControls.ts | 12 +- src/controls/OrbitControls.ts | 5 +- src/controls/PointerLockControls.ts | 22 +++- src/controls/StandardControlsEventMap.ts | 16 +++ src/controls/TrackballControls.ts | 6 +- src/controls/experimental/CameraControls.ts | 4 +- 11 files changed, 235 insertions(+), 18 deletions(-) create mode 100644 src/controls/EventDispatcher.ts create mode 100644 src/controls/StandardControlsEventMap.ts diff --git a/src/controls/ArcballControls.ts b/src/controls/ArcballControls.ts index 8a65882d..9a64a49c 100644 --- a/src/controls/ArcballControls.ts +++ b/src/controls/ArcballControls.ts @@ -18,8 +18,9 @@ import { OrthographicCamera, Mesh, Material, - EventDispatcher, } from 'three' +import { EventDispatcher } from './EventDispatcher' +import { StandardControlsEventMap } from './StandardControlsEventMap' type Camera = OrthographicCamera | PerspectiveCamera type Operation = 'PAN' | 'ROTATE' | 'ZOOM' | 'FOV' @@ -82,7 +83,7 @@ const _endEvent = { type: 'end' } * @param {HTMLElement=null} domElement Renderer's dom element * @param {Scene=null} scene The scene to be rendered */ -class ArcballControls extends EventDispatcher { +class ArcballControls extends EventDispatcher { private camera: OrthographicCamera | PerspectiveCamera | null private domElement: HTMLElement | null | undefined private scene: Scene | null | undefined diff --git a/src/controls/DeviceOrientationControls.ts b/src/controls/DeviceOrientationControls.ts index ab6a821a..b3cfe689 100644 --- a/src/controls/DeviceOrientationControls.ts +++ b/src/controls/DeviceOrientationControls.ts @@ -1,10 +1,12 @@ -import { Camera, Euler, EventDispatcher, MathUtils, Quaternion, Vector3 } from 'three' +import { Camera, Euler, MathUtils, Quaternion, Vector3 } from 'three' +import { EventDispatcher } from './EventDispatcher' +import { StandardControlsEventMap } from './StandardControlsEventMap' /** * W3C Device Orientation control (http://w3c.github.io/deviceorientation/spec-source-orientation.html) */ -class DeviceOrientationControls extends EventDispatcher { +class DeviceOrientationControls extends EventDispatcher { public object: Camera private changeEvent = { type: 'change' } diff --git a/src/controls/DragControls.ts b/src/controls/DragControls.ts index 86877de6..6c6fb52c 100644 --- a/src/controls/DragControls.ts +++ b/src/controls/DragControls.ts @@ -1,6 +1,34 @@ -import { Camera, EventDispatcher, Intersection, Matrix4, Object3D, Plane, Raycaster, Vector2, Vector3 } from 'three' +import { Camera, Intersection, Matrix4, Object3D, Plane, Raycaster, Vector2, Vector3 } from 'three' +import { EventDispatcher } from './EventDispatcher' + +export interface DragControlsEventMap { + /** + * Fires when the pointer is moved onto a 3D object, or onto one of its children. + */ + hoveron: { object: Object3D }; + + /** + * Fires when the pointer is moved out of a 3D object. + */ + hoveroff: { object: Object3D }; + + /** + * Fires when the user starts to drag a 3D object. + */ + dragstart: { object: Object3D }; + + /** + * Fires when the user drags a 3D object. + */ + drag: { object: Object3D }; + + /** + * Fires when the user has finished dragging a 3D object. + */ + dragend: { object: Object3D }; +} -class DragControls extends EventDispatcher { +class DragControls extends EventDispatcher { public enabled = true public transformGroup = false diff --git a/src/controls/EventDispatcher.ts b/src/controls/EventDispatcher.ts new file mode 100644 index 00000000..370c33b5 --- /dev/null +++ b/src/controls/EventDispatcher.ts @@ -0,0 +1,139 @@ +/* +Due to @types/three r168 breaking change +we have to manually copy the EventDispatcher class from three.js. +So this files merges the declarations from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/three/src/core/EventDispatcher.d.ts +with the implementation from https://github.com/mrdoob/three.js/blob/dev/src/core/EventDispatcher.js +More info in https://github.com/pmndrs/three-stdlib/issues/387 +*/ + +/** + * The minimal basic Event that can be dispatched by a {@link EventDispatcher<>}. + */ +export interface BaseEvent { + readonly type: TEventType; + // not defined in @types/three + target: any; +} + +/** + * The minimal expected contract of a fired Event that was dispatched by a {@link EventDispatcher<>}. + */ +export interface Event { + readonly type: TEventType; + readonly target: TTarget; +} + +export type EventListener = ( + event: TEventData & Event, +) => void; + +export class EventDispatcher { + // not defined in @types/three + private _listeners: any; + + /** + * Adds a listener to an event type. + * @param type The type of event to listen to. + * @param listener The function that gets called when the event is fired. + */ + addEventListener>( + type: T, + listener: EventListener, + ): void { + + if ( this._listeners === undefined ) this._listeners = {}; + + const listeners = this._listeners; + + if ( listeners[ type ] === undefined ) { + + listeners[ type ] = []; + + } + + if ( listeners[ type ].indexOf( listener ) === - 1 ) { + + listeners[ type ].push( listener ); + + } + + } + + /** + * Checks if listener is added to an event type. + * @param type The type of event to listen to. + * @param listener The function that gets called when the event is fired. + */ + hasEventListener>( + type: T, + listener: EventListener, + ): boolean { + + if ( this._listeners === undefined ) return false; + + const listeners = this._listeners; + + return listeners[ type ] !== undefined && listeners[ type ].indexOf( listener ) !== - 1; + + } + + /** + * Removes a listener from an event type. + * @param type The type of the listener that gets removed. + * @param listener The listener function that gets removed. + */ + removeEventListener>( + type: T, + listener: EventListener, + ): void { + + if ( this._listeners === undefined ) return; + + const listeners = this._listeners; + const listenerArray = listeners[ type ]; + + if ( listenerArray !== undefined ) { + + const index = listenerArray.indexOf( listener ); + + if ( index !== - 1 ) { + + listenerArray.splice( index, 1 ); + + } + + } + + } + + /** + * Fire an event type. + * @param event The event that gets fired. + */ + dispatchEvent>(event: BaseEvent & TEventMap[T]): void { + + if ( this._listeners === undefined ) return; + + const listeners = this._listeners; + const listenerArray = listeners[ event.type ]; + + if ( listenerArray !== undefined ) { + + event.target = this; + + // Make a copy, in case listeners are removed while iterating. + const array = listenerArray.slice( 0 ); + + for ( let i = 0, l = array.length; i < l; i ++ ) { + + array[ i ].call( this, event ); + + } + + event.target = null; + + } + + } + +} \ No newline at end of file diff --git a/src/controls/FirstPersonControls.ts b/src/controls/FirstPersonControls.ts index e6bbd251..b69983f6 100644 --- a/src/controls/FirstPersonControls.ts +++ b/src/controls/FirstPersonControls.ts @@ -1,8 +1,10 @@ -import { MathUtils, Spherical, Vector3, EventDispatcher, Camera } from 'three' +import { MathUtils, Spherical, Vector3, Camera } from 'three' +import { EventDispatcher } from './EventDispatcher' +import { StandardControlsEventMap } from './StandardControlsEventMap' const targetPosition = new Vector3() -export class FirstPersonControls extends EventDispatcher { +export class FirstPersonControls extends EventDispatcher<{}> { public object: Camera public domElement?: HTMLElement | null diff --git a/src/controls/FlyControls.ts b/src/controls/FlyControls.ts index bbd8da7d..4d473bdc 100644 --- a/src/controls/FlyControls.ts +++ b/src/controls/FlyControls.ts @@ -1,10 +1,18 @@ -import { Camera, EventDispatcher, Quaternion, Vector3 } from 'three' +import { Camera, Quaternion, Vector3 } from 'three' +import { EventDispatcher } from './EventDispatcher' function contextmenu(event: Event): void { event.preventDefault() } -class FlyControls extends EventDispatcher { +export interface FlyControlsEventMap { + /** + * Fires when the camera has been transformed by the controls. + */ + change: {}; +} + +class FlyControls extends EventDispatcher { public object: Camera public domElement: HTMLElement | Document = null! diff --git a/src/controls/OrbitControls.ts b/src/controls/OrbitControls.ts index 979d08d8..93d8a764 100644 --- a/src/controls/OrbitControls.ts +++ b/src/controls/OrbitControls.ts @@ -1,5 +1,4 @@ import { - EventDispatcher, Matrix4, MOUSE, OrthographicCamera, @@ -12,6 +11,8 @@ import { Ray, Plane, } from 'three' +import { EventDispatcher } from './EventDispatcher' +import { StandardControlsEventMap } from './StandardControlsEventMap' const _ray = new Ray() const _plane = new Plane() @@ -26,7 +27,7 @@ const TILT_LIMIT = Math.cos(70 * (Math.PI / 180)) const moduloWrapAround = (offset: number, capacity: number) => ((offset % capacity) + capacity) % capacity -class OrbitControls extends EventDispatcher { +class OrbitControls extends EventDispatcher { object: PerspectiveCamera | OrthographicCamera domElement: HTMLElement | undefined // Set to false to disable this control diff --git a/src/controls/PointerLockControls.ts b/src/controls/PointerLockControls.ts index 1aa7f2b9..ee93891e 100644 --- a/src/controls/PointerLockControls.ts +++ b/src/controls/PointerLockControls.ts @@ -1,4 +1,5 @@ -import { Euler, Camera, EventDispatcher, Vector3 } from 'three' +import { Euler, Camera, Vector3 } from 'three' +import { EventDispatcher } from './EventDispatcher' const _euler = new Euler(0, 0, 0, 'YXZ') const _vector = new Vector3() @@ -7,7 +8,24 @@ const _lockEvent = { type: 'lock' } const _unlockEvent = { type: 'unlock' } const _PI_2 = Math.PI / 2 -class PointerLockControls extends EventDispatcher { +export interface PointerLockControlsEventMap { + /** + * Fires when the user moves the mouse. + */ + change: {}; + + /** + * Fires when the pointer lock status is "locked" (in other words: the mouse is captured). + */ + lock: {}; + + /** + * Fires when the pointer lock status is "unlocked" (in other words: the mouse is not captured anymore). + */ + unlock: {}; +} + +class PointerLockControls extends EventDispatcher { public camera: Camera public domElement?: HTMLElement public isLocked: boolean diff --git a/src/controls/StandardControlsEventMap.ts b/src/controls/StandardControlsEventMap.ts new file mode 100644 index 00000000..cd3bd3cc --- /dev/null +++ b/src/controls/StandardControlsEventMap.ts @@ -0,0 +1,16 @@ +export interface StandardControlsEventMap { + /** + * Fires when the camera has been transformed by the controls. + */ + change: {}; + + /** + * Fires when an interaction was initiated. + */ + start: {}; + + /** + * Fires when an interaction has finished. + */ + end: {}; + } \ No newline at end of file diff --git a/src/controls/TrackballControls.ts b/src/controls/TrackballControls.ts index b523b5a5..5991f4f9 100644 --- a/src/controls/TrackballControls.ts +++ b/src/controls/TrackballControls.ts @@ -1,6 +1,8 @@ -import { EventDispatcher, MOUSE, Quaternion, Vector2, Vector3, PerspectiveCamera, OrthographicCamera } from 'three' +import { MOUSE, Quaternion, Vector2, Vector3, PerspectiveCamera, OrthographicCamera } from 'three' +import { EventDispatcher } from './EventDispatcher' +import { StandardControlsEventMap } from './StandardControlsEventMap' -class TrackballControls extends EventDispatcher { +class TrackballControls extends EventDispatcher { public enabled = true public screen = { left: 0, top: 0, width: 0, height: 0 } diff --git a/src/controls/experimental/CameraControls.ts b/src/controls/experimental/CameraControls.ts index c894ec6b..dd6e25c3 100644 --- a/src/controls/experimental/CameraControls.ts +++ b/src/controls/experimental/CameraControls.ts @@ -1,5 +1,4 @@ import { - EventDispatcher, MOUSE, Matrix4, OrthographicCamera, @@ -10,6 +9,7 @@ import { Vector2, Vector3, } from 'three' +import { EventDispatcher } from '../EventDispatcher' export type CHANGE_EVENT = { type: 'change' | 'start' | 'end' @@ -26,7 +26,7 @@ export const STATE = { TOUCH_DOLLY_ROTATE: 6, } -class CameraControls extends EventDispatcher { +class CameraControls extends EventDispatcher> { object: PerspectiveCamera | OrthographicCamera domElement: HTMLElement