UNPKG

@polygonjs/polygonjs

Version:

node-based WebGL 3D engine https://polygonjs.com

178 lines (165 loc) 6.26 kB
import {Object3D, Vector2} from 'three'; import {Constructor, ConvertToStrings} from '../../../../../types/GlobalTypes'; import {radToDeg} from '../../../../../core/math/_Module'; import {BaseRayObjectIntersectionsController} from './_BaseRayObjectIntersectionsController'; import { ObjectOptions, GPUOptions, CPUOptions, PriorityOptions, filterObjectsWithMatchButtonConfig, ButtonAndModifierOptions, ButtonAndModifierOptionsAsString, ButtonConfig, buttonConfigFromEvent, propertyMatchesButtonConfig, } from './Common'; import {ParamConfig} from '../../../../nodes/utils/params/ParamsConfig'; import {MouseButton} from '../../../../../core/MouseButton'; interface SwipeOptions { angle: number; angleMargin: number; minDistance: number; callback: () => void; } export interface ObjectToSwipeOptions extends ObjectOptions { swipe: SwipeOptions; config: ButtonAndModifierOptions; } export interface ObjectToSwipeOptionsAsString { priority: ConvertToStrings<PriorityOptions>; cpu?: ConvertToStrings<CPUOptions>; gpu?: ConvertToStrings<GPUOptions>; swipe: ConvertToStrings<SwipeOptions>; config: ButtonAndModifierOptionsAsString; } const _tmp = new Vector2(); const _buttonConfig: ButtonConfig = {button: MouseButton.LEFT, ctrl: false, shift: false, alt: false}; function degAngle(radians: number) { if (radians > Math.PI) { radians -= Math.PI * 2; } return radToDeg(radians); } export const ANGLE_DEGREES = { LEFT: degAngle(_tmp.set(-1, 0).angle()), RIGHT: degAngle(_tmp.set(1, 0).angle()), UP: degAngle(_tmp.set(0, 1).angle()), DOWN: degAngle(_tmp.set(0, -1).angle()), }; const DEFAULT_MIN_CURSOR_MOVE_DISTANCE = 0.05; export function SwipeParamConfig<TBase extends Constructor>(Base: TBase) { return class Mixin extends Base { /** @param angle */ angle = ParamConfig.FLOAT(0, { range: [-180, 180], rangeLocked: [true, false], }); /** @param angle margin */ angleMargin = ParamConfig.FLOAT(45, { range: [0, 180], rangeLocked: [true, false], }); /** @param min distance */ minDistance = ParamConfig.FLOAT(DEFAULT_MIN_CURSOR_MOVE_DISTANCE, { range: [0, 1], rangeLocked: [true, false], }); }; } function optionsContainsAngle(options: SwipeOptions, angle: number) { return angle >= options.angle - options.angleMargin && angle <= options.angle + options.angleMargin; } const _cursorDelta = new Vector2(); export class RayObjectIntersectionsSwipeController extends BaseRayObjectIntersectionsController { protected override _propertiesListByObject: Map<Object3D, ObjectToSwipeOptions[]> = new Map(); protected _intersectedStateOnPointerdownByObject: Map<Object3D, boolean> = new Map(); protected _intersectedStateOnPointerupByObject: Map<Object3D, boolean> = new Map(); private _objectsMatchingEventConfig: Object3D[] = []; private _objectsIntersectedOnPointerdown: Object3D[] = []; private _cursorOnPointerdown = new Vector2(); private _cursorOnPointerup = new Vector2(); private _pointerdownEvent: Readonly<PointerEvent | MouseEvent | TouchEvent> | undefined; private _bound = { pointerup: this._onPointerup.bind(this), }; onPointerdown(event: Readonly<PointerEvent | MouseEvent | TouchEvent>) { this._pointerdownEvent = event; if (this._objects.length == 0) { return; } filterObjectsWithMatchButtonConfig( event, this._objects, this._propertiesListByObject, this._objectsMatchingEventConfig ); if (this._objectsMatchingEventConfig.length == 0) { return; } document.addEventListener('pointerup', this._bound.pointerup); // we also need touchend, as pointerup appears to not be triggered once the cursor has moved document.addEventListener('touchend', this._bound.pointerup); this._setIntersectedState(this._objectsMatchingEventConfig, this._intersectedStateOnPointerdownByObject); this._getCursor(this._cursorOnPointerdown); } private _onPointerup(_event: Event) { document.removeEventListener('pointerup', this._bound.pointerup); document.removeEventListener('touchend', this._bound.pointerup); const event = this._pointerdownEvent; if (!event) { return; } this._pointerdownEvent = undefined; const objects = this._objects; this._objectsIntersectedOnPointerdown.length = 0; for (const object of objects) { const propertiesList = this._propertiesListByObject.get(object); if (propertiesList) { const isIntersectingOnPointerdown = this._intersectedStateOnPointerdownByObject.get(object); if (isIntersectingOnPointerdown) { this._objectsIntersectedOnPointerdown.push(object); } } } // I've considered using only the objects intersected in pointerdown for the pointerup detection, // but that prevents missed objects from occluding others if they are in front. // We therefore need to run the detection on all objects // instead, we can still optimise by not detecting anything if that list is empty if (this._objectsIntersectedOnPointerdown.length == 0) { return; } // check swipe angle this._getCursor(this._cursorOnPointerup); _cursorDelta.copy(this._cursorOnPointerup).sub(this._cursorOnPointerdown); let radians = _cursorDelta.angle(); const degrees = degAngle(radians); const distance = _cursorDelta.manhattanLength(); // this._setIntersectedState(objects, this._intersectedStateOnPointerupByObject); buttonConfigFromEvent(event, _buttonConfig); const objectsIntersectedOnPointerdown = this._objectsIntersectedOnPointerdown; for (const object of objectsIntersectedOnPointerdown) { const propertiesList = this._propertiesListByObject.get(object); if (propertiesList) { const isIntersectingOnPointerup = this._intersectedStateOnPointerupByObject.get(object); if (isIntersectingOnPointerup == true) { for (const properties of propertiesList) { if ( distance > properties.swipe.minDistance && optionsContainsAngle(properties.swipe, degrees) && propertyMatchesButtonConfig(properties.config, _buttonConfig) ) { properties.swipe.callback(); } } } } } } private _getCursor(target: Vector2) { const pointerEventsController = this._scene.eventsDispatcher.pointerEventsController; const cursor = pointerEventsController.cursor().value; target.copy(cursor); } }