spiegel-keyman/common/web/gesture-recognizer/src/engine/headless/gesturePath.ts
2023-07-25 14:44:39 +07:00

176 lines
No EOL
5.3 KiB
TypeScript

import EventEmitter from "eventemitter3";
import { InputSample } from "./inputSample.js";
import { CumulativePathStats } from "./cumulativePathStats.js";
/**
* Documents the expected typing of serialized versions of the `GesturePath` class.
*/
export type SerializedGesturePath<Type> = {
coords: InputSample<Type>[]; // ensures type match with public class property.
wasCancelled?: boolean;
}
interface EventMap<Type> {
'step': (sample: InputSample<Type>) => void,
'complete': () => void,
'invalidated': () => void
}
/**
* Models the path over time through coordinate space taken by a touchpoint during
* its active lifetime.
*
* _Supported events_:
*
* `'step'`: a new Event has been observed for this touchpoint, extending the path.
* - Parameters:
* - `sample: InputSample` - the coordinate & timestamp of the new observation.
*
* `'complete'`: the touchpoint is no longer active; a touch-end has been observed.
* - Provides no parameters.
* - Will be the last event raised by its instance, after any final 'segmentation'
* events.
* - Still precedes resolution Promise fulfillment on the `Segment` provided by
* the most recently-preceding 'segmentation' event.
* - And possibly recognition Promise fulfillment.
*
* `'invalidated'`: the touchpoint is no longer active; the path has crossed
* gesture-recognition boundaries and is no longer considered valid.
* - Provides no parameters.
* - Will precede the final 'segmentation' event for the 'end' segment
* - Will precede resolution Promise fulfillment on the `Segment` provided by
* the most recently-preceding 'segmentation' event.
* - And possibly recognition Promise fulfillment.
*/
export class GesturePath<Type> extends EventEmitter<EventMap<Type>> {
private samples: InputSample<Type>[] = [];
private _isComplete: boolean = false;
private _wasCancelled?: boolean;
private _stats: CumulativePathStats<Type>;
public get stats() {
// Is (practically) immutable, so it's safe to expose the instance directly.
return this._stats;
}
/**
* Initializes an empty path intended for tracking a newly-activated touchpoint.
*/
constructor() {
super();
this._stats = new CumulativePathStats();
}
/**
* Deserializes a GesturePath instance from its corresponding JSON.parse() object.
* @param jsonObj
*/
static deserialize<Type>(jsonObj: SerializedGesturePath<Type>): GesturePath<Type> {
const instance = new GesturePath<Type>();
instance.samples = [].concat(jsonObj.coords.map((obj) => ({...obj} as InputSample<Type>)));
instance._isComplete = true;
instance._wasCancelled = jsonObj.wasCancelled;
let stats = instance.samples.reduce((stats: CumulativePathStats<Type>, sample) => stats.extend(sample), new CumulativePathStats<Type>());
instance._stats = stats;
return instance;
}
/**
* Indicates whether or not the corresponding touchpoint is no longer active -
* either due to cancellation or by the user's direct release of the touchpoint.
*/
public get isComplete() {
return this._isComplete;
}
public get wasCancelled() {
return this._wasCancelled;
}
/**
* Extends the path with a newly-observed coordinate.
* @param sample
*/
extend(sample: InputSample<Type>) {
/* c8 ignore next 3 */
if(this._isComplete) {
throw new Error("Invalid state: this GesturePath has already terminated.");
}
// The tracked path should emit InputSample events before Segment events and
// resolution of Segment Promises.
this.samples.push(sample);
this._stats = this._stats.extend(sample);
this.emit('step', sample);
}
/**
* Finalizes the path.
* @param cancel Whether or not this finalization should trigger cancellation.
*/
terminate(cancel: boolean = false) {
/* c8 ignore next 3 */
if(this._isComplete) {
throw new Error("Invalid state: this GesturePath has already terminated.");
}
this._wasCancelled = cancel;
this._isComplete = true;
// If cancelling, do so before finishing segments
if(cancel) {
this.emit('invalidated');
}
// If not cancelling, signal completion after finishing segments.
if(!cancel) {
this.emit('complete');
}
this.removeAllListeners();
}
/**
* Returns all coordinate + timestamp pairings observed for the corresponding
* touchpoint's path over its lifetime thus far.
*/
public get coords(): readonly InputSample<Type>[] {
return this.samples;
}
/**
* Creates a serialization-friendly version of this instance for use by
* `JSON.stringify`.
*/
toJSON() {
let jsonClone: SerializedGesturePath<Type> = {
// Replicate array and its entries, but with certain fields of each entry missing.
// No .clientX, no .clientY.
coords: [].concat(this.samples.map((obj) => ({
targetX: obj.targetX,
targetY: obj.targetY,
t: obj.t,
item: obj.item
}))),
wasCancelled: this._wasCancelled
}
// Removes components of each sample that we don't want serialized.
for(let sample of jsonClone.coords) {
delete sample.clientX;
delete sample.clientY;
// No point in serializing an `undefined` 'item' entry.
if(sample.item === undefined) {
delete sample.item;
}
}
return jsonClone;
}
}