refactor(web): drops ComplexGestureSource

This commit is contained in:
Joshua A. Horton 2023-08-03 08:36:13 +07:00
parent d8fabfa7b1
commit 3f3dec5c02
5 changed files with 35 additions and 163 deletions

View file

@ -1,115 +0,0 @@
import EventEmitter from "eventemitter3";
import { SerializedSimpleGestureSource, SimpleGestureSource } from "./simpleGestureSource.js";
/**
* Documents the expected typing of serialized versions of the `ComplexGestureSource` class.
*/
export interface SerializedComplexGestureSource {
touchpoints: SerializedSimpleGestureSource[];
// gesture: Gesture;
}
interface EventMap<Type> {
'newcontact': (contact: SimpleGestureSource<Type>) => void;
'end': () => void;
'cancel': () => void;
}
/**
* Models all ongoing contact that is considered part of the same single gesture
* or sequence of chained Gestures over time. This may or may not involve
* multiple touch contact points / "SimpleGestureSource" instances.
*
* Note that multiple chained gestures may arise over the lifetime of a single
* instance of this class. For example, detecting a multitap requires
* multiple contact points over time, possibly with each tap arising as a
* potential 'last' tap gesture before new ones are received to continue the
* sequence.
*
* _Supported events_:
*
* `'cancel'`: all gesture recognition for this input is to be cancelled
* and left incomplete.
* - Provides no parameters.
*
* `'end'`: all gesture recognition for this input is to be resolved.
* - Provides no parameters.
*/
export class ComplexGestureSource<HoveredItemType> extends EventEmitter<EventMap<HoveredItemType>> {
public readonly touchpoints: SimpleGestureSource<HoveredItemType>[];
// --- Future design aspects ---
// private _gesture: Gesture;
// public get gesture() { return this._gesture };
private isActive = true;
constructor(basePoint: SimpleGestureSource<HoveredItemType>) {
super();
this.touchpoints = [ basePoint ];
this._attachPointHooks(basePoint);
}
private _attachPointHooks(touchpoint: SimpleGestureSource<HoveredItemType>) {
touchpoint.path.on('complete', () => {
this.isActive = false;
this.emit('end');
this.removeAllListeners();
});
touchpoint.path.on('invalidated', () => {
this.isActive = false;
this.emit('cancel');
this.removeAllListeners();
})
}
addTouchpoint(touchpoint: SimpleGestureSource<HoveredItemType>) {
this.touchpoints.push(touchpoint);
this._attachPointHooks(touchpoint);
}
cancel() {
if(this.isActive) {
for(let point of this.touchpoints) {
point.terminate(true);
}
}
}
end() {
if(this.isActive) {
for(let point of this.touchpoints) {
point.terminate(false);
}
}
}
public get hasSyncedPaths(): boolean {
if(this.touchpoints.length <= 1) {
return true;
} else {
const timestamp = this.touchpoints[0].currentSample.t;
for(let i=1; i < this.touchpoints.length; i++) {
if(this.touchpoints[i].currentSample.t != timestamp) {
return false;
}
}
return true;
}
}
/**
* Creates a serialization-friendly version of this instance for use by
* `JSON.stringify`.
*/
toJSON(): SerializedComplexGestureSource {
return {
touchpoints: this.touchpoints.map((point) => point.toJSON())
};
}
}

View file

@ -1,4 +1,3 @@
import { ComplexGestureSource } from "../../complexGestureSource.js";
import { SimpleGestureSource, SimpleGestureSourceSubview } from "../../simpleGestureSource.js";
import { GestureModel, GestureResolution, GestureResolutionSpec, RejectionDefault, ResolutionItemSpec } from "../specs/gestureModel.js";
@ -26,26 +25,26 @@ export class GestureMatcher<Type> {
private readonly publishedPromise: ManagedPromise<MatchResult<Type>>; // unsure on the actual typing at the moment.
private _result: MatchResult<Type>;
private baseSource: ComplexGestureSource<Type>;
private baseSources: SimpleGestureSource<Type>[];
public get promise() {
return this.publishedPromise.corePromise;
}
constructor(model: GestureModel<Type>, sourceObj: ComplexGestureSource<Type> | GestureMatcher<Type>) {
constructor(model: GestureModel<Type>, sourceObj: SimpleGestureSource<Type> | GestureMatcher<Type>) {
/* c8 ignore next 5 */
if(!model || !sourceObj) {
throw new Error("Construction of GestureMatcher requires a gesture-model spec and a source for related contact points.");
} else if(!model.sustainTimer && sourceObj instanceof ComplexGestureSource && sourceObj.touchpoints.length == 0) {
} else if(!model.sustainTimer && !(sourceObj)) {
throw new Error("If the provided gesture-model spec lacks a sustain timer, there must be an active contact point.");
}
// We condition on ComplexGestureSource since some unit tests mock the other type without
// instantiating the actual type.
const predecessor = sourceObj instanceof ComplexGestureSource<Type> ? null : sourceObj;
const source = predecessor ? null : (sourceObj as ComplexGestureSource<Type>);
const predecessor = sourceObj instanceof SimpleGestureSource<Type> ? null : sourceObj;
const source = predecessor ? null : (sourceObj as SimpleGestureSource<Type>);
this.baseSource = predecessor?.baseSource || source;
this.baseSources = predecessor?.baseSources || [source];
this.predecessor = predecessor;
this.publishedPromise = new ManagedPromise();
@ -62,7 +61,7 @@ export class GestureMatcher<Type> {
this.pathMatchers = [];
const sourceTouchpoints: SimpleGestureSource<Type>[] = source
? source.touchpoints
? [ source ]
: predecessor.pathMatchers.map((matcher) => matcher.source);
let offset = 0;
@ -264,6 +263,7 @@ export class GestureMatcher<Type> {
}
}
this.baseSources.push(simpleSource);
this.addContactInternal(simpleSource.constructSubview(false, true));
}

View file

@ -1,7 +1,6 @@
import EventEmitter from "eventemitter3";
import { InputEngineBase } from "./inputEngineBase.js";
import { ComplexGestureSource } from "./complexGestureSource.js";
import { SimpleGestureSource } from "./simpleGestureSource.js";
import { SimpleGestureSource, SimpleGestureSourceSubview } from "./simpleGestureSource.js";
interface EventMap<HoveredItemType> {
/**
@ -9,7 +8,7 @@ interface EventMap<HoveredItemType> {
* @param input
* @returns
*/
'inputstart': (input: ComplexGestureSource<HoveredItemType>) => void;
'inputstart': (input: SimpleGestureSource<HoveredItemType>) => void;
}
/**
@ -23,8 +22,7 @@ interface EventMap<HoveredItemType> {
export class TouchpointCoordinator<HoveredItemType> extends EventEmitter<EventMap<HoveredItemType>> {
private inputEngines: InputEngineBase<HoveredItemType>[];
private _activeSourcesMap: {[id: string]: ComplexGestureSource<HoveredItemType>} = {};
private _activeSources: ComplexGestureSource<HoveredItemType>[] = [];
private _activeSources: SimpleGestureSource<HoveredItemType>[] = [];
public constructor() {
super();
@ -36,59 +34,42 @@ export class TouchpointCoordinator<HoveredItemType> extends EventEmitter<EventMa
this.inputEngines.push(engine);
}
private readonly onNewTrackedPath = (touchpoint: SimpleGestureSource<HoveredItemType>) => {
private readonly onNewTrackedPath = async (touchpoint: SimpleGestureSource<HoveredItemType>) => {
this.addSimpleSourceHooks(touchpoint);
// ... stuff.
// ... stuff
// If no active ComplexGestureSource entries may match the incoming touchpoint, we have a new
// ComplexGestureSource.
const newInput = this.establishNewComplexSource(touchpoint);
this.emit('inputstart', newInput);
return false;
this.emit('inputstart', touchpoint);
}
private doGestureUpdate(source: ComplexGestureSource<HoveredItemType>) {
private doGestureUpdate(source: SimpleGestureSource<HoveredItemType>) {
// Should probably ensure data-updates for multi-contact gestures are synchronized
// before proceeding. Single-contact cases are inherently synchronized, of course.
//
// Should a gesture type have geometric requirements on the current location of active
// touchpaths, having a desync during a quick movement could cause the calculated
// distance between the locations to be markedly different than expected.
if(!source.hasSyncedPaths) {
return;
}
// TODO: stuff.
// TODO: stuff, including synchronization. Probably do that on the caller,
// rather than here?
}
private addSimpleSourceHooks(touchpoint: SimpleGestureSource<HoveredItemType>) {
// It will be possible for this._activeInputs[touchpoint.identifier] to change during certain
// gestures, so use that - within each handler - for lookups rather than the current `newInput`.
// ----------
touchpoint.path.on('step', () => this.doGestureUpdate(this._activeSourcesMap[touchpoint.identifier]));
touchpoint.path.on('step', () => this.doGestureUpdate(touchpoint));
touchpoint.path.on('invalidated', () => {
// TODO: on cancellation, is there any other cleanup to be done?
// Also mark the touchpoint as no longer active.
delete this._activeSourcesMap[touchpoint.identifier];
let i = this._activeSources.indexOf(touchpoint);
this._activeSources = this._activeSources.splice(i, 1);
});
touchpoint.path.on('complete', () => {
// TODO: on cancellation, is there any other cleanup to be done?
// Also mark the touchpoint as no longer active.
delete this._activeSourcesMap[touchpoint.identifier];
let i = this._activeSources.indexOf(touchpoint);
this._activeSources = this._activeSources.splice(i, 1);
});
}
private establishNewComplexSource(touchpoint: SimpleGestureSource<HoveredItemType>) {
const newInput = new ComplexGestureSource<HoveredItemType>(touchpoint);
this._activeSourcesMap[touchpoint.identifier] = newInput;
this._activeSources.push(newInput);
return newInput;
}
}

View file

@ -4,7 +4,6 @@ export { GestureRecognizer } from "./gestureRecognizer.js";
export { GestureRecognizerConfiguration } from "./configuration/gestureRecognizerConfiguration.js";
export { InputEngineBase } from "./headless/inputEngineBase.js";
export { InputSample } from "./headless/inputSample.js";
export { SerializedComplexGestureSource, ComplexGestureSource } from "./headless/complexGestureSource.js";
export { SerializedGesturePath, GesturePath } from "./headless/gesturePath.js";
export { SerializedSimpleGestureSource, SimpleGestureSource } from "./headless/simpleGestureSource.js";
export { MouseEventEngine } from "./mouseEventEngine.js";

View file

@ -94,10 +94,11 @@ export class TouchEventEngine<HoveredItemType> extends InputEventEngine<HoveredI
// Ensure the same timestamp is used for all touches being updated.
const timestamp = performance.now();
// Do not change to `changedTouches` - we need a sample for all active touches in order
// to facilitate path-update synchronization for multi-touch gestures.
for(let i=0; i < event.touches.length; i++) {
const touch = event.touches.item(i);
// During a touch-start, only _new_ touch contact points are listed here;
// we shouldn't signal "input start" for any previously-existing touch points,
// so `.changedTouches` is the best way forward.
for(let i=0; i < event.changedTouches.length; i++) {
const touch = event.changedTouches.item(i);
const sample = this.buildSampleFromTouch(touch, timestamp);
if(!ZoneBoundaryChecker.inputStartOutOfBoundsCheck(sample, this.config)) {
@ -118,8 +119,12 @@ export class TouchEventEngine<HoveredItemType> extends InputEventEngine<HoveredI
// Ensure the same timestamp is used for all touches being updated.
const timestamp = performance.now();
for(let i=0; i < event.changedTouches.length; i++) {
const touch = event.changedTouches.item(i);
// Do not change to `changedTouches` - we need a sample for all active touches in order
// to facilitate path-update synchronization for multi-touch gestures.
//
// May be worth doing changedTouches _first_ though.
for(let i=0; i < event.touches.length; i++) {
const touch = event.touches.item(i);
if(!this.hasActiveTouchpoint(touch.identifier)) {
continue;
@ -142,6 +147,8 @@ export class TouchEventEngine<HoveredItemType> extends InputEventEngine<HoveredI
onTouchEnd(event: TouchEvent) {
let propagationActive = true;
// Only lists touch contact points that have been lifted; touchmove is raised separately if any movement occurred.
for(let i=0; i < event.changedTouches.length; i++) {
const touch = event.changedTouches.item(i);