Merge pull request #9526 from keymanapp/feat/web/config-shifting-and-source-rigor

feat(web): allows shifting the recognition zone of an active GestureSource 🐵
This commit is contained in:
Joshua Horton 2023-09-27 14:14:30 +07:00 committed by GitHub
commit ca6b4f90fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 427 additions and 109 deletions

View file

@ -3,6 +3,9 @@
// after configuration.
import { InputSample } from "../headless/inputSample.js";
import { Mutable } from "../mutable.js";
import { Nonoptional } from "../nonoptional.js";
import { PaddedZoneSource } from "./paddedZoneSource.js";
import { RecognitionZoneSource } from "./recognitionZoneSource.js";
// For example, customization of a longpress timer's length need not be readonly.
@ -81,4 +84,34 @@ export interface GestureRecognizerConfiguration<HoveredItemType> {
* @returns
*/
readonly itemIdentifier?: (coord: Omit<InputSample<any>, 'item'>, target: EventTarget) => HoveredItemType;
}
export function preprocessRecognizerConfig<HoveredItemType>(
config: GestureRecognizerConfiguration<HoveredItemType>
): Nonoptional<GestureRecognizerConfiguration<HoveredItemType>> {
// Allows configuration pre-processing during this method.
let processingConfig: Mutable<Nonoptional<GestureRecognizerConfiguration<HoveredItemType>>> = {...config} as Nonoptional<GestureRecognizerConfiguration<HoveredItemType>>;
processingConfig.mouseEventRoot = processingConfig.mouseEventRoot ?? processingConfig.targetRoot;
processingConfig.touchEventRoot = processingConfig.touchEventRoot ?? processingConfig.targetRoot;
processingConfig.inputStartBounds = processingConfig.inputStartBounds ?? processingConfig.targetRoot;
processingConfig.maxRoamingBounds = processingConfig.maxRoamingBounds ?? processingConfig.targetRoot;
processingConfig.safeBounds = processingConfig.safeBounds ?? new PaddedZoneSource([2]);
processingConfig.itemIdentifier = processingConfig.itemIdentifier ?? (() => null);
if(!config.paddedSafeBounds) {
let paddingArray = config.safeBoundPadding;
if(typeof paddingArray == 'number') {
paddingArray = [ paddingArray ];
}
paddingArray = paddingArray ?? [3];
processingConfig.paddedSafeBounds = new PaddedZoneSource(processingConfig.safeBounds, paddingArray);
} else {
// processingConfig.paddedSafeBounds is already set via the spread operator above.
delete processingConfig.safeBoundPadding;
}
return processingConfig;
}

View file

@ -1,8 +1,6 @@
import { GestureRecognizerConfiguration } from "./configuration/gestureRecognizerConfiguration.js";
import { GestureRecognizerConfiguration, preprocessRecognizerConfig } from "./configuration/gestureRecognizerConfiguration.js";
import { MouseEventEngine } from "./mouseEventEngine.js";
import { Mutable } from "./mutable.js";
import { Nonoptional } from "./nonoptional.js";
import { PaddedZoneSource } from "./configuration/paddedZoneSource.js";
import { TouchEventEngine } from "./touchEventEngine.js";
import { TouchpointCoordinator } from "./headless/touchpointCoordinator.js";
@ -12,39 +10,9 @@ export class GestureRecognizer<HoveredItemType> extends TouchpointCoordinator<Ho
private readonly mouseEngine: MouseEventEngine<HoveredItemType>;
private readonly touchEngine: TouchEventEngine<HoveredItemType>;
protected static preprocessConfig<HoveredItemType>(
config: GestureRecognizerConfiguration<HoveredItemType>
): Nonoptional<GestureRecognizerConfiguration<HoveredItemType>> {
// Allows configuration pre-processing during this method.
let processingConfig: Mutable<Nonoptional<GestureRecognizerConfiguration<HoveredItemType>>> = {...config} as Nonoptional<GestureRecognizerConfiguration<HoveredItemType>>;
processingConfig.mouseEventRoot = processingConfig.mouseEventRoot ?? processingConfig.targetRoot;
processingConfig.touchEventRoot = processingConfig.touchEventRoot ?? processingConfig.targetRoot;
processingConfig.inputStartBounds = processingConfig.inputStartBounds ?? processingConfig.targetRoot;
processingConfig.maxRoamingBounds = processingConfig.maxRoamingBounds ?? processingConfig.targetRoot;
processingConfig.safeBounds = processingConfig.safeBounds ?? new PaddedZoneSource([2]);
processingConfig.itemIdentifier = processingConfig.itemIdentifier ?? (() => null);
if(!config.paddedSafeBounds) {
let paddingArray = config.safeBoundPadding;
if(typeof paddingArray == 'number') {
paddingArray = [ paddingArray ];
}
paddingArray = paddingArray ?? [3];
processingConfig.paddedSafeBounds = new PaddedZoneSource(processingConfig.safeBounds, paddingArray);
} else {
// processingConfig.paddedSafeBounds is already set via the spread operator above.
delete processingConfig.safeBoundPadding;
}
return processingConfig;
}
public constructor(config: GestureRecognizerConfiguration<HoveredItemType>) {
super();
this.config = GestureRecognizer.preprocessConfig(config);
this.config = preprocessRecognizerConfig(config);
this.mouseEngine = new MouseEventEngine<HoveredItemType>(this.config);
this.touchEngine = new TouchEventEngine<HoveredItemType>(this.config);

View file

@ -1,5 +1,7 @@
import { InputSample } from "./inputSample.js";
import { SerializedGesturePath, GesturePath } from "./gesturePath.js";
import { GestureRecognizerConfiguration, preprocessRecognizerConfig } from "../configuration/gestureRecognizerConfiguration.js";
import { Nonoptional } from "../nonoptional.js";
/**
* Documents the expected typing of serialized versions of the `GestureSource` class.
@ -47,6 +49,9 @@ export class GestureSource<HoveredItemType> {
private static _jsonIdSeed: -1;
// Assertion: must always contain an index 0 - the base recognizer config.
protected recognizerConfigStack: Nonoptional<GestureRecognizerConfiguration<HoveredItemType>>[];
/**
* Tracks the coordinates and timestamps of each update for the lifetime of this `GestureSource`.
*/
@ -60,10 +65,16 @@ export class GestureSource<HoveredItemType> {
* @param initialHoveredItem The initiating event's original target element
* @param isFromTouch `true` if sourced from a `TouchEvent`; `false` otherwise.
*/
constructor(identifier: number, isFromTouch: boolean) {
constructor(
identifier: number,
recognizerConfig: Nonoptional<GestureRecognizerConfiguration<HoveredItemType>> | Nonoptional<GestureRecognizerConfiguration<HoveredItemType>>[],
isFromTouch: boolean
) {
this.rawIdentifier = identifier;
this.isFromTouch = isFromTouch;
this._path = new GesturePath();
this.recognizerConfigStack = Array.isArray(recognizerConfig) ? recognizerConfig : [recognizerConfig];
}
/**
@ -76,7 +87,7 @@ export class GestureSource<HoveredItemType> {
const isFromTouch = jsonObj.isFromTouch;
const path = GesturePath.deserialize(jsonObj.path);
const instance = new GestureSource(id, isFromTouch);
const instance = new GestureSource(id, null, isFromTouch);
instance._path = path;
return instance;
}
@ -112,7 +123,7 @@ export class GestureSource<HoveredItemType> {
* @returns
*/
public constructSubview(startAtEnd: boolean, preserveBaseItem: boolean): GestureSourceSubview<HoveredItemType> {
return new GestureSourceSubview(this, startAtEnd, preserveBaseItem);
return new GestureSourceSubview(this, this.recognizerConfigStack, startAtEnd, preserveBaseItem);
}
/**
@ -142,10 +153,31 @@ export class GestureSource<HoveredItemType> {
return `${prefix}:${this.rawIdentifier}`;
}
public pushRecognizerConfig(config: Omit<GestureRecognizerConfiguration<HoveredItemType>, 'touchEventRoot'| 'mouseEventRoot'>) {
const configToProcess = {...config,
mouseEventRoot: this.recognizerConfigStack[0].mouseEventRoot,
touchEventRoot: this.recognizerConfigStack[0].touchEventRoot
}
this.recognizerConfigStack.push(preprocessRecognizerConfig(configToProcess));
}
public popRecognizerConfig() {
if(this.recognizerConfigStack.length == 1) {
throw new Error("Cannot 'pop' the original recognizer-configuration for this GestureSource.")
}
return this.recognizerConfigStack.pop();
}
public get currentRecognizerConfig() {
return this.recognizerConfigStack[this.recognizerConfigStack.length-1];
}
/**
* Creates a serialization-friendly version of this instance for use by
* `JSON.stringify`.
*/
/* c8 ignore start */
toJSON(): SerializedGestureSource {
let jsonClone: SerializedGestureSource = {
isFromTouch: this.isFromTouch,
@ -153,11 +185,15 @@ export class GestureSource<HoveredItemType> {
}
return jsonClone;
/* c8 ignore stop */
/* c8 ignore next 2 */
// esbuild or tsc seems to mangle the 'ignore stop' if put outside the ending brace.
}
}
export class GestureSourceSubview<HoveredItemType> extends GestureSource<HoveredItemType> {
private _baseSource: GestureSource<HoveredItemType>
private _baseStartIndex: number;
private subviewDisconnector: () => void;
/**
@ -167,11 +203,41 @@ export class GestureSourceSubview<HoveredItemType> extends GestureSource<Hovered
* @param initialHoveredItem The initiating event's original target element
* @param isFromTouch `true` if sourced from a `TouchEvent`; `false` otherwise.
*/
constructor(source: GestureSource<HoveredItemType>, startAtEnd: boolean, preserveBaseItem: boolean) {
super(source.rawIdentifier, source.isFromTouch);
constructor(
source: GestureSource<HoveredItemType>,
configStack: typeof GestureSource.prototype['recognizerConfigStack'],
startAtEnd: boolean,
preserveBaseItem: boolean
) {
let mayUpdate = true;
let start = 0;
let length = source.path.coords.length;
if(source instanceof GestureSourceSubview) {
start = source._baseStartIndex;
const expectedLength = start + length;
// Check against the full remaining length of the original source; does
// the subview provided to us include its source's most recent point?
const sampleCountSinceStart = source.baseSource.path.coords.length;
if(expectedLength != start + sampleCountSinceStart) {
mayUpdate = false;
}
}
super(source.rawIdentifier, configStack, source.isFromTouch);
const baseSource = this._baseSource = source instanceof GestureSourceSubview ? source._baseSource : source;
/**
* Provides a coordinate-system translation for source subviews.
* The base version still needs to use the original coord system, though.
*/
const translateSample = (sample: InputSample<HoveredItemType>) => {
const translation = this.recognizerTranslation;
// Provide a coordinate-system translation for source subviews.
// The base version still needs to use the original coord system, though.
return {...sample, targetX: sample.targetX - translation.x, targetY: sample.targetY - translation.y};
}
// Note: we don't particularly need subviews to track the actual coords aside from
// tracking related stats data. But... we don't have an "off-switch" for that yet.
let subpath: GesturePath<HoveredItemType>;
@ -179,13 +245,18 @@ export class GestureSourceSubview<HoveredItemType> extends GestureSource<Hovered
// Will hold the last sample _even if_ we don't save every coord that comes through.
const lastSample = source.path.stats.lastSample;
// Are we 'chop'ping off the existing path or preserving it? This sets the sample-copying
// configuration accordingly.
if(startAtEnd) {
subpath = new GesturePath<HoveredItemType>();
if(lastSample) {
subpath.extend(lastSample);
}
this._baseStartIndex = start = Math.max(start + length - 1, 0);
length = length > 0 ? 1 : 0;
} else {
subpath = source.path.clone();
this._baseStartIndex = start;
}
subpath = new GesturePath<HoveredItemType>();
for(let i=0; i < length; i++) {
subpath.extend(translateSample(baseSource.path.coords[start + i]));
}
this._path = subpath;
@ -196,25 +267,51 @@ export class GestureSourceSubview<HoveredItemType> extends GestureSource<Hovered
this._baseItem = lastSample?.item;
}
// Ensure that this 'subview' is updated whenever the "source of truth" is.
const completeHook = () => this.path.terminate(false);
const invalidatedHook = () => this.path.terminate(true);
const stepHook = (sample) => this.update(sample);
baseSource.path.on('complete', completeHook);
baseSource.path.on('invalidated', invalidatedHook);
baseSource.path.on('step', stepHook);
if(mayUpdate) {
// Ensure that this 'subview' is updated whenever the "source of truth" is.
const completeHook = () => this.path.terminate(false);
const invalidatedHook = () => this.path.terminate(true);
const stepHook = (sample: InputSample<HoveredItemType>) => {
super.update(translateSample(sample));
};
baseSource.path.on('complete', completeHook);
baseSource.path.on('invalidated', invalidatedHook);
baseSource.path.on('step', stepHook);
// But make sure we can "disconnect" it later once the gesture being matched
// with the subview has fully matched; it's good to have a snapshot left over.
this.subviewDisconnector = () => {
baseSource.path.off('complete', completeHook);
baseSource.path.off('invalidated', invalidatedHook);
baseSource.path.off('step', stepHook);
// But make sure we can "disconnect" it later once the gesture being matched
// with the subview has fully matched; it's good to have a snapshot left over.
this.subviewDisconnector = () => {
baseSource.path.off('complete', completeHook);
baseSource.path.off('invalidated', invalidatedHook);
baseSource.path.off('step', stepHook);
}
}
}
private get recognizerTranslation() {
// Allowing a 'null' config greatly simplifies many of our unit-test specs.
if(this.recognizerConfigStack.length == 1 || !this.currentRecognizerConfig) {
return {
x: 0,
y: 0
};
}
// Could compute all of this a single time & cache the value whenever a recognizer-config is pushed or popped.
const currentRecognizer = this.currentRecognizerConfig;
const currentClientRect = currentRecognizer.targetRoot.getBoundingClientRect();
const baseClientRect = this.recognizerConfigStack[0].targetRoot.getBoundingClientRect();
return {
x: currentClientRect.x - baseClientRect.x,
y: currentClientRect.y - baseClientRect.y
}
}
/**
* The original GestureSource this subview is based upon.
* The original GestureSource this subview is based upon. Note that the coordinate system may
* differ if a gesture stage/component has occurred that triggered a change to the active
* recognizer configuration. (e.g. a subkey menu is being displayed for a longpress interaction)
*/
public get baseSource() {
return this._baseSource;
@ -231,6 +328,18 @@ export class GestureSourceSubview<HoveredItemType> extends GestureSource<Hovered
}
}
public pushRecognizerConfig(config: Omit<GestureRecognizerConfiguration<HoveredItemType>, "touchEventRoot" | "mouseEventRoot">): void {
throw new Error("Pushing and popping of recognizer configurations should only be called on the base GestureSource");
}
public popRecognizerConfig(): Nonoptional<GestureRecognizerConfiguration<HoveredItemType>> {
throw new Error("Pushing and popping of recognizer configurations should only be called on the base GestureSource");
}
public update(sample: InputSample<HoveredItemType>): void {
throw new Error("Updates should be provided through the base GestureSource.")
}
/**
* Like `disconnect`, but this will also terminate the baseSource and prevent further
* updates for the true, original `GestureSource` instance. If the gesture-model

View file

@ -31,6 +31,18 @@ export abstract class InputEngineBase<HoveredItemType> extends EventEmitter<Even
return this._activeTouchpoints.find((point) => point.rawIdentifier == identifier);
}
/**
* During the lifetime of a GestureSource (a continuous path for a single touchpoint),
* it is possible that the legal area for the path may change. This function allows
* us to find the appropriate set of constraints for the path if any changes have been
* requested - say, for a subkey menu after a longpress.
* @param identifier
* @returns
*/
protected getConfigForId(identifier: number) {
return this.getTouchpointWithId(identifier).currentRecognizerConfig;
}
public dropTouchpointWithId(identifier: number) {
this._activeTouchpoints = this._activeTouchpoints.filter((point) => point.rawIdentifier != identifier);
}

View file

@ -32,7 +32,7 @@ export abstract class InputEventEngine<HoveredItemType> extends InputEngineBase<
}
protected onInputStart(identifier: number, sample: InputSample<HoveredItemType>, target: EventTarget, isFromTouch: boolean) {
const touchpoint = new GestureSource<HoveredItemType>(identifier, isFromTouch);
const touchpoint = new GestureSource<HoveredItemType>(identifier, this.config, isFromTouch);
touchpoint.update(sample);
this.addTouchpoint(touchpoint);

View file

@ -128,8 +128,9 @@ export class MouseEventEngine<HoveredItemType> extends InputEventEngine<HoveredI
}
this.preventPropagation(event);
const config = this.getConfigForId(this.activeIdentifier);
if(!ZoneBoundaryChecker.inputMoveCancellationCheck(sample, this.config, this.disabledSafeBounds)) {
if(!ZoneBoundaryChecker.inputMoveCancellationCheck(sample, config, this.disabledSafeBounds)) {
this.onInputMove(this.activeIdentifier, sample, event.target);
} else {
this.onInputMoveCancel(this.activeIdentifier, sample, event.target);

View file

@ -135,9 +135,11 @@ export class TouchEventEngine<HoveredItemType> extends InputEventEngine<HoveredI
propagationActive = false;
}
const config = this.getConfigForId(touch.identifier);
const sample = this.buildSampleFromTouch(touch, timestamp);
if(!ZoneBoundaryChecker.inputMoveCancellationCheck(sample, this.config, this.safeBoundMaskMap[touch.identifier])) {
if(!ZoneBoundaryChecker.inputMoveCancellationCheck(sample, config, this.safeBoundMaskMap[touch.identifier])) {
this.onInputMove(touch.identifier, sample, touch.target);
} else {
this.onInputMoveCancel(touch.identifier, sample, touch.target);

View file

@ -1,51 +1,244 @@
import { assert } from 'chai'
import { GestureSource, InputSample } from '@keymanapp/gesture-recognizer';
import { GestureRecognizerConfiguration, GestureSource, InputSample } from '@keymanapp/gesture-recognizer';
const helloSample: InputSample<string> = {
targetX: 1,
targetY: 2,
item: 'hello',
t: 101
};
const worldSample: InputSample<string> = {
targetX: 2,
targetY: 3,
item: 'world',
t: 121
};
// 1 -> 4, 'a' -> 'd'
const simpleSampleSequence: InputSample<string>[] = [1, 2, 3, 4].map((i) => {
return {
targetX: i,
targetY: i,
item: String.fromCharCode('a'.charCodeAt(0) + i - 1),
t: 20 * i
}
});
/**
* A minimalist mock for a recognizer-configuration based at <0, 0>.
*/
const mockedOriginConfig = {
targetRoot: {
getBoundingClientRect: () => {
return {
x: 0,
y: 0
};
}
}
};
/**
* A minimalist mock for a recognizer-configuration based at <-2, -2>.
*/
const mockedInitialConfig = {
targetRoot: {
getBoundingClientRect: () => {
return {
x: -2,
y: -2
};
}
}
};
/**
* A minimalist mock for a recognizer-configuration based at <2, 2>.
*/
const mockedShiftedConfig = {
targetRoot: {
getBoundingClientRect: () => {
return {
x: 2,
y: 2
};
}
}
};
// Should probably be a bit more thorough, but it's a start.
describe("GestureSource", function() {
describe(".constructSubview", function() {
it("properly propagates updates", () => {
let source = new GestureSource<string>(0, true);
describe("Subviews", function() {
it("construction: preserve current path & base item", () => {
let source = new GestureSource<string>(0, null, true);
for(let i=0; i < simpleSampleSequence.length; i++) {
source.update(simpleSampleSequence[i]);
}
let subview = source.constructSubview(false, true);
assert.equal(subview.path.coords.length, simpleSampleSequence.length);
assert.deepEqual(subview.path.coords, simpleSampleSequence);
assert.equal(subview.baseItem, simpleSampleSequence[0].item);
});
it("construction: preserve only most recent sample & base item", () => {
let source = new GestureSource<string>(0, null, true);
for(let i=0; i < simpleSampleSequence.length; i++) {
source.update(simpleSampleSequence[i]);
}
let subview = source.constructSubview(true, true);
assert.equal(subview.path.coords.length, 1);
const lastSample = simpleSampleSequence[simpleSampleSequence.length - 1];
assert.deepEqual(subview.path.coords, [lastSample]);
assert.equal(subview.baseItem, simpleSampleSequence[0].item);
});
it("construction: preserve only most recent sample", () => {
let source = new GestureSource<string>(0, null, true);
for(let i=0; i < simpleSampleSequence.length; i++) {
source.update(simpleSampleSequence[i]);
}
let subview = source.constructSubview(true, false);
assert.equal(subview.path.coords.length, 1);
const lastSample = simpleSampleSequence[simpleSampleSequence.length - 1];
assert.deepEqual(subview.path.coords, [lastSample]);
assert.equal(subview.baseItem, lastSample.item);
});
it("construction: preserve current path & base item, with translation", () => {
let source = new GestureSource<string>(0, mockedInitialConfig as any, true);
for(let i=0; i < simpleSampleSequence.length; i++) {
source.update(simpleSampleSequence[i]);
}
source.pushRecognizerConfig(mockedShiftedConfig as any);
let subview = source.constructSubview(false, true);
// When an alternate recognizer-config is pushed, the original source's version of
// the path should remain unchanged.
assert.deepEqual(source.path.coords, simpleSampleSequence);
// Subviews, on the other hand, should use the most appropriate current recognizer-config.
// It'll be "locked in" for the subview.
assert.equal(subview.path.coords.length, simpleSampleSequence.length);
assert.deepEqual(subview.path.coords, simpleSampleSequence.map((value) => {
return {
...value,
targetX: value.targetX - 4,
targetY: value.targetY - 4
}
}));
assert.equal(subview.baseItem, simpleSampleSequence[0].item);
});
it("properly propagate updates", () => {
let source = new GestureSource<string>(0, null, true);
let subview = source.constructSubview(false, true);
assert.equal(subview.path.coords.length, 0);
source.update(helloSample);
assert.equal(subview.path.coords.length, 1);
assert.deepEqual(subview.currentSample, helloSample);
});
it("may be disconnected from further updates", () => {
let source = new GestureSource<string>(0, null, true);
let subview = source.constructSubview(false, true);
source.update(helloSample);
subview.disconnect();
source.update(worldSample);
assert.equal(source.path.coords.length, 2);
// Should not receive sample2.
assert.equal(subview.path.coords.length, 1);
assert.deepEqual(subview.currentSample, helloSample);
});
it('is updated if constructed from "current" intermediate subview', () => {
let source = new GestureSource<string>(0, null, true);
let baseSubview = source.constructSubview(false, true);
source.update(helloSample);
// Is constructed from a 'current' subview - one including the base source's
// most recent sample.
let subview = baseSubview.constructSubview(false, true);
assert.equal(subview.path.coords.length, 1);
source.update(worldSample);
// BOTH should update in this scenario.
assert.equal(baseSubview.path.coords.length, 2);
assert.equal(subview.path.coords.length, 2);
assert.deepEqual(baseSubview.currentSample, worldSample);
assert.deepEqual(subview.currentSample, worldSample);
});
it('is not updated if constructed from old, not-up-to-date subview', () => {
let source = new GestureSource<string>(0, null, true);
let updatingSubview = source.constructSubview(false, true);
let oldSubview = source.constructSubview(false, true);
oldSubview.disconnect();
source.update(helloSample);
let subview = oldSubview.constructSubview(false, true);
assert.equal(subview.path.coords.length, 0);
let sample: InputSample<string> = {
targetX: 1,
targetY: 2,
item: 'hello',
t: 101
};
source.update(worldSample);
source.update(sample);
assert.equal(subview.path.coords.length, 1);
assert.equal(subview.currentSample, sample);
// Only the 'updating' one should update in this scenario.
//
// It's an error if the subview based on the non-updated subview updates,
// as there would be a gap in the path!
assert.equal(updatingSubview.path.coords.length, 2);
assert.equal(subview.path.coords.length, 0);
assert.deepEqual(updatingSubview.currentSample, worldSample);
assert.isNotOk(subview.currentSample);
});
it("propagates path termination (complete)", () => {
let source = new GestureSource<string>(0, true);
it("propagate path termination (complete)", () => {
let source = new GestureSource<string>(0, null, true);
let subview = source.constructSubview(true, true);
let subview2 = source.constructSubview(true, true);
assert.equal(subview.path.coords.length, 0);
let sample: InputSample<string> = {
targetX: 1,
targetY: 2,
item: 'hello',
t: 101
};
source.update(sample);
source.update(helloSample);
subview.terminate(false);
assert.equal(subview.path.coords.length, 1);
assert.equal(subview.currentSample, sample);
assert.deepEqual(subview.currentSample, helloSample);
assert.equal(subview.isPathComplete, true);
assert.equal(subview.path.wasCancelled, false);
assert.equal(subview2.isPathComplete, true);
assert.equal(subview2.path.wasCancelled, false);
});
it("acts read-only - does not allow direct mutations", () => {
let source = new GestureSource<string>(0, mockedInitialConfig as any, true);
let subview = source.constructSubview(false, true);
source.update(helloSample);
source.pushRecognizerConfig(mockedShiftedConfig as any);
assert.throws(() => subview.update(worldSample));
assert.throws(() => subview.pushRecognizerConfig(mockedOriginConfig as any));
assert.throws(() => subview.popRecognizerConfig());
// Should NOT throw; this is on the base source.
source.popRecognizerConfig();
});
});
});

View file

@ -870,7 +870,7 @@ describe("GestureMatcher", function() {
sources[0].path.on('invalidated', () => secondMatcher.update());
timedPromise(50).then(() => {
const secondContact = new GestureSource<string>(5, true);
const secondContact = new GestureSource<string>(5, null, true);
sources.push(secondContact);
secondMatcher.addContact(secondContact);
secondContact.update({

View file

@ -67,7 +67,7 @@ describe("PathMatcher", function() {
describe("Instant fulfillment modeling", function() {
it("resolve", async function() {
const emulatedContactPoint = new GestureSource<string>(1, true);
const emulatedContactPoint = new GestureSource<string>(1, null, true);
const modelMatcher = new gestures.matchers.PathMatcher(InstantResolutionModel, emulatedContactPoint);
const startSample = {
@ -86,7 +86,7 @@ describe("PathMatcher", function() {
});
it("reject", async function() {
const emulatedContactPoint = new GestureSource<string>(1, true);
const emulatedContactPoint = new GestureSource<string>(1, null, true);
const modelMatcher = new gestures.matchers.PathMatcher(InstantRejectionModel, emulatedContactPoint);
const startSample = {
@ -107,7 +107,7 @@ describe("PathMatcher", function() {
describe("Longpress: primary path modeling", function() {
it("resolve: path completed (long wait)", async function() {
const emulatedContactPoint = new GestureSource<string>(1, true);
const emulatedContactPoint = new GestureSource<string>(1, null, true);
const modelMatcher = new gestures.matchers.PathMatcher(MainLongpressSourceModel, emulatedContactPoint);
const startSample = {
@ -133,7 +133,7 @@ describe("PathMatcher", function() {
});
it("resolve: path not completed (long wait)", async function() {
const emulatedContactPoint = new GestureSource<string>(1, true);
const emulatedContactPoint = new GestureSource<string>(1, null, true);
const modelMatcher = new gestures.matchers.PathMatcher(MainLongpressSourceModel, emulatedContactPoint);
const startSample = {
@ -159,7 +159,7 @@ describe("PathMatcher", function() {
});
it("reject: path completed (short wait)", async function() {
const emulatedContactPoint = new GestureSource<string>(1, true);
const emulatedContactPoint = new GestureSource<string>(1, null, true);
const modelMatcher = new gestures.matchers.PathMatcher(MainLongpressSourceModel, emulatedContactPoint);
const startSample = {
@ -185,7 +185,7 @@ describe("PathMatcher", function() {
});
it("reject: path cancelled", async function() {
const emulatedContactPoint = new GestureSource<string>(1, true);
const emulatedContactPoint = new GestureSource<string>(1, null, true);
const modelMatcher = new gestures.matchers.PathMatcher(MainLongpressSourceModel, emulatedContactPoint);
const startSample = {
@ -218,7 +218,7 @@ describe("PathMatcher", function() {
});
it("reject: distance moved", async function() {
const emulatedContactPoint = new GestureSource<string>(1, true);
const emulatedContactPoint = new GestureSource<string>(1, null, true);
const modelMatcher = new gestures.matchers.PathMatcher(MainLongpressSourceModel, emulatedContactPoint);
const startSample = {
@ -253,7 +253,7 @@ describe("PathMatcher", function() {
});
it("reject: item changed", async function() {
const emulatedContactPoint = new GestureSource<string>(1, true);
const emulatedContactPoint = new GestureSource<string>(1, null, true);
const modelMatcher = new gestures.matchers.PathMatcher(MainLongpressSourceModel, emulatedContactPoint);
const startSample = {
@ -287,7 +287,7 @@ describe("PathMatcher", function() {
});
it("reject: disabled up-flick shortcut", async function() {
const emulatedContactPoint = new GestureSource<string>(1, true);
const emulatedContactPoint = new GestureSource<string>(1, null, true);
const modelMatcher = new gestures.matchers.PathMatcher(MainLongpressSourceModel, emulatedContactPoint);
const startSample = {
@ -313,7 +313,7 @@ describe("PathMatcher", function() {
});
it("resolve: enabled up-flick shortcut", async function() {
const emulatedContactPoint = new GestureSource<string>(1, true);
const emulatedContactPoint = new GestureSource<string>(1, null, true);
const modelMatcher = new gestures.matchers.PathMatcher(MainLongpressSourceModelWithShortcut, emulatedContactPoint);
const startSample = {
@ -341,7 +341,7 @@ describe("PathMatcher", function() {
describe("Modipress: primary path modeling", function () {
it("push: on path start", async function() {
const emulatedContactPoint = new GestureSource<string>(1, true);
const emulatedContactPoint = new GestureSource<string>(1, null, true);
const modelMatcher = new gestures.matchers.PathMatcher(ModipressStartModel, emulatedContactPoint);
const startSample = {
@ -360,7 +360,7 @@ describe("PathMatcher", function() {
});
it("pop: released", async function() {
const emulatedContactPoint = new GestureSource<string>(1, true);
const emulatedContactPoint = new GestureSource<string>(1, null, true);
const modelMatcher = new gestures.matchers.PathMatcher(ModipressEndModel, emulatedContactPoint);
const startSample = {
@ -379,7 +379,7 @@ describe("PathMatcher", function() {
});
it("pop: item changed", async function() {
const emulatedContactPoint = new GestureSource<string>(1, true);
const emulatedContactPoint = new GestureSource<string>(1, null, true);
const modelMatcher = new gestures.matchers.PathMatcher(ModipressEndModel, emulatedContactPoint);
const startSample = {
@ -407,7 +407,7 @@ describe("PathMatcher", function() {
describe("Simple Tap: primary path modeling", function() {
it("resolve: path completed (long wait)", async function() {
const emulatedContactPoint = new GestureSource<string>(1, true);
const emulatedContactPoint = new GestureSource<string>(1, null, true);
const modelMatcher = new gestures.matchers.PathMatcher(SimpleTapModel, emulatedContactPoint);
const startSample = {
@ -433,7 +433,7 @@ describe("PathMatcher", function() {
});
it("resolve: path completed (short wait)", async function() {
const emulatedContactPoint = new GestureSource<string>(1, true);
const emulatedContactPoint = new GestureSource<string>(1, null, true);
const modelMatcher = new gestures.matchers.PathMatcher(SimpleTapModel, emulatedContactPoint);
const startSample = {
@ -459,7 +459,7 @@ describe("PathMatcher", function() {
});
it("reject: path cancelled", async function() {
const emulatedContactPoint = new GestureSource<string>(1, true);
const emulatedContactPoint = new GestureSource<string>(1, null, true);
const modelMatcher = new gestures.matchers.PathMatcher(SimpleTapModel, emulatedContactPoint);
const startSample = {
@ -492,7 +492,7 @@ describe("PathMatcher", function() {
});
it("resolve: significant movement, but no item change", async function() {
const emulatedContactPoint = new GestureSource<string>(1, true);
const emulatedContactPoint = new GestureSource<string>(1, null, true);
const modelMatcher = new gestures.matchers.PathMatcher(SimpleTapModel, emulatedContactPoint);
const startSample = {
@ -527,7 +527,7 @@ describe("PathMatcher", function() {
});
it("reject: little movement, but item changed", async function() {
const emulatedContactPoint = new GestureSource<string>(1, true);
const emulatedContactPoint = new GestureSource<string>(1, null, true);
const modelMatcher = new gestures.matchers.PathMatcher(SimpleTapModel, emulatedContactPoint);
const startSample = {

View file

@ -45,7 +45,7 @@ function mockedPredecessor<Type>(
lastSample: InputSample<Type>,
baseItem?: Type
): MockedPredecessor<Type> {
const mockedSrc = new GestureSource<Type>(simSourceIdSeed++, true);
const mockedSrc = new GestureSource<Type>(simSourceIdSeed++, null, true);
mockedSrc.path.extend(lastSample);
mockedSrc.terminate(false);
@ -97,7 +97,7 @@ function prepareSourceForTimer<Type> (
timerSpec: SimSpecTimer<Type>,
startTime: number
): ReturnType<typeof prepareSimContact<Type>> {
const simpleSource = new GestureSource<Type>(simSourceIdSeed++, true);
const simpleSource = new GestureSource<Type>(simSourceIdSeed++, null, true);
const promise = timedPromise(startTime).then(() => {
// We're simulating a previously-completed contact point's path..
@ -124,7 +124,7 @@ function prepareSourceForRawSequence<Type>(
contactSpec: SimSpecSequence<Type>,
startTime: number
): ReturnType<typeof prepareSimContact<Type>> {
const simpleSource = new GestureSource<Type>(simSourceIdSeed++, true);
const simpleSource = new GestureSource<Type>(simSourceIdSeed++, null, true);
const promise = timedPromise(startTime).then(() => {
if(startTime != 0) {

View file

@ -23,7 +23,7 @@ export class HeadlessInputEngine<Type = any> extends InputEngineBase<Type> {
const tailSamples = originalSamples.slice(1);
const pathID = this.PATH_ID_SEED++;
let replayPoint = new GestureSource<Type>(pathID, recordedPoint.isFromTouch);
let replayPoint = new GestureSource<Type>(pathID, null, recordedPoint.isFromTouch);
replayPoint.update(headSample); // is included before the point is made available.
// Build promises designed to reproduce the events at the correct times.