mirror of
https://github.com/keymanapp/keyman.git
synced 2026-09-27 18:17:40 +00:00
287 lines
No EOL
7.8 KiB
TypeScript
287 lines
No EOL
7.8 KiB
TypeScript
import { CumulativePathStats } from "./cumulativePathStats.js";
|
|
import { InputSample } from "./inputSample.js";
|
|
import { type SegmentClass } from "./segmentClassifier.js";
|
|
|
|
export interface JSONSegment {
|
|
type: SegmentClass,
|
|
duration: number,
|
|
distance: number,
|
|
speed: number,
|
|
peakSpeed: number
|
|
angle: number,
|
|
cardinalDirection: string,
|
|
initialCoord: InputSample,
|
|
lastCoord: InputSample
|
|
}
|
|
|
|
/**
|
|
* Denotes one "segment" of the ongoing touchpath. These segments are then utilized as the basis
|
|
* for detecting and synthesizing gestures.
|
|
*
|
|
* By first breaking the touchpath into gestures, we gain the ability to define something of a
|
|
* finite-state-machine (FSM) model for each type of gesture we wish to support. While technically
|
|
* possible to do even without this step... we find that this makes such a goal far less complex.
|
|
*/
|
|
export class Segment {
|
|
/**
|
|
* Denotes the highest (mean + 1-sigma) speed seen among all subsegments comprising
|
|
* this segment.
|
|
*
|
|
* This is likely not the maximum speed observed, as that observation is likely an outlier
|
|
* and is not internally recorded. We rebuild this from stats observations.
|
|
*
|
|
* Think of it as acting like a "smoothed peak speed".
|
|
*/
|
|
private _peakSpeed: number;
|
|
|
|
private _type?: SegmentClass;
|
|
|
|
private _stats: CumulativePathStats | JSONSegment;
|
|
|
|
private _recognitionPromise: Promise<SegmentClass>;
|
|
private _recognitionPromiseResolver: (type: SegmentClass | PromiseLike<SegmentClass>) => void;
|
|
|
|
private _resolutionPromise: Promise<void>;
|
|
private _resolutionPromiseResolver: () => void;
|
|
|
|
private _isResolved: boolean = false;
|
|
|
|
/**
|
|
* Intended for internal-use only; this is utilized during the segmentation process.
|
|
*/
|
|
public constructor();
|
|
/**
|
|
* Reconstructs an instance based upon a previously-serialized `.toJSON()` call on this
|
|
* object.
|
|
* @param serializedObj
|
|
*/
|
|
public constructor(serializedObj: JSONSegment);
|
|
public constructor(serializedObj?: JSONSegment) {
|
|
if(serializedObj) {
|
|
// We'll use the loaded object as our core reference.
|
|
this._stats = serializedObj;
|
|
this._peakSpeed = serializedObj.peakSpeed; // Is retrieved from `this`, not `this._stats`.
|
|
this._type = serializedObj.type; // This one too.
|
|
|
|
// Ensure that the two promises are properly initialized for this construction type.
|
|
this._recognitionPromise = Promise.resolve(serializedObj.type);
|
|
this._resolutionPromise = Promise.resolve();
|
|
} else {
|
|
this._peakSpeed = 0;
|
|
|
|
this._recognitionPromise = new Promise<SegmentClass>((resolve) => {
|
|
this._recognitionPromiseResolver = resolve;
|
|
});
|
|
|
|
this._resolutionPromise = new Promise<void>((resolve) => {
|
|
this._resolutionPromiseResolver = () => {
|
|
this._isResolved = true;
|
|
resolve();
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Indicates the likely "highest sustained" speed observed within the `Segment`'s path.
|
|
*
|
|
* To be more precise, the mean + 1-sigma (average + one standard deviation) of the speed
|
|
* for the fastest component of the `Segment`.
|
|
*/
|
|
get peakSpeed(): number {
|
|
return this._peakSpeed;
|
|
}
|
|
|
|
protected setPeakSpeed(speed: number) {
|
|
if(this._isResolved) {
|
|
throw new Error("May not modify a resolved segment!");
|
|
}
|
|
this._peakSpeed = speed;
|
|
}
|
|
|
|
protected updateStats(totalStats: CumulativePathStats) {
|
|
if(this._isResolved) {
|
|
throw new Error("May not modify a resolved segment!");
|
|
}
|
|
this._stats = totalStats;
|
|
}
|
|
|
|
protected classifyType(type: SegmentClass) {
|
|
if(!this._stats) {
|
|
throw new Error("Cannot recognize the segment - lacking critical metadata");
|
|
}
|
|
|
|
if(this._isResolved) {
|
|
throw new Error("May not modify a resolved segment!");
|
|
}
|
|
|
|
if(this._type === undefined) {
|
|
this._type = type;
|
|
|
|
this._recognitionPromiseResolver(type);
|
|
} else if(this._type != type) {
|
|
throw new Error("May not change segment type once set!");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The starting point of this touchpath segment.
|
|
*/
|
|
get initialCoord(): InputSample {
|
|
if(this._stats instanceof CumulativePathStats) {
|
|
return this._stats.initialSample;
|
|
} else {
|
|
return this._stats.initialCoord;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The ending point of this touchpath segment.
|
|
*/
|
|
get lastCoord(): InputSample {
|
|
if(this._stats instanceof CumulativePathStats) {
|
|
return this._stats.lastSample;
|
|
} else {
|
|
return this._stats.lastCoord;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The duration of this touchpath segment, measured in ms.
|
|
*/
|
|
get duration(): number {
|
|
return this._stats.duration;
|
|
}
|
|
|
|
/**
|
|
* The net distance traveled by this touchpath segment. In other words,
|
|
* the distance between `initialCoord` and `lastCoord` (ignoring time).
|
|
*/
|
|
get distance(): number {
|
|
if(this._stats instanceof CumulativePathStats) {
|
|
return this._stats.netDistance;
|
|
} else {
|
|
return this._stats.distance;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The average speed of this touchpath segment - distance / duration.
|
|
*/
|
|
get speed(): number {
|
|
return this._stats.speed;
|
|
};
|
|
|
|
/**
|
|
* The angle traveled by this touchpath segment. Measured in radians.
|
|
*/
|
|
get angle(): number {
|
|
return this._stats.angle;
|
|
};
|
|
|
|
/**
|
|
* The 'directional bucket' for the detected angle. This will
|
|
* correspond to a cardinal or an intercardinal ('n', 'ne', etc.)
|
|
*/
|
|
get direction(): string {
|
|
return this._stats.cardinalDirection;
|
|
};
|
|
|
|
/**
|
|
* The classification of this Segment: 'start', 'hold', 'move', or 'end'.
|
|
*
|
|
* May be null if the Segment has not yet been `recognized`.
|
|
*
|
|
* @see `SegmentClass`
|
|
*/
|
|
get type(): SegmentClass {
|
|
return this._type || null;
|
|
}
|
|
|
|
protected _isRecognized(): boolean {
|
|
return this._type !== undefined;
|
|
}
|
|
|
|
/**
|
|
* A `Promise` that resolves when the segment's classification is determined
|
|
* - when we "recognize" its role in the touchpath.
|
|
*/
|
|
get whenRecognized(): Promise<SegmentClass> {
|
|
return this._recognitionPromise;
|
|
}
|
|
|
|
/**
|
|
* A `Promise` that resolves when the segment and its role within the touchpath
|
|
* are fully determined, as segmentation has noted the need for a new boundary -
|
|
* and thus, distinct and different behavior after the endpoint of this segment
|
|
* of the touchpath.
|
|
*/
|
|
get whenResolved(): Promise<void> {
|
|
return this._resolutionPromise;
|
|
}
|
|
|
|
protected resolve() {
|
|
if(!this._stats) {
|
|
throw new Error("Cannot resolve the segment - illegal state!");
|
|
}
|
|
this._resolutionPromiseResolver();
|
|
}
|
|
|
|
/**
|
|
* Provides a human-readable yet serialization-friendly version of this instance.
|
|
* @returns
|
|
*/
|
|
public toJSON(): JSONSegment {
|
|
const cleanSample: (sample: InputSample) => InputSample = (sample) => {
|
|
const clone = {... sample};
|
|
delete clone.clientX;
|
|
delete clone.clientY;
|
|
|
|
return clone;
|
|
}
|
|
|
|
return {
|
|
type: this.type,
|
|
duration: this.duration,
|
|
cardinalDirection: this.direction,
|
|
speed: this.speed,
|
|
distance: this.distance,
|
|
angle: this.angle,
|
|
peakSpeed: this.peakSpeed,
|
|
initialCoord: cleanSample(this.initialCoord),
|
|
lastCoord: cleanSample(this.lastCoord)
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Provides an editable typing for Segment utilized during the segmentation process;
|
|
* casting to the superclass provides an uneditable view into the Segment instead.
|
|
*
|
|
* This is intended for internal use only.
|
|
*/
|
|
export class SegmentImplementation extends Segment {
|
|
constructor() {
|
|
super();
|
|
}
|
|
|
|
public setPeakSpeed(speed: number) {
|
|
super.setPeakSpeed(speed);
|
|
}
|
|
|
|
public updateStats(stats: CumulativePathStats) {
|
|
super.updateStats(stats);
|
|
}
|
|
|
|
public classifyType(type: SegmentClass) {
|
|
super.classifyType(type);
|
|
}
|
|
|
|
public resolve() {
|
|
super.resolve();
|
|
}
|
|
|
|
public get isRecognized() {
|
|
return super._isRecognized();
|
|
}
|
|
} |