diff --git a/docs/scripts/camera.ts b/docs/scripts/camera.ts index 16744399..7409dd62 100644 --- a/docs/scripts/camera.ts +++ b/docs/scripts/camera.ts @@ -1,2 +1,2 @@ -export * from '../../src/cameras/OrthographicCameraAuto'; -export * from '../../src/cameras/PerspectiveCameraAuto'; +export * from '../../src/camera/OrthographicCameraAuto'; +export * from '../../src/camera/PerspectiveCameraAuto'; diff --git a/docs/scripts/events.ts b/docs/scripts/events.ts index 8aca4851..e840b0ae 100644 --- a/docs/scripts/events.ts +++ b/docs/scripts/events.ts @@ -1,5 +1,5 @@ export * from '../../src/events/CursorManager'; -export * from '../../src/events/Default'; +export * from '../../src/events/InteractionDefault'; export * from '../../src/events/DragAndDropManager'; export * from '../../src/events/Events'; export * from '../../src/events/EventsDispatcher'; diff --git a/eslint.config.js b/eslint.config.js index 7a1f2b07..9a7d29ec 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -31,6 +31,7 @@ export default [ '@typescript-eslint/no-dynamic-delete': 'off', '@typescript-eslint/no-invalid-void-type': 'off', '@typescript-eslint/no-this-alias': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/explicit-function-return-type': [ 'error', { allowExpressions: true diff --git a/index.html b/index.html index e7cbac64..4bec4428 100644 --- a/index.html +++ b/index.html @@ -25,7 +25,7 @@ - + \ No newline at end of file diff --git a/src/core/Main.ts b/src/Main.ts similarity index 100% rename from src/core/Main.ts rename to src/Main.ts diff --git a/src/binding/Binding.ts b/src/binding/Binding.ts deleted file mode 100644 index cfc09fbd..00000000 --- a/src/binding/Binding.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { Object3D, Scene } from 'three'; - -/** @internal */ -export interface BindingCallback { - getValue: () => T; - setValue: (value: T) => void; - key: string; -} - -/** @internal */ -export class Binding { - public static detectChanges(target: Object3D, resursive: boolean): void { - this.executeAllCallbacks(target); - if (resursive) { - for (const child of target.children) { - this.detectChanges(child, true); - } - } - } - - public static bindProperty(key: string, target: Object3D, getValue: () => T, renderOnChange?: boolean): void { - if (this.getIndexByKey(target, key) > -1) { - console.error('Cannot override property already bound.'); - return; - } - - this.addToBoundCallbacks(key, target, getValue.bind(target), renderOnChange); - if (target.scene) { - this.bindToScene(target); - } - } - - private static addToBoundCallbacks(key: string, target: Object3D, getValue: () => T, renderOnChange: boolean): void { - const setValue = this.createSetValue(key, target, renderOnChange); - const bindingCallback: BindingCallback = { key, getValue, setValue }; - target.__boundCallbacks.push(bindingCallback); - this.executeCallback(bindingCallback); - } - - private static createSetValue(key: string, target: Object3D, renderOnChange: boolean): (value: T) => void { - if (renderOnChange) { - return (value) => { - if (value !== target[key]) { - target[key] = value; - target.needsRender = true; - } - }; - } - return (value) => { - if (value !== target[key]) { - target[key] = value; - } - }; - } - - private static getIndexByKey(target: Object3D, key: string): number { - const boundCallbacks = target.__boundCallbacks; - for (let i = 0; i < boundCallbacks.length; i++) { - if (boundCallbacks[i].key === key) return i; - } - return -1; - } - - public static setManualDetectionMode(target: Object3D): void { - if (target.__manualDetection) return; - if (target.__boundCallbacks.length > 0) { - console.error('Cannot change detectChangesMode if a binding is already created.'); - } else { - target.__manualDetection = true; - } - } - - public static bindToScene(target: Object3D): void { - if (target.__boundCallbacks.length > 0) { - target.scene.__boundObjects.add(target); - } - } - - public static unbindFromScene(target: Object3D): void { - target.scene.__boundObjects.delete(target); - } - - public static unbindProperty(target: Object3D, key: string): void { - const index = this.getIndexByKey(target, key); - if (index > -1) { - target.__boundCallbacks.splice(index, 1); - if (target.scene) { - this.unbindFromScene(target); - } - } - } - - private static executeCallback(bindingCallback: BindingCallback): void { - bindingCallback.setValue(bindingCallback.getValue()); - } - - private static executeAllCallbacks(target: Object3D): void { - const callbacks = target.__boundCallbacks; - for (const callback of callbacks) { - this.executeCallback(callback); - } - } - - public static compute(scene: Scene): void { - const boundObjs = scene.__boundObjects; - for (const target of boundObjs) { - this.executeAllCallbacks(target); - } - } -} diff --git a/src/cameras/OrthographicCameraAuto.ts b/src/camera/OrthographicCameraAuto.ts similarity index 96% rename from src/cameras/OrthographicCameraAuto.ts rename to src/camera/OrthographicCameraAuto.ts index e4fcbe0d..d04c9ddf 100644 --- a/src/cameras/OrthographicCameraAuto.ts +++ b/src/camera/OrthographicCameraAuto.ts @@ -33,7 +33,7 @@ export class OrthographicCameraAuto extends OrthographicCamera { * @param near Camera frustum near plane. Default `0.1`. * @param far Camera frustum far plane. Default `2000`. */ - constructor(size = 2, fixedWidth = true, near?: number, far?: number) { + constructor(size = 2, fixedWidth = true, near = 0.1, far = 2000) { super(-1, 1, 1, -1, near, far); this._size = size; this._fixedWidth = fixedWidth; diff --git a/src/cameras/PerspectiveCameraAuto.ts b/src/camera/PerspectiveCameraAuto.ts similarity index 91% rename from src/cameras/PerspectiveCameraAuto.ts rename to src/camera/PerspectiveCameraAuto.ts index c680c18a..8cd1642a 100644 --- a/src/cameras/PerspectiveCameraAuto.ts +++ b/src/camera/PerspectiveCameraAuto.ts @@ -9,7 +9,7 @@ export class PerspectiveCameraAuto extends PerspectiveCamera { * @param near Camera frustum near plane distance. Default `0.1`. * @param far Camera frustum far plane distance. Default `2000`. */ - constructor(fov?: number, near?: number, far?: number) { + constructor(fov = 50, near = 0.1, far = 2000) { super(fov, undefined, near, far); this.on('viewportresize', (e) => { diff --git a/src/events/CursorManager.ts b/src/events/CursorManager.ts index 4d8399ec..f9303be2 100644 --- a/src/events/CursorManager.ts +++ b/src/events/CursorManager.ts @@ -1,5 +1,7 @@ import { Object3D } from 'three'; -import { InstancedMesh2 } from '../instancedMesh/InstancedMesh2.js'; + +// TODO: add BatchedMesh and InstancedMesh support +// TODO: add default cusor config /** Valid cursor values based on the CSS cursor property. */ export type CursorsKeys = 'auto' | 'default' | 'none' | 'context-menu' | 'help' | 'pointer' | 'progress' | 'wait' | @@ -7,46 +9,44 @@ export type CursorsKeys = 'auto' | 'default' | 'none' | 'context-menu' | 'help' 'all-scroll' | 'col-resize' | 'row-resize' | 'n-resize' | 'e-resize' | 's-resize' | 'w-resize' | 'ne-resize' | 'nw-resize' | 'se-resize' | 'sw-resize' | 'ew-resize' | 'ns-resize' | 'nesw-resize' | 'nwse-resize' | 'zoom-in' | 'zoom-out'; -const cursorSet = new Set([ +/** Represents a cursor, either by a CSS cursor key or a URL. */ +export type Cursor = CursorsKeys | String; + +const cursorSet = new Set([ 'auto', 'default', 'none', 'context-menu', 'help', 'pointer', 'progress', 'wait', 'cell', 'crosshair', 'text', 'vertical-text', 'alias', 'copy', 'move', 'no-drop', 'not-allowed', 'grab', 'grabbing', 'all-scroll', 'col-resize', 'row-resize', 'n-resize', 'e-resize', 's-resize', 'w-resize', 'ne-resize', 'nw-resize', 'se-resize', 'sw-resize', 'ew-resize', 'ns-resize', 'nesw-resize', 'nwse-resize', 'zoom-in', 'zoom-out' ]); -/** Represents a cursor, either by a CSS cursor key or a URL. */ -export type Cursor = CursorsKeys | String; - /** @internal */ export class CursorHandler { public enabled = true; - private _cursor: Cursor; - private _domElement: HTMLCanvasElement; + protected readonly _domElement: HTMLCanvasElement; + protected _currentCursor: Cursor | null = null; constructor(domElement: HTMLCanvasElement) { this._domElement = domElement; } - public update(objDragged: Object3D, objHovered: Object3D, objDropTarget: Object3D): void { - if (!this.enabled || !objHovered) return; - const cursor = this.getCursor(objDragged, objHovered, objDropTarget); - if (cursor !== this._cursor) { - this._cursor = cursor; - if (cursorSet.has(cursor as string)) { - this._domElement.style.cursor = cursor as string; - } else { - this._domElement.style.cursor = `url(${cursor}), default`; - } + public update(dragged: Object3D, hovered: Object3D, dropTarget: Object3D): void { + if (!this.enabled || !hovered) return; + + const cursor = this.getCursor(dragged, hovered, dropTarget); + + if (cursor !== this._currentCursor) { + this._currentCursor = cursor; + const cursorStyle = cursorSet.has(cursor) ? cursor as string : `url(${cursor}), default`; + this._domElement.style.cursor = cursorStyle; } } private getCursor(objDragged: Object3D, objHovered: Object3D, objDropTarget: Object3D): Cursor { - if (objDropTarget) return objDropTarget.cursorDrop ?? 'alias'; + if (objDropTarget) return objDropTarget.cursorDrop ?? 'copy'; if (objDragged) return objDragged.cursorDrag ?? 'grabbing'; if (objHovered.cursor) return objHovered.cursor; - if ((objHovered as InstancedMesh2).isInstancedMesh2) { - if (!(objHovered as InstancedMesh2).__enabledStateHovered) return 'default'; - } else if (!objHovered.enabledState) return 'default'; - return objHovered.draggable ? 'grab' : 'pointer'; + if (!objHovered.enabledState) return 'default'; + if (objHovered.draggable) return 'grab'; + return objHovered.isInteractable ? 'pointer' : 'default'; } } diff --git a/src/events/Default.ts b/src/events/Default.ts deleted file mode 100644 index 03fbf248..00000000 --- a/src/events/Default.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * This class defines default settings for newly created Object3D instances. - */ -export class Default { - /** The default setting for 'focusable' for newly created Object3Ds. */ - public static focusable = true; - /** The default setting for 'draggable' for newly created Object3Ds. */ - public static draggable = false; - /** The default setting for 'interceptByRaycaster' for newly created Object3Ds. */ - public static interceptByRaycaster = true; -} diff --git a/src/events/DragAndDropManager.ts b/src/events/DragAndDropManager.ts index eb2353c1..fb3e7247 100644 --- a/src/events/DragAndDropManager.ts +++ b/src/events/DragAndDropManager.ts @@ -1,5 +1,5 @@ import { Plane, Matrix4, Vector3, Raycaster, Camera, Object3D } from 'three'; -import { DragEventExt, InteractionEvents, IntersectionExt } from './Events.js'; +import { EzDragEvent, EzInteractionEvents, EzIntersection } from './Events.js'; import { InstancedMesh2 } from '../instancedMesh/InstancedMesh2.js'; import { InstancedMeshEntity } from '../instancedMesh/InstancedMeshEntity.js'; @@ -20,7 +20,7 @@ export class DragAndDropManager { private _dataTransfer: { [x: string]: any }; private _lastDropTarget: Object3D; private _raycaster: Raycaster; - private _startIntersection: IntersectionExt; + private _startIntersection: EzIntersection; public get target(): Object3D { return this._target; } public get findDropTarget(): boolean { return this._target.findDropTarget; } @@ -38,7 +38,7 @@ export class DragAndDropManager { return false; } - public performDrag(event: PointerEvent, camera: Camera, dropTargetIntersection: IntersectionExt): void { + public performDrag(event: PointerEvent, camera: Camera, dropTargetIntersection: EzIntersection): void { if (!event.isPrimary) return; this._plane.setFromNormalAndCoplanarPoint(camera.getWorldDirection(this._plane.normal), this._worldPosition.setFromMatrixPosition(this._targetMatrixWorld)); @@ -62,7 +62,7 @@ export class DragAndDropManager { this.dropTargetEvent(event, dropTargetIntersection); } - public initDrag(event: PointerEvent, target: Object3D, instanceId: number, intersection: IntersectionExt): void { + public initDrag(event: PointerEvent, target: Object3D, instanceId: number, intersection: EzIntersection): void { if (this.isDragButton(event) && target?.draggable) { if (instanceId >= 0) { if ((target as InstancedMesh2).isInstancedMesh2 && (target as InstancedMesh2).__enabledStateHovered) { @@ -146,15 +146,15 @@ export class DragAndDropManager { this._lastDropTarget = undefined; } - private trigger(type: keyof InteractionEvents, event: PointerEvent, target: Object3D, cancelable: boolean, position?: Vector3, relatedTarget?: Object3D, intersection?: IntersectionExt): DragEventExt { + private trigger(type: keyof EzInteractionEvents, event: PointerEvent, target: Object3D, cancelable: boolean, position?: Vector3, relatedTarget?: Object3D, intersection?: EzIntersection): EzDragEvent { if (target) { - const dragEvent = new DragEventExt(event, cancelable, this._dataTransfer, position, relatedTarget, intersection); + const dragEvent = new EzDragEvent(event, cancelable, this._dataTransfer, position, relatedTarget, intersection); target.__eventsDispatcher.dispatchDOMAncestor(type, dragEvent); return dragEvent; } } - public dropTargetEvent(event: PointerEvent, dropTargetIntersection: IntersectionExt): void { + public dropTargetEvent(event: PointerEvent, dropTargetIntersection: EzIntersection): void { if (this.findDropTarget) { const dropTarget = dropTargetIntersection?.object; const lastDropTarget = this._lastDropTarget; diff --git a/src/events/Events.ts b/src/events/Events.ts index 6a757c57..97cc2bd3 100644 --- a/src/events/Events.ts +++ b/src/events/Events.ts @@ -1,14 +1,13 @@ import { Camera, Intersection, Object3D, Vector3, WebGLRenderer } from 'three'; -import { InstancedMeshEntity } from '../instancedMesh/InstancedMeshEntity.js'; import { Hitbox } from './Hitbox.js'; -export type MiscUpdateEvents = MiscEvents & UpdateEvents; -export type Events = InteractionEvents & MiscUpdateEvents; +export type EzMiscUpdateEvents = EzMiscEvents & EzUpdateEvents; +export type EzEvents = EzInteractionEvents & EzMiscUpdateEvents; /** * Represents events related to updates. These events do not propagate to parents. */ -export interface UpdateEvents { +export interface EzUpdateEvents { /** Event triggered when the position of the object changes. */ positionchange: never; /** Event triggered when the scale of the object changes. */ @@ -16,92 +15,88 @@ export interface UpdateEvents { /** Event triggered when the rotation of the object changes. */ rotationchange: never; /** Event triggered when the enabledState of the object changes (either its own or the parent's `enabled` property). */ - enabledchange: PropertyChangeEvent; + enabledchange: EzPropertyChangeEvent; /** Event triggered when the visibilityState of the object changes (either its own or the parent's `visible` property). */ - visiblechange: PropertyChangeEvent; + visiblechange: EzPropertyChangeEvent; } /** * Represents miscellaneous events. These events do not propagate to parents. */ -export interface MiscEvents { +export interface EzMiscEvents { /** Event triggered on first render and every time an object is rendered with a different viewport size from the previous one. */ - viewportresize: ViewportResizeEvent; + viewportresize: EzViewportResizeEvent; /** Event triggered every frame, before 'animate'. Usually used to prepare object animations. */ - beforeanimate: AnimateEvent; + beforeanimate: EzAnimateEvent; /** Event triggered every frame. Used to animate objects. */ - animate: AnimateEvent; + animate: EzAnimateEvent; /** Event triggered every frame, after 'animate'. Usually used if you want to operate after the animation is computed. */ - afteranimate: AnimateEvent; + afteranimate: EzAnimateEvent; } /** * Represents interaction events. These events propagate to parents. - * @typeparam T - The primary target type. - * @typeparam R - The related target type. - * @typeparam RD - The related target type on drag events. */ -export interface InteractionEvents { +export interface EzInteractionEvents { /** Event triggered when a pointer enters the target. */ - pointerover: PointerEventExt; + pointerover: EzPointerEvent; /** Event triggered when a pointer enters the target (no propagation). */ - pointerenter: PointerEventExt; + pointerenter: EzPointerEvent; /** Event triggered when a pointer leaves the target. */ - pointerout: PointerEventExt; + pointerout: EzPointerEvent; /** Event triggered when a pointer leaves the target (no propagation). */ - pointerleave: PointerEventExt; + pointerleave: EzPointerEvent; /** Event triggered when a pointer moves over the target. */ - pointermove: PointerEventExt; + pointermove: EzPointerEvent; /** Event triggered when a pointer button is pressed. */ - pointerdown: PointerEventExt; + pointerdown: EzPointerEvent; /** Event triggered when a pointer button is released. */ - pointerup: PointerEventExt; + pointerup: EzPointerEvent; /** Event triggered if pointer is on target. Triggers every frame and only works if the scene has 'continuousRaycasting' equal to true. */ - pointerintersection: PointerIntersectionEvent; + pointerintersection: EzPointerIntersection; /** Event triggered when a click event occurs. */ - click: PointerEventExt; + click: EzPointerEvent; /** Event triggered when a double click event occurs. */ - dblclick: PointerEventExt; + dblclick: EzPointerEvent; /** Event triggered when scrolling the mouse wheel. */ - wheel: WheelEventExt; + wheel: EzPointerEvent; /** Event triggered when target gains focus (no propagation). */ - focusin: FocusEventExt; + focusin: EzEvent; /** Event triggered when target loses focus (no propagation). */ - focusout: FocusEventExt; + focusout: EzEvent; /** Event triggered when target gains focus. */ - focus: FocusEventExt; + focus: EzEvent; /** Event triggered when target loses focus. */ - blur: FocusEventExt; + blur: EzEvent; /** Event triggered on the focused object when a key is pressed. */ - keydown: KeyboardEventExt; + keydown: EzEvent; /** Event triggered on the focused object when a key is released. */ - keyup: KeyboardEventExt; + keyup: EzEvent; /** Event triggered when the target is dragged. */ - drag: DragEventExt; + drag: EzDragEvent; /** Event triggered when dragging starts. */ - dragstart: DragEventExt; + dragstart: EzDragEvent; /** Event triggered when dragging ends. */ - dragend: DragEventExt; + dragend: EzDragEvent; /** Event triggered when dragging is canceled (Can be canceled pressing 'ESC'). This is triggered on target and dropTarget. */ - dragcancel: DragEventExt; + dragcancel: EzDragEvent; /** Event triggered when a draggable object enters a drop target. */ - dragenter: DragEventExt; + dragenter: EzDragEvent; /** * Event triggered when a draggable object moves over the drop target. * Triggers every frame if the scene has 'continuousRaycastingDropTarget' equal to true. */ - dragover: DragEventExt; + dragover: EzDragEvent; /** Event triggered when a draggable object leaves a drop target. */ - dragleave: DragEventExt; + dragleave: EzDragEvent; /** Event triggered when a draggable object is dropped onto a drop target. */ - drop: DragEventExt; + drop: EzDragEvent; } /** * Represents an extended intersection between a ray and 3D objects in a scene. */ -export interface IntersectionExt extends Intersection { - object: Object3D; +export interface EzIntersection extends Intersection { /** The hitbox hit by the raycaster. */ hitbox: Hitbox; } @@ -109,38 +104,53 @@ export interface IntersectionExt extends Intersection { /** * Represents a custom extended event. */ -export class EventExt { - /** A boolean value indicating whether or not the event bubbles up through the DOM. */ - public get bubbles(): boolean { return this._bubbles; } - /** A boolean value indicating whether the event is cancelable. */ - public readonly cancelable: boolean; - /** A reference to the currently registered target for the event. This is the object to which the event is currently slated to be sent. It's possible this has been changed along the way through retargeting. */ - public currentTarget: T; - /** Indicates whether or not the call to event.preventDefault() canceled the event. */ - public get defaultPrevented(): boolean { return this._defaultPrevented; } +export class EzEvent { + /** Original dom event. */ + public readonly domEvent: Ev | null; + /** The case-insensitive name identifying the type of the event. */ + public readonly type: keyof EzEvents; /** A reference to the object to which the event was originally dispatched. */ - public get target(): T { return this._target; } + public readonly target: Object3D; + /** TODO */ + public readonly targetInstanceId?: number; + /** A reference to the currently registered target for the event. This is the object to which the event is currently slated to be sent. It's possible this has been changed along the way through retargeting. */ + public readonly currentTarget: Object3D; + /** TODO */ + public readonly currentTargetInstanceId?: number; /** The time at which the event was created (in milliseconds). By specification, this value is time since epoch—but in reality, browsers' definitions vary. In addition, work is underway to change this to be a DOMHighResTimeStamp instead. */ public readonly timeStamp = performance.now(); - /** The case-insensitive name identifying the type of the event. */ - public get type(): keyof Events { return this._type; } + /** A boolean value indicating whether the event is cancelable. */ + public readonly cancelable: boolean; + /** A boolean value indicating whether or not the event bubbles up through the DOM. */ + public bubbles = true; + /** Indicates whether or not the call to event.preventDefault() canceled the event. */ + public defaultPrevented = false; - /** @internal */ public _defaultPrevented: boolean; - /** @internal */ public _stoppedImmediatePropagation: boolean; - /** @internal */ public _bubbles: boolean; - /** @internal */ public _type: keyof Events; - /** @internal */ public _target: T; + protected _stoppedImmediatePropagation = false; /** - * @param cancelable A boolean value indicating whether the event is cancelable. + * TODO + * @param domEvent Original dom event. + * @param type The case-insensitive name identifying the type of the event. + * @param target A reference to the object to which the event was originally dispatched. + * @param targetInstanceId TODO + * @param currentTarget A reference to the currently registered target for the event. This is the object to which the event is currently slated to be sent. It's possible this has been changed along the way through retargeting. + * @param currentTargetInstanceId TODO + * @param cancelable A boolean value indicating whether the event is cancelable. Default is `false`. */ - constructor(cancelable = false) { + constructor(domEvent: Ev | null, type: keyof EzEvents, target: Object3D, targetInstanceId?: number, currentTarget = target, currentTargetInstanceId = targetInstanceId, cancelable = false) { + this.domEvent = domEvent; + this.type = type; + this.target = target; + this.targetInstanceId = targetInstanceId; + this.currentTarget = currentTarget; + this.currentTargetInstanceId = currentTargetInstanceId; this.cancelable = cancelable; } /** Cancels the event. */ public preventDefault(): void { - this._defaultPrevented = true; + this.defaultPrevented = true; } /** For this particular event, prevent all other listeners from being called. This includes listeners attached to the same element as well as those attached to elements that will be traversed later (during the capture phase, for instance). */ @@ -150,224 +160,77 @@ export class EventExt { /** Stops the propagation of events further along in the Object3D hierarchy. */ public stopPropagation(): void { - this._bubbles = false; + this.bubbles = false; } } /** - * Represents a custom extended mouse event. - * @template T - The type of the primary target for the event (default is `Object3D`). - * @template R - The type of the related target for the event (default is `Object3D`). + * Represents a custom extended pointer event. */ -export class MouseEventExt extends EventExt { - /** Original dom event. */ - public readonly domEvent: MouseEvent; - /** Returns true if the alt key was down when the mouse event was fired. */ - public get altKey(): boolean { return this.domEvent.altKey; } - /** The button number that was pressed (if applicable) when the mouse event was fired. */ - public get button(): number { return this.domEvent.button; } - /** The buttons being pressed (if any) when the mouse event was fired. */ - public get buttons(): number { return this.domEvent.buttons; } - /** The X coordinate of the mouse pointer in local (DOM content) coordinates. */ - public get clientX(): number { return this.domEvent.clientX; } - /** The Y coordinate of the mouse pointer in local (DOM content) coordinates. */ - public get clientY(): number { return this.domEvent.clientY; } - /** Returns true if the control key was down when the mouse event was fired. */ - public get ctrlKey(): boolean { return this.domEvent.ctrlKey; } - /** Returns true if the meta key was down when the mouse event was fired. */ - public get metaKey(): boolean { return this.domEvent.metaKey; } - /** The X coordinate of the pointer relative to the position of the last event. */ - public get movementX(): number { return this.domEvent.movementX; } - /** The Y coordinate of the pointer relative to the position of the last event. */ - public get movementY(): number { return this.domEvent.movementY; } - /** The X coordinate of the mouse pointer relative to the position of the padding edge of the target node. */ - public get offsetX(): number { return this.domEvent.offsetX; } - /** The Y coordinate of the mouse pointer relative to the position of the padding edge of the target node. */ - public get offsetY(): number { return this.domEvent.offsetY; } - /** The X coordinate of the mouse pointer relative to the whole document. */ - public get pageX(): number { return this.domEvent.pageX; } - /** The Y coordinate of the mouse pointer relative to the whole document. */ - public get pageY(): number { return this.domEvent.pageY; } - /** The secondary target for the event, if there is one. */ - public readonly relatedTarget: R; - /** The X coordinate of the mouse pointer in global (screen) coordinates. */ - public get screenX(): number { return this.domEvent.screenX; } - /** The Y coordinate of the mouse pointer in global (screen) coordinates. */ - public get screenY(): number { return this.domEvent.screenY; } - /** Returns true if the shift key was down when the mouse event was fired. */ - public get shiftKey(): boolean { return this.domEvent.shiftKey; } +export class EzPointerEvent extends EzEvent { /** Returns the intersection information between the mouse event and 3D objects in the scene. */ - public readonly intersection: IntersectionExt; + public readonly intersection: EzIntersection; + /** The secondary target for the event, if there is one. */ + public readonly relatedTarget?: Object3D; + /** TODO */ + public readonly relatedTargetInstanceId?: number; /** - * @param event Original dom event. - * @param intersection The intersection information between the mouse event and 3D objects in the scene. - * @param relatedTarget The secondary target for the event. - * @param cancelable A boolean value indicating whether the event is cancelable. + * @param intersection Returns the intersection information between the mouse event and 3D objects in the scene. + * @param relatedTarget The secondary target for the event, if there is one. + * @param relatedTargetInstanceId TODO */ - constructor(event: MouseEvent, intersection: IntersectionExt, relatedTarget?: R, cancelable?: boolean) { - super(cancelable); - this.domEvent = event; - this.intersection = intersection; + constructor(domEvent: Ev, type: keyof EzEvents, intersection: EzIntersection, target: Object3D, targetInstanceId?: number, currentTarget?: Object3D, + currentTargetInstanceId?: number, relatedTarget?: Object3D, relatedTargetInstanceId?: number, cancelable?: boolean) { + super(domEvent, type, target, targetInstanceId, currentTarget, currentTargetInstanceId, cancelable); this.relatedTarget = relatedTarget; + this.relatedTargetInstanceId = relatedTargetInstanceId; + this.intersection = intersection; } - - /** Returns the current state of the specified modifier key. See KeyboardEvent.getModifierState() for details. */ - public getModifierState(keyArg: string): boolean { - return this.domEvent.getModifierState(keyArg); - } -} - -/** - * Represents a custom extended pointer event. - * @template T - The type of the primary target for the event (default is `Object3D`). - * @template R - The type of the related target for the event (default is `Object3D`). - */ -export class PointerEventExt extends MouseEventExt { - declare public readonly domEvent: PointerEvent; - /** A unique identifier for the pointer causing the event. */ - public get pointerId(): number { return this.domEvent.pointerId; } - /** The width (magnitude on the X axis), in CSS pixels, of the contact geometry of the pointer. */ - public get width(): number { return this.domEvent.width; } - /** The height (magnitude on the Y axis), in CSS pixels, of the contact geometry of the pointer. */ - public get height(): number { return this.domEvent.height; } - /** The normalized pressure of the pointer input in the range 0 to 1, where 0 and 1 represent the minimum and maximum pressure the hardware is capable of detecting, respectively. */ - public get pressure(): number { return this.domEvent.pressure; } - /** The normalized tangential pressure of the pointer input (also known as barrel pressure or cylinder stress) in the range -1 to 1, where 0 is the neutral position of the control. */ - public get tangentialPressure(): number { return this.domEvent.tangentialPressure; } - /** The plane angle (in degrees, in the range of -90 to 90) between the Y–Z plane and the plane containing both the pointer (e.g. pen stylus) axis and the Y axis. */ - public get tiltX(): number { return this.domEvent.tiltX; } - /** The plane angle (in degrees, in the range of -90 to 90) between the X–Z plane and the plane containing both the pointer (e.g. pen stylus) axis and the X axis. */ - public get tiltY(): number { return this.domEvent.tiltY; } - /** The clockwise rotation of the pointer (e.g. pen stylus) around its major axis in degrees, with a value in the range 0 to 359. */ - public get twist(): number { return this.domEvent.twist; } - /** Indicates the device type that caused the event (mouse, pen, touch, etc.). */ - public get pointerType(): string { return this.domEvent.pointerType; } - /** Indicates if the pointer represents the primary pointer of this pointer type. */ - public get isPrimary(): boolean { return this.domEvent.isPrimary; } } /** * Represents a custom extended drag event. - * @template T - The type of the primary target for the event (default is `Object3D`). - * @template R - The type of the related target for the event (default is `Object3D` or `InstancedMeshEntity`). */ -export class DragEventExt extends PointerEventExt { +export class EzDragEvent extends EzPointerEvent { /** The data that is transferred during a drag and drop interaction. */ public readonly dataTransfer: { [x: string]: any }; - /** Returns the new position of the dragged object.' */ - public readonly position: Vector3; + /** Returns the new position of the dragged object. */ + public readonly position?: Vector3; /** - * @param event Original dom event. - * @param cancelable A boolean value indicating whether the event is cancelable. * @param dataTransfer The data that is transferred during a drag and drop interaction. - * @param position The new position of the dragged object. - * @param relatedTarget The secondary target for the event. - * @param intersection The intersection information between the mouse event and 3D objects in the scene. + * @param position Returns the new position of the dragged object. */ - constructor(event?: PointerEvent, cancelable?: boolean, dataTransfer: { [x: string]: any } = {}, position?: Vector3, relatedTarget?: R, intersection?: IntersectionExt) { - super(event, intersection, relatedTarget, cancelable); + constructor(domEvent: PointerEvent, type: keyof EzEvents, target: Object3D, intersection: EzIntersection, dataTransfer: { [x: string]: any } = {}, + position?: Vector3, targetInstanceId?: number, currentTarget?: Object3D, currentTargetInstanceId?: number, relatedTarget?: Object3D, + relatedTargetInstanceId?: number, cancelable?: boolean) { + super(domEvent, type, intersection, target, targetInstanceId, currentTarget, currentTargetInstanceId, relatedTarget, relatedTargetInstanceId, cancelable); this.position = position; this.dataTransfer = dataTransfer; } } -/** - * Represents a custom extended wheel event. - * @template T - The type of the primary target for the event (default is `Object3D`). - * @template R - The type of the related target for the event (default is `Object3D`). - */ -export class WheelEventExt extends MouseEventExt { - declare public readonly domEvent: WheelEvent; - /* Returns an unsigned long representing the unit of the delta* values' scroll amount. Permitted values are: 0 = pixels, 1 = lines, 2 = pages. */ - public get deltaMode(): number { return this.domEvent.deltaMode; } - /** Returns a double representing the horizontal scroll amount. */ - public get deltaX(): number { return this.domEvent.deltaX; } - /** Returns a double representing the vertical scroll amount. */ - public get deltaY(): number { return this.domEvent.deltaY; } - /** Returns a double representing the scroll amount for the z-axis. */ - public get deltaZ(): number { return this.domEvent.deltaZ; } -} - /** * Represents a pointer intersection event. - * @template T - The type of the primary target for the event (default is `Object3D`). */ -export class PointerIntersectionEvent extends EventExt { +export class EzPointerIntersection extends EzEvent { /** Returns the intersection information between the mouse event and 3D objects in the scene. */ - public readonly intersection: IntersectionExt; + public readonly intersection: EzIntersection; /** * @param intersection The intersection information between the mouse event and 3D objects in the scene. */ - constructor(intersection: IntersectionExt) { - super(); + constructor(type: keyof EzEvents, intersection: EzIntersection, target: Object3D, targetInstanceId?: number, currentTarget?: Object3D, currentTargetInstanceId?: number, cancelable?: boolean) { + super(null, type, target, targetInstanceId, currentTarget, currentTargetInstanceId, cancelable); this.intersection = intersection; } } -/** - * Represents a custom extended keyboard event. - * @template T - The type of the primary target for the event (default is `Object3D`). - */ -export class KeyboardEventExt extends EventExt { - /** Original dom event. */ - public readonly domEvent: KeyboardEvent; - /** Returns a boolean value that is true if the Alt (Option or ⌥ on macOS) key was active when the key event was generated. */ - public get altKey(): boolean { return this.domEvent.altKey; } - /** Returns a string with the code value of the physical key represented by the event. */ - public get code(): string { return this.domEvent.code; } - /** Returns a boolean value that is true if the Ctrl key was active when the key event was generated. */ - public get ctrlKey(): boolean { return this.domEvent.ctrlKey; } - /** Returns a string representing the key value of the key represented by the event. */ - public get key(): string { return this.domEvent.key; } - /** Returns a number representing the location of the key on the keyboard or other input device. Visit https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/location for more info. */ - public get location(): number { return this.domEvent.location; } - /** Returns a boolean value that is true if the Meta key (on Mac keyboards, the ⌘ Command key; on Windows keyboards, the Windows key (⊞)) was active when the key event was generated. */ - public get metaKey(): boolean { return this.domEvent.metaKey; } - /** Returns a boolean value that is true if the key is being held down such that it is automatically repeating. */ - public get repeat(): boolean { return this.domEvent.repeat; } - /** Returns a boolean value that is true if the Shift key was active when the key event was generated. */ - public get shiftKey(): boolean { return this.domEvent.shiftKey; } - - /** - * @param event Original dom event. - * @param cancelable A boolean value indicating whether the event is cancelable. - */ - constructor(event: KeyboardEvent, cancelable: boolean) { - super(cancelable); - this.domEvent = event; - } - - /** Returns a boolean value indicating if a modifier key such as Alt, Shift, Ctrl, or Meta, was pressed when the event was created. */ - public getModifierState(keyArg: string): boolean { - return this.domEvent.getModifierState(keyArg); - } -} - -/** - * Represents a custom extended focus event. - * @template T - The type of the primary target for the event (default is `Object3D`). - * @template R - The type of the related target for the event (default is `Object3D`). - */ -export class FocusEventExt extends EventExt { - /** The secondary target for the event. */ - public relatedTarget: R; - - /** - * @param relatedTarget The secondary target for the event. - */ - constructor(relatedTarget: R) { - super(); - this.relatedTarget = relatedTarget; - } -} - /** * Represents an event related to resizing of a renderer. */ -export interface ViewportResizeEvent { +export interface EzViewportResizeEvent { /** Returns new render width. */ width: number; /** Returns the render height. */ @@ -381,7 +244,7 @@ export interface ViewportResizeEvent { /** * Represents an event related to animation. */ -export interface AnimateEvent { +export interface EzAnimateEvent { /** The difference in time between the current animation frame and the previous one (in milliseconds). */ delta: DOMHighResTimeStamp; /** The total amount of time that has passed since the animation started (in milliseconds). */ @@ -392,7 +255,7 @@ export interface AnimateEvent { * Represents a property change event. * @template V - The type of the new value associated with the property change. */ -export interface PropertyChangeEvent { +export interface EzPropertyChangeEvent { /** A reference to the object to which the event was originally dispatched. */ target: Object3D; /** The new value associated with the property change. */ diff --git a/src/events/EventsDispatcher.ts b/src/events/EventsDispatcher.ts index 85814096..ab68636c 100644 --- a/src/events/EventsDispatcher.ts +++ b/src/events/EventsDispatcher.ts @@ -1,44 +1,65 @@ import { InstancedMesh, Object3D } from 'three'; import { applyObject3DRotationPatch, applyObject3DVector3Patch } from '../patch/Object3D.js'; -import { EventExt, Events, InteractionEvents, MiscUpdateEvents, UpdateEvents } from './Events.js'; -import { EventsCache } from './MiscEventsManager.js'; +import { EzEvent, EzEvents, EzInteractionEvents, EzMiscUpdateEvents, EzUpdateEvents } from './Events.js'; +import { EventsCache, register } from './MiscEventsManager.js'; +import { patchPosition, patchScale } from '../patch/Vector3.js'; + +export type EventCallback = (event?: EzEvents[K]) => void; /** @internal */ export class EventsDispatcher { public parent: Object3D; - public listeners: { [K in keyof Events]?: ((event?: Events[K]) => void)[] } = {}; + public listeners: { [K in keyof EzEvents]?: EventCallback[] } = {}; constructor(parent: Object3D) { this.parent = parent; } - public add(type: K, listener: (event: Events[K]) => void): (event: Events[K]) => void { + public add(type: K, event: EventCallback): EventCallback { if (!this.listeners[type]) { this.listeners[type] = []; - if (type === 'positionchange' || type === 'scalechange') { - applyObject3DVector3Patch(this.parent); - } else if (type === 'rotationchange') { - applyObject3DRotationPatch(this.parent); - } else if (type === 'drop' || type === 'dragenter' || type === 'dragleave' || type === 'dragover') { - this.parent.__isDropTarget = true; + + switch (type) { + case 'positionchange': + patchPosition(this.parent); + break; + case 'scalechange': + patchScale(this.parent); + break; + case 'rotationchange': + applyObject3DRotationPatch(this.parent); + break; + case 'drop': + case 'dragenter': + case 'dragleave': + case 'dragover': + this.parent.__isDropTarget = true; + break; } } - if (this.listeners[type].indexOf(listener) < 0) { - this.listeners[type].push(listener); + + if (this.listeners[type].indexOf(event) === -1) { + this.listeners[type].push(event); } - EventsCache.push(type, this.parent); - return listener; + + register(type, this.parent); + + return event; } - public has(type: K, listener: (event: Events[K]) => void): boolean { - return this.listeners[type]?.indexOf(listener) > -1; + public has(type: K, event: EventCallback): boolean { + const index = this.listeners[type]?.indexOf(event) ?? -1; + return index > -1; } - public remove(type: K, listener: (event: Events[K]) => void): void { - const index = this.listeners[type]?.indexOf(listener) ?? -1; + public remove(type: K, event: EventCallback): void { + const listener = this.listeners[type]; + const index = listener?.indexOf(event) ?? -1; + if (index > -1) { - this.listeners[type].splice(index, 1); - if (this.listeners[type].length === 0) { + listener!.splice(index, 1); + + if (listener!.length === 0) { EventsCache.remove(type, this.parent); this.parent.__isDropTarget = this.isDropTarget(); } @@ -51,7 +72,7 @@ export class EventsDispatcher { && (l['drop']?.length > 0 || l['dragenter']?.length > 0 || l['dragleave']?.length > 0 || l['dragover']?.length > 0); } - public dispatchDOM(type: K, event: InteractionEvents[K]): void { + public dispatchDOM(type: K, event: EzInteractionEvents[K]): void { event._bubbles = false; event._stoppedImmediatePropagation = false; event._defaultPrevented = false; @@ -60,7 +81,7 @@ export class EventsDispatcher { this.executeDOM(type, event); } - public dispatchDOMAncestor(type: K, event: InteractionEvents[K]): void { + public dispatchDOMAncestor(type: K, event: EzInteractionEvents[K]): void { let target = this.parent; event._bubbles = true; event._stoppedImmediatePropagation = false; @@ -73,7 +94,7 @@ export class EventsDispatcher { } } - private executeDOM(type: K, event: InteractionEvents[K]): void { + private executeDOM(type: K, event: EzInteractionEvents[K]): void { if (!this.listeners[type]) return; const target = event.currentTarget = this.parent; for (const callback of this.listeners[type]) { @@ -82,14 +103,14 @@ export class EventsDispatcher { } } - public dispatch(type: T, event?: MiscUpdateEvents[T]): void { + public dispatch(type: T, event?: EzMiscUpdateEvents[T]): void { if (!this.listeners[type]) return; for (const callback of this.listeners[type]) { callback.call(this.parent, event as any); } } - public dispatchDescendant(type: T, event?: UpdateEvents[T]): void { + public dispatchDescendant(type: T, event?: EzUpdateEvents[T]): void { const target = this.parent; target.__eventsDispatcher.dispatch(type, event as any); if (!target.children) return; @@ -98,16 +119,16 @@ export class EventsDispatcher { } } - public dispatchManual(type: T, event?: Events[T]): void { - if ((event as EventExt)?.cancelable !== undefined) { - return this.dispatchDOM(type as keyof InteractionEvents, event as any); + public dispatchManual(type: T, event?: EzEvents[T]): void { + if ((event as EzEvent)?.cancelable !== undefined) { + return this.dispatchDOM(type as keyof EzInteractionEvents, event as any); } - this.dispatch(type as keyof MiscUpdateEvents, event as any); + this.dispatch(type as keyof EzMiscUpdateEvents, event as any); } - public dispatchAncestorManual(type: T, event?: Events[T]): void { - if ((event as EventExt)?.cancelable !== undefined) { - this.dispatchDOMAncestor(type as keyof InteractionEvents, event as any); + public dispatchAncestorManual(type: T, event?: EzEvents[T]): void { + if ((event as EzEvent)?.cancelable !== undefined) { + this.dispatchDOMAncestor(type as keyof EzInteractionEvents, event as any); } } } diff --git a/src/events/InteractionDefault.ts b/src/events/InteractionDefault.ts new file mode 100644 index 00000000..356bc0d7 --- /dev/null +++ b/src/events/InteractionDefault.ts @@ -0,0 +1,14 @@ +interface InteractionDefault { +/** The default setting for 'focusable' for newly created Object3Ds. Default: true. */ + focusable: boolean; + /** The default setting for 'draggable' for newly created Object3Ds. Default: false. */ + draggable: boolean; + /** The default setting for 'interceptByRaycaster' for newly created Object3Ds. Default: true. */ + interceptByRaycaster: boolean; +} + +export const INTERACTION_DEFAULT: InteractionDefault = { + focusable: true, + draggable: false, + interceptByRaycaster: true +}; diff --git a/src/events/InteractionEventsQueue.ts b/src/events/InteractionEventsQueue.ts index 4a5d650e..12827501 100644 --- a/src/events/InteractionEventsQueue.ts +++ b/src/events/InteractionEventsQueue.ts @@ -3,7 +3,7 @@ * Syncronize DOM events with the frame generation, discarding ripetitive pointermove event. */ export class InteractionEventsQueue { - public multitouch: boolean; + public multitouch = false; private _items: Event[] = []; public enqueue(event: Event): void { diff --git a/src/events/InteractionManager.ts b/src/events/InteractionManager.ts index e6c5b495..2039860c 100644 --- a/src/events/InteractionManager.ts +++ b/src/events/InteractionManager.ts @@ -2,7 +2,7 @@ import { Object3D, WebGLRenderer } from 'three'; import { RenderManager } from '../rendering/RenderManager.js'; import { CursorHandler } from './CursorManager.js'; import { DragAndDropManager } from './DragAndDropManager.js'; -import { InteractionEvents, IntersectionExt, KeyboardEventExt, PointerEventExt, PointerIntersectionEvent, WheelEventExt } from './Events.js'; +import { EzInteractionEvents, EzIntersection, EzKeyboardEvent, EzPointerEvent, EzPointerIntersection, EzWheelEvent } from './Events.js'; import { InteractionEventsQueue } from './InteractionEventsQueue.js'; import { RaycasterManager } from './RaycasterManager.js'; @@ -12,8 +12,8 @@ export class InteractionManager { public cursorManager: CursorHandler; public dragManager: DragAndDropManager; public queue = new InteractionEventsQueue(); - private _intersection: { [x: string]: IntersectionExt } = {}; - private _intersectionDropTarget: IntersectionExt; + private _intersection: { [x: string]: EzIntersection } = {}; + private _intersectionDropTarget: EzIntersection; private _renderManager: RenderManager; private _primaryIdentifier: number; private _pointerDownTarget: { [x: string]: Object3D } = {}; @@ -98,34 +98,34 @@ export class InteractionManager { } } - private triggerPointer(type: keyof InteractionEvents, event: PointerEvent, target: Object3D, relatedTarget?: Object3D): void { + private triggerPointer(type: keyof EzInteractionEvents, event: PointerEvent, target: Object3D, relatedTarget?: Object3D): void { if (target?.enabledState) { - const pointerEvent = new PointerEventExt(event, this._intersection[event.pointerId], relatedTarget); + const pointerEvent = new EzPointerEvent(event, this._intersection[event.pointerId], relatedTarget); target.__eventsDispatcher.dispatchDOM(type, pointerEvent); } } - private triggerAncestorPointer(type: keyof InteractionEvents, event: PointerEvent, target: Object3D, relatedTarget?: Object3D, cancelable?: boolean): PointerEventExt { + private triggerAncestorPointer(type: keyof EzInteractionEvents, event: PointerEvent, target: Object3D, relatedTarget?: Object3D, cancelable?: boolean): EzPointerEvent { if (target?.enabledState) { - const pointerEvent = new PointerEventExt(event, this._intersection[event.pointerId], relatedTarget, cancelable); + const pointerEvent = new EzPointerEvent(event, this._intersection[event.pointerId], relatedTarget, cancelable); target.__eventsDispatcher.dispatchDOMAncestor(type, pointerEvent); return pointerEvent; } } - private triggerAncestorWheel(event: WheelEvent, intersection: IntersectionExt): void { + private triggerAncestorWheel(event: WheelEvent, intersection: EzIntersection): void { const target = intersection?.object ?? this._renderManager.activeScene; if (target?.enabledState) { - const wheelEvent = new WheelEventExt(event, intersection); + const wheelEvent = new EzWheelEvent(event, intersection); target.__eventsDispatcher.dispatchDOMAncestor('wheel', wheelEvent); } } - private triggerAncestorKeyboard(type: keyof InteractionEvents, event: KeyboardEvent, cancelable: boolean): KeyboardEventExt { + private triggerAncestorKeyboard(type: keyof EzInteractionEvents, event: KeyboardEvent, cancelable: boolean): EzKeyboardEvent { const scene = this._renderManager.activeScene; const target = scene.focusedObject ?? scene; if (target.enabledState) { - const keyboardEvent = new KeyboardEventExt(event, cancelable); + const keyboardEvent = new EzKeyboardEvent(event, cancelable); target.__eventsDispatcher.dispatchDOMAncestor(type, keyboardEvent); return keyboardEvent; } @@ -217,7 +217,7 @@ export class InteractionManager { const intersection = this._intersection[this._primaryIdentifier]; const target = intersection?.object ?? this._renderManager.hoveredScene; if (target?.enabledState) { - target.__eventsDispatcher.dispatchDOMAncestor('pointerintersection', new PointerIntersectionEvent(intersection)); + target.__eventsDispatcher.dispatchDOMAncestor('pointerintersection', new EzPointerIntersection(intersection)); } } } @@ -313,7 +313,7 @@ export class InteractionManager { this.triggerAncestorKeyboard('keyup', event, false); } - private setDropTarget(intersections: IntersectionExt[]): void { + private setDropTarget(intersections: EzIntersection[]): void { const int = intersections[0]; this._intersectionDropTarget = (int?.object.__isDropTarget && int.object.enabledState) ? int : undefined; const scene = this._renderManager.activeScene; diff --git a/src/events/MiscEventsManager.ts b/src/events/MiscEventsManager.ts index 81ee68d4..7109dc1d 100644 --- a/src/events/MiscEventsManager.ts +++ b/src/events/MiscEventsManager.ts @@ -1,72 +1,80 @@ import { Camera, Object3D, Scene } from 'three'; -import { Events, MiscEvents } from './Events.js'; +import { EzEvents, EzMiscEvents } from './Events.js'; -type SceneEventsCache = { [x: string]: Set }; +// TODO: we can use a distincArray instead of Set + +const miscEvents: (keyof EzMiscEvents)[] = ['viewportresize', 'beforeanimate', 'animate', 'afteranimate']; +const allowedEventsSet = new Set(miscEvents); /** @internal */ -export class EventsCache { - private static readonly _allowedEventsSet = new Set(['viewportresize', 'beforeanimate', 'animate', 'afteranimate'] as (keyof MiscEvents)[]); - private static _events: { [x: number]: SceneEventsCache } = {}; - - public static push(type: keyof Events, target: Object3D): void { - const scene = target.scene; - if (scene && this._allowedEventsSet.has(type)) { - this.pushScene(scene, type, target); - } - } +export function register(type: keyof EzEvents, target: Object3D): void { + const scene = target.scene; - public static update(target: Object3D): void { - this.updateEvent(target, 'viewportresize'); - this.updateEvent(target, 'beforeanimate'); - this.updateEvent(target, 'animate'); - this.updateEvent(target, 'afteranimate'); + if (scene && allowedEventsSet.has(type)) { + registerToScene(scene, type, target); } +} + +/** @internal */ +export function registerAll(target: Object3D): void { + const listeners = target.__eventsDispatcher.listeners; + const scene = target.scene; + + for (const type of miscEvents) { + const eventsCount = listeners[type]?.length ?? 0; - private static updateEvent(target: Object3D, name: keyof Events): void { - if (target.__eventsDispatcher.listeners[name]?.length > 0) { - this.pushScene(target.scene, name, target); + if (eventsCount > 0) { + registerToScene(scene, type, target); } } +} - private static pushScene(scene: Scene, type: keyof Events, target: Object3D): void { - const sceneCache = this._events[scene.id] ?? (this._events[scene.id] = {}); - const eventCache = sceneCache[type] ?? (sceneCache[type] = new Set()); - eventCache.add(target); - } +function registerToScene(scene: Scene, type: keyof EzEvents, target: Object3D): void { + scene.__registeredEventsObjects ??= {}; + const registeredEventsObjects = scene.__registeredEventsObjects; + registeredEventsObjects[type] ??= new Set(); + registeredEventsObjects[type].add(target); +} - public static removeAll(target: Object3D): void { - const sceneCache = this._events[target.scene?.id]; - if (sceneCache) { - for (const key in sceneCache) { - const eventCache = sceneCache[key]; - eventCache.delete(target); - } +/** @internal */ +export function unregisterAll(target: Object3D): void { + const registeredEventsObjects = target.scene?.__registeredEventsObjects; + + if (registeredEventsObjects) { + for (const type in registeredEventsObjects) { + registeredEventsObjects[type].delete(target); } } +} - public static remove(type: keyof Events, target: Object3D): void { - const sceneCache = this._events[target.scene?.id]; - if (sceneCache) { - sceneCache[type]?.delete(target); - } +/** @internal */ +export function unregister(type: keyof EzEvents, target: Object3D): void { + const registeredEventsObjects = target.scene?.__registeredEventsObjects; + + if (registeredEventsObjects) { + registeredEventsObjects[type]?.delete(target); } +} - public static dispatchEvent(scene: Scene, type: K, event?: Events[K]): void { - const sceneCache = this._events[scene?.id]; - if (sceneCache?.[type]) { - for (const target of sceneCache[type]) { - target.__eventsDispatcher.dispatch(type, event); - } +/** @internal */ +export function dispatchMiscEvent(scene: Scene, type: K, event?: EzEvents[K]): void { + const registeredEventsObjects = scene.__registeredEventsObjects; + + if (registeredEventsObjects?.[type]) { + for (const target of registeredEventsObjects[type]) { + target.__eventsDispatcher.dispatch(type, event); } } +} - public static dispatchEventExcludeCameras(scene: Scene, type: K, event?: Events[K]): void { - const sceneCache = this._events[scene?.id]; - if (sceneCache?.[type]) { - for (const target of sceneCache[type]) { - if (!(target as Camera).isCamera) { - target.__eventsDispatcher.dispatch(type, event); - } +/** @internal */ +export function dispatchMiscEventExcludeCameras(scene: Scene, type: K, event?: EzEvents[K]): void { + const registeredEventsObjects = scene.__registeredEventsObjects; + + if (registeredEventsObjects?.[type]) { + for (const target of registeredEventsObjects[type]) { + if (!(target as Camera).isCamera) { + target.__eventsDispatcher.dispatch(type, event); } } } diff --git a/src/events/RaycasterManager.ts b/src/events/RaycasterManager.ts index 75323d1c..0565298d 100644 --- a/src/events/RaycasterManager.ts +++ b/src/events/RaycasterManager.ts @@ -1,6 +1,6 @@ import { Object3D, PerspectiveCamera, Raycaster, Vector2 } from 'three'; import { RenderManager } from '../rendering/RenderManager.js'; -import { IntersectionExt } from './Events.js'; +import { EzIntersection } from './Events.js'; import { Hitbox } from './Hitbox.js'; /** @@ -9,12 +9,12 @@ import { Hitbox } from './Hitbox.js'; * @param b - The second intersection to compare. * @returns A negative value if `a` should precede `b`, a positive value if `b` should precede `a`, or zero if their order is indeterminate. */ -export type RaycasterSortComparer = (a: IntersectionExt, b: IntersectionExt) => number; +export type RaycasterSortComparer = (a: EzIntersection, b: EzIntersection) => number; /** @internal */ export class RaycasterManager { public raycaster = new Raycaster(); - public raycasterSortComparer: RaycasterSortComparer = (a: IntersectionExt, b: IntersectionExt) => a.distance - b.distance; + public raycasterSortComparer: RaycasterSortComparer = (a: EzIntersection, b: EzIntersection) => a.distance - b.distance; public pointer = new Vector2(); public pointerOnCanvas = false; private _computedPointer = new Vector2(); @@ -24,8 +24,8 @@ export class RaycasterManager { this._renderManager = renderManager; } - public getIntersections(event: PointerEvent, isDragging: boolean, excluded?: Object3D): IntersectionExt[] { - const intersections: IntersectionExt[] = []; + public getIntersections(event: PointerEvent, isDragging: boolean, excluded?: Object3D): EzIntersection[] { + const intersections: EzIntersection[] = []; const canvasBounds = this._renderManager.renderer.domElement.getBoundingClientRect(); this.pointer.set(event.clientX - canvasBounds.left, event.clientY - canvasBounds.top); if (this.getComputedMousePosition(this.pointer, this._computedPointer, isDragging, event.isPrimary)) { @@ -52,7 +52,7 @@ export class RaycasterManager { return true; } - private raycastObjects(object: Object3D, target: IntersectionExt[], excluded?: Object3D): IntersectionExt[] { + private raycastObjects(object: Object3D, target: EzIntersection[], excluded?: Object3D): EzIntersection[] { if (object === excluded) return; if (object.interceptByRaycaster && object.visible) { diff --git a/src/index.ts b/src/index.ts index 303fbf30..72a66d3e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,14 +2,13 @@ import { MaterialExtPrototype } from './patch/Material.js'; import { Object3DExtPrototype } from './patch/Object3D.js'; import { SceneExtPrototype } from './patch/Scene.js'; -export * from '@three.ez/asset-manager'; +export * as Asset from '@three.ez/asset-manager'; -export * from './binding/Binding.js'; -export * from './cameras/OrthographicCameraAuto.js'; -export * from './cameras/PerspectiveCameraAuto.js'; -export * from './core/Main.js'; +export * from './Main.js'; +export * from './camera/OrthographicCameraAuto.js'; +export * from './camera/PerspectiveCameraAuto.js'; export * from './events/CursorManager.js'; -export * from './events/Default.js'; +export * from './events/InteractionDefault.js'; export * from './events/DragAndDropManager.js'; export * from './events/Events.js'; export * from './events/EventsDispatcher.js'; @@ -18,12 +17,7 @@ export * from './events/InteractionManager.js'; export * from './events/InteractionEventsQueue.js'; export * from './events/MiscEventsManager.js'; export * from './events/RaycasterManager.js'; -export * from './instancedMesh/EventsDispatcherInstanced.js'; -export * from './instancedMesh/InstancedMesh2.js'; -export * from './instancedMesh/InstancedMeshEntity.js'; export * from './patch/Euler.js'; -export * from './patch/Material.js'; -export * from './patch/Matrix4.js'; export * from './patch/Object3D.js'; export * from './patch/Quaternion.js'; export * from './patch/Scene.js'; @@ -32,16 +26,6 @@ export * from './patch/Vector3.js'; export * from './patch/WebGLRenderer.js'; export * from './rendering/RenderManager.js'; export * from './rendering/RenderView.js'; -export * from './tweening/Actions.js'; -export * from './tweening/Easings.js'; -export * from './tweening/Tween.js'; -export * from './tweening/TweenManager.js'; -export * from './tweening/RunningTween.js'; -export * from './utils/IntersectionUtils.js'; -export * from './utils/Query.js'; -export * from './utils/Stats.js'; -export * from './utils/Utils.js'; -export * from './utils/VectorUtils.js'; declare module 'three' { export interface Material extends MaterialExtPrototype { } diff --git a/src/instancedMesh/EventsDispatcherInstanced.ts b/src/instancedMesh/EventsDispatcherInstanced.ts deleted file mode 100644 index 15f0e44d..00000000 --- a/src/instancedMesh/EventsDispatcherInstanced.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { EventExt, InteractionEvents, MiscEvents } from '../events/Events.js'; -import { InstancedMeshEntity } from './InstancedMeshEntity.js'; - -export type InstancedMiscUpdateEvents = Omit; -export type InstancedInteractionEvents = Omit, - 'focusout' | 'focusin' | 'pointerleave' | 'pointerenter' | 'dragenter' | 'dragover' | 'dragleave' | 'drop'>; -export type InstancedEvents = InstancedMiscUpdateEvents & InstancedInteractionEvents; - -/** @internal */ -export class EventsDispatcherInstanced { - public parent: InstancedMeshEntity; - public listeners: { [K in keyof InstancedEvents]?: ((event?: InstancedEvents[K]) => void)[] } = {}; - - constructor(parent: InstancedMeshEntity) { - this.parent = parent; - } - - public add(type: K, listener: (event: InstancedEvents[K]) => void): (event: InstancedEvents[K]) => void { - if (!this.listeners[type]) { - this.listeners[type] = []; - } - if (this.listeners[type].indexOf(listener) < 0) { - this.listeners[type].push(listener); - } - return listener; - } - - public has(type: K, listener: (event: InstancedEvents[K]) => void): boolean { - return this.listeners[type]?.indexOf(listener) > -1; - } - - public remove(type: K, listener: (event: InstancedEvents[K]) => void): void { - const index = this.listeners[type]?.indexOf(listener) ?? -1; - if (index > -1) { - this.listeners[type].splice(index, 1); - } - } - - public dispatchDOM(type: K, event: InstancedInteractionEvents[K]): void { - event._bubbles = false; - event._stoppedImmediatePropagation = false; - event._defaultPrevented = false; - event._type = type; - event._target = this.parent; - this.executeDOM(type, event); - } - - private executeDOM(type: K, event: InstancedInteractionEvents[K]): void { - if (!this.listeners[type]) return; - const target = event.currentTarget = this.parent; - for (const callback of this.listeners[type]) { - if (event._stoppedImmediatePropagation) break; - callback.call(target, event as any); - } - } - - public dispatch(type: T, event?: InstancedMiscUpdateEvents[T]): void { - if (!this.listeners[type]) return; - for (const callback of this.listeners[type]) { - callback.call(this.parent, event as any); - } - } - - public dispatchManual(type: T, event?: InstancedEvents[T]): void { - if ((event as EventExt)?.cancelable !== undefined) { - return this.dispatchDOM(type as keyof InstancedInteractionEvents, event as any); - } - this.dispatch(type as keyof InstancedMiscUpdateEvents, event as any); - } -} diff --git a/src/instancedMesh/InstancedMesh2.ts b/src/instancedMesh/InstancedMesh2.ts deleted file mode 100644 index 8746e230..00000000 --- a/src/instancedMesh/InstancedMesh2.ts +++ /dev/null @@ -1,292 +0,0 @@ -import { BufferGeometry, Color, ColorRepresentation, DynamicDrawUsage, InstancedMesh, Material, Matrix4 } from 'three'; -import { AnimateEvent, DragEventExt, FocusEventExt, IntersectionExt, KeyboardEventExt, PointerEventExt, PointerIntersectionEvent, WheelEventExt } from '../events/Events.js'; -import { InstancedMeshEntity } from './InstancedMeshEntity.js'; - -function overrideProperty(...names: (keyof InstancedMesh2)[]): void { - for (const name of names) { - Object.defineProperty(InstancedMesh2.prototype, name, { - get: function (this: InstancedMesh2) { return this._hoveredInstance[name]; }, - set: function () { /* console.warn(`Cannot set ${name} in InstancedMesh2. Set it in InstancedMeshEntity instead.`) */ }, - configurable: true - }); - } -} - -/** - * Extends the InstancedMesh class to provide individual management of each instance, similar to an Object3D. - * @deprecated - */ -export class InstancedMesh2 extends InstancedMesh { - /** A flag indicating that this is an instance of InstancedMesh2. */ - public isInstancedMesh2 = true; - /** - * An array storing individual InstancedMeshEntity instances associated with this InstancedMesh2. - * Each element represents a separate instance that can be managed individually. - */ - public instances: InstancedMeshEntity[] = []; - /** @internal */ public _hoveredInstance: InstancedMeshEntity; - /** @internal */ public _focusedInstance: InstancedMeshEntity; - /** @internal */ public _clickingInstance: InstancedMeshEntity; - /** @internal */ public _draggingInstance: InstancedMeshEntity; - /** @internal */ public _tempMatrix = new Matrix4(); - /** @internal */ public _tempColor = new Color(); - /** @internal */ public _animate: boolean; - /** @internal */ public get __enabledStateHovered(): boolean { return this._hoveredInstance.enabled && super.enabledState; } - private _lastPointerMove: PointerEventExt; - private _lastClick: PointerEventExt; - - /** - * Gets the currently hovered instance. - */ - public get hoveredInstance(): InstancedMeshEntity { return this._hoveredInstance; } - - /** - * Gets the currently focused instance. - */ - public get focusedInstance(): InstancedMeshEntity { return this._focusedInstance; } - - /** - * Gets the currently clicking instance. - */ - public get clickingInstance(): InstancedMeshEntity { return this._clickingInstance; } - - /** - * Gets the currently dragging instance. - */ - public get draggingInstance(): InstancedMeshEntity { return this._draggingInstance; } - - /** - * @param geometry The geometry for the instanced mesh. - * @param material The material to apply to the instanced mesh. - * @param count The number of instances to create. - * @param singleInstanceType The type of individual instance to create. - * @param animate A flag indicating whether the 'animate' event will be triggered for each instance (optional, default: false). - * @param color The default color to apply to each instance (optional). - */ - constructor(geometry: BufferGeometry, material: Material, count: number, singleInstanceType: typeof InstancedMeshEntity, animate = false, color?: ColorRepresentation) { - super(geometry, material, count); - color = this._tempColor.set(color); - - this._animate = animate; - if (animate) { - this.instanceMatrix.setUsage(DynamicDrawUsage); - } - - for (let i = 0; i < count; i++) { - this.instances.push(new singleInstanceType(this, i, color)); - } - - this.on('animate', this.animate.bind(this)); - this.on('pointerintersection', this.pointerIntersection.bind(this)); - this.on('pointermove', this.pointerMove.bind(this)); - this.on('pointerleave', this.pointerLeave.bind(this)); - this.on('focusin', this.focusIn.bind(this)); - this.on('focusout', this.focusOut.bind(this)); - this.on('click', this.click.bind(this)); - this.on('pointerdown', this.pointerDown.bind(this)); - this.on('pointerup', this.pointerUp.bind(this)); - this.on('keydown', this.keyDown.bind(this)); - this.on('keyup', this.keyUp.bind(this)); - this.on('wheel', this.wheel.bind(this)); - this.on('drag', this.drag.bind(this)); - this.on('dragstart', this.dragStart.bind(this)); - this.on('dragend', this.dragEnd.bind(this)); - this.on('dragcancel', this.dragCancel.bind(this)); - } - - /** - * Set the focus to the specified instance, if focus is enabled for the InstancedMesh2, or clears the focus if no target is provided. - * @param target Optional. The instance to focus on. If not provided, the focus is cleared. - */ - public focus(target?: InstancedMeshEntity): void { - if (!this.__focused) return; - - const focusableObj = target?.focusable ? target : undefined; - if ((!target || focusableObj?.enabled) && this._focusedInstance !== focusableObj) { - const oldFocusedObj = this._focusedInstance; - this._focusedInstance = focusableObj; - - if (oldFocusedObj?.enabled) { - oldFocusedObj.__focused = false; - oldFocusedObj.__eventsDispatcher.dispatchDOM('blur', new FocusEventExt(focusableObj)); - } - - if (focusableObj) { - focusableObj.__focused = true; - focusableObj.__eventsDispatcher.dispatchDOM('focus', new FocusEventExt(oldFocusedObj)); - } - - this.needsRender = true; - } - } - - private pointerOverOut(intersection: IntersectionExt, domEvent: PointerEvent): void { - const hoveredInstance = this.instances[intersection.instanceId]; - if (this._hoveredInstance !== hoveredInstance) { - const oldHoveredInstance = this._hoveredInstance; - - this._hoveredInstance = hoveredInstance; - if (hoveredInstance.enabled) { - hoveredInstance.__hovered = true; - } - - if (oldHoveredInstance) { - oldHoveredInstance.__hovered = false; - if (oldHoveredInstance.enabled) { - const event = new PointerEventExt(domEvent, intersection, hoveredInstance); - oldHoveredInstance.__eventsDispatcher.dispatchDOM('pointerout', event); - } - } - - if (hoveredInstance.enabled) { - const event = new PointerEventExt(domEvent, intersection, oldHoveredInstance); - hoveredInstance.__eventsDispatcher.dispatchDOM('pointerover', event); - } - } - } - - private animate(e: AnimateEvent): void { - if (this._animate) { - for (let i = 0; i < this.count; i++) { - this.instances[i].__eventsDispatcher.dispatch('animate', e); - } - } - } - - private pointerIntersection(e: PointerIntersectionEvent): void { - this.pointerOverOut(e.intersection, this._lastPointerMove?.domEvent); - if (this._hoveredInstance.enabled) { - const event = new PointerIntersectionEvent(e.intersection); - this._hoveredInstance.__eventsDispatcher.dispatchDOM('pointerintersection', event); - } - } - - private pointerMove(e: PointerEventExt): void { - this._lastPointerMove = e; - this.pointerOverOut(e.intersection, e.domEvent); - if (this._hoveredInstance.enabled) { - const event = new PointerEventExt(e.domEvent, e.intersection); - this._hoveredInstance.__eventsDispatcher.dispatchDOM('pointermove', event); - } - } - - private pointerLeave(e: PointerEventExt): void { - const instance = this._hoveredInstance; - instance.__hovered = false; - this._hoveredInstance = undefined; - if (instance.enabled) { - const event = new PointerEventExt(e.domEvent, e.intersection); - instance.__eventsDispatcher.dispatchDOM('pointerout', event); - } - } - - private focusIn(): void { - this.focus(this._hoveredInstance); - } - - private focusOut(): void { - this.focus(); - } - - private click(e: PointerEventExt): void { - const target = this.instances[e.intersection.instanceId]; - if (target.enabled) { - const event = new PointerEventExt(e.domEvent, e.intersection); - target.__eventsDispatcher.dispatchDOM('click', event); - if (e.intersection.instanceId === this._lastClick?.intersection.instanceId && e.timeStamp - this._lastClick.timeStamp <= 300) { - const event = new PointerEventExt(e.domEvent, e.intersection); - target.__eventsDispatcher.dispatchDOM('dblclick', event); - this._lastClick = undefined; - } else { - this._lastClick = e; - } - } - } - - private pointerDown(e: PointerEventExt): void { - const target = this.instances[e.intersection.instanceId]; - if (target.enabled) { - this._clickingInstance = target; - target.__clicking = true; - const event = new PointerEventExt(e.domEvent, e.intersection, undefined, true); - target.__eventsDispatcher.dispatchDOM('pointerdown', event); - if (!event._defaultPrevented) { - this.focus(target); - } else { - e.preventDefault(); - } - } - } - - private pointerUp(e: PointerEventExt): void { - const instance = this._clickingInstance; - if (instance) { - instance.__clicking = false; - if (this._clickingInstance.enabled) { - const event = new PointerEventExt(e.domEvent, e.intersection); - instance.__eventsDispatcher.dispatchDOM('pointerup', event); - } - this._clickingInstance = undefined; - } - } - - private keyDown(e: KeyboardEventExt): void { - if (this._focusedInstance.enabled) { - const event = new KeyboardEventExt(e.domEvent, true); - this._focusedInstance.__eventsDispatcher.dispatchDOM('keydown', event); - if (event._defaultPrevented) { - e.preventDefault(); - } - } - } - - private keyUp(e: KeyboardEventExt): void { - if (this._focusedInstance.enabled) { - const event = new KeyboardEventExt(e.domEvent, false); - this._focusedInstance.__eventsDispatcher.dispatchDOM('keyup', event); - } - } - - private wheel(e: WheelEventExt): void { - if (this._hoveredInstance.enabled) { - const event = new WheelEventExt(e.domEvent, e.intersection); - this._hoveredInstance.__eventsDispatcher.dispatchDOM('wheel', event); - } - } - - private drag(e: DragEventExt): void { - const event = new DragEventExt(e.domEvent, true, e.dataTransfer, e.position, e.relatedTarget, e.intersection); - this._draggingInstance.__eventsDispatcher.dispatchDOM('drag', event); - if (event._defaultPrevented) { - e.preventDefault(); - } - } - - private dragStart(e: DragEventExt): void { - this._draggingInstance = this.instances[e.intersection.instanceId]; - this._draggingInstance.__dragging = true; - const event = new DragEventExt(e.domEvent, false, e.dataTransfer, e.position, e.relatedTarget, e.intersection); - this._draggingInstance.__eventsDispatcher.dispatchDOM('dragstart', event); - } - - private dragEnd(e: DragEventExt): void { - const instance = this._draggingInstance; - instance.__dragging = false; - this._draggingInstance = undefined; - const event = new DragEventExt(e.domEvent, false, e.dataTransfer, e.position, e.relatedTarget, e.intersection); - instance.__eventsDispatcher.dispatchDOM('dragend', event); - this.computeBoundingSphere(); - } - - private dragCancel(e: DragEventExt): void { - const event = new DragEventExt(e.domEvent, e.cancelable, e.dataTransfer, e.position, e.relatedTarget, e.intersection); - this._draggingInstance.__eventsDispatcher.dispatchDOM('dragcancel', event); - if (event._defaultPrevented) { - e.preventDefault(); - } - } -} - -overrideProperty('cursor', 'cursorDrag', 'cursorDrop', 'draggable', 'findDropTarget'); - -// TODO pointeridPrimary on focus and assign hovered, clicking, ecc. diff --git a/src/instancedMesh/InstancedMeshEntity.ts b/src/instancedMesh/InstancedMeshEntity.ts deleted file mode 100644 index 363813ed..00000000 --- a/src/instancedMesh/InstancedMeshEntity.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { Color, ColorRepresentation, EventDispatcher, Matrix4, Quaternion, Vector3 } from 'three'; -import { Cursor } from '../events/CursorManager.js'; -import { EventsDispatcherInstanced, InstancedEvents } from './EventsDispatcherInstanced.js'; -import { Tween } from '../tweening/Tween.js'; -import { InstancedMesh2 } from './InstancedMesh2.js'; - -const tempQuaternion = new Quaternion(); - -/** - * Represents an individual instance within an InstancedMesh2, providing properties and methods for interaction and transformation. - */ -export class InstancedMeshEntity extends EventDispatcher { - /** A flag indicating that this is an instance of InstancedMeshEntity. */ - public isInstancedMeshEntity = true; - /** The parent InstancedMesh2 that contains this instance. */ - public parent: InstancedMesh2; - /** An identifier for this individual instance within an InstancedMesh2. */ - public instanceId: number; - /** A Vector3 representing the object's local position. Default is (0, 0, 0). */ - public readonly position = new Vector3(); - /** The object's local scale. Default is Vector3(1, 1, 1). */ - public readonly scale = new Vector3(1, 1, 1); - /** Object's local rotation as a Quaternion. */ - public readonly quaternion = new Quaternion(); - /** - * Determines if the object is enabled. (default: true). - * If set to true, it allows triggering all InteractionEvents; otherwise, events are disabled. - */ - public enabled = true; - /** Indicates whether the object can receive focus (default: true). */ - public focusable = true; - /** Indicates whether the object is draggable (default: false). */ - public draggable = false; - /** Determines when the object is dragged, whether it will have to search for any drop targets (default: false). */ - public findDropTarget = false; - /** Cursor style when interacting with the object. */ - public cursor: Cursor; - /** Cursor style when dragging the object. */ - public cursorDrag: Cursor; - /** Cursor style when dropping an object onto this one. */ - public cursorDrop: Cursor; - /** @internal */ public __eventsDispatcher: EventsDispatcherInstanced; - /** @internal */ public __hovered = false; - /** @internal */ public __focused = false; - /** @internal */ public __clicking = false; - /** @internal */ public __dragging = false; - - /** Indicates if the primary pointer is over this object. */ - public get hovered(): boolean { return this.__hovered; } - /** Indicates if the object is currently focused. */ - public get focused(): boolean { return this.__focused; } - /** Indicates if the object is currently being clicked. */ - public get clicking(): boolean { return this.__clicking; } - /** Indicates if the object is currently being dragged. */ - public get dragging(): boolean { return this.__dragging; } - /** Retrieves the combined enabled state considering parent objects. */ - public get enabledState(): boolean { return this.enabled && this.parent.enabledState; } - - /** - * The global transform of the object. - */ - public get matrixWorld(): Matrix4 { - const matrix = this.parent._tempMatrix; - matrix.compose(this.position, this.quaternion, this.scale); - this.parent.updateWorldMatrix(true, false); - return matrix.premultiply(this.parent.matrixWorld); - } - - /** - * @param parent - The parent InstancedMesh2 that contains this instance. - * @param index - The index of this instance within the parent InstancedMesh2. - * @param color - The initial color representation for this instance (optional). - */ - constructor(parent: InstancedMesh2, index: number, color?: ColorRepresentation) { - super(); - this.parent = parent; - this.instanceId = index; - this.__eventsDispatcher = new EventsDispatcherInstanced(this); - if (color !== undefined) { - this.setColor(color); - } - } - - /** - * Sets the color of this instance. - * @param color - The color representation to set. - */ - public setColor(color: ColorRepresentation): void { - const parent = this.parent; - parent.setColorAt(this.instanceId, parent._tempColor.set(color)); - parent.instanceColor.needsUpdate = true; - } - - /** - * Gets the color of this instance. - * @param color - An optional target Color object to store the result (optional). - * @returns The color representation of this instance. - */ - public getColor(color = this.parent._tempColor): Color { - this.parent.getColorAt(this.instanceId, color); - return color; - } - - /** - * Updates the local transform. - */ - public updateMatrix(): void { - const parent = this.parent; - const matrix = parent._tempMatrix; - matrix.compose(this.position, this.quaternion, this.scale); - parent.setMatrixAt(this.instanceId, matrix); - parent.instanceMatrix.needsUpdate = true; - } - - /** - * Applies the matrix transform to the object and updates the object's position, rotation, and scale. - * @param m Matrix to apply. - * @returns The instance of the object. - */ - public applyMatrix4(m: Matrix4): this { - const parent = this.parent; - const matrix = parent._tempMatrix; - matrix.compose(this.position, this.quaternion, this.scale); - matrix.premultiply(m); - matrix.decompose(this.position, this.quaternion, this.scale); - parent.setMatrixAt(this.instanceId, matrix); - parent.instanceMatrix.needsUpdate = true; - return this; - } - - /** - * Applies the rotation represented by the quaternion to the object. - * @param q Quaternion to apply. - * @returns The instance of the object. - */ - public applyQuaternion(q: Quaternion): this { - this.quaternion.premultiply(q); - return this; - } - - /** - * Rotate an object along an axis in object space. The axis is assumed to be normalized. - * @param axis A normalized vector in object space. - * @param angle The angle in radians. - * @returns The instance of the object. - */ - public rotateOnAxis(axis: Vector3, angle: number): this { - tempQuaternion.setFromAxisAngle(axis, angle); - this.quaternion.multiply(tempQuaternion); - return this; - } - - /** - * Rotate an object along an axis in world space. The axis is assumed to be normalized. Method Assumes no rotated parent. - * @param axis A normalized vector in world space. - * @param angle The angle in radians. - * @returns The instance of the object. - */ - public rotateOnWorldAxis(axis: Vector3, angle: number): this { - tempQuaternion.setFromAxisAngle(axis, angle); - this.quaternion.premultiply(tempQuaternion); - return this; - } - - /** - * Applies focus to the object. - */ - public applyFocus(): void { - this.parent.focus(this); - } - - /** - * Applies blur (removes focus) from the object. - */ - public applyBlur(): void { - if (this.parent.focusedInstance === this) { - this.parent.focus(); - } - } - - /** - * Attaches an event listener to the object. - * @param type - The type of event to listen for. - * @param listener - The callback function to execute when the event occurs. - * @returns A function to remove the event listener. - */ - public on(types: K | K[], listener: (event?: InstancedEvents[K]) => void): (event?: InstancedEvents[K]) => void { - if (typeof types === 'string') { - return this.__eventsDispatcher.add(types, listener) as (event?: InstancedEvents[K]) => void; - } - for (const type of types as any) { - this.__eventsDispatcher.add(type, listener); - } - return listener; - } - - /** - * Checks if the object has a specific event listener. - * @param type - The type of event to check for. - * @param listener - The callback function to check. - * @returns `true` if the event listener is attached; otherwise, `false`. - */ - public hasEvent(type: K, listener: (event?: InstancedEvents[K]) => void): boolean { - return this.__eventsDispatcher.has(type, listener); - } - - /** - * Removes an event listener from the object. - * @param type - The type of event to remove the listener from. - * @param listener - The callback function to remove. - */ - public off(type: K, listener: (event?: InstancedEvents[K]) => void): void { - this.__eventsDispatcher.remove(type, listener); - } - - /** - * Triggers a specific event on the object. - * @param type - The type of event to trigger. - * @param event - Optional event data to pass to the listeners. - */ - public trigger(type: K, event?: InstancedEvents[K]): void { - this.__eventsDispatcher.dispatchManual(type, event); - } - - /** - * Initiates a Tween animation for the object. - * @returns A Tween instance for further configuration. - */ - public tween(): Tween { - return new Tween(this); - } -} diff --git a/src/patch/Euler.ts b/src/patch/Euler.ts index 8debd149..f8cb1d76 100644 --- a/src/patch/Euler.ts +++ b/src/patch/Euler.ts @@ -1,28 +1,15 @@ import { Object3D } from 'three'; /** @internal */ -export function applyEulerPatch(target: Object3D): void { - target.__onChangeBaseEuler = target.rotation._onChangeCallback; - if (target.scene?.__smartRendering) { - setEulerChangeCallbackSR(target); - } else { - setEulerChangeCallback(target); +export function patchRotation(target: Object3D): void { + if (!target.__onChangeEulerBase) { + target.__onChangeEulerBase = target.rotation._onChangeCallback; + target.rotation._onChangeCallback = onChangeEuler; } } -/** @internal */ -export function setEulerChangeCallbackSR(target: Object3D): void { - target.rotation._onChangeCallback = () => { - target.__onChangeBaseEuler(); - target.needsRender = true; - target.__eventsDispatcher.dispatch('rotationchange'); - }; -} - -/** @internal */ -export function setEulerChangeCallback(target: Object3D): void { - target.rotation._onChangeCallback = () => { - target.__onChangeBaseEuler(); - target.__eventsDispatcher.dispatch('rotationchange'); - }; +function onChangeEuler(this: Object3D): void { + this.__onChangeEulerBase!(); + this.needsRender = true; + this.__eventsDispatcher.dispatch('rotationchange'); } diff --git a/src/patch/Material.ts b/src/patch/Material.ts deleted file mode 100644 index b6e9b4e0..00000000 --- a/src/patch/Material.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Material } from 'three'; -import { Tween } from '../tweening/Tween.js'; - -/** - * Represents the prototype for extended Material functionality. - */ -export interface MaterialExtPrototype { - /** - * Initiates a Tween animation for the material. - * @param id - Unique identifier. If you start a new tween, the old one with the same id (if specified) will be stopped. - * @template T - The type of the target. - * @returns A Tween instance for further configuration. - */ - tween(id?: string): Tween; -} - -Material.prototype.tween = function (id?: string) { - return new Tween(this as T).setId(id); -}; diff --git a/src/patch/Matrix4.ts b/src/patch/Matrix4.ts deleted file mode 100644 index 00cd9b6f..00000000 --- a/src/patch/Matrix4.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Object3D } from 'three'; - -/** @internal Override compose method because is called for every rendered object and can be performance critical after Vector3 patch. */ -export function applyMatrix4Patch(parent: Object3D): void { - parent.matrix.compose = function (position: any, quaternion: any, scale: any) { - const te = this.elements; - - const x = quaternion._x, y = quaternion._y, z = quaternion._z, w = quaternion._w; - const x2 = x + x, y2 = y + y, z2 = z + z; - const xx = x * x2, xy = x * y2, xz = x * z2; - const yy = y * y2, yz = y * z2, zz = z * z2; - const wx = w * x2, wy = w * y2, wz = w * z2; - - const sx = scale._x, sy = scale._y, sz = scale._z; - - te[0] = (1 - (yy + zz)) * sx; - te[1] = (xy + wz) * sx; - te[2] = (xz - wy) * sx; - te[3] = 0; - - te[4] = (xy - wz) * sy; - te[5] = (1 - (xx + zz)) * sy; - te[6] = (yz + wx) * sy; - te[7] = 0; - - te[8] = (xz + wy) * sz; - te[9] = (yz - wx) * sz; - te[10] = (1 - (xx + yy)) * sz; - te[11] = 0; - - te[12] = position._x; - te[13] = position._y; - te[14] = position._z; - te[15] = 1; - - return this; - }; -} diff --git a/src/patch/Object3D.ts b/src/patch/Object3D.ts index 8abc4a47..9569245b 100644 --- a/src/patch/Object3D.ts +++ b/src/patch/Object3D.ts @@ -1,28 +1,21 @@ import { Object3D, Scene } from 'three'; -import { Binding, BindingCallback } from '../binding/Binding.js'; import { Cursor } from '../events/CursorManager.js'; -import { Default } from '../events/Default.js'; -import { Events, InteractionEvents } from '../events/Events.js'; +import { INTERACTION_DEFAULT } from '../events/InteractionDefault.js'; +import { EzEvents, EzInteractionEvents } from '../events/Events.js'; import { EventsDispatcher } from '../events/EventsDispatcher.js'; import { Hitbox } from '../events/Hitbox.js'; -import { Tween } from '../tweening/Tween.js'; -import { querySelector, querySelectorAll } from '../utils/Query.js'; -import { applyEulerPatch } from './Euler.js'; -import { applyMatrix4Patch } from './Matrix4.js'; -import { applyQuaternionPatch } from './Quaternion.js'; +import { patchRotation } from './Euler.js'; +import { patchQuaternion } from './Quaternion.js'; import { removeSceneReference, setSceneReference } from './Scene.js'; -import { applyVec3Patch } from './Vector3.js'; +import { patchPosition, patchScale } from './Vector3.js'; + +// TODO: override matrix4 prototype to use new props like _x instead of x? /** * Represents the prototype for extended Object3D functionality. */ export interface Object3DExtPrototype { - /** @internal */ __boundCallbacks: BindingCallback[]; - /** @internal */ __manualDetection: boolean; /** @internal */ __eventsDispatcher: EventsDispatcher; - /** @internal */ __vec3Patched: boolean; - /** @internal */ __rotationPatched: boolean; - /** @internal */ __smartRenderingPatched: boolean; /** @internal */ __enabled: boolean; /** @internal */ __visible: boolean; /** @internal */ __hovered: boolean; @@ -30,9 +23,9 @@ export interface Object3DExtPrototype { /** @internal */ __clicking: boolean; /** @internal */ __dragging: boolean; /** @internal */ __isDropTarget: boolean; - /** @internal */ __baseVisibleDescriptor: PropertyDescriptor; - /** @internal */ __onChangeBaseEuler: () => void; - /** @internal */ __onChangeBaseQuat: () => void; + /** @internal */ __baseVisibleDescriptor: PropertyDescriptor | undefined; + /** @internal */ __onChangeEulerBase: (() => void) | null; + /** @internal */ __onChangeQuaternionBase: (() => void) | null; /** * Determines if the object is enabled. Default is `true`. * If set to true, it allows triggering all InteractionEvents; otherwise, events are disabled. @@ -40,31 +33,37 @@ export interface Object3DExtPrototype { enabled: boolean; /** * Determines if the **object** and **all of its children** can be intercepted by the main raycaster. - * @default DEFAULT_INTERCEPT_BY_RAYCASTER (true). + * @default INTERACTION_DEFAULT.interceptByRaycaster (true). */ interceptByRaycaster: boolean; /** Array of hitboxes for collision detection. */ - hitboxes: Hitbox[]; + hitboxes: Hitbox[] | undefined; /** Indicates which object will be dragged instead of this one. */ - dragTarget: Object3D; - /** Indicates whether the object can receive focus. Default is DEFAULT_FOCUSABLE (`true`). */ + dragTarget: Object3D | undefined; + /** + * Indicates whether the object can receive focus. + * @default INTERACTION_DEFAULT.focusable (true). + */ focusable: boolean; - /** Indicates whether the object is draggable. Default is DEFAULT_DRAGGABLE (`false`). */ + /** + * Indicates whether the object is draggable. + * @default INTERACTION_DEFAULT.draggable (false). + */ draggable: boolean; /** Determines when the object is dragged, whether it will have to search for any drop targets. Default is `false`. */ findDropTarget: boolean; /** Reference to the scene the object belongs to. */ scene: Scene; /** Cursor style when interacting with the object. */ - cursor: Cursor; + cursor: Cursor | undefined; /** Cursor style when dragging the object. */ - cursorDrag: Cursor; + cursorDrag: Cursor | undefined; /** Cursor style when dropping an object onto this one. */ - cursorDrop: Cursor; + cursorDrop: Cursor | undefined; /** Indicates whether the scene needs rendering. */ needsRender: boolean; /** Indicates the tags to be searched using the querySelector and `querySelectorAll` methods. */ - tags: Set; + tags: Set | undefined; /** Indicates if the primary pointer is over this object. */ get hovered(): boolean; /** Indicates if the object is currently focused. */ @@ -78,7 +77,7 @@ export interface Object3DExtPrototype { /** Retrieves the combined visibility state considering parent objects. */ get visibilityState(): boolean; /** Retrieves the first possible focusable object. */ - get firstFocusable(): Object3D; + get firstFocusable(): Object3D | null; /** * Applies focus to the object. */ @@ -93,32 +92,32 @@ export interface Object3DExtPrototype { * @param listener - The callback function to execute when the event occurs. * @returns A function to remove the event listener. */ - on(type: K | K[], listener: (this: this, event?: Events[K]) => void): (event?: Events[K]) => void; + on(type: K | K[], listener: (this: this, event?: EzEvents[K]) => void): (event?: EzEvents[K]) => void; /** * Checks if the object has a specific event listener. * @param type - The type of event to check for. * @param listener - The callback function to check. * @returns `true` if the event listener is attached; otherwise, `false`. */ - hasEvent(type: K, listener: (event?: Events[K]) => void): boolean; + hasEvent(type: K, listener: (event?: EzEvents[K]) => void): boolean; /** * Removes an event listener from the object. * @param type - The type of event to remove the listener from. * @param listener - The callback function to remove. */ - off(type: K, listener: (event?: Events[K]) => void): void; + off(type: K, listener: (event?: EzEvents[K]) => void): void; /** * Triggers a specific event on the object. * @param type - The type of event to trigger. * @param event - Optional event data to pass to the listeners. */ - trigger(type: K, event?: Events[K]): void; + trigger(type: K, event?: EzEvents[K]): void; /** * Triggers a specific event on the object and all its ancestors. * @param type - The type of event to trigger. * @param event - Optional event data to pass to the listeners. */ - triggerAncestor(type: K, event?: InteractionEvents[K]): void; + triggerAncestor(type: K, event?: EzInteractionEvents[K]): void; /** * Activates manual detection mode for bindings. * When this method is used, all bindings will no longer be calculated automatically. @@ -158,7 +157,7 @@ export interface Object3DExtPrototype { * @param query - The query string to match against the Object3D elements. * @returns The first Object3D element that matches the query, or undefined if no match is found. */ - querySelector(query: string): Object3D; + querySelector(query: string): Object3D | null; /** * Finds and returns a list of Object3D elements that match the specified query string. * This method follows a similar syntax to CSS selectors. @@ -168,14 +167,6 @@ export interface Object3DExtPrototype { querySelectorAll(query: string): Object3D[]; } -Object3D.prototype.findDropTarget = false; -Object3D.prototype.__manualDetection = false; -Object3D.prototype.__focused = false; -Object3D.prototype.__clicking = false; -Object3D.prototype.__dragging = false; -Object3D.prototype.__hovered = false; - -Object3D.prototype.__visible = true; Object.defineProperty(Object3D.prototype, 'visible', { get: function (this: Object3D) { return this.__visible; }, set: function (this: Object3D, value: boolean) { @@ -187,7 +178,6 @@ Object.defineProperty(Object3D.prototype, 'visible', { configurable: true }); -Object3D.prototype.__enabled = true; Object.defineProperty(Object3D.prototype, 'enabled', { get: function (this: Object3D) { return this.__enabled; }, set: function (this: Object3D, value: boolean) { @@ -204,7 +194,7 @@ Object.defineProperty(Object3D.prototype, 'enabled', { Object.defineProperty(Object3D.prototype, 'firstFocusable', { get: function (this: Object3D) { - let obj = this; + let obj: Object3D | null = this; while (obj?.focusable === false) { obj = obj.parent; } @@ -214,20 +204,20 @@ Object.defineProperty(Object3D.prototype, 'firstFocusable', { Object.defineProperty(Object3D.prototype, 'enabledState', { get: function (this: Object3D) { - let obj = this; + let obj: Object3D | null = this; do { if (!obj.enabled) return false; - } while (obj = obj.parent); + } while ((obj = obj.parent)); return true; } }); Object.defineProperty(Object3D.prototype, 'visibilityState', { get: function (this: Object3D) { - let obj = this; + let obj: Object3D | null = this; do { if (!obj.visible) return false; - } while (obj = obj.parent); + } while ((obj = obj.parent)); return true; } }); @@ -237,8 +227,9 @@ Object.defineProperty(Object3D.prototype, 'needsRender', { return this.scene?.needsRender; }, set: function (this: Object3D, value: boolean) { - if (!this.scene) return; - this.scene.needsRender = value; + if (this.scene) { + this.scene.needsRender = value; + } } }); @@ -266,7 +257,7 @@ Object.defineProperty(Object3D.prototype, 'isDragging', { } }); -Object3D.prototype.on = function (this: Object3D, types: K | K[], listener: (event: Events[K]) => void): (event: Events[K]) => void { +Object3D.prototype.on = function (this: Object3D, types: K | K[], listener: (event: EzEvents[K]) => void): (event: EzEvents[K]) => void { if (typeof types === 'string') { return this.__eventsDispatcher.add(types, listener); } @@ -276,29 +267,35 @@ Object3D.prototype.on = function (this: Object3D, types: return listener; }; -Object3D.prototype.hasEvent = function (type: K, listener: (event: Events[K]) => void): boolean { +Object3D.prototype.hasEvent = function (type: K, listener: (event: EzEvents[K]) => void): boolean { return this.__eventsDispatcher.has(type, listener); }; -Object3D.prototype.off = function (type: K, listener: (event: Events[K]) => void): void { +Object3D.prototype.off = function (type: K, listener: (event: EzEvents[K]) => void): void { this.__eventsDispatcher.remove(type, listener); }; -Object3D.prototype.trigger = function (type: T, event?: Events[T]): void { +Object3D.prototype.trigger = function (type: T, event?: EzEvents[T]): void { this.__eventsDispatcher.dispatchManual(type, event); }; -Object3D.prototype.triggerAncestor = function (type: T, event?: Events[T]): void { +Object3D.prototype.triggerAncestor = function (type: T, event?: EzEvents[T]): void { this.__eventsDispatcher.dispatchAncestorManual(type, event); }; Object.defineProperty(Object3D.prototype, 'userData', { // needed to inject code in constructor set: function (this: Object3D, value) { - this.focusable = Default.focusable; - this.draggable = Default.draggable; - this.interceptByRaycaster = Default.interceptByRaycaster; - this.tags = new Set(); - this.__boundCallbacks = []; + this.focusable = INTERACTION_DEFAULT.focusable; + this.draggable = INTERACTION_DEFAULT.draggable; + this.interceptByRaycaster = INTERACTION_DEFAULT.interceptByRaycaster; + this.findDropTarget = false; + this.__manualDetection = false; + this.__focused = false; + this.__clicking = false; + this.__dragging = false; + this.__hovered = false; + this.__visible = true; + this.__enabled = true; this.__eventsDispatcher = new EventsDispatcher(this); Object.defineProperty(this, 'userData', { @@ -374,18 +371,13 @@ Object3D.prototype.remove = function (object: Object3D) { /** @internal */ export function applyObject3DVector3Patch(target: Object3D): void { - if (!target.__vec3Patched) { - applyVec3Patch(target); - applyMatrix4Patch(target); - target.__vec3Patched = true; - } + patchPosition(target); + patchScale(target); + // TODO: we can patch matrix4 too? } /** @internal */ export function applyObject3DRotationPatch(target: Object3D): void { - if (!target.__rotationPatched) { - applyQuaternionPatch(target); - applyEulerPatch(target); - target.__rotationPatched = true; - } + patchQuaternion(target); + patchRotation(target); } diff --git a/src/patch/Quaternion.ts b/src/patch/Quaternion.ts index 1751a0c1..e11ecb88 100644 --- a/src/patch/Quaternion.ts +++ b/src/patch/Quaternion.ts @@ -1,28 +1,15 @@ import { Object3D } from 'three'; /** @internal */ -export function applyQuaternionPatch(target: Object3D): void { - target.__onChangeBaseQuat = target.quaternion._onChangeCallback; - if (target.scene?.__smartRendering) { - setQuatChangeCallbackSR(target); - } else { - setQuatChangeCallback(target); +export function patchQuaternion(target: Object3D): void { + if (!target.__onChangeQuaternionBase) { + target.__onChangeQuaternionBase = target.quaternion._onChangeCallback; + target.quaternion._onChangeCallback = onChangeQuaternion; } } -/** @internal */ -export function setQuatChangeCallback(target: Object3D): void { - target.quaternion._onChangeCallback = () => { - target.__onChangeBaseQuat(); - target.__eventsDispatcher.dispatch('rotationchange'); - }; -} - -/** @internal */ -export function setQuatChangeCallbackSR(target: Object3D): void { - target.quaternion._onChangeCallback = () => { - target.__onChangeBaseQuat(); - target.needsRender = true; - target.__eventsDispatcher.dispatch('rotationchange'); - }; +function onChangeQuaternion(this: Object3D): void { + this.__onChangeQuaternionBase!(); + this.needsRender = true; + this.__eventsDispatcher.dispatch('rotationchange'); } diff --git a/src/patch/Scene.ts b/src/patch/Scene.ts index febbd126..c47e05c1 100644 --- a/src/patch/Scene.ts +++ b/src/patch/Scene.ts @@ -1,18 +1,18 @@ import { Object3D, Scene } from 'three'; import { EventsCache } from '../events/MiscEventsManager.js'; import { activeSmartRendering, applySmartRenderingPatch, removeSmartRenderingPatch } from './SmartRendering.js'; -import { Binding } from '../binding/Binding.js'; -import { FocusEventExt, IntersectionExt } from '../events/Events.js'; +import { EzFocusEvent, EzIntersection } from '../events/Events.js'; import { addBase, removeBase } from './Object3D.js'; import { EventsDispatcher } from '../events/EventsDispatcher.js'; -import { Default } from '../events/Default.js'; +import { INTERACTION_DEFAULT } from '../events/InteractionDefault.js'; /** * Represents the prototype for extending Scene functionality. */ export interface SceneExtPrototype { - /** @internal */ __boundObjects: Set; - /** @internal */ __smartRendering: boolean; + /** @internal */ __registeredEventsObjects: { [x: string]: Set } | undefined; + /** @internal */ __boundObjects: Set | undefined; + /** @internal */ __smartRendering: boolean | undefined; /** * A flag indicating whether continuous raycasting is enabled (default: false). * When set to true, main raycasting occurs every frame, while false triggers raycasting only upon mouse movement. @@ -26,9 +26,9 @@ export interface SceneExtPrototype { */ continuousRaycastingDropTarget: boolean; /** An array of intersections computed from the pointer (primary pointer only). */ - intersections: IntersectionExt[]; + intersections: EzIntersection[]; /** An array of intersections computed from the pointer if an object is dragged and has 'findDropTarget' set to true (primary pointer only). */ - intersectionsDropTarget: IntersectionExt[]; + intersectionsDropTarget: EzIntersection[]; /** A reference to the currently focused Object3D within the scene. */ focusedObject: Object3D; /** @@ -73,14 +73,14 @@ Scene.prototype.focus = function (target?: Object3D): void { if (oldFocusedObj?.enabledState) { oldFocusedObj.__focused = false; - oldFocusedObj.__eventsDispatcher.dispatchDOMAncestor('blur', new FocusEventExt(focusableObj)); - oldFocusedObj.__eventsDispatcher.dispatchDOM('focusout', new FocusEventExt(focusableObj)); + oldFocusedObj.__eventsDispatcher.dispatchDOMAncestor('blur', new EzFocusEvent(focusableObj)); + oldFocusedObj.__eventsDispatcher.dispatchDOM('focusout', new EzFocusEvent(focusableObj)); } if (focusableObj) { focusableObj.__focused = true; - focusableObj.__eventsDispatcher.dispatchDOMAncestor('focus', new FocusEventExt(oldFocusedObj)); - focusableObj.__eventsDispatcher.dispatchDOM('focusin', new FocusEventExt(oldFocusedObj)); + focusableObj.__eventsDispatcher.dispatchDOMAncestor('focus', new EzFocusEvent(oldFocusedObj)); + focusableObj.__eventsDispatcher.dispatchDOM('focusin', new EzFocusEvent(oldFocusedObj)); } this.needsRender = true; @@ -108,8 +108,8 @@ Scene.prototype.remove = function (object: Object3D) { Object.defineProperty(Scene.prototype, 'userData', { // needed to inject code in constructor set: function (this: Scene, value) { this.focusable = false; - this.draggable = Default.draggable; - this.interceptByRaycaster = Default.interceptByRaycaster; + this.draggable = INTERACTION_DEFAULT.draggable; + this.interceptByRaycaster = INTERACTION_DEFAULT.interceptByRaycaster; this.tags = new Set(); this.__boundCallbacks = []; this.__eventsDispatcher = new EventsDispatcher(this); diff --git a/src/patch/Vector3.ts b/src/patch/Vector3.ts index d55c0326..d759ea04 100644 --- a/src/patch/Vector3.ts +++ b/src/patch/Vector3.ts @@ -1,590 +1,43 @@ -import { BufferAttribute, Camera, Color, Cylindrical, Euler, MathUtils, Matrix3, Matrix4, Object3D, Quaternion, Spherical, Vector3, Vector3Tuple } from 'three'; +import { Object3D, Vector3 } from 'three'; +import { Vector3Ext } from './prototype/Vector3Ext.js'; + +// TODO: create unpatch method? /** @internal */ -export function applyVec3Patch(target: Object3D): void { - patchVector(target.position); - patchVector(target.scale); - if (target.scene?.__smartRendering) { - setVec3ChangeCallbackSR(target); - } else { - setVec3ChangeCallback(target); +export function patchPosition(target: Object3D): void { + if (!(target.position as Vector3Ext).isVector3Ext) { + patchVector3Prototype(target.position); } + (target.position as Vector3Ext)._onChangeCallback = onChangePosition; } /** @internal */ -export function setVec3ChangeCallback(target: Object3D): void { - (target.position as Vector3Ext)._onChangeCallback = () => target.__eventsDispatcher.dispatch('positionchange'); - (target.scale as Vector3Ext)._onChangeCallback = () => target.__eventsDispatcher.dispatch('scalechange'); +export function patchScale(target: Object3D): void { + if (!(target.scale as Vector3Ext).isVector3Ext) { + patchVector3Prototype(target.scale); + } + (target.scale as Vector3Ext)._onChangeCallback = onChangeScale; } -/** @internal */ -export function setVec3ChangeCallbackSR(target: Object3D): void { - (target.position as Vector3Ext)._onChangeCallback = () => { - target.needsRender = true; - target.__eventsDispatcher.dispatch('positionchange'); - }; - (target.scale as Vector3Ext)._onChangeCallback = () => { - target.needsRender = true; - target.__eventsDispatcher.dispatch('scalechange'); - }; +function onChangePosition(this: Object3D): void { + this.needsRender = true; + this.__eventsDispatcher.dispatch('positionchange'); } -function patchVector(vec3: Vector3): void { +function onChangeScale(this: Object3D): void { + this.needsRender = true; + this.__eventsDispatcher.dispatch('scalechange'); +} + +function patchVector3Prototype(vec3: Vector3): void { (vec3 as Vector3Ext)._x = vec3.x; (vec3 as Vector3Ext)._y = vec3.y; (vec3 as Vector3Ext)._z = vec3.z; + /** @ts-expect-error The operand of a 'delete' operator must be optional */ delete vec3.x; + /** @ts-expect-error The operand of a 'delete' operator must be optional */ delete vec3.y; + /** @ts-expect-error The operand of a 'delete' operator must be optional */ delete vec3.z; Object.setPrototypeOf(vec3, Vector3Ext.prototype); } - -/** @LASTREV 162 Vector3 */ -class Vector3Ext { - public _x: number; - public _y: number; - public _z: number; - public _onChangeCallback: () => void; - public isVector3: true; - - public get x(): number { return this._x; } - public set x(value: number) { - this._x = value; - this._onChangeCallback(); - } - - public get y(): number { return this._y; } - public set y(value: number) { - this._y = value; - this._onChangeCallback(); - } - - public get z(): number { return this._z; } - public set z(value: number) { - this._z = value; - this._onChangeCallback(); - } - - public set(x: number, y: number, z: number): this { - if (z === undefined) z = this._z; - this._x = x; - this._y = y; - this._z = z; - this._onChangeCallback(); - return this; - } - - public setScalar(scalar: number): this { - this._x = scalar; - this._y = scalar; - this._z = scalar; - this._onChangeCallback(); - return this; - } - - public setX(x: number): this { - this._x = x; - this._onChangeCallback(); - return this; - } - - public setY(y: number): this { - this._y = y; - this._onChangeCallback(); - return this; - } - - public setZ(z: number): this { - this._z = z; - this._onChangeCallback(); - return this; - } - - public setComponent(index: number, value: number): this { - switch (index) { - case 0: - this._x = value; - break; - case 1: - this._y = value; - break; - case 2: - this._z = value; - break; - default: throw new Error('index is out of range: ' + index); - } - - this._onChangeCallback(); - return this; - } - - public getComponent(index: number): number { - switch (index) { - case 0: return this._x; - case 1: return this._y; - case 2: return this._z; - default: throw new Error('index is out of range: ' + index); - } - } - - public clone(): Vector3 { - return new (Vector3.prototype as any).constructor(this._x, this._y, this._z); - } - - public copy(v: Vector3, update?: boolean): this { - this._x = v.x; - this._y = v.y; - this._z = v.z; - if (update !== false) this._onChangeCallback(); - return this; - } - - public add(v: Vector3): this { - this._x += v.x; - this._y += v.y; - this._z += v.z; - this._onChangeCallback(); - return this; - } - - public addScalar(s: number): this { - this._x += s; - this._y += s; - this._z += s; - this._onChangeCallback(); - return this; - } - - public addVectors(a: Vector3, b: Vector3): this { - this._x = a.x + b.x; - this._y = a.y + b.y; - this._z = a.z + b.z; - this._onChangeCallback(); - return this; - } - - public addScaledVector(v: Vector3, s: number): this { - this._x += v.x * s; - this._y += v.y * s; - this._z += v.z * s; - this._onChangeCallback(); - return this; - } - - public sub(v: Vector3): this { - this._x -= v.x; - this._y -= v.y; - this._z -= v.z; - this._onChangeCallback(); - return this; - } - - public subScalar(s: number): this { - this._x -= s; - this._y -= s; - this._z -= s; - this._onChangeCallback(); - return this; - } - - public subVectors(a: Vector3, b: Vector3): this { - this._x = a.x - b.x; - this._y = a.y - b.y; - this._z = a.z - b.z; - this._onChangeCallback(); - return this; - } - - public multiply(v: Vector3): this { - this._x *= v.x; - this._y *= v.y; - this._z *= v.z; - this._onChangeCallback(); - return this; - } - - public multiplyScalar(scalar: number, update?: boolean): this { - this._x *= scalar; - this._y *= scalar; - this._z *= scalar; - if (update !== false) this._onChangeCallback(); - return this; - } - - public multiplyVectors(a: Vector3, b: Vector3): this { - this._x = a.x * b.x; - this._y = a.y * b.y; - this._z = a.z * b.z; - this._onChangeCallback(); - return this; - } - - public applyEuler(euler: Euler): this { - return this.applyQuaternion(_quaternion.setFromEuler(euler)); - } - - public applyAxisAngle(axis: Vector3, angle: number): this { - return this.applyQuaternion(_quaternion.setFromAxisAngle(axis, angle)); - } - - public applyMatrix3(m: Matrix3, update?: boolean): this { - const x = this._x, y = this._y, z = this._z; - const e = m.elements; - this._x = e[0] * x + e[3] * y + e[6] * z; - this._y = e[1] * x + e[4] * y + e[7] * z; - this._z = e[2] * x + e[5] * y + e[8] * z; - if (update !== false) this._onChangeCallback(); - return this; - } - - public applyNormalMatrix(m: Matrix3): this { - return this.applyMatrix3(m, false).normalize(); - } - - public applyMatrix4(m: Matrix4, update?: boolean): this { - const x = this._x, y = this._y, z = this._z; - const e = m.elements; - const w = 1 / (e[3] * x + e[7] * y + e[11] * z + e[15]); - this._x = (e[0] * x + e[4] * y + e[8] * z + e[12]) * w; - this._y = (e[1] * x + e[5] * y + e[9] * z + e[13]) * w; - this._z = (e[2] * x + e[6] * y + e[10] * z + e[14]) * w; - if (update !== false) this._onChangeCallback(); - return this; - } - - public applyQuaternion(q: Quaternion): this { - const vx = this._x, vy = this._y, vz = this._z; - const qx = q.x, qy = q.y, qz = q.z, qw = q.w; - const tx = 2 * (qy * vz - qz * vy); - const ty = 2 * (qz * vx - qx * vz); - const tz = 2 * (qx * vy - qy * vx); - this._x = vx + qw * tx + qy * tz - qz * ty; - this._y = vy + qw * ty + qz * tx - qx * tz; - this._z = vz + qw * tz + qx * ty - qy * tx; - this._onChangeCallback(); - return this; - } - - public project(camera: Camera): this { - return this.applyMatrix4(camera.matrixWorldInverse, false).applyMatrix4(camera.projectionMatrix); - } - - public unproject(camera: Camera): this { - return this.applyMatrix4(camera.projectionMatrixInverse, false).applyMatrix4(camera.matrixWorld); - } - - public transformDirection(m: Matrix4): this { - const x = this._x, y = this._y, z = this._z; - const e = m.elements; - this._x = e[0] * x + e[4] * y + e[8] * z; - this._y = e[1] * x + e[5] * y + e[9] * z; - this._z = e[2] * x + e[6] * y + e[10] * z; - return this.normalize(); - } - - public divide(v: Vector3): this { - this._x /= v.x; - this._y /= v.y; - this._z /= v.z; - this._onChangeCallback(); - return this; - } - - public divideScalar(scalar: number, update?: boolean): this { - return this.multiplyScalar(1 / scalar, update); - } - - public min(v: Vector3): this { - this._x = Math.min(this._x, v.x); - this._y = Math.min(this._y, v.y); - this._z = Math.min(this._z, v.z); - this._onChangeCallback(); - return this; - } - - public max(v: Vector3): this { - this._x = Math.max(this._x, v.x); - this._y = Math.max(this._y, v.y); - this._z = Math.max(this._z, v.z); - this._onChangeCallback(); - return this; - } - - public clamp(min: Vector3, max: Vector3): this { - this._x = Math.max(min.x, Math.min(max.x, this._x)); - this._y = Math.max(min.y, Math.min(max.y, this._y)); - this._z = Math.max(min.z, Math.min(max.z, this._z)); - this._onChangeCallback(); - return this; - } - - public clampScalar(minVal: number, maxVal: number): this { - this._x = Math.max(minVal, Math.min(maxVal, this._x)); - this._y = Math.max(minVal, Math.min(maxVal, this._y)); - this._z = Math.max(minVal, Math.min(maxVal, this._z)); - this._onChangeCallback(); - return this; - } - - public clampLength(min: number, max: number): this { - const length = this.length(); - return this.divideScalar(length || 1, false).multiplyScalar(Math.max(min, Math.min(max, length))); - } - - public floor(): this { - this._x = Math.floor(this._x); - this._y = Math.floor(this._y); - this._z = Math.floor(this._z); - this._onChangeCallback(); - return this; - } - - public ceil(): this { - this._x = Math.ceil(this._x); - this._y = Math.ceil(this._y); - this._z = Math.ceil(this._z); - this._onChangeCallback(); - return this; - } - - public round(): this { - this._x = Math.round(this._x); - this._y = Math.round(this._y); - this._z = Math.round(this._z); - this._onChangeCallback(); - return this; - } - - public roundToZero(): this { - this._x = Math.trunc(this._x); - this._y = Math.trunc(this._y); - this._z = Math.trunc(this._z); - this._onChangeCallback(); - return this; - } - - public negate(): this { - this._x = -this._x; - this._y = -this._y; - this._z = -this._z; - this._onChangeCallback(); - return this; - } - - public dot(v: Vector3): number { - return this._x * v.x + this._y * v.y + this._z * v.z; - } - - public lengthSq(): number { - return this._x * this._x + this._y * this._y + this._z * this._z; - } - - public length(): number { - return Math.sqrt(this._x * this._x + this._y * this._y + this._z * this._z); - } - - public manhattanLength(): number { - return Math.abs(this._x) + Math.abs(this._y) + Math.abs(this._z); - } - - public normalize(update?: boolean): this { - return this.divideScalar(this.length() || 1, update); - } - - public setLength(length: number): this { - return this.normalize(false).multiplyScalar(length); - } - - public lerp(v: Vector3, alpha: number): this { - this._x += (v.x - this._x) * alpha; - this._y += (v.y - this._y) * alpha; - this._z += (v.z - this._z) * alpha; - this._onChangeCallback(); - return this; - } - - public lerpVectors(v1: Vector3, v2: Vector3, alpha: number): this { - this._x = v1.x + (v2.x - v1.x) * alpha; - this._y = v1.y + (v2.y - v1.y) * alpha; - this._z = v1.z + (v2.z - v1.z) * alpha; - this._onChangeCallback(); - return this; - } - - public cross(v: Vector3): this { - return this.crossVectors(this as Vector3, v); - } - - public crossVectors(a: Vector3, b: Vector3): this { - const ax = a.x, ay = a.y, az = a.z; - const bx = b.x, by = b.y, bz = b.z; - this._x = ay * bz - az * by; - this._y = az * bx - ax * bz; - this._z = ax * by - ay * bx; - this._onChangeCallback(); - return this; - } - - public projectOnVector(v: Vector3): this { - const denominator = v.lengthSq(); - if (denominator === 0) return this.set(0, 0, 0); - const scalar = v.dot(this) / denominator; - return this.copy(v, false).multiplyScalar(scalar); - } - - public projectOnPlane(planeNormal: Vector3): this { - _vector.copy(this).projectOnVector(planeNormal); - return this.sub(_vector); - } - - public reflect(normal: Vector3): this { - return this.sub(_vector.copy(normal).multiplyScalar(2 * this.dot(normal))); - } - - public angleTo(v: Vector3): number { - const denominator = Math.sqrt(this.lengthSq() * v.lengthSq()); - if (denominator === 0) return Math.PI / 2; - const theta = this.dot(v) / denominator; - return Math.acos(MathUtils.clamp(theta, -1, 1)); - } - - public distanceTo(v: Vector3): number { - return Math.sqrt(this.distanceToSquared(v)); - } - - public distanceToSquared(v: Vector3): number { - const dx = this._x - v.x, dy = this._y - v.y, dz = this._z - v.z; - return dx * dx + dy * dy + dz * dz; - } - - public manhattanDistanceTo(v: Vector3): number { - return Math.abs(this._x - v.x) + Math.abs(this._y - v.y) + Math.abs(this._z - v.z); - } - - public setFromSpherical(s: Spherical): this { - return this.setFromSphericalCoords(s.radius, s.phi, s.theta); - } - - public setFromSphericalCoords(radius: number, phi: number, theta: number): this { - const sinPhiRadius = Math.sin(phi) * radius; - this._x = sinPhiRadius * Math.sin(theta); - this._y = Math.cos(phi) * radius; - this._z = sinPhiRadius * Math.cos(theta); - this._onChangeCallback(); - return this; - } - - public setFromCylindrical(c: Cylindrical): this { - return this.setFromCylindricalCoords(c.radius, c.theta, c.y); - } - - public setFromCylindricalCoords(radius: number, theta: number, y: number): this { - this._x = radius * Math.sin(theta); - this._y = y; - this._z = radius * Math.cos(theta); - this._onChangeCallback(); - return this; - } - - public setFromMatrixPosition(m: Matrix4): this { - const e = m.elements; - this._x = e[12]; - this._y = e[13]; - this._z = e[14]; - this._onChangeCallback(); - return this; - } - - public setFromMatrixScale(m: Matrix4): this { - const sx = this.setFromMatrixColumn(m, 0).length(); - const sy = this.setFromMatrixColumn(m, 1).length(); - const sz = this.setFromMatrixColumn(m, 2).length(); - this._x = sx; - this._y = sy; - this._z = sz; - this._onChangeCallback(); - return this; - } - - public setFromMatrixColumn(m: Matrix4, index: number): this { - return this.fromArray(m.elements, index * 4); - } - - public setFromMatrix3Column(m: Matrix3, index: number): this { - return this.fromArray(m.elements, index * 3); - } - - public setFromEuler(e: any): this { - this._x = e._x; - this._y = e._y; - this._z = e._z; - this._onChangeCallback(); - return this; - } - - public setFromColor(c: Color): this { - this._x = c.r; - this._y = c.g; - this._z = c.b; - this._onChangeCallback(); - return this; - } - - public equals(v: Vector3): boolean { - return ((v.x === this._x) && (v.y === this._y) && (v.z === this._z)); - } - - public fromArray(array: number[], offset = 0): this { - this._x = array[offset]; - this._y = array[offset + 1]; - this._z = array[offset + 2]; - this._onChangeCallback(); - return this; - } - - public toArray(array?: Vector3Tuple, offset?: 0): Vector3Tuple; - public toArray(array: number[] = [], offset = 0): number[] { - array[offset] = this._x; - array[offset + 1] = this._y; - array[offset + 2] = this._z; - return array; - } - - public fromBufferAttribute(attribute: BufferAttribute, index: number): this { - this._x = attribute.getX(index); - this._y = attribute.getY(index); - this._z = attribute.getZ(index); - this._onChangeCallback(); - return this; - } - - public random(): this { - this._x = Math.random(); - this._y = Math.random(); - this._z = Math.random(); - this._onChangeCallback(); - return this; - } - - public randomDirection(): this { - const theta = Math.random() * Math.PI * 2; - const u = Math.random() * 2 - 1; - const c = Math.sqrt(1 - u * u); - this.x = c * Math.cos(theta); - this.y = u; - this.z = c * Math.sin(theta); - this._onChangeCallback(); - return this; - } - - *[Symbol.iterator](): Generator { - yield this._x; - yield this._y; - yield this._z; - } -} - -Vector3Ext.prototype.isVector3 = true; - -const _vector = new Vector3(); -const _quaternion = new Quaternion(); diff --git a/src/patch/WebGLRenderer.ts b/src/patch/WebGLRenderer.ts index 8d50823f..a250f07f 100644 --- a/src/patch/WebGLRenderer.ts +++ b/src/patch/WebGLRenderer.ts @@ -1,6 +1,6 @@ import { Camera, Scene, Vector4, WebGLRenderer } from 'three'; import { Main } from '../core/Main.js'; -import { ViewportResizeEvent } from '../events/Events.js'; +import { EzViewportResizeEvent } from '../events/Events.js'; import { EventsCache } from '../events/MiscEventsManager.js'; const viewportSize = new Vector4(); @@ -22,7 +22,7 @@ export function applyWebGLRendererPatch(main: Main): void { } function handleViewportResize(renderer: WebGLRenderer, scene: Scene, camera: Camera): void { - let event: ViewportResizeEvent; + let event: EzViewportResizeEvent; if (!lastViewportSizes[scene.id]) { lastViewportSizes[scene.id] = new Vector4(-1); diff --git a/src/patch/prototype/Vector3Ext.ts b/src/patch/prototype/Vector3Ext.ts new file mode 100644 index 00000000..be4c04b3 --- /dev/null +++ b/src/patch/prototype/Vector3Ext.ts @@ -0,0 +1,563 @@ +import type { BufferAttribute, Camera, Color, Cylindrical, Euler, Matrix3, Matrix4, Spherical } from 'three'; +import { MathUtils, Quaternion, Vector3 } from 'three'; + +/** + * @internal + * @lastCommit #30603 + */ +export abstract class Vector3Ext implements Vector3 { + /** @ts-expect-error Property '_x' has no initializer and is not definitely assigned in the constructor */ + public _x: number; + /** @ts-expect-error Property '_y' has no initializer and is not definitely assigned in the constructor */ + public _y: number; + /** @ts-expect-error Property '_z' has no initializer and is not definitely assigned in the constructor */ + public _z: number; + /** @ts-expect-error Property '_onChangeCallback' has no initializer and is not definitely assigned in the constructor */ + public _onChangeCallback: () => void; + /** @ts-expect-error Property 'isVector3' has no initializer and is not definitely assigned in the constructor */ + public isVector3: true; + /** @ts-expect-error Property 'isVector3Ext' has no initializer and is not definitely assigned in the constructor */ + public isVector3Ext: true; + + public get x(): number { return this._x; } + public set x(value: number) { + this._x = value; + this._onChangeCallback(); + } + + public get y(): number { return this._y; } + public set y(value: number) { + this._y = value; + this._onChangeCallback(); + } + + public get z(): number { return this._z; } + public set z(value: number) { + this._z = value; + this._onChangeCallback(); + } + + public set(x: number, y: number, z: number): this { + z ??= this._z; + this._x = x; + this._y = y; + this._z = z; + this._onChangeCallback(); + return this; + } + + public setScalar(scalar: number): this { + this._x = scalar; + this._y = scalar; + this._z = scalar; + this._onChangeCallback(); + return this; + } + + public setX(x: number): this { + this._x = x; + this._onChangeCallback(); + return this; + } + + public setY(y: number): this { + this._y = y; + this._onChangeCallback(); + return this; + } + + public setZ(z: number): this { + this._z = z; + this._onChangeCallback(); + return this; + } + + public setComponent(index: number, value: number): this { + switch (index) { + case 0: + this._x = value; + break; + case 1: + this._y = value; + break; + case 2: + this._z = value; + break; + default: throw new Error('index is out of range: ' + index); + } + + this._onChangeCallback(); + return this; + } + + public getComponent(index: number): number { + switch (index) { + case 0: return this._x; + case 1: return this._y; + case 2: return this._z; + default: throw new Error('index is out of range: ' + index); + } + } + + public clone(): this { + return new Vector3(this._x, this._y, this._z) as this; + } + + public copy(v: Vector3, update?: boolean): this { + this._x = v.x; + this._y = v.y; + this._z = v.z; + if (update !== false) this._onChangeCallback(); + return this; + } + + public add(v: Vector3): this { + this._x += v.x; + this._y += v.y; + this._z += v.z; + this._onChangeCallback(); + return this; + } + + public addScalar(s: number): this { + this._x += s; + this._y += s; + this._z += s; + this._onChangeCallback(); + return this; + } + + public addVectors(a: Vector3, b: Vector3): this { + this._x = a.x + b.x; + this._y = a.y + b.y; + this._z = a.z + b.z; + this._onChangeCallback(); + return this; + } + + public addScaledVector(v: Vector3, s: number): this { + this._x += v.x * s; + this._y += v.y * s; + this._z += v.z * s; + this._onChangeCallback(); + return this; + } + + public sub(v: Vector3): this { + this._x -= v.x; + this._y -= v.y; + this._z -= v.z; + this._onChangeCallback(); + return this; + } + + public subScalar(s: number): this { + this._x -= s; + this._y -= s; + this._z -= s; + this._onChangeCallback(); + return this; + } + + public subVectors(a: Vector3, b: Vector3): this { + this._x = a.x - b.x; + this._y = a.y - b.y; + this._z = a.z - b.z; + this._onChangeCallback(); + return this; + } + + public multiply(v: Vector3): this { + this._x *= v.x; + this._y *= v.y; + this._z *= v.z; + this._onChangeCallback(); + return this; + } + + public multiplyScalar(scalar: number, update?: boolean): this { + this._x *= scalar; + this._y *= scalar; + this._z *= scalar; + if (update !== false) this._onChangeCallback(); + return this; + } + + public multiplyVectors(a: Vector3, b: Vector3): this { + this._x = a.x * b.x; + this._y = a.y * b.y; + this._z = a.z * b.z; + this._onChangeCallback(); + return this; + } + + public applyEuler(euler: Euler): this { + return this.applyQuaternion(_quaternion.setFromEuler(euler)); + } + + public applyAxisAngle(axis: Vector3, angle: number): this { + return this.applyQuaternion(_quaternion.setFromAxisAngle(axis, angle)); + } + + public applyMatrix3(m: Matrix3, update?: boolean): this { + const x = this._x, y = this._y, z = this._z; + const e = m.elements; + this._x = e[0] * x + e[3] * y + e[6] * z; + this._y = e[1] * x + e[4] * y + e[7] * z; + this._z = e[2] * x + e[5] * y + e[8] * z; + if (update !== false) this._onChangeCallback(); + return this; + } + + public applyNormalMatrix(m: Matrix3): this { + return this.applyMatrix3(m, false).normalize(); + } + + public applyMatrix4(m: Matrix4, update?: boolean): this { + const x = this._x, y = this._y, z = this._z; + const e = m.elements; + const w = 1 / (e[3] * x + e[7] * y + e[11] * z + e[15]); + this._x = (e[0] * x + e[4] * y + e[8] * z + e[12]) * w; + this._y = (e[1] * x + e[5] * y + e[9] * z + e[13]) * w; + this._z = (e[2] * x + e[6] * y + e[10] * z + e[14]) * w; + if (update !== false) this._onChangeCallback(); + return this; + } + + public applyQuaternion(q: Quaternion): this { + const vx = this._x, vy = this._y, vz = this._z; + const qx = q.x, qy = q.y, qz = q.z, qw = q.w; + const tx = 2 * (qy * vz - qz * vy); + const ty = 2 * (qz * vx - qx * vz); + const tz = 2 * (qx * vy - qy * vx); + this._x = vx + qw * tx + qy * tz - qz * ty; + this._y = vy + qw * ty + qz * tx - qx * tz; + this._z = vz + qw * tz + qx * ty - qy * tx; + this._onChangeCallback(); + return this; + } + + public project(camera: Camera): this { + return this.applyMatrix4(camera.matrixWorldInverse, false).applyMatrix4(camera.projectionMatrix); + } + + public unproject(camera: Camera): this { + return this.applyMatrix4(camera.projectionMatrixInverse, false).applyMatrix4(camera.matrixWorld); + } + + public transformDirection(m: Matrix4): this { + const x = this._x, y = this._y, z = this._z; + const e = m.elements; + this._x = e[0] * x + e[4] * y + e[8] * z; + this._y = e[1] * x + e[5] * y + e[9] * z; + this._z = e[2] * x + e[6] * y + e[10] * z; + return this.normalize(); + } + + public divide(v: Vector3): this { + this._x /= v.x; + this._y /= v.y; + this._z /= v.z; + this._onChangeCallback(); + return this; + } + + public divideScalar(scalar: number, update?: boolean): this { + return this.multiplyScalar(1 / scalar, update); + } + + public min(v: Vector3): this { + this._x = Math.min(this._x, v.x); + this._y = Math.min(this._y, v.y); + this._z = Math.min(this._z, v.z); + this._onChangeCallback(); + return this; + } + + public max(v: Vector3): this { + this._x = Math.max(this._x, v.x); + this._y = Math.max(this._y, v.y); + this._z = Math.max(this._z, v.z); + this._onChangeCallback(); + return this; + } + + public clamp(min: Vector3, max: Vector3): this { + this._x = clamp(this._x, min.x, max.x); + this._y = clamp(this._y, min.y, max.y); + this._z = clamp(this._z, min.z, max.z); + this._onChangeCallback(); + return this; + } + + public clampScalar(minVal: number, maxVal: number): this { + this._x = clamp(this._x, minVal, maxVal); + this._y = clamp(this._y, minVal, maxVal); + this._z = clamp(this._z, minVal, maxVal); + this._onChangeCallback(); + return this; + } + + public clampLength(min: number, max: number): this { + const length = this.length(); + return this.divideScalar(length || 1, false).multiplyScalar(clamp(length, min, max)); + } + + public floor(): this { + this._x = Math.floor(this._x); + this._y = Math.floor(this._y); + this._z = Math.floor(this._z); + this._onChangeCallback(); + return this; + } + + public ceil(): this { + this._x = Math.ceil(this._x); + this._y = Math.ceil(this._y); + this._z = Math.ceil(this._z); + this._onChangeCallback(); + return this; + } + + public round(): this { + this._x = Math.round(this._x); + this._y = Math.round(this._y); + this._z = Math.round(this._z); + this._onChangeCallback(); + return this; + } + + public roundToZero(): this { + this._x = Math.trunc(this._x); + this._y = Math.trunc(this._y); + this._z = Math.trunc(this._z); + this._onChangeCallback(); + return this; + } + + public negate(): this { + this._x = -this._x; + this._y = -this._y; + this._z = -this._z; + this._onChangeCallback(); + return this; + } + + public dot(v: Vector3): number { + return this._x * v.x + this._y * v.y + this._z * v.z; + } + + public lengthSq(): number { + return this._x * this._x + this._y * this._y + this._z * this._z; + } + + public length(): number { + return Math.sqrt(this._x * this._x + this._y * this._y + this._z * this._z); + } + + public manhattanLength(): number { + return Math.abs(this._x) + Math.abs(this._y) + Math.abs(this._z); + } + + public normalize(update?: boolean): this { + return this.divideScalar(this.length() || 1, update); + } + + public setLength(length: number): this { + return this.normalize(false).multiplyScalar(length); + } + + public lerp(v: Vector3, alpha: number): this { + this._x += (v.x - this._x) * alpha; + this._y += (v.y - this._y) * alpha; + this._z += (v.z - this._z) * alpha; + this._onChangeCallback(); + return this; + } + + public lerpVectors(v1: Vector3, v2: Vector3, alpha: number): this { + this._x = v1.x + (v2.x - v1.x) * alpha; + this._y = v1.y + (v2.y - v1.y) * alpha; + this._z = v1.z + (v2.z - v1.z) * alpha; + this._onChangeCallback(); + return this; + } + + public cross(v: Vector3): this { + return this.crossVectors(this, v); + } + + public crossVectors(a: Vector3, b: Vector3): this { + const ax = a.x, ay = a.y, az = a.z; + const bx = b.x, by = b.y, bz = b.z; + this._x = ay * bz - az * by; + this._y = az * bx - ax * bz; + this._z = ax * by - ay * bx; + this._onChangeCallback(); + return this; + } + + public projectOnVector(v: Vector3): this { + const denominator = v.lengthSq(); + if (denominator === 0) return this.set(0, 0, 0); + const scalar = v.dot(this) / denominator; + return this.copy(v, false).multiplyScalar(scalar); + } + + public projectOnPlane(planeNormal: Vector3): this { + _vector.copy(this).projectOnVector(planeNormal); + return this.sub(_vector); + } + + public reflect(normal: Vector3): this { + return this.sub(_vector.copy(normal).multiplyScalar(2 * this.dot(normal))); + } + + public angleTo(v: Vector3): number { + const denominator = Math.sqrt(this.lengthSq() * v.lengthSq()); + if (denominator === 0) return Math.PI / 2; + const theta = this.dot(v) / denominator; + return Math.acos(clamp(theta, -1, 1)); + } + + public distanceTo(v: Vector3): number { + return Math.sqrt(this.distanceToSquared(v)); + } + + public distanceToSquared(v: Vector3): number { + const dx = this._x - v.x, dy = this._y - v.y, dz = this._z - v.z; + return dx * dx + dy * dy + dz * dz; + } + + public manhattanDistanceTo(v: Vector3): number { + return Math.abs(this._x - v.x) + Math.abs(this._y - v.y) + Math.abs(this._z - v.z); + } + + public setFromSpherical(s: Spherical): this { + return this.setFromSphericalCoords(s.radius, s.phi, s.theta); + } + + public setFromSphericalCoords(radius: number, phi: number, theta: number): this { + const sinPhiRadius = Math.sin(phi) * radius; + this._x = sinPhiRadius * Math.sin(theta); + this._y = Math.cos(phi) * radius; + this._z = sinPhiRadius * Math.cos(theta); + this._onChangeCallback(); + return this; + } + + public setFromCylindrical(c: Cylindrical): this { + return this.setFromCylindricalCoords(c.radius, c.theta, c.y); + } + + public setFromCylindricalCoords(radius: number, theta: number, y: number): this { + this._x = radius * Math.sin(theta); + this._y = y; + this._z = radius * Math.cos(theta); + this._onChangeCallback(); + return this; + } + + public setFromMatrixPosition(m: Matrix4): this { + const e = m.elements; + this._x = e[12]; + this._y = e[13]; + this._z = e[14]; + this._onChangeCallback(); + return this; + } + + public setFromMatrixScale(m: Matrix4): this { + const sx = this.setFromMatrixColumn(m, 0).length(); + const sy = this.setFromMatrixColumn(m, 1).length(); + const sz = this.setFromMatrixColumn(m, 2).length(); + this._x = sx; + this._y = sy; + this._z = sz; + this._onChangeCallback(); + return this; + } + + public setFromMatrixColumn(m: Matrix4, index: number): this { + return this.fromArray(m.elements, index * 4); + } + + public setFromMatrix3Column(m: Matrix3, index: number): this { + return this.fromArray(m.elements, index * 3); + } + + public setFromEuler(e: any): this { + this._x = e._x; + this._y = e._y; + this._z = e._z; + this._onChangeCallback(); + return this; + } + + public setFromColor(c: Color): this { + this._x = c.r; + this._y = c.g; + this._z = c.b; + this._onChangeCallback(); + return this; + } + + public equals(v: Vector3): boolean { + return ((v.x === this._x) && (v.y === this._y) && (v.z === this._z)); + } + + public fromArray(array: number[], offset = 0): this { + this._x = array[offset]; + this._y = array[offset + 1]; + this._z = array[offset + 2]; + this._onChangeCallback(); + return this; + } + + public toArray(array: any = [], offset = 0): any { + array[offset] = this._x; + array[offset + 1] = this._y; + array[offset + 2] = this._z; + return array; + } + + public fromBufferAttribute(attribute: BufferAttribute, index: number): this { + this._x = attribute.getX(index); + this._y = attribute.getY(index); + this._z = attribute.getZ(index); + this._onChangeCallback(); + return this; + } + + public random(): this { + this._x = Math.random(); + this._y = Math.random(); + this._z = Math.random(); + this._onChangeCallback(); + return this; + } + + public randomDirection(): this { + const theta = Math.random() * Math.PI * 2; + const u = Math.random() * 2 - 1; + const c = Math.sqrt(1 - u * u); + this._x = c * Math.cos(theta); + this._y = u; + this._z = c * Math.sin(theta); + this._onChangeCallback(); + return this; + } + + * [Symbol.iterator](): Iterator { + yield this._x; + yield this._y; + yield this._z; + } +} + +Vector3Ext.prototype.isVector3 = true; +Vector3Ext.prototype.isVector3Ext = true; + +const clamp = MathUtils.clamp; +const _vector = new Vector3(); +const _quaternion = new Quaternion(); diff --git a/src/tweening/Actions.ts b/src/tweening/Actions.ts deleted file mode 100644 index 1b93a53d..00000000 --- a/src/tweening/Actions.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { Color, ColorRepresentation, Euler, MathUtils, Quaternion, Vector2, Vector3, Vector4 } from 'three'; -import { Easing, EasingFunction, Easings } from './Easings.js'; -import { RunningAction } from './RunningTween.js'; -import { Tween } from './Tween.js'; - -const easings = new Easings(); -export type Vector = Vector2 | Vector3 | Vector4; -export type AllowedTypes = number | Vector | Quaternion | Euler | ColorRepresentation; -export type Omitype = { [P in keyof T as T[P] extends U ? never : P]: T[P] }; -export type PickType = { [P in keyof T as T[P] extends U ? P : never]: T[P] }; -export type TransformType = { [P in keyof T]: T[P] extends U ? T[P] | V : T[P] }; -export type TransformedTypes = TransformType, Vector, number>; -export type FilteredType = PickType>, AllowedTypes>; -export type Motion = { [key in keyof FilteredType]: FilteredType[key] }; -export type SetMotion = { [key in keyof T]?: T[key] }; - -/** - * Interface for configuring motion animations in a Tween. - * You can specify easing, callback functions, and progress tracking functions. - * @template T - The type of the target object being tweened. - */ -export interface MotionConfig { - /** The easing function to control the animation's progression. */ - easing?: Easing; - /** - * A callback function to execute when the animation completes. - * @param target - The target object that was tweened. - */ - onComplete?: (target: T) => void; - /** - * A callback function to execute when the animation starts. - * @param target - The target object that is being tweened. - */ - onStart?: (target: T) => void; - /** - * A callback function to be executed after each property has been updated. - * @param target - The target object that is being tweened. - */ - onUpdate?: (target: T) => void; - /** - * A callback function to be executed before each property is updated. - * @param target - The target object that is being tweened. - * @param key - The key or property being animated. - * @param start - The initial value of the animated property. - * @param end - The final value of the animated property. - * @param alpha - The current animation progress as a normalized value (0 to 1). - * @returns If `false`, will not assign a new value to the property. - */ - onProgress?: (target: T, key: string, start: AllowedTypes, end: AllowedTypes, alpha: number) => boolean | void; -} - -/** @internal */ -export interface ActionDescriptor { - actions?: RunningAction[]; - tweens?: Tween[]; - config?: MotionConfig; -} - -/** @internal */ -export interface IAction { - init?(target: T): ActionDescriptor; - hasActions: boolean; - isRepeat?: boolean; - isYoyo?: boolean; - isTween?: boolean; - times?: number; -} - -/** @internal */ -export class ActionRepeat implements IAction { - public hasActions = false; - public isRepeat = true; - constructor(public times: number) { } -} - -/** @internal */ -export class ActionYoyo implements IAction { - public hasActions = false; - public isYoyo = true; - constructor(public times: number) { } -} - -/** @internal */ -export class ActionTween implements IAction { - public hasActions = true; - public isTween = true; - public tweens: Tween[] = []; - - constructor(...tweens: Tween[]) { - for (const tween of tweens) { - this.tweens.push(tween.clone()); - } - } -} - -/** @internal */ -export class ActionCallback implements IAction { - public hasActions = true; - constructor(public callback: () => void) { } - - public init(): ActionDescriptor { - return { actions: [{ callback: this.callback, time: 0 }] }; - } -} - -/** @internal */ -export class ActionDelay implements IAction { - public hasActions = true; - constructor(public time: number) { } - - public init(): ActionDescriptor { - return { actions: [{ callback: () => { }, time: this.time }] }; - } -} - -/** @internal */ -export class ActionMotion implements IAction { - public hasActions = true; - constructor(public time: number, public motion: Motion | SetMotion, public config: MotionConfig, public isBy: boolean) { } - - public init(target: T): ActionDescriptor { - const actions: RunningAction[] = []; - for (const key in this.motion) { - if (key === 'easing') continue; - const actionValue = this.motion[key]; - const targetValue = target[key]; - const action = this.vector(key, actionValue as Vector, targetValue as Vector) - ?? this.quaternion(key, actionValue as Quaternion, targetValue as Quaternion) - ?? this.euler(key, actionValue as Euler, targetValue as Euler) - ?? this.color(key, actionValue as Color, targetValue as Color) - ?? this.number(target, key, actionValue as number); - if (action) { - actions.push(action); - } - } - return { actions, config: this.config }; - } - - private getEasing(): EasingFunction { - const easing = this.config?.easing ?? Easings.DEFAULT_EASING; - return typeof easing === 'string' ? (easings[easing].bind(easings) ?? easings.linear) : easing; - } - - private vector(key: string, actionValue: Vector | number, targetValue: Vector): RunningAction { - if (!targetValue) return; - if ((targetValue as Vector2).isVector2 || (targetValue as Vector3).isVector3 || (targetValue as Vector4).isVector4) { - const value = typeof actionValue === 'number' ? targetValue.clone().setScalar(actionValue) : actionValue; - return { - key, - time: this.time, - easing: this.getEasing(), - start: targetValue.clone(), - end: this.isBy ? value.clone().add(targetValue as Vector4) : value, - callback: (start, end, alpha) => { targetValue.lerpVectors(start as Vector4, end as Vector4, alpha); } - }; - } - } - - private quaternion(key: string, actionValue: Quaternion, targetValue: Quaternion): RunningAction { - if (targetValue?.isQuaternion) { - return { - key, - time: this.time, - easing: this.getEasing(), - start: targetValue.clone(), - end: this.isBy ? actionValue.clone().premultiply(targetValue) : actionValue, - callback: (start, end, alpha) => { targetValue.slerpQuaternions(start, end, alpha); } - }; - } - } - - private euler(key: string, actionValue: Euler, targetValue: Euler): RunningAction { - if (targetValue?.isEuler) { - return { - key, - time: this.time, - easing: this.getEasing(), - start: targetValue.clone(), - end: this.isBy ? new Euler(actionValue.x + targetValue.x, actionValue.y + targetValue.y, actionValue.z + targetValue.z) : actionValue, - callback: (start, end, alpha) => { - targetValue.set(MathUtils.lerp(start.x, end.x, alpha), MathUtils.lerp(start.y, end.y, alpha), MathUtils.lerp(start.z, end.z, alpha)); - } - }; - } - } - - private color(key: string, actionValue: ColorRepresentation, targetValue: Color): RunningAction { - if (targetValue?.isColor) { - return { - key, - time: this.time, - easing: this.getEasing(), - start: targetValue.clone(), - end: this.isBy ? new Color(actionValue).add(targetValue) : new Color(actionValue), - callback: (start, end, alpha) => { targetValue.lerpColors(start, end, alpha); } - }; - } - } - - private number(target: T, key: string, actionValue: number): RunningAction { - if (typeof actionValue === 'number') - return { - key, - time: this.time, - easing: this.getEasing(), - start: target[key], - end: this.isBy ? actionValue + target[key] : actionValue, - callback: (start, end, alpha) => { target[key] = MathUtils.lerp(start, end, alpha); } - }; - } -} diff --git a/src/tweening/Easings.ts b/src/tweening/Easings.ts deleted file mode 100644 index 266a4815..00000000 --- a/src/tweening/Easings.ts +++ /dev/null @@ -1,147 +0,0 @@ -/** Type representing an easing function that takes a single numeric parameter and returns a numeric result. */ -export type EasingFunction = (x: number) => number; -/** Type representing an easing type, which can be either a predefined easing function or a custom easing function. */ -export type Easing = keyof Easings | EasingFunction; - -/** - * Class that provides various easing functions for tweening animations. - * For more info on these easing functions, check https://easings.net. - */ -export class Easings { - /** The default easing function used when no easing is specified. */ - public static DEFAULT_EASING: keyof Easings = 'easeInOutExpo'; - - public linear(x: number): number { - return x; - } - - public easeInSine(x: number): number { - return 1 - Math.cos((x * Math.PI) / 2); - } - - public easeOutSine(x: number): number { - return Math.sin((x * Math.PI) / 2); - } - - public easeInOutSine(x: number): number { - return -(Math.cos(Math.PI * x) - 1) / 2; - } - - public easeInQuad(x: number): number { - return x * x; - } - - public easeOutQuad(x: number): number { - return 1 - (1 - x) * (1 - x); - } - - public easeInOutQuad(x: number): number { - return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2; - } - - public easeInCubic(x: number): number { - return x * x * x; - } - - public easeOutCubic(x: number): number { - return 1 - Math.pow(1 - x, 3); - } - - public easeInOutCubic(x: number): number { - return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2; - } - - public easeInQuart(x: number): number { - return x * x * x * x; - } - - public easeOutQuart(x: number): number { - return 1 - Math.pow(1 - x, 4); - } - - public easeInOutQuart(x: number): number { - return x < 0.5 ? 8 * x * x * x * x : 1 - Math.pow(-2 * x + 2, 4) / 2; - } - - public easeInQuint(x: number): number { - return x * x * x * x * x; - } - - public easeOutQuint(x: number): number { - return 1 - Math.pow(1 - x, 5); - } - - public easeInOutQuint(x: number): number { - return x < 0.5 ? 16 * x * x * x * x * x : 1 - Math.pow(-2 * x + 2, 5) / 2; - } - - public easeInExpo(x: number): number { - return x === 0 ? 0 : Math.pow(2, 10 * x - 10); - } - - public easeOutExpo(x: number): number { - return x === 1 ? 1 : 1 - Math.pow(2, -10 * x); - } - - public easeInOutExpo(x: number): number { - return x === 0 ? 0 : x === 1 ? 1 : x < 0.5 ? Math.pow(2, 20 * x - 10) / 2 : (2 - Math.pow(2, -20 * x + 10)) / 2; - } - - public easeInCirc(x: number): number { - return 1 - Math.sqrt(1 - Math.pow(x, 2)); - } - - public easeOutCirc(x: number): number { - return Math.sqrt(1 - Math.pow(x - 1, 2)); - } - - public easeInOutCirc(x: number): number { - return x < 0.5 ? (1 - Math.sqrt(1 - Math.pow(2 * x, 2))) / 2 : (Math.sqrt(1 - Math.pow(-2 * x + 2, 2)) + 1) / 2; - } - - public easeInBack(x: number): number { - return 2.70158 * x * x * x - 1.70158 * x * x; - } - - public easeOutBack(x: number): number { - return 1 + 2.70158 * Math.pow(x - 1, 3) + 1.70158 * Math.pow(x - 1, 2); - } - - public easeInOutBack(x: number): number { - const c2 = 1.70158 * 1.525; - return x < 0.5 ? (Math.pow(2 * x, 2) * ((c2 + 1) * 2 * x - c2)) / 2 : (Math.pow(2 * x - 2, 2) * ((c2 + 1) * (x * 2 - 2) + c2) + 2) / 2; - } - - public easeInElastic(x: number): number { - return x === 0 ? 0 : x === 1 ? 1 : -Math.pow(2, 10 * x - 10) * Math.sin((x * 10 - 10.75) * (2 * Math.PI) / 3); - } - - public easeOutElastic(x: number): number { - return x === 0 ? 0 : x === 1 ? 1 : Math.pow(2, -10 * x) * Math.sin((x * 10 - 0.75) * (2 * Math.PI) / 3) + 1; - } - - public easeInOutElastic(x: number): number { - const c5 = (2 * Math.PI) / 4.5; - return x === 0 ? 0 : x === 1 ? 1 : x < 0.5 ? -(Math.pow(2, 20 * x - 10) * Math.sin((20 * x - 11.125) * c5)) / 2 : (Math.pow(2, -20 * x + 10) * Math.sin((20 * x - 11.125) * c5)) / 2 + 1; - } - - public easeInBounce(x: number): number { - return 1 - this.easeOutBounce(1 - x); - } - - public easeOutBounce(x: number): number { - if (x < 1 / 2.75) { - return 7.5625 * x * x; - } else if (x < 2 / 2.75) { - return 7.5625 * (x -= 1.5 / 2.75) * x + 0.75; - } else if (x < 2.5 / 2.75) { - return 7.5625 * (x -= 2.25 / 2.75) * x + 0.9375; - } else { - return 7.5625 * (x -= 2.625 / 2.75) * x + 0.984375; - } - } - - public easeInOutBounce(x: number): number { - return x < 0.5 ? (1 - this.easeOutBounce(1 - 2 * x)) / 2 : (1 + this.easeOutBounce(2 * x - 1)) / 2; - } -} diff --git a/src/tweening/RunningTween.ts b/src/tweening/RunningTween.ts deleted file mode 100644 index 66f93caf..00000000 --- a/src/tweening/RunningTween.ts +++ /dev/null @@ -1,341 +0,0 @@ -import { ActionTween, MotionConfig } from './Actions.js'; -import { EasingFunction } from './Easings.js'; -import { Tween } from './Tween.js'; -import { TweenManager } from './TweenManager.js'; - -type UpdateCallback = (start?: T, end?: T, alpha?: number) => void; - -interface RunningBlock { - tweens?: Tween[]; - runningTweens?: RunningTween[]; - actions?: RunningAction[]; - elapsedTime: number; - totalTime: number; - reversed?: boolean; - originallyReversed?: boolean; - tweensStarted?: boolean; - config?: MotionConfig; -} - -/** @internal */ -export interface RunningAction { - time: number; - callback: UpdateCallback; - easing?: EasingFunction; - start?: T; - end?: T; - key?: string; -} - -/** - * This class represents a running tween for a specific target object. - * It manages the execution of actions and tweens associated with the tween. - * Don't instantiate this manually. - * @template T - The type of the target object. - */ -export class RunningTween { - /** @internal */ public root: RunningTween; - /** @internal */ public tween: Tween; - /** @internal */ public target: T; - /** @internal */ public actionIndex = -1; - /** @internal */ public currentBlock?: RunningBlock; - /** @internal */ public history: RunningBlock[] = []; - /** @internal */ public reversed?: boolean; - /** @internal */ public originallyReversed?: boolean; - /** @internal */ public repeat?: boolean; - /** @internal */ public repetitions: { [x: number]: number } = {}; - /** @internal */ public _finished = false; - /** - * Indicates whether the execution of the running tween is paused. - * If set to `true`, the tween will not progress until it is resumed. - */ - public paused = false; - /** - * The time scale factor for the running tween. - * It determines the speed at which the tween progresses. - * A value of `1` represents normal speed, while `0.5` would be half-speed, and `2` would be double-speed. - */ - public timeScale = 1; - - /** - * Indicates whether the running tween has finished executing. - */ - public get finished(): boolean { return this._finished; } - - /** - * Don't instantiate this manually. - */ - constructor(target: T, tween: Tween) { - this.target = target; - this.tween = tween; - } - - /** - * Set the time scale for the running tween. - * @param value - The time scale value to apply. - * @returns The updated RunningTween instance. - */ - public setTimeScale(value: number): this { - this.timeScale = value; - return this; - } - - /** - * Pause the execution of the running tween. - */ - public pause(): void { - this.paused = true; - } - - /** - * Resume the execution of the running tween if it was paused. - */ - public resume(): void { - this.paused = false; - } - - /** - * Stop the running tween, causing it to finish immediately. - */ - public stop(): void { - TweenManager.stop(this); - } - - /** - * Complete the running tween, causing it to finish immediately. - */ - public complete(): void { - TweenManager.complete(this); - } - - /** - * Revert the running tween to its initial state (Not implemented yet). - */ - public revert(): void { - console.error('Revert method not implemented yet.'); // handle (!blockHistory) - } - - /** @internal */ - public getBlock(): RunningBlock { - this.currentBlock?.config?.onComplete?.call(this.target, this.target); - const block = this.getCurrentBlock(); - block?.config?.onStart?.call(this.target, this.target); - if (!this.tween.blockHistory && !this.reversed && !this.repeat && block) { - this.history.push(block); - } - this.currentBlock = block; - return block; - } - - /** @internal */ - private getCurrentBlock(): RunningBlock { - if (this.reversed) return this.getPrevBlock(); - return this.repeat ? this.getRepeatBlock() : this.getNextBlock(); - } - - /** @internal */ - private getPrevBlock(): RunningBlock { - if (this.actionIndex > 0) { - const block = this.history[--this.actionIndex]; - block.reversed = !block.originallyReversed; - block.elapsedTime = 0; - block.tweensStarted = false; - return block; - } - } - - /** @internal */ - private getRepeatBlock(): RunningBlock { - if (this.actionIndex < this.history.length - 1) { - const block = this.history[++this.actionIndex]; - block.reversed = block.originallyReversed; - block.elapsedTime = 0; - block.tweensStarted = false; - return block; - } - } - - /** @internal */ - private getNextBlock(): RunningBlock { - while (++this.actionIndex < this.tween.actions.length) { - const action = this.tween.actions[this.actionIndex]; - if (action.isRepeat) { - this.handleRepetition(action.times); - } else if (action.isYoyo) { - const block = this.handleYoyo(action.times); - if (block) return block; - } else if (action.isTween) { - return this.handleTween(action as ActionTween); - } else { - return this.handleMotion(); - } - } - } - - /** @internal */ - private cloneBlock(block: RunningBlock): RunningBlock { - return { - elapsedTime: 0, - totalTime: block.totalTime, - reversed: !block.reversed, - originallyReversed: !block.reversed, - actions: block.actions, - tweens: block.tweens, - config: block.config, - runningTweens: this.cloneRunningTweens(block.runningTweens) - }; - } - - /** @internal */ - private cloneRunningTweens(runningTweens: RunningTween[]): RunningTween[] { - if (!runningTweens) return; - const ret: RunningTween[] = []; - for (const runningTween of runningTweens) { - const runningCloned = new RunningTween(runningTween.target, runningTween.tween); - runningCloned.timeScale = runningTween.timeScale; - runningCloned.root = runningTween.root; - runningCloned.history = runningTween.history; - runningCloned.actionIndex = !runningTween.reversed ? runningTween.history.length : -1; - runningCloned.originallyReversed = !runningTween.reversed; - runningCloned.repeat = runningTween.reversed; - ret.push(runningCloned); - } - return ret; - } - - /** @internal */ - private handleMotion(): RunningBlock { - const descriptor = this.tween.actions[this.actionIndex].init(this.target); - return { - config: descriptor.config, - actions: descriptor.actions, - elapsedTime: 0, - totalTime: Math.max(...descriptor.actions.map((x) => x.time)) - }; - } - - /** @internal */ - private handleTween(action: ActionTween): RunningBlock { - return { - tweens: action.tweens, - elapsedTime: 0, - totalTime: 0 - }; - } - - /** @internal */ - private handleRepetition(times: number): void { - const repeat = this.repetitions; - repeat[this.actionIndex] ??= 0; - if (repeat[this.actionIndex] < times) { - repeat[this.actionIndex]++; - do { - this.actionIndex--; - } while (this.actionIndex > -1 && !this.tween.actions[this.actionIndex].hasActions); - this.actionIndex--; - } - } - - /** @internal */ - private handleYoyo(times: number): RunningBlock { - const repeat = this.repetitions; - repeat[this.actionIndex] ??= 0; - if (repeat[this.actionIndex] < times) { - repeat[this.actionIndex]++; - if (repeat[this.actionIndex--] < 3) { - return this.cloneBlock(this.history[this.history.length - 1]); - } - const block = this.history[this.history.length - 2]; - block.elapsedTime = 0; - block.tweensStarted = false; - return block; - } - } - - /** @internal */ - public execute(delta: number): boolean { - if (this.paused) return true; - delta *= this.timeScale; - do { - delta = Math.min(this.executeBlock(delta), this.getTweensDelta(this.currentBlock)); - } while (delta >= 0 && this.getBlock()); - return delta < 0; - } - - /** @internal */ - private executeBlock(delta: number): number { - const block = this.currentBlock; - block.elapsedTime += delta; - this.executeActions(block); - this.executeTweens(block, delta); - return block.elapsedTime - block.totalTime; - } - - /** @internal */ - private executeActions(block: RunningBlock): void { - if (block.actions) { - for (const action of block.actions) { - const elapsedTime = Math.min(1, block.elapsedTime / (action.time + 10 ** -12)); - const alpha = block.reversed ? 1 - elapsedTime : elapsedTime; - const easedAlpha = action.easing?.call(this, alpha) ?? alpha; - const applyValue = block.config?.onProgress?.call(this.target, this.target, action.key, action.start, action.end, easedAlpha); - if (applyValue !== false) { - action.callback(action.start, action.end, easedAlpha); - } - } - block.config?.onUpdate?.call(this.target, this.target); - } - } - - /** @internal */ - private executeTweens(block: RunningBlock, delta: number): void { - if (block.tweens && !block.tweensStarted) { - if (!block.runningTweens) { - block.runningTweens = []; - for (const tween of block.tweens) { - this.executeTween(block, delta, tween); - } - } else { - for (const runningTween of block.runningTweens) { - runningTween.executeExistingTween(delta, this.reversed); - } - } - block.tweensStarted = true; - } - } - - /** @internal */ - private executeTween(block: RunningBlock, delta: number, tween: Tween): void { - const runningTween = TweenManager.createChildren(this.target, tween, this.root ?? this); - block.runningTweens.push(runningTween); - if (runningTween.execute(delta)) { - TweenManager.addChildren(runningTween); - } - } - - /** @internal */ - private executeExistingTween(delta: number, reversed: boolean): void { - this.reversed = reversed ? !this.originallyReversed : this.originallyReversed; - this.repeat = !this.reversed; - this.actionIndex = this.reversed ? this.history.length : -1; - this.getBlock(); - if (this.execute(delta)) { - TweenManager.addChildren(this); - } - } - - /** @internal */ - public getTweensDelta(block: RunningBlock): number { - let delta = Number.MAX_SAFE_INTEGER; - if (block.runningTweens) { - for (const exTween of block.runningTweens) { - const indexLastBlock = (exTween.repeat || exTween.reversed) ? exTween.actionIndex : exTween.history.length - 1; - const lastBlock = exTween.history[indexLastBlock]; - delta = Math.min(delta, lastBlock.elapsedTime - lastBlock.totalTime, exTween.getTweensDelta(lastBlock)); - } - } - return delta; - } -} - -// TODO write rever function and consider also to create set action diff --git a/src/tweening/Tween.ts b/src/tweening/Tween.ts deleted file mode 100644 index a7c315cf..00000000 --- a/src/tweening/Tween.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { ActionCallback, ActionDelay, ActionMotion, ActionRepeat, ActionTween, ActionYoyo, IAction, Motion, MotionConfig, SetMotion } from './Actions.js'; -import { RunningTween } from './RunningTween.js'; -import { TweenManager } from './TweenManager.js'; - -/** - * A Tween represents a series of actions that can be applied to a target object to create animations or sequences of events. - * @template T - The type of the target object. - */ -export class Tween { - /** @internal */ public actions: IAction[] = []; - /** @internal */ public blockHistory = false; - /** @internal */ public infiniteLoop = false; - /** The object to apply the tween to. */ - public target: T; - /** Tags used for filtering and management. */ - public tags: string[] = []; - /** Unique identifier. If specified, the old tween with the same id will be stopped. */ - public id: string; - - /** - * @param target - The object to apply the tween to. - */ - constructor(target: T) { - this.target = target; - } - - /** - * Set a unique identifier for the Tween. If specified, stops the old tween with the same id. - * @param id The identifier to assign to the Tween. - * @returns The updated Tween instance. - */ - public setId(id: string): this { - this.id = id; - return this; - } - - /** - * Set tags for the Tween, which can be used for filtering and management. - * @param tags - Tags to associate with the Tween. - * @returns The updated Tween instance. - */ - public setTags(...tags: string[]): this { - this.tags = tags; - return this; - } - - /** - * Set the target object for the Tween. - * @param target - The object to apply the tween to. - * @returns The updated Tween instance. - */ - public setTarget(target: T): this { - this.target = target; - return this; - } - - /** - * Define a motion from the current state to a new state over a specified time. - * @param time - The duration of the motion in milliseconds. - * @param action - The motion configuration. - * @param config - Additional motion configuration options. - * @returns The updated Tween instance. - */ - public to(time: number, action: Motion, config?: MotionConfig): this { - this.actions.push(new ActionMotion(time, action, config, false)); - return this; - } - - /** - * Define a relative motion from the current state. - * @param time - The duration of the motion in milliseconds. - * @param action - The motion configuration. - * @param config - Additional motion configuration options. - * @returns The updated Tween instance. - */ - public by(time: number, action: Motion, config?: MotionConfig): this { - this.actions.push(new ActionMotion(time, action, config, true)); - return this; - } - - /** - * Define a movement from the current state to a new state instantaneously. - * @param action - The motion configuration. - * @returns The updated Tween instance. - */ - public set(action: SetMotion): this { - this.actions.push(new ActionMotion(0, action, {}, false)); - return this; - } - - /** - * Add a callback action to the Tween. - * @param callback - The callback function to execute. - * @returns The updated Tween instance. - */ - public call(callback: () => void): this { - this.actions.push(new ActionCallback(callback)); - return this; - } - - /** - * Add a delay before executing the next action. - * @param time - The duration of the delay in milliseconds. - * @returns The updated Tween instance. - */ - public delay(time: number): this { - this.actions.push(new ActionDelay(time)); - return this; - } - - /** - * Repeat the last action for a specific number of times. - * @param times - The number of times to repeat the action. - * @returns The updated Tween instance. - */ - public repeat(times = 1): this { - if (times === Infinity) { - this.blockHistory = true; - this.infiniteLoop = true; - } - if (this.actions[this.actions.length - 1].isRepeat) { - this.actions[this.actions.length - 1].times += times; - } else { - this.actions.push(new ActionRepeat(times)); - } - return this; - } - - /** - * Repeat the last action indefinitely. - * @returns The updated Tween instance. - */ - public repeatForever(): this { - return this.repeat(Infinity); - } - - /** - * Apply a yoyo effect to the last action, making it reverse its motion, for a specific number of times. - * @param times - The number of times to yoyo the last action. - * @returns The updated Tween instance. - */ - public yoyo(times = 1): this { - if (times === Infinity) { - this.infiniteLoop = true; - } - if (this.actions[this.actions.length - 1].isYoyo) { - this.actions[this.actions.length - 1].times += times; - } else { - this.actions.push(new ActionYoyo(times)); - } - return this; - } - - /** - * Apply a yoyo effect to the last action, making it reverse its motion, indefinitely. - * @returns The updated Tween instance. - */ - public yoyoForever(): this { - return this.yoyo(Infinity); - } - - /** - * Chain another Tween to execute after this Tween. - * @param tween - The Tween to chain. - * @returns The updated Tween instance. - */ - public then(tween: Tween): this { - this.actions.push(new ActionTween(tween)); - this.infiniteLoop ||= tween.infiniteLoop; - return this; - } - - /** - * Run multiple Tweens in parallel. - * @param tweens - The Tweens to run in parallel. - * @returns The updated Tween instance. - */ - public parallel(...tweens: Tween[]): this { - this.actions.push(new ActionTween(...tweens)); - this.infiniteLoop ||= tweens.some((x) => x.infiniteLoop); - return this; - } - - /** - * Run multiple Tweens in sequence. - * @param tweens - The Tweens to run in sequence. - * @returns The updated Tween instance. - */ - public sequence(...tweens: Tween[]): this { - for (const tween of tweens) { - this.actions.push(new ActionTween(tween)); - this.infiniteLoop ||= tween.infiniteLoop; - } - return this; - } - - /** - * Chain actions from another Tween to this Tween. - * @param tween - The Tween containing actions to chain. - * @returns The updated Tween instance. - */ - public chain(tween: Tween): this { - this.actions.push(...tween.actions); - this.infiniteLoop ||= tween.infiniteLoop; - return this; - } - - /** - * Clone the Tween instance. - * @returns A new Tween instance with the same configuration. - */ - public clone(): Tween { - const tween = new Tween(this.target); - tween.actions = [...this.actions]; - tween.tags = [...this.tags]; - tween.infiniteLoop = this.infiniteLoop; - return tween; - } - - /** - * Start the Tween and create a RunningTween instance. - * @returns A RunningTween instance that controls the execution of the Tween. - */ - public start(): RunningTween { - if (this.id !== undefined) { - TweenManager.stopById(this.id); - } - return TweenManager.create(this.target, this); - } -} diff --git a/src/tweening/TweenManager.ts b/src/tweening/TweenManager.ts deleted file mode 100644 index 3f3d1d5f..00000000 --- a/src/tweening/TweenManager.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { RunningTween } from './RunningTween.js'; -import { Tween } from './Tween.js'; - -/** - * This class is responsible for managing and controlling running tweens. - */ -export class TweenManager { - private static _running: RunningTween[] = []; - private static _runningChildren: RunningTween[] = []; - - /** @internal */ - public static create(target: T, tween: Tween): RunningTween { - const runningTween = new RunningTween(target, tween); - runningTween.getBlock(); - this._running.push(runningTween); - return runningTween; - } - - /** @internal */ - public static createChildren(target: T, tween: Tween, root: RunningTween): RunningTween { - const runningTween = new RunningTween(target, tween); - runningTween.root = root; - runningTween.getBlock(); - return runningTween; - } - - /** @internal */ - public static addChildren(runningTween: RunningTween): void { - this._runningChildren.push(runningTween); - } - - /** @internal */ - public static update(delta: number): void { - const runningChildren = this._runningChildren; - for (let i = runningChildren.length - 1; i >= 0; i--) { - const rc = runningChildren[i]; - if (!rc.execute(delta * rc.root.timeScale)) { - runningChildren.splice(i, 1); - } - } - - const running = this._running; - for (let i = running.length - 1; i >= 0; i--) { - if (!running[i].execute(delta)) { - running[i]._finished = true; - running.splice(i, 1); - } - } - } - - /** @internal */ - public static stop(runningTween: RunningTween): void { - const index = this._running.indexOf(runningTween); - if (index < 0) return; - this._running.splice(index, 1); - runningTween._finished = true; - runningTween.paused = false; - - const runningChildren = this._runningChildren; - for (let i = runningChildren.length - 1; i >= 0; i--) { - if (runningChildren[i].root === runningTween) { - runningChildren.splice(i, 1); - } - } - } - - /** @internal */ - public static complete(runningTween: RunningTween): void { - const index = this._running.indexOf(runningTween); - if (index < 0) return; - runningTween.paused = false; - - const runningChildren = this._runningChildren; - for (let i = runningChildren.length - 1; i >= 0; i--) { - if (runningChildren[i].root === runningTween && !runningChildren[i].execute(Infinity)) { - runningChildren.splice(i, 1); - } - } - - if (runningTween.tween.infiniteLoop || !runningTween.execute(Infinity)) { - this._running.splice(index, 1); - runningTween._finished = true; - } - } - - /** - * Stop the running tween with a specific id. - * @param id Tween identifier. - */ - public static stopById(id: string): void { - for (let i = this._running.length - 1; i >= 0; i--) { - if (this._running[i].tween.id === id) { - this._running[i].stop(); - return; - } - } - } - - /** - * Stop all running tweens. - */ - public static stopAll(): void { - for (let i = this._running.length - 1; i >= 0; i--) { - this._running[i].stop(); - } - } - - /** - * Stop all running tweens with a specific tag. - * @param tag - The tag to filter running tweens. - */ - public static stopAllByTag(tag: string): void { - for (let i = this._running.length - 1; i >= 0; i--) { - if (this._running[i].tween.tags.indexOf(tag) > -1) { - this._running[i].stop(); - } - } - } - - /** - * Complete all running tweens. - */ - public static completeAll(): void { - for (let i = this._running.length - 1; i >= 0; i--) { - this._running[i].complete(); - } - } - - /** - * Complete all running tweens with a specific tag. - * @param tag - The tag to filter running tweens. - */ - public static completeAllByTag(tag: string): void { - for (let i = this._running.length - 1; i >= 0; i--) { - if (this._running[i].tween.tags.indexOf(tag) > -1) { - this._running[i].complete(); - } - } - } -} diff --git a/src/utils/IntersectionUtils.ts b/src/utils/IntersectionUtils.ts deleted file mode 100644 index 0b1a5d83..00000000 --- a/src/utils/IntersectionUtils.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { Box3, Vector3 } from 'three'; -import { ObjVec3, TEMP, VectorObject3D, VectorUtils } from './VectorUtils.js'; - -/** - * Class that provides a set of utilities for calculating intersections between 2D and 3D geometric objects. - */ -export class IntersectionUtils { - // https://paulbourke.net/geometry/pointlineplane/ - /** - * Computes the intersection between two 2D lines defined by points `a1` and `a2`, and `b1` and `b2`. - * - * @param a1 - The first point of the first line. - * @param a2 - The second point of the first line. - * @param b1 - The first point of the second line. - * @param b2 - The second point of the second line. - * @param target - (Optional) The vector to store the intersection point. If omitted, a new vector will be created. - * @returns The intersection point of the two lines or `undefined` if the lines are parallel. - * - * @see {@link https://paulbourke.net/geometry/pointlineplane/} - */ - public static line_line_2D(a1: VectorObject3D, a2: VectorObject3D, b1: VectorObject3D, b2: VectorObject3D, target = new Vector3()): Vector3 { - const [a1c, a2c, b1c, b2c] = VectorUtils.getPositionsFromObject3D([a1, a2, b1, b2]); - const denominator = (b2c.y - b1c.y) * (a2c.x - a1c.x) - (b2c.x - b1c.x) * (a2c.y - a1c.y); - if (denominator === 0) return; // parallel - const ua = ((b2c.x - b1c.x) * (a1c.y - b1c.y) - (b2c.y - b1c.y) * (a1c.x - b1c.x)) / denominator; - return target.set(a1c.x + ua * (a2c.x - a1c.x), a1c.y + ua * (a2c.y - a1c.y), 0); - } - - /** - * Computes the intersection between two 2D line segments defined by points `a1` and `a2`, and `b1` and `b2`. - * - * @param a1 - The first point of the first segment. - * @param a2 - The second point of the first segment. - * @param b1 - The first point of the second segment. - * @param b2 - The second point of the second segment. - * @param target - (Optional) The vector to store the intersection point. If omitted, a new vector will be created. - * @returns The intersection point of the two segments or `undefined` if the segments do not intersect. - * - * @see {@link https://paulbourke.net/geometry/pointlineplane/} - */ - public static segment_segment_2D(a1: VectorObject3D, a2: VectorObject3D, b1: VectorObject3D, b2: VectorObject3D, target = new Vector3()): Vector3 { - const [a1c, a2c, b1c, b2c] = VectorUtils.getPositionsFromObject3D([a1, a2, b1, b2]); - const denominator = (b2c.y - b1c.y) * (a2c.x - a1c.x) - (b2c.x - b1c.x) * (a2c.y - a1c.y); - if (denominator === 0) return; // parallel - const ua = ((b2c.x - b1c.x) * (a1c.y - b1c.y) - (b2c.y - b1c.y) * (a1c.x - b1c.x)) / denominator; - const ub = ((a2c.x - a1c.x) * (a1c.y - b1c.y) - (a2c.y - a1c.y) * (a1c.x - b1c.x)) / denominator; - if (ua < 0 || ua > 1 || ub < 0 || ub > 1) return; - return target.set(a1c.x + ua * (a2c.x - a1c.x), a1c.y + ua * (a2c.y - a1c.y), 0); - } - - /** - * Computes the intersection between two 3D lines defined by points `a1` and `a2`, and `b1` and `b2`. - * - * @param a1 - The first point of the first line. - * @param a2 - The second point of the first line. - * @param b1 - The first point of the second line. - * @param b2 - The second point of the second line. - * @param target - (Optional) The vector to store the intersection point. If omitted, a new vector will be created. - * @param tolerance - (Optional) The tolerance for evaluating the intersection. The default value is 10^-6. - * @returns The intersection point of the two lines or `undefined` if the lines are parallel or do not intersect. - * - * @see {@link https://paulbourke.net/geometry/pointlineplane/} - */ - public static line_line_3D(a1: ObjVec3, a2: ObjVec3, b1: ObjVec3, b2: ObjVec3, target = new Vector3(), tolerance = 10 ** -6): Vector3 { - const [p1c, p2c, p3c, p4c] = VectorUtils.getPositionsFromObject3D([a1, a2, b1, b2]); - - const p13 = TEMP[0].subVectors(p1c, p3c); - const p43 = TEMP[1].subVectors(p4c, p3c); - - if (p43.lengthSq() < tolerance) return; - const p21 = TEMP[2].subVectors(p2c, p1c); - if (p21.lengthSq() < tolerance) return; - - const d1343 = p13.x * p43.x + p13.y * p43.y + p13.z * p43.z; - const d4321 = p43.x * p21.x + p43.y * p21.y + p43.z * p21.z; - const d1321 = p13.x * p21.x + p13.y * p21.y + p13.z * p21.z; - const d4343 = p43.x * p43.x + p43.y * p43.y + p43.z * p43.z; - const d2121 = p21.x * p21.x + p21.y * p21.y + p21.z * p21.z; - - const denom = d2121 * d4343 - d4321 * d4321; - if (Math.abs(denom) < tolerance) return; - - const numer = d1343 * d4321 - d1321 * d4343; - - const mua = numer / denom; - const Pa = target.set(p1c.x + mua * p21.x, p1c.y + mua * p21.y, p1c.z + mua * p21.z); - - // const mub = (d1343 + d4321 * mua) / d4343; - // const Pb = new Vector3(p3.x + mub * p43.x, p3.y + mub * p43.y, p3.z + mub * p43.z); - - return Pa; - } - - /** - * Checks if a 3D line intersects an Axis-Aligned Bounding Box (AABB) defined by `box`. - * - * @param rayOrigin - The origin of the line. - * @param rayDir - The direction of the line. - * @param box - The AABB to check for intersection with. - * @returns `true` if the line intersects the AABB, otherwise `false`. - */ - public static line_boxAABB(rayOrigin: Vector3, rayDir: Vector3, box: Box3): boolean { - const invdirx = 1 / rayDir.x, invdiry = 1 / rayDir.y, invdirz = 1 / rayDir.z; - let tmin = 0, tmax = Infinity, bmin: number, bmax: number, dmin: number, dmax: number; - - if (invdirx >= 0) { - bmin = box.min.x; - bmax = box.max.x; - } else { - bmin = box.max.x; - bmax = box.min.x; - } - - dmin = (bmin - rayOrigin.x) * invdirx; - dmax = (bmax - rayOrigin.x) * invdirx; - - tmin = dmin > tmin ? dmin : tmin; // in this order ignore NaN error - tmax = dmax < tmax ? dmax : tmax; - - if (invdiry >= 0) { - bmin = box.min.y; - bmax = box.max.y; - } else { - bmin = box.max.y; - bmax = box.min.y; - } - - dmin = (bmin - rayOrigin.y) * invdiry; - dmax = (bmax - rayOrigin.y) * invdiry; - - tmin = dmin > tmin ? dmin : tmin; - tmax = dmax < tmax ? dmax : tmax; - - if (invdirz >= 0) { - bmin = box.min.z; - bmax = box.max.z; - } else { - bmin = box.max.z; - bmax = box.min.z; - } - - dmin = (bmin - rayOrigin.z) * invdirz; - dmax = (bmax - rayOrigin.z) * invdirz; - - tmin = dmin > tmin ? dmin : tmin; - tmax = dmax < tmax ? dmax : tmax; - - return tmin <= tmax; - } - - /** - * Checks if a 3D line segment defined by points `p1` and `p2` intersects an Axis-Aligned Bounding Box (AABB) defined by `box`. - * - * @param p1 - The first point of the segment. - * @param p2 - The second point of the segment. - * @param box - The AABB to check for intersection with. - * @returns `true` if the segment intersects the AABB and is within the segment's length, otherwise `false`. - */ - public static segment_boxAABB(p1: Vector3, p2: Vector3, box: Box3): boolean { - const rayDir = TEMP[0].subVectors(p2, p1).normalize(); - const distance = p1.distanceTo(p2); - const invdirx = 1 / rayDir.x, invdiry = 1 / rayDir.y, invdirz = 1 / rayDir.z; - let tmin = 0, tmax = Infinity, bmin: number, bmax: number, dmin: number, dmax: number; - - if (invdirx >= 0) { - bmin = box.min.x; - bmax = box.max.x; - } else { - bmin = box.max.x; - bmax = box.min.x; - } - - dmin = (bmin - p1.x) * invdirx; - dmax = (bmax - p1.x) * invdirx; - - tmin = dmin > tmin ? dmin : tmin; // in this order ignore NaN error - tmax = dmax < tmax ? dmax : tmax; - - if (invdiry >= 0) { - bmin = box.min.y; - bmax = box.max.y; - } else { - bmin = box.max.y; - bmax = box.min.y; - } - - dmin = (bmin - p1.y) * invdiry; - dmax = (bmax - p1.y) * invdiry; - - tmin = dmin > tmin ? dmin : tmin; - tmax = dmax < tmax ? dmax : tmax; - - if (invdirz >= 0) { - bmin = box.min.z; - bmax = box.max.z; - } else { - bmin = box.max.z; - bmax = box.min.z; - } - - dmin = (bmin - p1.z) * invdirz; - dmax = (bmax - p1.z) * invdirz; - - tmin = dmin > tmin ? dmin : tmin; - tmax = dmax < tmax ? dmax : tmax; - - return tmin <= tmax && distance >= tmin; - } -} diff --git a/src/utils/Query.ts b/src/utils/Query.ts deleted file mode 100644 index 448e2e10..00000000 --- a/src/utils/Query.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { Object3D } from 'three'; - -interface Attribute { - key: string; - value: string; - operator?: string; -} - -interface QueryBlock { - tags: string[]; - attributes: Attribute[]; - type?: string; - recursive?: boolean; - prev?: QueryBlock; - next?: QueryBlock; -} - -interface NewBlockData { - char: string; - end?: number; -} - -/** @internal */ -export function querySelector(target: Object3D, query: string): Object3D { - const queryBlocks = parse(query); - const blocks: QueryBlock[][] = []; - - for (let i = 0; i < queryBlocks.length; i++) { - blocks[i] = [queryBlocks[i]]; - } - - return search(target, blocks); -} - -/** @internal */ -export function querySelectorAll(target: Object3D, query: string): Object3D[] { - const result: Object3D[] = []; - const queryBlocks = parse(query); - const blocks: QueryBlock[][] = []; - - for (let i = 0; i < queryBlocks.length; i++) { - blocks[i] = [queryBlocks[i]]; - } - - searchAll(target, blocks, result); - return result; -} - -function search(target: Object3D, blocks: QueryBlock[][]): Object3D { - const newBlocks: QueryBlock[][] = []; - - for (const blockList of blocks) { - const newBlock: QueryBlock[] = []; - newBlocks.push(newBlock); - - for (const block of blockList) { - if (validateBlock(target, block, newBlock)) return target; - } - - const lastNewBlock = newBlock[newBlock.length - 1]; - if (lastNewBlock === undefined || lastNewBlock.prev !== lastNewBlock) newBlock.push(blockList[blockList.length - 1]); - } - - for (const child of target.children) { - const obj = search(child, newBlocks); - if (obj) return obj; - } -} - -function searchAll(target: Object3D, blocks: QueryBlock[][], result: Object3D[]): void { - const newBlocks: QueryBlock[][] = []; - let added = false; - - for (const blockList of blocks) { - const newBlock: QueryBlock[] = []; - newBlocks.push(newBlock); - - for (const block of blockList) { - if (validateBlock(target, block, newBlock) && !added) { - result.push(target); - if (target.children.length === 0) return; - added = true; - } - } - - const lastNewBlock = newBlock[newBlock.length - 1]; - if (lastNewBlock === undefined || lastNewBlock.prev !== lastNewBlock) newBlock.push(blockList[blockList.length - 1]); - } - - for (const child of target.children) { - searchAll(child, newBlocks, result); - } -} - -function validateBlock(target: Object3D, block: QueryBlock, newBlock: QueryBlock[]): boolean { - if (areValidConditions(target, block)) { - if (block.next) { - newBlock.push(block.next); - } else { - return true; - } - } - return false; -} - -function areValidConditions(target: Object3D, block: QueryBlock): boolean { - return checkType(target, block.type) && checkTags(target, block.tags) && checkAttributes(target, block.attributes); -} - -function checkType(target: Object3D, type: string): boolean { - return !type || target.type === type; -} - -function checkTags(target: Object3D, tags: string[]): boolean { - for (const tag of tags) { - if (!target.tags.has(tag)) return false; - } - return true; -} - -function checkAttributes(target: Object3D, attributes: Attribute[]): boolean { - for (const attribute of attributes) { - switch (attribute.operator) { - case undefined: - if (getValue(target, attribute.key) !== attribute.value) return false; - break; - case '*': - if (!getValue(target, attribute.key).includes(attribute.value)) return false; - break; - case '$': - if (!getValue(target, attribute.key).endsWith(attribute.value)) return false; - break; - case '^': - if (!getValue(target, attribute.key).startsWith(attribute.value)) return false; - break; - } - } - return true; -} - -function getValue(target: Object3D, key: string): string { - const value = target[key]; - return typeof value === 'string' ? value : value?.toString(); -} - -function parse(query: string): QueryBlock[] { - const blocks: QueryBlock[] = []; - let currentBlock: QueryBlock = { attributes: [], tags: [] }; - let end = 0, i = 0; - - blocks.push(currentBlock); - currentBlock.prev = currentBlock; - query = query.trim(); - - while ((i = end) < query.length) { - let char = query[i]; - - const result = getBlock(query, i); - if (result) { - if (result.char === ',') { - currentBlock = { attributes: [], tags: [] }; - blocks.push(currentBlock); - currentBlock.prev = currentBlock; - } else { - const newBlock: QueryBlock = { attributes: [], tags: [], recursive: result.char === ' ' }; - currentBlock.next = newBlock; - newBlock.prev = getPrev(newBlock, currentBlock); - currentBlock = newBlock; - } - - i = result.end; - char = query[i]; - } - - end = getNextIndex(query, i + 1); - - if (char === '.') addTag(query, i, end, currentBlock); - else if (char === '[') addAttribute(query, i, end, currentBlock); - else addType(query, i, end, currentBlock); - } - - return blocks; -} - -function getBlock(query: string, index: number): NewBlockData { - let ret: NewBlockData; - - for (; index < query.length; index++) { - const char = query[index]; - if (char !== ' ' && char !== '>' && char !== ',') break; - if (!ret) { - ret = { char }; - } else if (char !== ' ') { - ret.char = char; - } - } - - if (ret) ret.end = index; - return ret; -} - -function getPrev(newBlock: QueryBlock, oldBlock: QueryBlock): QueryBlock { - if (newBlock.recursive) return newBlock; - while (oldBlock !== oldBlock.prev) { - oldBlock = oldBlock.prev; - } - return oldBlock; -} - -function getNextIndex(query: string, index: number): number { - for (; index < query.length; index++) { - const char = query[index]; - if (char === '.' || char === ' ' || char === '>' || char === '[' || char === ',') break; - } - return index; -} - -function addTag(query: string, start: number, end: number, block: QueryBlock): void { - block.tags.push(query.substring(start + 1, end)); -} - -function addType(query: string, start: number, end: number, block: QueryBlock): void { - block.type = query.substring(start, end); -} - -function addAttribute(query: string, start: number, end: number, block: QueryBlock): void { - const sub = query.substring(start + 1, end - 1); - const split = sub.split('='); - const lastChar = split[0][split[0].length - 1]; - if (lastChar === '*' || lastChar === '$' || lastChar === '^') { - block.attributes.push({ key: split[0].slice(0, -1), value: split[1], operator: lastChar }); - } else { - block.attributes.push({ key: split[0], value: split[1] }); - } -} diff --git a/src/utils/Stats.ts b/src/utils/Stats.ts deleted file mode 100644 index 6d411dd0..00000000 --- a/src/utils/Stats.ts +++ /dev/null @@ -1,342 +0,0 @@ -import { WebGLRenderer } from 'three'; - -export class Stats { - public dom = document.createElement('div'); - private _minimal: boolean; - private mode = 0; - private readonly renderer: WebGLRenderer; - private readonly gl: WebGL2RenderingContext; - private readonly ext: any; - private query: WebGLQuery; - private readonly queries: WebGLQuery[] = []; - private beginTime: number = null; - private endTime: number = null; - private readonly fpsPanel: GraphPanel; - private readonly cpuPanel: GraphPanel; - private readonly gpuPanel: GraphPanel; - private readonly infoPanel: TextPanel; - - private readonly clickPanelCallback = (event): void => { - event.preventDefault(); - this.showPanel(++this.mode % this.dom.children.length); - }; - - public get minimal(): boolean { return this._minimal; } - public set minimal(value: boolean) { this.switchMinimal(value); } - - constructor(renderer: WebGLRenderer) { - this.renderer = renderer; - - this.fpsPanel = this.addPanel(new GraphPanel('FPS', '#0ff', '#002', 1000, false)); - this.cpuPanel = this.addPanel(new GraphPanel('ms CPU', '#0f0', '#020', 100, true)); - - if (renderer) { - this.gl = renderer.getContext() as WebGL2RenderingContext; - this.ext = this.gl?.getExtension('EXT_disjoint_timer_query_webgl2'); - if (this.ext) { - this.gpuPanel = this.addPanel(new GraphPanel('ms GPU', '#ff0', '#220', 100, true)); - } - } - - this.infoPanel = this.addPanel(new TextPanel('INFO', ['Calls', 'Triangles'], '#fff', '#022', 100)); - - this.dom.addEventListener('contextmenu', (e) => { - e.preventDefault(); - this.switchMinimal(!this._minimal); - }, false); - - this.switchMinimal(false); - } - - public switchMinimal(value: boolean): void { - this._minimal = value; - - if (!value) { - this.dom.removeEventListener('click', this.clickPanelCallback); - this.showAllPanels(); - this.dom.style.cssText = 'position:fixed; top:0; left:0; opacity:0.8; z-index:10000; display:flex;'; - return; - } - - this.dom.addEventListener('click', this.clickPanelCallback, false); - this.dom.style.cssText = 'position:fixed; top:0; left:0; opacity:0.8; z-index:10000; cursor:pointer;'; - - this.showPanel(0); - } - - public addPanel(panel: GenericPanel): GenericPanel { - this.dom.appendChild(panel.dom); - return panel; - } - - public showPanel(id: number): void { - this.mode = id; - - for (let i = 0; i < this.dom.children.length; i++) { - (this.dom.children[i] as HTMLElement).style.display = i === id ? 'block' : 'none'; - } - } - - public showAllPanels(): void { - for (const element of this.dom.children) { - (element as HTMLElement).style.display = 'block'; - } - } - - public begin(): void { - this.beginTime = performance.now(); - } - - public beginQuery(): void { - if (!this.gpuPanel) return; - - this.query = this.gl.createQuery(); - this.gl.beginQuery(this.ext.TIME_ELAPSED_EXT, this.query); - } - - public end(): void { - this.endTime = performance.now(); - } - - public endQuery(): void { - if (!this.gpuPanel) return; - - this.gl.endQuery(this.ext.TIME_ELAPSED_EXT); - this.queries.push(this.query); - this.query = null; - } - - public getQueriesTime(): number { - const gl = this.gl; - const ext = this.ext; - let time = 0; - - for (let i = this.queries.length - 1; i >= 0; i--) { - const query = this.queries[i]; - const available = gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE); - const disjoint = gl.getParameter(ext.GPU_DISJOINT_EXT); - - if (available && !disjoint) { - const elapsed = gl.getQueryParameter(query, gl.QUERY_RESULT); - const duration = elapsed * 1e-6; // Convert nanoseconds to milliseconds - time += duration; - gl.deleteQuery(query); - this.queries.splice(i, 1); - } - } - - return time; - } - - public update(): void { - const time = this.endTime; - this.cpuPanel.update(time, time - this.beginTime, 33, 2); // we can pass these two params in the constructor - this.fpsPanel.update(time, 1, 144, 0); - this.gpuPanel?.update(time, this.getQueriesTime(), 33, 2); - this.infoPanel?.update(time, [this.renderer.info.render.calls, this.renderer.info.render.triangles, this.renderer.info.render.points, this.renderer.info.render.lines]); - } -} - -export class GraphPanel { - public dom: HTMLCanvasElement; - private readonly context: CanvasRenderingContext2D; - private prevTime: number = null; - private sum = 0; - private count = 0; - private readonly PR = Math.round(window.devicePixelRatio || 1); - private readonly WIDTH = 90 * this.PR; - private readonly HEIGHT = 48 * this.PR; - private readonly TEXT_X = 3 * this.PR; - private readonly TEXT_Y = 2 * this.PR; - private readonly GRAPH_X = 3 * this.PR; - private readonly GRAPH_Y = 15 * this.PR; - private readonly GRAPH_WIDTH = 84 * this.PR; - private readonly GRAPH_HEIGHT = 30 * this.PR; - - constructor(private readonly name: string, private readonly fg: string, private readonly bg: string, private readonly updateTime: number, private readonly getAverage) { - this.dom = document.createElement('canvas'); - this.dom.width = this.WIDTH; - this.dom.height = this.HEIGHT; - this.dom.style.cssText = 'width:90px;height:48px'; - - this.context = this.dom.getContext('2d'); - this.context.font = 'bold ' + (10 * this.PR) + 'px monospace'; - this.context.textBaseline = 'top'; - - this.context.fillStyle = bg; - this.context.fillRect(0, 0, this.WIDTH, this.HEIGHT); - - this.context.fillStyle = fg; - this.context.fillText(name, this.TEXT_X, this.TEXT_Y + 1 * this.PR); - this.context.fillRect(this.GRAPH_X, this.GRAPH_Y, this.GRAPH_WIDTH, this.GRAPH_HEIGHT); - - this.context.fillStyle = bg; - this.context.globalAlpha = 0.9; - this.context.fillRect(this.GRAPH_X, this.GRAPH_Y, this.GRAPH_WIDTH, this.GRAPH_HEIGHT); - } - - public update(time: number, value: number, maxValue: number, decimals: number): void { - if (this.prevTime === null) { - // first update - this.prevTime = time; - return; - } - - this.sum += value; - this.count++; - - if (time < this.prevTime + this.updateTime) return; - - value = this.getAverage ? this.sum / this.count : this.sum; - this.prevTime += this.updateTime * Math.floor((time - this.prevTime) / this.updateTime); - this.count = 0; - this.sum = 0; - - this.context.fillStyle = this.bg; - this.context.globalAlpha = 1; - this.context.fillRect(0, 0, this.WIDTH, this.GRAPH_Y); - this.context.fillStyle = this.fg; - this.context.fillText(`${value.toFixed(decimals)} ${this.name}`, this.TEXT_X, this.TEXT_Y + 1 * this.PR); - - this.context.drawImage(this.dom, this.GRAPH_X + this.PR, this.GRAPH_Y, this.GRAPH_WIDTH - this.PR, this.GRAPH_HEIGHT, this.GRAPH_X, this.GRAPH_Y, this.GRAPH_WIDTH - this.PR, this.GRAPH_HEIGHT); - this.context.fillRect(this.GRAPH_X + this.GRAPH_WIDTH - this.PR, this.GRAPH_Y, this.PR, this.GRAPH_HEIGHT); - - this.context.fillStyle = this.bg; - this.context.globalAlpha = 0.9; - this.context.fillRect(this.GRAPH_X + this.GRAPH_WIDTH - this.PR, this.GRAPH_Y, this.PR, Math.round((1 - (value / maxValue)) * this.GRAPH_HEIGHT)); - } -} - -export class TextPanel { - public dom: HTMLCanvasElement; - private readonly context: CanvasRenderingContext2D; - private prevTime: number = null; - private sumTriangles = 0; - private sumCalls = 0; - private count = 0; - private readonly PR = Math.round(window.devicePixelRatio || 1); - private WIDTH = 126 * this.PR; - private readonly HEIGHT = 48 * this.PR; - private readonly TEXT_X = 3 * this.PR; - private readonly TEXT_Y = 2 * this.PR; - private readonly GRAPH_X = 3 * this.PR; - private readonly GRAPH_Y = 15 * this.PR; - private GRAPH_WIDTH = this.WIDTH - 6 * this.PR; - private readonly GRAPH_HEIGHT = 30 * this.PR; - private readonly PADDING_V = 4.3 * this.PR; - private readonly PADDING_H = 1 * this.PR; - private readonly TEXT_SPACE = 14 * this.PR; - private COLUMN_SPACE = this.GRAPH_WIDTH / 2; - - // TODO: need to generalize call and triangles in props - constructor(private readonly name: string, private readonly properties: string[], private readonly fg: string, private readonly bg: string, private readonly updateTime: number) { - this.dom = document.createElement('canvas'); - this.dom.width = this.WIDTH; - this.dom.height = this.HEIGHT; - this.dom.style.cssText = `width:${this.WIDTH / this.PR}px;height:${this.HEIGHT / this.PR}px`; - - this.context = this.dom.getContext('2d'); - this.context.font = 'bold ' + (10 * this.PR) + 'px monospace'; - this.context.textBaseline = 'top'; - - this.context.fillStyle = bg; - this.context.fillRect(0, 0, this.WIDTH, this.HEIGHT); - - this.context.fillStyle = fg; - this.context.fillText(this.name, this.TEXT_X, this.TEXT_Y + 1 * this.PR); - this.context.fillRect(this.GRAPH_X, this.GRAPH_Y, this.GRAPH_WIDTH, this.GRAPH_HEIGHT); - - this.context.fillStyle = bg; - this.context.globalAlpha = 0.9; - this.context.fillRect(this.GRAPH_X, this.GRAPH_Y, this.GRAPH_WIDTH, this.GRAPH_HEIGHT); - } - - public update(time: number, [calls, triangles, points, lines]): void { - if (this.prevTime === null) { - this.prevTime = time; - return; - } - - this.sumTriangles += triangles; - this.sumCalls += calls; - this.count++; - - if (time < this.prevTime + this.updateTime) return; - - calls = Math.round(this.sumCalls / this.count); - triangles = Math.round(this.sumTriangles / this.count); - - this.prevTime += this.updateTime * Math.floor((time - this.prevTime) / this.updateTime); - this.count = 0; - this.sumCalls = 0; - this.sumTriangles = 0; - - const stringOut = [ - `Calls ${this._formatNumber(calls)}`, - `Tris ${this._formatNumber(triangles)}`, - `Lines ${this._formatNumber(lines)}`, - `Points ${this._formatNumber(points)}` - ]; - - let maxRowLenght = 0; - for (let i = 0; i < stringOut.length / 2; i++) { - maxRowLenght = Math.max((stringOut[i].length + stringOut[i + 2].length) * 8, maxRowLenght); - } - this.context.clearRect(0, 0, this.WIDTH, this.HEIGHT); - this.WIDTH = maxRowLenght * this.PR; - this.GRAPH_WIDTH = this.WIDTH - 6 * this.PR; - this.COLUMN_SPACE = this.GRAPH_WIDTH / 2; - - this.dom.width = this.WIDTH; - this.dom.height = this.HEIGHT; - this.dom.style.cssText = `width:${this.WIDTH / this.PR}px;height:${this.HEIGHT / this.PR - }px`; - - this.context.font = 'bold ' + 10 * this.PR + 'px monospace'; - this.context.textBaseline = 'top'; - - this.context.fillStyle = this.bg; - this.context.globalAlpha = 1; - this.context.fillRect(0, 0, this.WIDTH, this.HEIGHT); - - this.context.fillStyle = this.fg; - this.context.fillText(this.name, this.TEXT_X, this.TEXT_Y + 1 * this.PR); - this.context.fillRect(this.GRAPH_X, this.GRAPH_Y, this.GRAPH_WIDTH, this.GRAPH_HEIGHT); - - this.context.fillStyle = this.bg; - this.context.globalAlpha = 0.9; - this.context.fillRect(this.GRAPH_X, this.GRAPH_Y, this.GRAPH_WIDTH, this.GRAPH_HEIGHT); - - this.context.fillStyle = this.fg; - - const columns = 2; - const rows = Math.ceil(stringOut.length / columns); - - for (let i = 0; i < stringOut.length; i++) { - const column = Math.floor(i / rows); - const row = i % rows; - this.context.fillText(stringOut[i], this.GRAPH_X + this.PADDING_H + this.COLUMN_SPACE * column, this.GRAPH_Y + this.TEXT_SPACE * row + this.PADDING_V); - } - - this.context.fillStyle = this.bg; - this.context.globalAlpha = 0.9; - this.context.fillRect(this.GRAPH_X + this.GRAPH_WIDTH - this.PR, this.GRAPH_Y, this.PR, (0 * this.GRAPH_HEIGHT)); - } - - /** - * Adds a suffix (B for billion, M for million, K for thousand) to a large number. - * @param {number} input - The numeric value to foment. - * @return {string} The number formatted with an appropriate suffix. - */ - private _formatNumber(input: number): string { - if (input >= 1e9) { - return (input / 1e9).toFixed(2) + 'ʙ'; - } else if (input >= 1e6) { - return (input / 1e6).toFixed(2) + 'ᴍ'; - } else if (input >= 1e3) { - return (input / 1e3).toFixed(2) + 'ᴋ'; - } else { - return input.toString(); - } - } -} diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts deleted file mode 100644 index b9710435..00000000 --- a/src/utils/Utils.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { Camera, Object3D, Plane, Ray, SkinnedMesh, Vector3 } from 'three'; - -/** - * A type definition representing a collection of 3D nodes where each node is identified by a unique string key. - */ -export type Nodes = { [x: string]: Object3D }; - -/** - * - * A utility class providing helper methods for various operations. - */ -export class Utils { - private static readonly _plane = new Plane(); - private static readonly _temp = new Vector3(); - - /** - * Calculates the intersection point of a ray with a plane in world coordinates. - * @param ray - The ray to intersect with the plane. - * @param camera - The camera used as a reference for the plane's orientation. - * @param distance - The distance from the camera to the plane. - * @returns The intersection point as Vector3. - */ - public static getSceneIntersection(ray: Ray, camera: Camera, distance: number): Vector3 { - this._plane.setFromNormalAndCoplanarPoint(camera.getWorldDirection(this._plane.normal), camera.getWorldPosition(this._temp)); - this._plane.translate(this._temp.copy(this._plane.normal).setLength(distance)); - return ray.intersectPlane(this._plane, this._temp); - } - - /** - * Set for all children of the target, the draggable flag to true and a dragTarget. - * @param target - The Object3D whose children you want to enable as draggable elements. - * @param dragTarget - The Object3D that will act as the drag target for the children. - */ - public static setChildrenDragTarget(target: Object3D, dragTarget: Object3D): void { - target.traverse((obj) => { - obj.draggable = true; - obj.dragTarget = dragTarget; - }); - } - - /** - * Computes bounding spheres for child objects within the specified Object3D hierarchy. - * @param target - The root Object3D from which to start computing bounding spheres for children. - */ - public static computeBoundingSphereChildren(target: Object3D): void { - target.traverse((obj) => { - obj.updateMatrixWorld(); - if ((obj as SkinnedMesh).computeBoundingSphere) { - (obj as SkinnedMesh).computeBoundingSphere(); - } - }); - } - - /** - * Retrieves a map of objects in the scene graph (Object3D) starting from a root object. - * Each object is mapped using its unique name as the key in the resulting object. - * @param target - The root object to begin generating the object map from. - * @returns An object containing objects mapped by their names. - */ - public static getNodes(target: Object3D): Nodes { - return this.generateNodesFromObject(target, {}, {}); - } - - protected static generateNodesFromObject(object: Object3D, nodes: Nodes, nameCollision: { [x: string]: number }): Nodes { - const name = this.getNodeName(object, nameCollision); - nodes[name] = object; - - for (const child of object.children) { - this.generateNodesFromObject(child, nodes, nameCollision); - } - - return nodes; - } - - protected static getNodeName(object: Object3D, nameCollision: { [x: string]: number }): string { - const key = object.name; - if (nameCollision[key] === undefined) { - nameCollision[key] = 0; - return key; - } - return `${key}_${++nameCollision[key]}`; - } -} diff --git a/src/utils/VectorUtils.ts b/src/utils/VectorUtils.ts deleted file mode 100644 index 933c75e5..00000000 --- a/src/utils/VectorUtils.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { Object3D, Vector2, Vector3 } from 'three'; - -export type VectorObject3D = Vector2 | Vector3 | Object3D; -export type ObjVec3 = Vector3 | Object3D; -/** @internal */ export const TEMP: Vector3[] = [...Array(4)].map(() => new Vector3()); -const ORIGIN = new Vector3(); - -export class VectorUtils { - public static readonly DEFAULT_NORMAL = new Vector3(0, 0, 1); - - public static getPositionFromObject3D(item: VectorObject3D): Vector3 { - return (item as Object3D).isObject3D ? (item as Object3D).position : (item as Vector3); - } - - public static getPositionsFromObject3D(items: VectorObject3D[]): Vector3[] { - const ret: Vector3[] = []; - for (const item of items) { - ret.push(this.getPositionFromObject3D(item)); - } - return ret; - } - - public static computeSign(point: Vector3, origin: Vector3, normal: Vector3): number { - return Math.sign(TEMP[0].subVectors(point, origin).dot(normal)); - } - - public static haveSameDirection(v1: Vector3, v2: Vector3, tolerance = 10 ** -2): boolean { - return TEMP[0].copy(v1).normalize().dot(TEMP[1].copy(v2).normalize()) > 1 - tolerance; - } - - public static haveOppositeDirection(v1: Vector3, v2: Vector3, tolerance = 10 ** -2): boolean { - return TEMP[0].copy(v1).normalize().dot(TEMP[1].copy(v2).normalize()) < tolerance - 1; - } - - public static perpendicular(dir: Vector3, target = new Vector3(), normal = this.DEFAULT_NORMAL): Vector3 { - return target.crossVectors(dir, normal); - } - - public static perpendicularSigned(dir: Vector3, referencePoint: Vector3, target = new Vector3(), normal = this.DEFAULT_NORMAL): Vector3 { - target.crossVectors(dir, normal); - return this.computeSign(referencePoint, ORIGIN, target) !== 1 ? target : target.multiplyScalar(-1); - } - - public static perpendicularByPoints(p1: ObjVec3, p2: ObjVec3, target = new Vector3(), normal = this.DEFAULT_NORMAL): Vector3 { - const [p1c, p2c] = this.getPositionsFromObject3D([p1, p2]); - return target.crossVectors(TEMP[0].subVectors(p1c, p2c), normal); - } - - public static perpendicularSignedByPoints(p1: ObjVec3, p2: ObjVec3, refPoint: ObjVec3, target = new Vector3(), normal = this.DEFAULT_NORMAL): Vector3 { - const [p1c, p2c, r] = this.getPositionsFromObject3D([p1, p2, refPoint]); - target.crossVectors(TEMP[0].subVectors(p1c, p2c), normal); - return this.computeSign(r, p1c, target) !== 1 ? target : target.multiplyScalar(-1); - } - - public static bisector(v1: Vector3, v2: Vector3, target = new Vector3()): Vector3 { - TEMP[0].copy(v1).normalize(); - TEMP[1].copy(v2).normalize(); - return target.addVectors(TEMP[0], TEMP[1]).normalize(); - } - - public static bisectorByPoints(p1: ObjVec3, p2: ObjVec3, center: ObjVec3, target = new Vector3()): Vector3 { - const [p1c, p2c, c] = this.getPositionsFromObject3D([p1, p2, center]); - return this.bisector(TEMP[2].subVectors(p1c, c), TEMP[3].subVectors(p2c, c), target); - } - - public static arePointsOnSameSide(origin: ObjVec3, dir: Vector3, points: ObjVec3[]): boolean { - const [o, ...p] = this.getPositionsFromObject3D([origin, ...points]); - const sign = this.computeSign(p[0], o, dir); - for (let i = 1; i < points.length; i++) { - if (sign !== this.computeSign(p[i], o, dir)) return false; - } - return true; - } - - public static arePointsOnSameSideByPoints(p1: ObjVec3, p2: ObjVec3, points: ObjVec3[], normal = this.DEFAULT_NORMAL): boolean { - const [p1c, p2c, ...p] = this.getPositionsFromObject3D([p1, p2, ...points]); - const dir = this.perpendicularByPoints(p1c, p2c, TEMP[0], normal); - const sign = this.computeSign(p[0], p1c, dir); - for (let i = 1; i < points.length; i++) { - if (sign !== this.computeSign(p[i], p1c, dir)) return false; - } - return true; - } - - // normal must be normalized - public static angleSignedFromOrigin(a: Vector3, b: Vector3, normal = this.DEFAULT_NORMAL): number { - return Math.atan2(TEMP[0].crossVectors(a, b).dot(normal), a.dot(b)); - } - - public static angleSignedByPoints(p1: ObjVec3, p2: ObjVec3, center: ObjVec3, normal = this.DEFAULT_NORMAL): number { - const [p1c, p2c, c] = this.getPositionsFromObject3D([p1, p2, center]); - const a = TEMP[0].subVectors(p1c, c); - const b = TEMP[1].subVectors(p2c, c); - return Math.atan2(TEMP[2].crossVectors(a, b).dot(normal), a.dot(b)); - } - - public static projectOnLine(point: ObjVec3, l1: ObjVec3, l2: ObjVec3, target = new Vector3()): Vector3 { - const [vc, p1c, p2c] = this.getPositionsFromObject3D([point, l1, l2]); - return target.subVectors(vc, p1c).projectOnVector(TEMP[0].subVectors(p1c, p2c)).add(p1c); - } -} diff --git a/tsconfig.json b/tsconfig.json index c3299857..4809c73e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,18 +1,25 @@ { - "compilerOptions": { - "target": "ES2020", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "strict": true, - "declaration": true, - "sourceMap": true, - "declarationMap": true, - "noImplicitOverride": true, - "strictNullChecks": false, - "stripInternal": true, - "outDir": "dist/src", - "esModuleInterop": true, - "noImplicitAny": false, - "skipLibCheck": true - } + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noErrorTruncation": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "outDir": "dist/src", + "paths": { + "@three.ez/main": [ + "./src/index.js" + ] + }, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "stripInternal": true, + "target": "ES2020" + } } \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 9edb9f63..50d145d3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,6 +5,11 @@ import { externalizeDeps } from 'vite-plugin-externalize-deps'; export default defineConfig(({ command }) => ({ publicDir: command === 'build' ? false : 'public', + resolve: { + alias: { + '@three.ez/main': resolve(__dirname, 'src/index.ts') + } + }, build: { sourcemap: true, lib: {