@clankagent/puck 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -0
- package/CHANGELOG.md +20 -1
- package/README.md +5 -3
- package/VERIFICATION.md +17 -3
- package/dist/calibration.d.ts +1 -1
- package/dist/calibration.js +6 -2
- package/dist/gestures.d.ts +26 -1
- package/dist/gestures.js +126 -21
- package/dist/graph.d.ts +19 -2
- package/dist/graph.js +56 -4
- package/dist/index.d.ts +6 -2
- package/dist/index.js +2 -0
- package/dist/press-tilt-calibration.d.ts +32 -0
- package/dist/press-tilt-calibration.js +88 -0
- package/dist/tilt-calibration.d.ts +26 -0
- package/dist/tilt-calibration.js +46 -0
- package/dist/tune.d.ts +21 -0
- package/dist/tune.js +17 -13
- package/docs/agents.md +8 -0
- package/docs/api.md +10 -3
- package/docs/lab.md +35 -0
- package/docs/press-tilt.md +131 -0
- package/docs/roadmap.md +44 -0
- package/docs/troubleshooting.md +1 -1
- package/docs/upgrading.md +18 -3
- package/examples/gesture-session.mjs +4 -3
- package/llms.txt +4 -2
- package/package.json +1 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { validateGestureRecording } from './recording.js';
|
|
2
|
+
import { createGestureTune, defaultGestureTune } from './tune.js';
|
|
3
|
+
export const tiltDirections = ['rx+', 'rx-', 'ry+', 'ry-'];
|
|
4
|
+
const quantile = (v, p) => { const a = [...v].sort((x, y) => x - y), at = (a.length - 1) * p, i = Math.floor(at); return a[i] + (a[Math.ceil(at)] - a[i]) * (at - i); };
|
|
5
|
+
/** Infer combined single excursions from raw z/rx/ry. Preserves the base simple/double tune.
|
|
6
|
+
* Coverage is inferred evidence, not ground-truth labels or measured classification accuracy.
|
|
7
|
+
*/
|
|
8
|
+
export function calibratePressTilts(input, settings = {}) {
|
|
9
|
+
const recordings = Array.isArray(input) ? input : [input];
|
|
10
|
+
const minimum = settings.minimumPerAction ?? 3, base = settings.baseTune ?? defaultGestureTune;
|
|
11
|
+
if (!Number.isInteger(minimum) || minimum < 3 || minimum > 100 || recordings.length < 1 || recordings.length > 20)
|
|
12
|
+
throw new RangeError('Use 1–20 recordings and at least three examples per action.');
|
|
13
|
+
const counts = {};
|
|
14
|
+
for (const d of ['push', 'pull'])
|
|
15
|
+
for (const tilt of tiltDirections)
|
|
16
|
+
counts[`${d}.${tilt}`] = 0;
|
|
17
|
+
const actions = [], issues = [];
|
|
18
|
+
let ambiguous = false;
|
|
19
|
+
function finish(rows, recording) {
|
|
20
|
+
if (!rows.length)
|
|
21
|
+
return;
|
|
22
|
+
const peak = (k) => rows.reduce((a, b) => Math.abs(b.input[k]) > Math.abs(a.input[k]) ? b : a);
|
|
23
|
+
const px = peak('rx'), py = peak('ry'), pz = peak('z');
|
|
24
|
+
const axis = Math.abs(px.input.rx) >= Math.abs(py.input.ry) ? 'rx' : 'ry', top = axis === 'rx' ? px : py;
|
|
25
|
+
const strength = Math.abs(top.input[axis]), other = Math.max(...rows.map(r => Math.abs(r.input[axis === 'rx' ? 'ry' : 'rx'])));
|
|
26
|
+
if (strength < .25)
|
|
27
|
+
return; // Plain presses are evidence for the separate simple/double calibration.
|
|
28
|
+
const direction = pz.input.z > 0 ? 'push' : 'pull', sign = direction === 'push' ? 1 : -1;
|
|
29
|
+
const pressure = rows.find(r => r.input.z * sign >= base[direction].activation);
|
|
30
|
+
const onset = rows.find(r => Math.abs(r.input[axis]) >= .25);
|
|
31
|
+
if (!pressure || !onset || onset.t < pressure.t || rows[rows.length - 1].t - rows[0].t > 1500 || rows.some(r => r.input.z * sign < -base[direction === 'push' ? 'pull' : 'push'].activation) || strength < other * 1.25) {
|
|
32
|
+
ambiguous = true;
|
|
33
|
+
issues.push('Excluded a tilt with ambiguous direction, pressure order, reversal or duration.');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const tilt = (axis + (top.input[axis] > 0 ? '+' : '-'));
|
|
37
|
+
actions.push({ direction, tilt, kind: 'single', recording, start: pressure.t, end: rows[rows.length - 1].t, pressurePeak: Math.abs(pz.input.z), tiltPeak: strength, pressureAtTiltPeak: Math.abs(top.input.z), onsetDelayMs: onset.t - pressure.t });
|
|
38
|
+
counts[`${direction}.${tilt}`]++;
|
|
39
|
+
}
|
|
40
|
+
recordings.forEach((recording, index) => {
|
|
41
|
+
validateGestureRecording(recording);
|
|
42
|
+
let run = [], quietAt = null, armed = false;
|
|
43
|
+
for (const row of recording.timeline) {
|
|
44
|
+
if (row.type === 'reset') {
|
|
45
|
+
if (run.length)
|
|
46
|
+
issues.push('Discarded an interrupted excursion.');
|
|
47
|
+
run = [];
|
|
48
|
+
quietAt = null;
|
|
49
|
+
armed = false;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (row.type !== 'input')
|
|
53
|
+
continue;
|
|
54
|
+
const magnitude = Math.max(Math.abs(row.input.z), Math.abs(row.input.rx), Math.abs(row.input.ry));
|
|
55
|
+
if (magnitude <= .06) {
|
|
56
|
+
armed = true;
|
|
57
|
+
quietAt ?? (quietAt = row.t);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (run.length && quietAt !== null && row.t - quietAt > 180) {
|
|
61
|
+
finish(run, index);
|
|
62
|
+
run = [];
|
|
63
|
+
}
|
|
64
|
+
if (armed)
|
|
65
|
+
run.push(row);
|
|
66
|
+
quietAt = null;
|
|
67
|
+
}
|
|
68
|
+
const inputs = recording.timeline.filter(r => r.type === 'input'), last = inputs[inputs.length - 1];
|
|
69
|
+
if (run.length) {
|
|
70
|
+
if (last?.type === 'input' && Math.max(Math.abs(last.input.z), Math.abs(last.input.rx), Math.abs(last.input.ry)) <= .06)
|
|
71
|
+
finish(run, index);
|
|
72
|
+
else
|
|
73
|
+
issues.push('Discarded an unfinished excursion at recording end.');
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
const missing = Object.keys(counts).filter(k => counts[k] < minimum);
|
|
77
|
+
let tune = null;
|
|
78
|
+
if (!missing.length && !ambiguous) {
|
|
79
|
+
const peaks = actions.map(a => a.tiltPeak), activation = Math.min(.25, Math.min(...peaks) * .8);
|
|
80
|
+
const center = peaks[0] + peaks.reduce((sum, v) => sum + v - peaks[0], 0) / peaks.length;
|
|
81
|
+
tune = createGestureTune({ ...base.toJSON(), pressTilt: {
|
|
82
|
+
force: { center, low: Math.min(center, quantile(peaks, .1)), high: Math.max(center, quantile(peaks, .9)), activation, release: activation * .48 },
|
|
83
|
+
minMs: 25, armMs: Math.max(450, Math.ceil((Math.max(...actions.map(a => a.onsetDelayMs)) + 80) / 50) * 50), relaxMs: 180,
|
|
84
|
+
maxMs: Math.max(1000, Math.ceil(Math.max(...actions.map(a => a.end - a.start)) * 1.5 / 50) * 50), dominance: 1.25,
|
|
85
|
+
} });
|
|
86
|
+
}
|
|
87
|
+
return { status: ambiguous ? 'ambiguous' : missing.length ? 'incomplete' : 'ready', tune, counts, missing, actions, issues: [...new Set(issues)] };
|
|
88
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { CalibrationStats } from './calibration.js';
|
|
2
|
+
import type { TiltDirection } from './gestures.js';
|
|
3
|
+
import type { GestureRecording } from './recording.js';
|
|
4
|
+
import type { GestureTune } from './tune.js';
|
|
5
|
+
export type TiltActionName = `${TiltDirection}.${'single' | 'double'}`;
|
|
6
|
+
export interface TiltCalibrationAction {
|
|
7
|
+
direction: TiltDirection;
|
|
8
|
+
kind: 'single' | 'double';
|
|
9
|
+
start: number;
|
|
10
|
+
end: number;
|
|
11
|
+
recording: number;
|
|
12
|
+
}
|
|
13
|
+
export interface TiltCalibration {
|
|
14
|
+
status: 'ready' | 'incomplete' | 'ambiguous';
|
|
15
|
+
tune: GestureTune | null;
|
|
16
|
+
counts: Record<TiltActionName, number>;
|
|
17
|
+
missing: TiltActionName[];
|
|
18
|
+
actions: TiltCalibrationAction[];
|
|
19
|
+
stats: Partial<Record<TiltDirection, CalibrationStats>>;
|
|
20
|
+
issues: string[];
|
|
21
|
+
}
|
|
22
|
+
/** Reuse the raw pulse parser on rx/ry; never pair across capture/reset boundaries. */
|
|
23
|
+
export declare function calibrateTilts(input: GestureRecording | readonly GestureRecording[], settings?: {
|
|
24
|
+
baseTune?: GestureTune;
|
|
25
|
+
minimumPerAction?: number;
|
|
26
|
+
}): TiltCalibration;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { calibrateGestures } from './calibration.js';
|
|
2
|
+
import { validateGestureRecording } from './recording.js';
|
|
3
|
+
import { createGestureTune, defaultGestureTune } from './tune.js';
|
|
4
|
+
const names = { clockwise: 'rx+', counterclockwise: 'rx-', push: 'ry+', pull: 'ry-' };
|
|
5
|
+
/** Reuse the raw pulse parser on rx/ry; never pair across capture/reset boundaries. */
|
|
6
|
+
export function calibrateTilts(input, settings = {}) {
|
|
7
|
+
const captures = Array.isArray(input) ? input : [input];
|
|
8
|
+
const base = settings.baseTune ?? defaultGestureTune;
|
|
9
|
+
const mapped = captures.map(c => {
|
|
10
|
+
validateGestureRecording(c);
|
|
11
|
+
let pressureOwned = false;
|
|
12
|
+
const timeline = c.timeline.map(row => {
|
|
13
|
+
if (row.type === 'reset') {
|
|
14
|
+
pressureOwned = false;
|
|
15
|
+
return row;
|
|
16
|
+
}
|
|
17
|
+
if (row.type !== 'input')
|
|
18
|
+
return row;
|
|
19
|
+
const v = row.input;
|
|
20
|
+
if (Math.max(Math.abs(v.z), Math.abs(v.rz), Math.abs(v.rx), Math.abs(v.ry)) <= .06)
|
|
21
|
+
pressureOwned = false;
|
|
22
|
+
if (Math.max(Math.abs(v.rx), Math.abs(v.ry)) < .15 && (v.z >= base.push.activation || -v.z >= base.pull.activation || Math.abs(v.rz) >= base.rotation.activation))
|
|
23
|
+
pressureOwned = true;
|
|
24
|
+
if (pressureOwned)
|
|
25
|
+
return { type: 'reset', t: row.t };
|
|
26
|
+
return { ...row, input: { x: 0, y: 0, z: v.ry, rz: v.rx, rx: 0, ry: 0 } };
|
|
27
|
+
});
|
|
28
|
+
return { ...c, timeline };
|
|
29
|
+
});
|
|
30
|
+
const result = calibrateGestures(mapped, { minimumPerAction: settings.minimumPerAction, detectionFloor: .06 });
|
|
31
|
+
const rename = (key) => { const [direction, kind] = key.split('.'); return `${names[direction]}.${kind}`; };
|
|
32
|
+
const counts = Object.fromEntries(Object.entries(result.counts).map(([k, v]) => [rename(k), v]));
|
|
33
|
+
let tune = null;
|
|
34
|
+
if (result.tune) {
|
|
35
|
+
const t = result.tune;
|
|
36
|
+
const rxActivation = Math.min(t.rotation.activation, Math.min(result.stats.clockwise.min, result.stats.counterclockwise.min) * .85);
|
|
37
|
+
const rx = { ...t.rotation, activation: rxActivation, release: Math.min(t.rotation.release, rxActivation * .8) };
|
|
38
|
+
const ry = Object.fromEntries(['center', 'low', 'high', 'activation', 'release'].map(k => [k, (t.push[k] + t.pull[k]) / 2]));
|
|
39
|
+
tune = createGestureTune({ ...base.toJSON(), standaloneTilt: { rx, ry, timing: { ...t.timing, minPulseMs: Math.min(25, t.timing.minPulseMs), neutralMs: 10 } } });
|
|
40
|
+
}
|
|
41
|
+
return { status: result.status, tune, counts, missing: result.missing.map(rename),
|
|
42
|
+
actions: result.actions.map(a => ({ direction: names[a.direction], kind: a.kind, start: a.start, end: a.end, recording: a.recording })),
|
|
43
|
+
stats: Object.fromEntries(Object.entries(result.stats).map(([k, v]) => [names[k], v])),
|
|
44
|
+
issues: result.issues.map(issue => issue.replace(/counterclockwise|clockwise|push|pull/g, k => names[k])),
|
|
45
|
+
};
|
|
46
|
+
}
|
package/dist/tune.d.ts
CHANGED
|
@@ -6,6 +6,24 @@ export interface ForceBand {
|
|
|
6
6
|
readonly activation: number;
|
|
7
7
|
readonly release: number;
|
|
8
8
|
}
|
|
9
|
+
export interface PressTiltTune {
|
|
10
|
+
readonly force: ForceBand;
|
|
11
|
+
readonly minMs: number;
|
|
12
|
+
readonly armMs: number;
|
|
13
|
+
readonly relaxMs: number;
|
|
14
|
+
readonly maxMs: number;
|
|
15
|
+
readonly dominance: number;
|
|
16
|
+
}
|
|
17
|
+
export interface StandaloneTiltTune {
|
|
18
|
+
readonly rx: ForceBand;
|
|
19
|
+
readonly ry: ForceBand;
|
|
20
|
+
readonly timing: {
|
|
21
|
+
readonly minPulseMs: number;
|
|
22
|
+
readonly maxPulseMs: number;
|
|
23
|
+
readonly neutralMs: number;
|
|
24
|
+
readonly doubleMs: number;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
9
27
|
export interface GestureTuneData {
|
|
10
28
|
readonly version: 1;
|
|
11
29
|
readonly rotation: ForceBand;
|
|
@@ -18,6 +36,9 @@ export interface GestureTuneData {
|
|
|
18
36
|
readonly doubleMs: number;
|
|
19
37
|
};
|
|
20
38
|
readonly dominance: number;
|
|
39
|
+
/** Optional for backward-compatible restoration of 0.2 tunes. Does not enable tilt mode by itself. */
|
|
40
|
+
readonly pressTilt?: PressTiltTune;
|
|
41
|
+
readonly standaloneTilt?: StandaloneTiltTune;
|
|
21
42
|
}
|
|
22
43
|
export interface GestureTune extends GestureTuneData {
|
|
23
44
|
soften(amount: number): GestureTune;
|
package/dist/tune.js
CHANGED
|
@@ -5,47 +5,51 @@ const base = {
|
|
|
5
5
|
pull: { center: .289, low: .18, high: .357, activation: .12, release: .06 },
|
|
6
6
|
timing: { minPulseMs: 35, maxPulseMs: 650, neutralMs: 25, doubleMs: 400 },
|
|
7
7
|
dominance: 1.4,
|
|
8
|
+
pressTilt: { force: { center: .596, low: .401, high: .803, activation: .24, release: .115 }, minMs: 25, armMs: 450, relaxMs: 180, maxMs: 1000, dominance: 1.25 },
|
|
9
|
+
standaloneTilt: { rx: { center: .458, low: .319, high: .601, activation: .13, release: .104 }, ry: { center: .671, low: .536, high: .806, activation: .28, release: .16 }, timing: { minPulseMs: 25, maxPulseMs: 650, neutralMs: 10, doubleMs: 400 } },
|
|
8
10
|
};
|
|
9
11
|
const finite = (x) => typeof x === 'number' && Number.isFinite(x);
|
|
10
12
|
/** Restore a plain JSON tune; all nested objects are copied and frozen. */
|
|
11
13
|
export function createGestureTune(data = base) {
|
|
12
14
|
if (!data || data.version !== 1 || !finite(data.dominance) || data.dominance < 1)
|
|
13
15
|
throw new RangeError('Invalid tune version or dominance.');
|
|
14
|
-
for (const
|
|
15
|
-
const b = data[key];
|
|
16
|
+
for (const b of [data.rotation, data.push, data.pull, ...(data.pressTilt ? [data.pressTilt.force] : []), ...(data.standaloneTilt ? [data.standaloneTilt.rx, data.standaloneTilt.ry] : [])]) {
|
|
16
17
|
if (!b || ![b.center, b.low, b.high, b.activation, b.release].every(finite)
|
|
17
18
|
|| b.low < 0 || b.low > b.center || b.center > b.high || b.high > 1
|
|
18
19
|
|| b.release < 0 || b.activation <= b.release || b.activation > 1 || b.center <= 0)
|
|
19
20
|
throw new RangeError('Invalid force band.');
|
|
20
21
|
}
|
|
21
22
|
const t = data.timing;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
throw new RangeError('Invalid tune
|
|
25
|
-
const
|
|
23
|
+
const p = data.pressTilt;
|
|
24
|
+
if (p && (![p.minMs, p.armMs, p.relaxMs, p.maxMs, p.dominance].every(finite) || p.minMs < 0 || p.armMs < p.minMs || p.relaxMs < 0 || p.relaxMs > p.armMs || p.armMs > p.maxMs || p.maxMs > 10000 || p.dominance <= 1))
|
|
25
|
+
throw new RangeError('Invalid press-tilt tune.');
|
|
26
|
+
for (const timing of [t, ...(data.standaloneTilt ? [data.standaloneTilt.timing] : [])])
|
|
27
|
+
if (!timing || ![timing.minPulseMs, timing.maxPulseMs, timing.neutralMs, timing.doubleMs].every(v => finite(v) && v >= 0)
|
|
28
|
+
|| timing.minPulseMs > timing.maxPulseMs || timing.maxPulseMs > 10000 || timing.doubleMs > 10000 || timing.neutralMs > 1000)
|
|
29
|
+
throw new RangeError('Invalid tune timing.');
|
|
30
|
+
const s = data.standaloneTilt;
|
|
31
|
+
const value = Object.freeze({ version: 1, rotation: Object.freeze({ ...data.rotation }), push: Object.freeze({ ...data.push }), pull: Object.freeze({ ...data.pull }), timing: Object.freeze({ ...t }), dominance: data.dominance, ...(p ? { pressTilt: Object.freeze({ ...p, force: Object.freeze({ ...p.force }) }) } : {}), ...(s ? { standaloneTilt: Object.freeze({ rx: Object.freeze({ ...s.rx }), ry: Object.freeze({ ...s.ry }), timing: Object.freeze({ ...s.timing }) }) } : {}) });
|
|
26
32
|
const amount = (x, subtract) => { if (!finite(x) || x < 0 || (subtract && x >= 1) || x > 10)
|
|
27
33
|
throw new RangeError('Amount must be a non-negative fraction; soften/narrow require less than 1.'); return subtract ? 1 - x : 1 + x; };
|
|
28
34
|
function edit(factor, mode) {
|
|
29
|
-
|
|
30
|
-
for (const key of ['rotation', 'push', 'pull']) {
|
|
31
|
-
const b = value[key];
|
|
35
|
+
function band(b) {
|
|
32
36
|
if (mode === 'force') {
|
|
33
37
|
// Scale each band uniformly; saturation preserves ordering and hysteresis.
|
|
34
38
|
const f = Math.min(factor, 1 / Math.max(b.high, b.activation));
|
|
35
|
-
|
|
39
|
+
return { center: b.center * f, low: b.low * f, high: b.high * f, activation: b.activation * f, release: b.release * f };
|
|
36
40
|
}
|
|
37
41
|
else {
|
|
38
42
|
const activation = Math.max(.001, b.center - (b.center - b.activation) * factor);
|
|
39
|
-
|
|
43
|
+
return { center: b.center, low: Math.max(0, b.center - (b.center - b.low) * factor), high: Math.min(1, b.center + (b.high - b.center) * factor), activation, release: activation * b.release / b.activation };
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
|
-
return createGestureTune({ ...value, ...
|
|
46
|
+
return createGestureTune({ ...value, rotation: band(value.rotation), push: band(value.push), pull: band(value.pull), ...(value.pressTilt ? { pressTilt: { ...value.pressTilt, force: band(value.pressTilt.force) } } : {}), ...(value.standaloneTilt ? { standaloneTilt: { ...value.standaloneTilt, rx: band(value.standaloneTilt.rx), ry: band(value.standaloneTilt.ry) } } : {}) });
|
|
43
47
|
}
|
|
44
48
|
return Object.freeze({ ...value,
|
|
45
49
|
soften(x) { return edit(amount(x, true), 'force'); }, harden(x) { return edit(amount(x, false), 'force'); },
|
|
46
50
|
narrow(x) { return edit(amount(x, true), 'spread'); }, widen(x) { return edit(amount(x, false), 'spread'); },
|
|
47
51
|
toJSON() { return value; },
|
|
48
|
-
toOptions() { return { twistActivation: value.rotation.activation, twistRelease: value.rotation.release, pushActivation: value.push.activation, pushRelease: value.push.release, pullActivation: value.pull.activation, pullRelease: value.pull.release, ...value.timing, dominance: value.dominance, singleMode: 'exclusive' }; },
|
|
52
|
+
toOptions() { const p = value.pressTilt, s = value.standaloneTilt; return { twistActivation: value.rotation.activation, twistRelease: value.rotation.release, pushActivation: value.push.activation, pushRelease: value.push.release, pullActivation: value.pull.activation, pullRelease: value.pull.release, ...value.timing, dominance: value.dominance, singleMode: 'exclusive', ...(p ? { tiltActivation: p.force.activation, tiltRelease: p.force.release, tiltMinMs: p.minMs, tiltArmMs: p.armMs, tiltRelaxMs: p.relaxMs, tiltMaxMs: p.maxMs, tiltDominance: p.dominance } : {}), ...(s ? { tiltXActivation: s.rx.activation, tiltXRelease: s.rx.release, tiltYActivation: s.ry.activation, tiltYRelease: s.ry.release, standaloneMinPulseMs: s.timing.minPulseMs, standaloneMaxPulseMs: s.timing.maxPulseMs, standaloneNeutralMs: s.timing.neutralMs, standaloneDoubleMs: s.timing.doubleMs } : {}) }; },
|
|
49
53
|
});
|
|
50
54
|
}
|
|
51
55
|
export const defaultGestureTune = createGestureTune();
|
package/docs/agents.md
CHANGED
|
@@ -24,3 +24,11 @@ This guide is for agents building applications with Puck. Repository contributio
|
|
|
24
24
|
Exercise a single, a double, neutral rearming, a long hold, a reversal, report silence, blur/disconnect and teardown. Confirm one action handler invocation per expected event and no completed gesture caused by disconnect. Check incomplete calibration and JSON tune restoration. Use performance.now() consistently in a browser and an explicit monotonic clock in deterministic tests.
|
|
25
25
|
|
|
26
26
|
For library contributions, run `pnpm check` from the source checkout. The tests use built ESM modules. Read CONTRIBUTING before proposing API/profile changes. Do not edit generated dist files as the source of truth.
|
|
27
|
+
|
|
28
|
+
## Tilt integrations (0.3)
|
|
29
|
+
|
|
30
|
+
Read [standalone and combined tilt](press-tilt.md) before enabling these families.
|
|
31
|
+
Use `calibrateTilts` for standalone singles/doubles and `calibratePressTilts` for
|
|
32
|
+
pressure-first combinations; do not interpret one family's coverage as another.
|
|
33
|
+
Handle expanded direction unions and optional event.tilt. Keep modes separate
|
|
34
|
+
from persisted tune data, and never claim inferred labels are ground truth.
|
package/docs/api.md
CHANGED
|
@@ -8,9 +8,9 @@ This reference describes 0.2.0, including its experimental gesture APIs. Emitted
|
|
|
8
8
|
|
|
9
9
|
| Entry point | Runtime exports |
|
|
10
10
|
|---|---|
|
|
11
|
-
| `@clankagent/puck` | `decodeCombinedReport`, `neutralInput`, `createPanZoom`, `createGestures`, `createGestureTune`, `defaultGestureTune`, `gesturePresets`, `createGestureRecorder`, `validateGestureRecording`, `calibrateGestures`, `gestureDirections` |
|
|
11
|
+
| `@clankagent/puck` | `decodeCombinedReport`, `neutralInput`, `createPanZoom`, `createGestures`, `createGestureTune`, `defaultGestureTune`, `gesturePresets`, `createGestureRecorder`, `validateGestureRecording`, `calibrateGestures`, `gestureDirections`, `calibratePressTilts`, `calibrateTilts`, `tiltDirections` |
|
|
12
12
|
| `@clankagent/puck/webhid` | `connectWebHid`, `combinedProfile` |
|
|
13
|
-
| `@clankagent/puck/graph` | `createGestureGraph`, `renderGestureGraphSvg` |
|
|
13
|
+
| `@clankagent/puck/graph` | `createGestureGraph`, `createPressTiltGraph`, `createTiltGraph`, `renderGestureGraphSvg` |
|
|
14
14
|
|
|
15
15
|
`InputState` has six finite normalized axes: `x,y,z,rx,ry,rz`, each in [-1,1]. They represent deflection, not angles. For the verified profile, positive `rz` is clockwise and positive `z` is push down; negative values are counterclockwise and pull up. Force bands use positive magnitudes [0,1]. Times are milliseconds. Recorder and calibration times are relative to capture start; live recognizer event times use the supplied clock.
|
|
16
16
|
|
|
@@ -47,6 +47,13 @@ Timing defaults: `minPulseMs:35`, `maxPulseMs:650`, `neutralMs:25`, `doubleMs:40
|
|
|
47
47
|
|
|
48
48
|
Low-level gates are `activation/release`, `pressActivation/pressRelease`, `twistActivation/twistRelease`, and `clockwiseActivation/clockwiseRelease` (likewise `counterclockwise`, `push`, `pull`). Precedence: direction → axis → shared → default tune. `createGestures({})` matches the no-argument form. Gates require `0 <= release < activation <= 1`; invalid options throw `RangeError`.
|
|
49
49
|
|
|
50
|
+
For opt-in standalone rx/ry and combined pressure/tilt options, event shapes,
|
|
51
|
+
timing, calibration results and graph constructors, see the [tilt API](press-tilt.md).
|
|
52
|
+
`PulseDirection` names the original four; `TiltDirection` names rx+/rx-/ry+/ry-;
|
|
53
|
+
`GestureDirection` is their union. `GestureEvent.tilt` is optional and only present
|
|
54
|
+
for a combined single. `PressMode` is simple/auto/tilt. Exported tune types include
|
|
55
|
+
`PressTiltTune` and `StandaloneTiltTune`.
|
|
56
|
+
|
|
50
57
|
## Tunes
|
|
51
58
|
|
|
52
59
|
`createGestureTune(data?)` validates and copies a version-1 `GestureTuneData`: `{version, rotation, push, pull, timing, dominance}`. Each band contains `{center,low,high,activation,release}`; timing contains the four millisecond fields above. Returned objects are deeply frozen.
|
|
@@ -83,4 +90,4 @@ The parser infers actions from raw shape, independent of order and recognized ev
|
|
|
83
90
|
|
|
84
91
|
A clear calls `onInput(neutralInput)` then `onReset()`. For gestures, ignore that exact sentinel in onInput and reset the recognizer in onReset, so a lifecycle change cannot finish a press. Real zero reports are separate objects. Custom decoders should return fresh input objects for physical reports. Motion can consume the sentinel directly. Close removes listeners; your app must cancel its own frame loop.
|
|
85
92
|
|
|
86
|
-
`DeviceProfile` contains vendorId, productId, optional usagePage/usage, and `decode(reportId,data)`. An injected `hid` implements `HidAccess` for tests/custom transport integration. The default `combinedProfile` uses the verified IDs from the README. Do not infer additional device support from vendor ID alone.
|
|
93
|
+
`DeviceProfile` contains vendorId, productId, optional usagePage/usage, and `decode(reportId,data)`. An injected `hid` implements `HidAccess` for tests/custom transport integration. The default `combinedProfile` uses the verified IDs from the README. Do not infer additional device support from vendor ID alone.
|
package/docs/lab.md
CHANGED
|
@@ -25,3 +25,38 @@ provide their own storage. This example uses a same-origin session token for
|
|
|
25
25
|
writes, no CORS and no user authentication: keep it on loopback or behind a
|
|
26
26
|
trusted private proxy. Failed saves retain a retry/download backup. `pnpm
|
|
27
27
|
lab:analyze` reads the newest device recording, or accepts a JSON file path.
|
|
28
|
+
|
|
29
|
+
## Combined-gesture research
|
|
30
|
+
|
|
31
|
+
Open the lab with `?capture=press-move` for freeform push-move/pull-move capture.
|
|
32
|
+
Use natural pressure, including relaxation while rotating or tilting the cap.
|
|
33
|
+
Aim for three examples per push/pull × movement direction, plus ordinary single and
|
|
34
|
+
double presses for comparison; no required order or guided sequence. Multiple
|
|
35
|
+
captures are fine if two minutes is too short.
|
|
36
|
+
|
|
37
|
+
The recorder saves all six raw axes regardless of existing recognition gates.
|
|
38
|
+
The combined-tilt parser shows eight-direction coverage and six lanes for
|
|
39
|
+
pressure and tilt. Its tune preserves the base simple/double-press settings.
|
|
40
|
+
See the [combined gesture guide](press-tilt.md) and [1.0 criteria](roadmap.md).
|
|
41
|
+
|
|
42
|
+
For standalone tilt research, use `?capture=tilt`. Record at least three
|
|
43
|
+
singles and three doubles for each of the four rx/ry directions, without
|
|
44
|
+
deliberate push/pull or twist. Any order is fine. These captures are marked
|
|
45
|
+
separately and analyzed with the standalone tuner. Choose the Analyze selector
|
|
46
|
+
to override automatic family detection. Enable Combine with loaded recordings
|
|
47
|
+
before loading another partial capture. Use this tune enables the matching
|
|
48
|
+
family. The standalone toggle and push/pull mode can also be set independently.
|
|
49
|
+
|
|
50
|
+
## Verify capture before starting
|
|
51
|
+
|
|
52
|
+
Connect the device and move the cap once. Start recording remains disabled
|
|
53
|
+
until an actual device movement report arrives. During capture the report and
|
|
54
|
+
movement counts must rise when you move; all six axes are retained independently
|
|
55
|
+
of the gesture gates. Captures without movement are not saved as usable
|
|
56
|
+
recordings. Disconnect ends and saves a partial capture if movement was received.
|
|
57
|
+
The download backup remains available even for an empty diagnostic capture.
|
|
58
|
+
|
|
59
|
+
Simulator recording is only enabled explicitly with `?source=simulator` (or
|
|
60
|
+
`&source=simulator` after an existing query). It is labeled as simulator input
|
|
61
|
+
and is not physical calibration evidence. Reload after lab updates; a reload
|
|
62
|
+
requires reconnecting the device.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Standalone and combined tilts (0.3.0)
|
|
2
|
+
|
|
3
|
+
[Documentation](../README.md) · [Roadmap](roadmap.md)
|
|
4
|
+
|
|
5
|
+
These experimental APIs are opt-in. Existing simple gestures remain the default.
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
import {createGestures, defaultGestureTune} from '@clankagent/puck';
|
|
9
|
+
const gestures = createGestures({...defaultGestureTune.toOptions(), pressMode:'auto'});
|
|
10
|
+
// A combined event: {direction:'push', tilt:'rx+', kind:'single', timestamp, durationMs}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`pressMode` is `simple` (existing default), `auto` (choose plain or combined), or
|
|
14
|
+
`tilt` (combined actions plus plain doubles; suppress plain singles). `pushMode`
|
|
15
|
+
and `pullMode` override it independently. Combined modes require
|
|
16
|
+
`singleMode:'exclusive'`. The `rx+`, `rx-`, `ry+`, `ry-` labels are device-axis
|
|
17
|
+
directions; applications map these to their own screen orientation.
|
|
18
|
+
|
|
19
|
+
Pressure arms tilt detection. The cap may relax while tilting; it need not stay
|
|
20
|
+
above the pressure activation gate. A brief full release can still be joined
|
|
21
|
+
to a subsequent tilt. Sustained tilt on the strongest direction chooses the
|
|
22
|
+
action; a substantially stronger later direction can replace a preparatory
|
|
23
|
+
tilt. A pressure reversal cancels the excursion. Pre-existing tilt cannot arm a
|
|
24
|
+
combination. Neutral, interruption and maximum duration prevent repeated holds.
|
|
25
|
+
|
|
26
|
+
| Option | Default | Meaning |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `tiltActivation`, `tiltRelease` | .24, .115 | Normalized force gates |
|
|
29
|
+
| `tiltMinMs` | 25 | Short noise dwell, not a target execution speed |
|
|
30
|
+
| `tiltArmMs` | 450 | Maximum time from pressure to tilt onset |
|
|
31
|
+
| `tiltRelaxMs` | 180 | Maximum gap from pressure release to resumed tilt |
|
|
32
|
+
| `tiltMaxMs` | 1000 | Maximum combined excursion duration |
|
|
33
|
+
| `tiltDominance` | 1.25 | Required directional separation |
|
|
34
|
+
|
|
35
|
+
The reference input is treated as the slower end of normal. Faster movements
|
|
36
|
+
are accepted; the upper allowances are not mandatory waits. Combined events
|
|
37
|
+
emit on full release plus neutral dwell, without waiting for a plain double.
|
|
38
|
+
Repeated combinations are separate single actions; the recognizer does not pair
|
|
39
|
+
them into combined doubles. Ordinary doubles retain completion-to-completion
|
|
40
|
+
timing. With an unusually short `doubleMs`, plain singles wait at least
|
|
41
|
+
`tiltRelaxMs + tiltMinMs` to leave time for a possible combination.
|
|
42
|
+
|
|
43
|
+
## Tunes, recordings and graphs
|
|
44
|
+
|
|
45
|
+
Tune JSON version 1 accepts optional `pressTilt` containing `force` (a standard
|
|
46
|
+
force band), `minMs`, `armMs`, `relaxMs`, `maxMs`, and `dominance`. Old tunes
|
|
47
|
+
remain restorable. The default tune includes these settings, but does not
|
|
48
|
+
enable combined recognition. Immutable force edits also edit the tilt band;
|
|
49
|
+
timings stay unchanged. Save the mode separately from the tune.
|
|
50
|
+
|
|
51
|
+
`calibratePressTilts(recordingOrArray, {baseTune?, minimumPerAction?})` derives
|
|
52
|
+
tilt settings from raw z/rx/ry, independent of event labels and group order. It
|
|
53
|
+
requires at least three inferred examples of all eight push/pull × tilt
|
|
54
|
+
combinations. The result has `status`, `tune`, `counts`, `missing`, `actions`
|
|
55
|
+
and `issues`; only use a non-null tune. The base defaults to
|
|
56
|
+
`defaultGestureTune`; ordinary press/twist force and double timing are preserved.
|
|
57
|
+
No inferred count is a claim of labeled accuracy. Simulator input is useful
|
|
58
|
+
for tests, not physical evidence. The parser uses fixed exploratory boundaries
|
|
59
|
+
(.06 neutral, .25 tilt detection, 180 ms quiet gap); very different motion may
|
|
60
|
+
need manual review. It does not learn every timing value from one capture.
|
|
61
|
+
|
|
62
|
+
From `@clankagent/puck/graph`, `createPressTiltGraph(recording,tune,{actions,
|
|
63
|
+
recordingIndex?,start?,end?})` produces six lanes (push, pull, four tilts) for
|
|
64
|
+
`renderGestureGraphSvg`. Each combination marks both its pressure and tilt lane.
|
|
65
|
+
Select a short window to see pressure relaxation instead of comparing an
|
|
66
|
+
entire minute at once.
|
|
67
|
+
|
|
68
|
+
The session example accepts `options:{pressMode:'auto'}` and records the exact
|
|
69
|
+
configuration. Lifecycle reset still cancels all pending actions. Apps retain
|
|
70
|
+
ownership of camera arbitration, recording storage, clocks and rendering.
|
|
71
|
+
|
|
72
|
+
## Standalone directional singles and doubles
|
|
73
|
+
|
|
74
|
+
Enable `standaloneTilt:true` to recognize rx+/rx−/ry+/ry− without a deliberate
|
|
75
|
+
push, pull or twist. Events use `direction:'rx+'` (or another tilt direction)
|
|
76
|
+
and `kind:'single'|'double'`, with no `tilt` field. Signs are device axes, not
|
|
77
|
+
screen directions. Map them in the consuming app.
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
const gestures = createGestures({
|
|
81
|
+
...defaultGestureTune.toOptions(),
|
|
82
|
+
standaloneTilt: true,
|
|
83
|
+
pressMode: 'auto', // optional: also recognize pressure-first combinations
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The first qualifying motion owns the excursion: pressure-first can become a
|
|
88
|
+
combination; tilt-first remains standalone even if incidental pressure follows.
|
|
89
|
+
Neutral rx/ry completes a standalone pulse. Reversals without neutral cancel.
|
|
90
|
+
A double pairs completions within its own double window. Exclusive singles
|
|
91
|
+
wait; immediate singles are additive, as with ordinary presses. Combined modes
|
|
92
|
+
still require exclusive singles. Long holds and lifecycle resets do not emit taps.
|
|
93
|
+
|
|
94
|
+
| Standalone option | Default |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `tiltXActivation`, `tiltXRelease` | .13, .104 |
|
|
97
|
+
| `tiltYActivation`, `tiltYRelease` | .28, .16 |
|
|
98
|
+
| `standaloneMinPulseMs`, `standaloneMaxPulseMs` | 25, 650 |
|
|
99
|
+
| `standaloneNeutralMs`, `standaloneDoubleMs` | 10, 400 |
|
|
100
|
+
|
|
101
|
+
Tune version 1 also accepts optional `standaloneTilt:{rx,ry,timing}`. Each axis
|
|
102
|
+
has a force band shared by its positive/negative directions; timing contains
|
|
103
|
+
`minPulseMs,maxPulseMs,neutralMs,doubleMs`. Old JSON remains valid. Presets and
|
|
104
|
+
immutable edits include these bands; `toOptions()` supplies thresholds without
|
|
105
|
+
enabling recognition. Store your mode choices alongside the tune.
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
import {calibrateTilts, createGestureTune} from '@clankagent/puck';
|
|
109
|
+
import {createTiltGraph, renderGestureGraphSvg} from '@clankagent/puck/graph';
|
|
110
|
+
const result = calibrateTilts(recordings, {baseTune: defaultGestureTune});
|
|
111
|
+
if (result.tune) {
|
|
112
|
+
const saved = JSON.stringify(result.tune);
|
|
113
|
+
const restored = createGestureTune(JSON.parse(saved));
|
|
114
|
+
const gestures = createGestures({...restored.toOptions(), standaloneTilt:true});
|
|
115
|
+
}
|
|
116
|
+
// Display result.status, counts, missing and issues even when tune is null.
|
|
117
|
+
const model = createTiltGraph(recordings[0], result.tune ?? defaultGestureTune,
|
|
118
|
+
{actions:result.actions, recordingIndex:0});
|
|
119
|
+
const svg = renderGestureGraphSvg(model);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`calibrateTilts(recordingOrArray,{baseTune?,minimumPerAction?})` requires three
|
|
123
|
+
singles and three doubles per direction (eight action types), in any order.
|
|
124
|
+
It accepts 1–20 captures, preserves capture/reset boundaries, and ignores
|
|
125
|
+
pressure-first excursions. The result includes status, tune, counts, missing,
|
|
126
|
+
actions, stats and issues. Other gesture families retain their base settings.
|
|
127
|
+
Use one copy of each capture; duplicates are not new evidence. The parser uses
|
|
128
|
+
.06 exploratory neutral/detection and .15 early-pressure separation; review
|
|
129
|
+
unusual motion manually. Closely spaced singles may be inferred as doubles.
|
|
130
|
+
The graph shows four signed tilt lanes, force bands and inferred action spans.
|
|
131
|
+
These inferred labels are useful tuning evidence, not measured recognition accuracy.
|
package/docs/roadmap.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Toward Puck 1.0
|
|
2
|
+
|
|
3
|
+
[Documentation](../README.md) · [Changelog](../CHANGELOG.md)
|
|
4
|
+
|
|
5
|
+
1.0 means the public contracts and interaction behavior are dependable enough
|
|
6
|
+
for applications to adopt without chasing API changes. It does not mean every
|
|
7
|
+
SpaceMouse model or every possible gesture is supported.
|
|
8
|
+
|
|
9
|
+
## 0.3: tilt families, still experimental
|
|
10
|
+
|
|
11
|
+
0.3 adds push/pull + rx/ry tilt and standalone tilt singles/doubles, with
|
|
12
|
+
explicit arbitration and ordinary doubles preserved. Defaults use freeform
|
|
13
|
+
physical evidence, supplemented by synthetic boundary tests. This is enough
|
|
14
|
+
for an experimental release, not for 1.0: next evaluate unintended activations
|
|
15
|
+
during ordinary pan/zoom, repeat sessions, and real application adoption.
|
|
16
|
+
The contracts and calibration heuristics remain open to refinement.
|
|
17
|
+
|
|
18
|
+
## Requirements for 1.0
|
|
19
|
+
|
|
20
|
+
- **Stable contracts:** settle event shapes, press modes, tune editing and
|
|
21
|
+
persisted recording/tune formats. Define backward-compatible restoration and
|
|
22
|
+
errors, and publish a migration guide for any pre-1.0 changes.
|
|
23
|
+
- **Predictable interactions:** cover boundary timing, mixed gestures, diagonal
|
|
24
|
+
input, incidental cross-axis motion, holds, reversals, report silence,
|
|
25
|
+
cancellation and double presses. Identical reports must produce identical
|
|
26
|
+
events regardless of render cadence.
|
|
27
|
+
- **Physical evidence:** evaluate ordinary and combined gestures on real
|
|
28
|
+
recordings, including unintended activations during pan/zoom. Separate
|
|
29
|
+
inferred intent from known intended actions; publish the tested scope and
|
|
30
|
+
remaining uncertainty without shipping private captures.
|
|
31
|
+
- **Honest calibration:** provide useful coverage and ambiguity feedback for
|
|
32
|
+
every gesture family advertised as automatically tunable. Do not treat the
|
|
33
|
+
existing eight-action calibration as evidence for directional gestures.
|
|
34
|
+
- **Usable integration:** ship complete browser and transport-independent
|
|
35
|
+
examples, typed configuration, accessible recording graphs, lifecycle tests,
|
|
36
|
+
and clear guidance for keeping gesture actions and camera movement exclusive.
|
|
37
|
+
- **Release reliability:** verify CI, the packaged documentation and public
|
|
38
|
+
exports, then install and exercise the actual registry release. Keep changelog
|
|
39
|
+
entries linked to adoption documentation.
|
|
40
|
+
|
|
41
|
+
Additional device profiles, physical buttons, framework adapters and built-in
|
|
42
|
+
rendering are not prerequisites. Add them only for concrete, verified use cases.
|
|
43
|
+
Until these requirements are met, keep gesture APIs experimental and use 0.x
|
|
44
|
+
releases to refine them. There is no date-based promise for 1.0.
|
package/docs/troubleshooting.md
CHANGED
|
@@ -24,4 +24,4 @@
|
|
|
24
24
|
| Recorder stops adding entries | Inspect full; stop/save and start another capture. Defaults are two minutes / 50000 timeline entries. |
|
|
25
25
|
| Graph overlays wrong session | Pass the zero-based recordingIndex used in the combined calibration. |
|
|
26
26
|
|
|
27
|
-
When reporting a problem, include package version, runtime/browser, OS, device IDs and connection type, relevant configuration, expected/actual events and a minimal synthetic reproduction when possible. Remove personal information from any raw capture. See [contribution guidance](../CONTRIBUTING.md) for evidence needed for a new profile.
|
|
27
|
+
When reporting a problem, include package version, runtime/browser, OS, device IDs and connection type, relevant configuration, expected/actual events and a minimal synthetic reproduction when possible. Remove personal information from any raw capture. See [contribution guidance](../CONTRIBUTING.md) for evidence needed for a new profile.
|
package/docs/upgrading.md
CHANGED
|
@@ -9,14 +9,29 @@ Read the changelog for the version you are adopting, then use docs from that sam
|
|
|
9
9
|
Gesture, tune, recording, calibration and graph APIs are included in 0.2.0. In your consuming app:
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
pnpm add @clankagent/puck@0.
|
|
12
|
+
pnpm add @clankagent/puck@0.3.0
|
|
13
13
|
pnpm list @clankagent/puck
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
Commit your app's updated manifest and lockfile. Use the documentation shipped with that package or the v0.
|
|
16
|
+
Commit your app's updated manifest and lockfile. Use the documentation shipped with that package or the v0.3.0 source tag. The new gesture APIs remain experimental; existing motion integrations retain their behavior.
|
|
17
17
|
|
|
18
18
|
To test future unreleased source, build a chosen revision with `pnpm install --frozen-lockfile`, `pnpm check` and `pnpm pack --pack-destination artifacts`, then install the generated tarball into your app with `pnpm add /path/to/package.tgz`. Record the source commit alongside it; a working-tree package version alone does not identify unreleased changes.
|
|
19
19
|
|
|
20
|
+
## From 0.2 to 0.3
|
|
21
|
+
|
|
22
|
+
No new gesture family is enabled automatically. Add `standaloneTilt:true` for
|
|
23
|
+
rx/ry singles and doubles; add `pressMode:'auto'` or `'tilt'` for push/pull +
|
|
24
|
+
tilt combinations. Keep `singleMode:'exclusive'` when combined modes are enabled.
|
|
25
|
+
See the [tilt integration guide](press-tilt.md) for event examples, default gates,
|
|
26
|
+
freeform calibration and graphs. Feed every physical report and advance the clock
|
|
27
|
+
as before; camera arbitration remains application-owned.
|
|
28
|
+
|
|
29
|
+
`GestureDirection` now also includes four rx/ry directions. TypeScript consumers
|
|
30
|
+
with exhaustive direction maps must extend them, even if the feature stays off.
|
|
31
|
+
Use exported `PulseDirection` for APIs intentionally limited to the original four.
|
|
32
|
+
Combined events add an optional `tilt`; route those before handling plain pressure.
|
|
33
|
+
Tune version stays 1 with optional new fields; old tune JSON is supported.
|
|
34
|
+
|
|
20
35
|
## Existing pan/zoom applications
|
|
21
36
|
|
|
22
37
|
No migration is required for the new opt-in features: decoding, `createPanZoom` and `/webhid` behavior and defaults are unchanged from the original baseline. Keep your existing camera, input ownership and render loop.
|
|
@@ -41,4 +56,4 @@ To add gestures alongside motion, reuse the existing connection and frame loop.
|
|
|
41
56
|
- **Calibration quality:** require three singles and three doubles in each direction. Group order does not matter. Closely spaced singles can look like a double; inferred counts are not labeled accuracy. Avoid duplicate captures as new evidence.
|
|
42
57
|
- **Data versions:** package versions are separate from tune/recording `version: 1`. Validate restored data through public APIs; do not manually change a format version to bypass validation.
|
|
43
58
|
|
|
44
|
-
Before shipping, check a single, double, held input, neutral rearming, blur/disconnect and teardown. Check JSON restoration and incomplete calibration if used. [Troubleshooting](troubleshooting.md) maps common symptoms to fixes.
|
|
59
|
+
Before shipping, check a single, double, held input, neutral rearming, blur/disconnect and teardown. Check JSON restoration and incomplete calibration if used. [Troubleshooting](troubleshooting.md) maps common symptoms to fixes.
|
|
@@ -2,8 +2,9 @@ import { createGestures, createGestureRecorder, defaultGestureTune, neutralInput
|
|
|
2
2
|
import { connectWebHid } from '@clankagent/puck/webhid';
|
|
3
3
|
|
|
4
4
|
/** Call from a click handler. Returns null when the chooser is cancelled. */
|
|
5
|
-
export async function connectGestureSession({ tune = defaultGestureTune, onEvents = console.log, onDisconnect = () => {} } = {}) {
|
|
6
|
-
const
|
|
5
|
+
export async function connectGestureSession({ tune = defaultGestureTune, options = {}, onEvents = console.log, onDisconnect = () => {} } = {}) {
|
|
6
|
+
const configuration = {...tune.toOptions(),...options};
|
|
7
|
+
const gestures = createGestures(configuration);
|
|
7
8
|
let recorder = null;
|
|
8
9
|
let frameId;
|
|
9
10
|
let stopped = false;
|
|
@@ -35,7 +36,7 @@ export async function connectGestureSession({ tune = defaultGestureTune, onEvent
|
|
|
35
36
|
if (stopped) throw new Error('Session has ended.');
|
|
36
37
|
if (recorder) throw new Error('Stop the current recording first.');
|
|
37
38
|
gestures.reset();
|
|
38
|
-
recorder = createGestureRecorder({ startTimeMs: performance.now(),
|
|
39
|
+
recorder = createGestureRecorder({ startTimeMs: performance.now(), options:configuration, source: 'device' });
|
|
39
40
|
},
|
|
40
41
|
stopRecording() {
|
|
41
42
|
if (!recorder) return null;
|
package/llms.txt
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> TypeScript/ESM six-axis input, pan/zoom, tunable cap gestures and freeform calibration. Zero runtime dependencies.
|
|
4
4
|
|
|
5
|
-
Documentation targets 0.
|
|
5
|
+
Documentation targets 0.3.0, including experimental gesture/tune/recording/calibration/graph APIs. Match documentation to your installed version.
|
|
6
6
|
|
|
7
7
|
## Documentation
|
|
8
8
|
|
|
@@ -23,5 +23,7 @@ Documentation targets 0.2.0, including experimental gesture/tune/recording/calib
|
|
|
23
23
|
- [Contribution workflow](CONTRIBUTING.md)
|
|
24
24
|
- [Repository agent instructions](AGENTS.md)
|
|
25
25
|
- [Local gesture lab](docs/lab.md)
|
|
26
|
+
- [Next release and 1.0 criteria](docs/roadmap.md)
|
|
27
|
+
- [Standalone and combined tilt API](docs/press-tilt.md)
|
|
26
28
|
|
|
27
|
-
Exact build signatures are in dist/index.d.ts, dist/webhid.d.ts and dist/graph.d.ts. Import from @clankagent/puck, @clankagent/puck/webhid and @clankagent/puck/graph only.
|
|
29
|
+
Exact build signatures are in dist/index.d.ts, dist/webhid.d.ts and dist/graph.d.ts. Import from @clankagent/puck, @clankagent/puck/webhid and @clankagent/puck/graph only.
|