mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-09 10:25:32 +00:00
refactor(web): drops ComplexGestureSource
This commit is contained in:
parent
d8fabfa7b1
commit
3f3dec5c02
5 changed files with 35 additions and 163 deletions
|
|
@ -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())
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue