@polygonjs/polygonjs
Version:
node-based WebGL 3D engine https://polygonjs.com
178 lines (165 loc) • 6.26 kB
text/typescript
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);
}
}