Skip to content

Commit

Permalink
fix: controls event dispatcher types (#388)
Browse files Browse the repository at this point in the history
  • Loading branch information
RodrigoHamuy authored Dec 11, 2024
1 parent 6b51b6d commit 5d9e6ec
Show file tree
Hide file tree
Showing 11 changed files with 235 additions and 18 deletions.
5 changes: 3 additions & 2 deletions src/controls/ArcballControls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<StandardControlsEventMap> {
private camera: OrthographicCamera | PerspectiveCamera | null
private domElement: HTMLElement | null | undefined
private scene: Scene | null | undefined
Expand Down
6 changes: 4 additions & 2 deletions src/controls/DeviceOrientationControls.ts
Original file line number Diff line number Diff line change
@@ -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<StandardControlsEventMap> {
public object: Camera

private changeEvent = { type: 'change' }
Expand Down
32 changes: 30 additions & 2 deletions src/controls/DragControls.ts
Original file line number Diff line number Diff line change
@@ -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<DragControlsEventMap> {
public enabled = true
public transformGroup = false

Expand Down
139 changes: 139 additions & 0 deletions src/controls/EventDispatcher.ts
Original file line number Diff line number Diff line change
@@ -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<TEventType extends string = string> {
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<TEventType extends string = string, TTarget = unknown> {
readonly type: TEventType;
readonly target: TTarget;
}

export type EventListener<TEventData, TEventType extends string, TTarget> = (
event: TEventData & Event<TEventType, TTarget>,
) => void;

export class EventDispatcher<TEventMap extends {} = {}> {
// 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<T extends Extract<keyof TEventMap, string>>(
type: T,
listener: EventListener<TEventMap[T], T, this>,
): 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<T extends Extract<keyof TEventMap, string>>(
type: T,
listener: EventListener<TEventMap[T], T, this>,
): 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<T extends Extract<keyof TEventMap, string>>(
type: T,
listener: EventListener<TEventMap[T], T, this>,
): 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<T extends Extract<keyof TEventMap, string>>(event: BaseEvent<T> & 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;

}

}

}
6 changes: 4 additions & 2 deletions src/controls/FirstPersonControls.ts
Original file line number Diff line number Diff line change
@@ -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

Expand Down
12 changes: 10 additions & 2 deletions src/controls/FlyControls.ts
Original file line number Diff line number Diff line change
@@ -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<FlyControlsEventMap> {
public object: Camera
public domElement: HTMLElement | Document = null!

Expand Down
5 changes: 3 additions & 2 deletions src/controls/OrbitControls.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
EventDispatcher,
Matrix4,
MOUSE,
OrthographicCamera,
Expand All @@ -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()
Expand All @@ -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<StandardControlsEventMap> {
object: PerspectiveCamera | OrthographicCamera
domElement: HTMLElement | undefined
// Set to false to disable this control
Expand Down
22 changes: 20 additions & 2 deletions src/controls/PointerLockControls.ts
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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<PointerLockControlsEventMap> {
public camera: Camera
public domElement?: HTMLElement
public isLocked: boolean
Expand Down
16 changes: 16 additions & 0 deletions src/controls/StandardControlsEventMap.ts
Original file line number Diff line number Diff line change
@@ -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: {};
}
6 changes: 4 additions & 2 deletions src/controls/TrackballControls.ts
Original file line number Diff line number Diff line change
@@ -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<StandardControlsEventMap> {
public enabled = true

public screen = { left: 0, top: 0, width: 0, height: 0 }
Expand Down
4 changes: 2 additions & 2 deletions src/controls/experimental/CameraControls.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
EventDispatcher,
MOUSE,
Matrix4,
OrthographicCamera,
Expand All @@ -10,6 +9,7 @@ import {
Vector2,
Vector3,
} from 'three'
import { EventDispatcher } from '../EventDispatcher'

export type CHANGE_EVENT = {
type: 'change' | 'start' | 'end'
Expand All @@ -26,7 +26,7 @@ export const STATE = {
TOUCH_DOLLY_ROTATE: 6,
}

class CameraControls extends EventDispatcher {
class CameraControls extends EventDispatcher<Record<string, {}>> {
object: PerspectiveCamera | OrthographicCamera
domElement: HTMLElement

Expand Down

0 comments on commit 5d9e6ec

Please sign in to comment.