@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
package/AGENTS.md
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
# Package development
|
|
2
2
|
|
|
3
|
+
For requested Puck updates, complete implementation, tests, linked changelog/docs,
|
|
4
|
+
commit and push, CI verification, versioned npm publication, and a clean install
|
|
5
|
+
check before reporting completion. Stop only for an actual blocker or an explicit
|
|
6
|
+
source-only request.
|
|
7
|
+
|
|
3
8
|
Puck is a public library hosted at https://github.com/clankagent/puck, published as @clankagent/puck. Maintain the focused library scope: source, tests, documentation and small code examples. Do not add a website, test page, local server or router setup unless explicitly requested.
|
|
4
9
|
|
|
5
10
|
Keep the package self-contained. Do not introduce references to private applications, internal servers, local recordings, machine paths, or user metadata. No dependencies on surrounding workspaces. Preserve the established feel when extracting or optimizing; validate timing and neutral behavior with tests.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
User-facing changes, compatibility impact, and links for adopting them. **Unreleased** describes source on main, not an available npm version. Package versions and tune/recording JSON format versions are independent.
|
|
4
4
|
|
|
5
|
+
## 0.3.0 — 2026-09-19
|
|
6
|
+
|
|
7
|
+
- Added opt-in [standalone rx/ry singles and doubles and push/pull + tilt](docs/press-tilt.md).
|
|
8
|
+
Combined modes support pressure relaxation and preserve ordinary doubles.
|
|
9
|
+
Faster execution is accepted; timing allowances are upper bounds.
|
|
10
|
+
- Added `calibrateTilts`, `calibratePressTilts`, matching graph constructors,
|
|
11
|
+
and immutable, serializable tuning fields. Each family needs three examples
|
|
12
|
+
of its eight action types; freeform order and multiple captures are supported.
|
|
13
|
+
- Updated the lab with automatic family detection, an explicit analysis selector,
|
|
14
|
+
standalone controls, per-action inspection and reusable tune export.
|
|
15
|
+
- Fixed empty/disconnected captures being accepted as usable recordings. Partial
|
|
16
|
+
captures survive disconnect; simulator capture requires explicit test mode.
|
|
17
|
+
- Defined [1.0 readiness criteria](docs/roadmap.md). APIs remain experimental.
|
|
18
|
+
|
|
19
|
+
Existing gesture behavior is unchanged unless enabled. The TypeScript direction
|
|
20
|
+
union expands and combined events have an optional tilt field; see the
|
|
21
|
+
[upgrade guide](docs/upgrading.md#from-02-to-03). Tune/recording format stays 1,
|
|
22
|
+
and old tune JSON is supported. [Compare versions](https://github.com/clankagent/puck/compare/v0.2.0...v0.3.0).
|
|
23
|
+
|
|
5
24
|
## 0.2.0 — 2026-09-19
|
|
6
25
|
|
|
7
26
|
### Added
|
|
@@ -35,4 +54,4 @@ This is a source-history reference, not a claim that an npm release was publishe
|
|
|
35
54
|
- Optional WebHID connection, foreground lifecycle resets and cleanup.
|
|
36
55
|
- ESM and TypeScript declarations, no runtime dependencies.
|
|
37
56
|
|
|
38
|
-
[Baseline source](https://github.com/clankagent/puck/tree/f065d4a) · [Motion behavior and hardware](docs/motion.md)
|
|
57
|
+
[Baseline source](https://github.com/clankagent/puck/tree/f065d4a) · [Motion behavior and hardware](docs/motion.md)
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Six-axis input, responsive pan/zoom, and tunable single/double gestures for SpaceMouse applications. TypeScript, ESM, zero runtime dependencies. Your app owns its camera, rendering and storage.
|
|
4
4
|
|
|
5
|
-
**Version note:** these docs target 0.
|
|
5
|
+
**Version note:** these docs target 0.3.0. Gesture recognition, tunes, recording, calibration and graphs are included, with experimental APIs. See the [changelog](CHANGELOG.md) and [upgrade guide](docs/upgrading.md) when adopting updates.
|
|
6
6
|
|
|
7
7
|
## Start here
|
|
8
8
|
|
|
@@ -16,6 +16,8 @@ Six-axis input, responsive pan/zoom, and tunable single/double gestures for Spac
|
|
|
16
16
|
| Fix input or calibration problems | [Troubleshooting](docs/troubleshooting.md) |
|
|
17
17
|
| Integrate using an agent | [Agent integration guide](docs/agents.md), [plain-text index](llms.txt) |
|
|
18
18
|
| Try and record real input | [Gesture lab](docs/lab.md) |
|
|
19
|
+
| Understand the next release and 1.0 criteria | [Roadmap](docs/roadmap.md) |
|
|
20
|
+
| Add standalone or pressure-first tilts | [Push/pull + tilt](docs/press-tilt.md) |
|
|
19
21
|
|
|
20
22
|
For a registry release, install with `pnpm add @clankagent/puck`. For this checkout:
|
|
21
23
|
|
|
@@ -41,11 +43,11 @@ const gestures = createGestures(tune);
|
|
|
41
43
|
|---|---|
|
|
42
44
|
| Core `@clankagent/puck` | Decode reports; process motion and gestures; record samples; derive tunes. No browser globals, timers or storage. |
|
|
43
45
|
| Optional `/webhid` | Device chooser, reports and connection lifecycle. |
|
|
44
|
-
| Optional `/graph` |
|
|
46
|
+
| Optional `/graph` | Gesture-family graph data and a standalone SVG renderer. |
|
|
45
47
|
| Your app | Render loop, action handling, camera, capture controls, persistence and UI. |
|
|
46
48
|
|
|
47
49
|
The verified device profile is vendor `0x256f`, product `0xc63a`, Bluetooth on Windows. Other layouts and physical buttons are not implemented. Gestures are **cap pulses**, not button clicks. See [hardware scope](docs/motion.md#hardware-scope) before adding a profile.
|
|
48
50
|
|
|
49
51
|
## Development
|
|
50
52
|
|
|
51
|
-
Use Node 24 and the pnpm version pinned in `package.json`. `pnpm check` builds and runs tests. Read [CONTRIBUTING.md](CONTRIBUTING.md) before proposing an API or device profile; [AGENTS.md](AGENTS.md) describes repository development constraints. [DESIGN.md](DESIGN.md) explains architecture and [VERIFICATION.md](VERIFICATION.md) records validation. MIT licensed.
|
|
53
|
+
Use Node 24 and the pnpm version pinned in `package.json`. `pnpm check` builds and runs tests. Read [CONTRIBUTING.md](CONTRIBUTING.md) before proposing an API or device profile; [AGENTS.md](AGENTS.md) describes repository development constraints. [DESIGN.md](DESIGN.md) explains architecture and [VERIFICATION.md](VERIFICATION.md) records validation. MIT licensed.
|
package/VERIFICATION.md
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
- Fixed mixed report/rAF clock timestamps by using performance.now() in both
|
|
21
21
|
callbacks. Lifecycle neutral sentinels cancel rather than complete gestures.
|
|
22
22
|
- At this stage the gesture work was a local experiment. Later SDK and delivery
|
|
23
|
-
verification is recorded below; inferred replay does not establish subjective feel.
|
|
23
|
+
verification is recorded below; inferred replay does not establish subjective feel.
|
|
24
24
|
|
|
25
25
|
## Freeform recording (2026-09-18)
|
|
26
26
|
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
CLI. Replayed double matched the recorded event; a pending final single was
|
|
34
34
|
reported correctly. Simulator evidence is not physical gesture calibration.
|
|
35
35
|
- User feedback: vertical push/pull activation should be lower than rotation.
|
|
36
|
-
Capture raw data first, then infer suitable separate thresholds from natural use.
|
|
36
|
+
Capture raw data first, then infer suitable separate thresholds from natural use.
|
|
37
37
|
|
|
38
38
|
## Direction threshold refinement
|
|
39
39
|
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
- 33 tests pass. Synthetic regressions cover gentle vertical input, independent
|
|
45
45
|
signs, cross-axis pressure during twist, partial release and invalid thresholds.
|
|
46
46
|
- Physical freeform replay yields 18 doubles and 17 singles under the candidate
|
|
47
|
-
profile. These are inferred gesture groups, not ground-truth accuracy labels.
|
|
47
|
+
profile. These are inferred gesture groups, not ground-truth accuracy labels.
|
|
48
48
|
|
|
49
49
|
## Reusable tune / calibration SDK
|
|
50
50
|
|
|
@@ -76,3 +76,17 @@
|
|
|
76
76
|
reusable examples ship, while the lab server and raw recordings stay excluded.
|
|
77
77
|
- Gesture APIs remain experimental and are not yet an npm release. Source delivery
|
|
78
78
|
includes the gesture lab; captured device recordings remain outside the repository.
|
|
79
|
+
|
|
80
|
+
## 0.3 gesture scope
|
|
81
|
+
|
|
82
|
+
Synthetic tests cover four standalone directions, singles/doubles, brief neutral
|
|
83
|
+
valleys, pressure-first vs tilt-first ownership, reversals, holds, lifecycle
|
|
84
|
+
resets, multi-capture calibration and graph overlays. Combined recognition was
|
|
85
|
+
replayed against physical freeform input at original, 2× and 3× timestamps;
|
|
86
|
+
39 inferred combinations and four plain actions were preserved. The original
|
|
87
|
+
35-action simple reference remained unchanged with both families enabled.
|
|
88
|
+
Two standalone captures supply at least three inferred singles and doubles per
|
|
89
|
+
direction; interrupted/outlying excursions are excluded. Inferred labels are
|
|
90
|
+
not ground truth or an accuracy score. No new hardware compatibility is claimed.
|
|
91
|
+
Ordinary pan/zoom false-positive rates and multi-session robustness remain
|
|
92
|
+
1.0 validation work. Raw captures and user metadata are not distributed.
|
package/dist/calibration.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GestureDirection } from './gestures.js';
|
|
1
|
+
import type { PulseDirection as GestureDirection } from './gestures.js';
|
|
2
2
|
import type { GestureTune } from './tune.js';
|
|
3
3
|
import type { GestureRecording } from './recording.js';
|
|
4
4
|
export declare const gestureDirections: readonly ['clockwise', 'counterclockwise', 'push', 'pull'];
|
package/dist/calibration.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createGestureTune } from './tune.js';
|
|
|
2
2
|
import { validateGestureRecording } from './recording.js';
|
|
3
3
|
export const gestureDirections = ['clockwise', 'counterclockwise', 'push', 'pull'];
|
|
4
4
|
const quantile = (values, p) => { const a = [...values].sort((x, y) => x - y); const at = (a.length - 1) * p, lo = Math.floor(at), hi = Math.ceil(at); return a[lo] + (a[hi] - a[lo]) * (at - lo); };
|
|
5
|
-
const mean = (values) => values.reduce((a, b) => a + b, 0) / values.length;
|
|
5
|
+
const mean = (values) => values[0] + values.reduce((a, b) => a + (b - values[0]), 0) / values.length;
|
|
6
6
|
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
|
|
7
7
|
/** Infer actions from raw shape and timing, independent of event labels and action order.
|
|
8
8
|
* Closely spaced singles can be indistinguishable from doubles: inferred counts are not ground truth.
|
|
@@ -18,6 +18,10 @@ export function calibrateGestures(input, settings = {}) {
|
|
|
18
18
|
function episode(points, direction, recording, segment) {
|
|
19
19
|
if (points.length < 3 || points[0].v > floor || points[points.length - 1].v > floor)
|
|
20
20
|
return;
|
|
21
|
+
if ((direction === 'push' || direction === 'pull') && points.some(p => p.tilt >= .25)) {
|
|
22
|
+
issues.push('Excluded a press with tilt; use calibratePressTilts for combined gestures.');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
21
25
|
const peaks = [];
|
|
22
26
|
for (let i = 1; i < points.length - 1; i++)
|
|
23
27
|
if (points[i].v >= Math.max(.06, floor * 2.5) && points[i].v >= points[i - 1].v && points[i].v > points[i + 1].v)
|
|
@@ -74,7 +78,7 @@ export function calibrateGestures(input, settings = {}) {
|
|
|
74
78
|
}
|
|
75
79
|
if (row.type !== 'input')
|
|
76
80
|
continue;
|
|
77
|
-
const point = { t: row.t, v: Math.max(0, sign * (vertical ? row.input.z : row.input.rz)), other: Math.abs(vertical ? row.input.rz : row.input.z) };
|
|
81
|
+
const point = { t: row.t, v: Math.max(0, sign * (vertical ? row.input.z : row.input.rz)), other: Math.abs(vertical ? row.input.rz : row.input.z), tilt: Math.max(Math.abs(row.input.rx), Math.abs(row.input.ry)) };
|
|
78
82
|
if (point.v > floor) {
|
|
79
83
|
if (!run.length)
|
|
80
84
|
run = previous ? [{ ...previous, t: point.t }] : [];
|
package/dist/gestures.d.ts
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
import type { InputState } from './input.js';
|
|
2
2
|
import type { GestureTune } from './tune.js';
|
|
3
|
-
export type
|
|
3
|
+
export type PulseDirection = 'clockwise' | 'counterclockwise' | 'push' | 'pull';
|
|
4
|
+
export type TiltDirection = 'rx+' | 'rx-' | 'ry+' | 'ry-';
|
|
5
|
+
export type GestureDirection = PulseDirection | TiltDirection;
|
|
6
|
+
export type PressMode = 'simple' | 'tilt' | 'auto';
|
|
4
7
|
export interface GestureOptions {
|
|
8
|
+
standaloneTilt?: boolean;
|
|
9
|
+
tiltXActivation?: number;
|
|
10
|
+
tiltXRelease?: number;
|
|
11
|
+
tiltYActivation?: number;
|
|
12
|
+
tiltYRelease?: number;
|
|
13
|
+
standaloneMinPulseMs?: number;
|
|
14
|
+
standaloneMaxPulseMs?: number;
|
|
15
|
+
standaloneNeutralMs?: number;
|
|
16
|
+
standaloneDoubleMs?: number;
|
|
17
|
+
/** Simple preserves 0.2 behavior; tilt suppresses plain singles; auto chooses. Plain doubles work in all modes. */
|
|
18
|
+
pressMode?: PressMode;
|
|
19
|
+
pushMode?: PressMode;
|
|
20
|
+
pullMode?: PressMode;
|
|
21
|
+
tiltActivation?: number;
|
|
22
|
+
tiltRelease?: number;
|
|
23
|
+
tiltMinMs?: number;
|
|
24
|
+
tiltArmMs?: number;
|
|
25
|
+
tiltRelaxMs?: number;
|
|
26
|
+
tiltMaxMs?: number;
|
|
27
|
+
tiltDominance?: number;
|
|
5
28
|
activation?: number;
|
|
6
29
|
release?: number;
|
|
7
30
|
/** Optional axis-specific thresholds; fall back to activation/release. */
|
|
@@ -28,6 +51,8 @@ export interface GestureOptions {
|
|
|
28
51
|
}
|
|
29
52
|
export interface GestureEvent {
|
|
30
53
|
direction: GestureDirection;
|
|
54
|
+
/** Present for a combined push/pull + tilt. Such events have kind single. */
|
|
55
|
+
tilt?: TiltDirection;
|
|
31
56
|
kind: 'single' | 'double';
|
|
32
57
|
timestamp: number;
|
|
33
58
|
durationMs: number;
|
package/dist/gestures.js
CHANGED
|
@@ -8,18 +8,44 @@ export function createGestures(configuration = defaultGestureTune) {
|
|
|
8
8
|
const pressRelease = options.pressRelease ?? options.release;
|
|
9
9
|
const twistActivation = options.twistActivation ?? options.activation ?? defaultGestureTune.rotation.activation;
|
|
10
10
|
const twistRelease = options.twistRelease ?? options.release ?? defaultGestureTune.rotation.release;
|
|
11
|
+
const standalone = options.standaloneTilt ?? false;
|
|
12
|
+
const st = defaultGestureTune.standaloneTilt;
|
|
13
|
+
const tx = { activation: options.tiltXActivation ?? st.rx.activation, release: options.tiltXRelease ?? st.rx.release };
|
|
14
|
+
const ty = { activation: options.tiltYActivation ?? st.ry.activation, release: options.tiltYRelease ?? st.ry.release };
|
|
11
15
|
const thresholds = {
|
|
12
16
|
clockwise: { activation: options.clockwiseActivation ?? twistActivation, release: options.clockwiseRelease ?? twistRelease },
|
|
13
17
|
counterclockwise: { activation: options.counterclockwiseActivation ?? twistActivation, release: options.counterclockwiseRelease ?? twistRelease },
|
|
14
18
|
push: { activation: options.pushActivation ?? pressActivation ?? defaultGestureTune.push.activation, release: options.pushRelease ?? pressRelease ?? defaultGestureTune.push.release },
|
|
15
19
|
pull: { activation: options.pullActivation ?? pressActivation ?? defaultGestureTune.pull.activation, release: options.pullRelease ?? pressRelease ?? defaultGestureTune.pull.release },
|
|
20
|
+
'rx+': tx, 'rx-': tx, 'ry+': ty, 'ry-': ty,
|
|
16
21
|
};
|
|
17
22
|
const minPulseMs = options.minPulseMs ?? 35;
|
|
18
23
|
const maxPulseMs = options.maxPulseMs ?? 650;
|
|
19
24
|
const neutralMs = options.neutralMs ?? defaultGestureTune.timing.neutralMs;
|
|
20
25
|
const doubleMs = options.doubleMs ?? defaultGestureTune.timing.doubleMs;
|
|
21
26
|
const singleMode = options.singleMode ?? 'exclusive';
|
|
27
|
+
const standaloneMin = options.standaloneMinPulseMs ?? st.timing.minPulseMs, standaloneMax = options.standaloneMaxPulseMs ?? st.timing.maxPulseMs;
|
|
28
|
+
const standaloneNeutral = options.standaloneNeutralMs ?? st.timing.neutralMs, standaloneDouble = options.standaloneDoubleMs ?? st.timing.doubleMs;
|
|
29
|
+
if (typeof standalone !== 'boolean' || ![standaloneMin, standaloneMax, standaloneNeutral, standaloneDouble].every(Number.isFinite) || standaloneMin < 0 || standaloneMax < standaloneMin || standaloneNeutral < 0 || standaloneDouble < 0)
|
|
30
|
+
throw new RangeError('Invalid standalone tilt options.');
|
|
31
|
+
const isTilt = (d) => d.startsWith('rx') || d.startsWith('ry');
|
|
32
|
+
const dwell = (d) => isTilt(d) ? standaloneNeutral : neutralMs;
|
|
33
|
+
const doubleWindow = (d) => isTilt(d) ? standaloneDouble : doubleMs;
|
|
22
34
|
const dominance = options.dominance ?? 1.4;
|
|
35
|
+
const modes = { push: options.pushMode ?? options.pressMode ?? 'simple', pull: options.pullMode ?? options.pressMode ?? 'simple' };
|
|
36
|
+
const tiltActivation = options.tiltActivation ?? defaultGestureTune.pressTilt.force.activation, tiltRelease = options.tiltRelease ?? defaultGestureTune.pressTilt.force.release;
|
|
37
|
+
const tiltMinMs = options.tiltMinMs ?? 25, tiltArmMs = options.tiltArmMs ?? 450;
|
|
38
|
+
const tiltRelaxMs = options.tiltRelaxMs ?? 180, tiltMaxMs = options.tiltMaxMs ?? 1000;
|
|
39
|
+
const tiltDominance = options.tiltDominance ?? 1.25;
|
|
40
|
+
const combinedEnabled = modes.push !== 'simple' || modes.pull !== 'simple';
|
|
41
|
+
const tiltEnabled = combinedEnabled || standalone;
|
|
42
|
+
const tiltMode = (d) => (d === 'push' || d === 'pull') && modes[d] !== 'simple';
|
|
43
|
+
if ([options.pressMode, options.pushMode, options.pullMode].some(v => v !== undefined && !['simple', 'tilt', 'auto'].includes(v))
|
|
44
|
+
|| ![tiltActivation, tiltRelease, tiltMinMs, tiltArmMs, tiltRelaxMs, tiltMaxMs, tiltDominance].every(Number.isFinite)
|
|
45
|
+
|| tiltRelease < 0 || tiltActivation <= tiltRelease || tiltActivation > 1 || tiltDominance <= 1
|
|
46
|
+
|| tiltMinMs < 0 || tiltArmMs < tiltMinMs || tiltArmMs > tiltMaxMs || tiltMaxMs > 10000
|
|
47
|
+
|| tiltRelaxMs < 0 || tiltRelaxMs > tiltArmMs || (combinedEnabled && singleMode !== 'exclusive'))
|
|
48
|
+
throw new RangeError('Invalid press-tilt options; combined gestures require exclusive single mode.');
|
|
23
49
|
if (![activation, release, twistActivation, twistRelease, minPulseMs, maxPulseMs, neutralMs, doubleMs, dominance].every(Number.isFinite)
|
|
24
50
|
|| release < 0 || activation <= release || activation > 1 || minPulseMs < 0
|
|
25
51
|
|| twistRelease < 0 || twistActivation <= twistRelease || twistActivation > 1
|
|
@@ -28,13 +54,28 @@ export function createGestures(configuration = defaultGestureTune) {
|
|
|
28
54
|
|| !['exclusive', 'immediate'].includes(singleMode))
|
|
29
55
|
throw new RangeError('Invalid gesture options.');
|
|
30
56
|
let last = -Infinity;
|
|
31
|
-
let z = 0, rz = 0;
|
|
57
|
+
let z = 0, rz = 0, rx = 0, ry = 0;
|
|
32
58
|
let phase = 'blocked';
|
|
33
59
|
let active = null;
|
|
34
60
|
let releasedAt = null;
|
|
35
61
|
let pending = null;
|
|
36
|
-
const
|
|
37
|
-
|
|
62
|
+
const tiltNeutral = () => Math.max(Math.abs(rx), Math.abs(ry)) <= tiltRelease;
|
|
63
|
+
const standaloneRest = () => Math.abs(rx) <= tx.release && Math.abs(ry) <= ty.release;
|
|
64
|
+
const neutral = () => active && isTilt(active.direction) ? standaloneRest() : Math.abs(z) <= thresholds[z >= 0 ? 'push' : 'pull'].release
|
|
65
|
+
&& Math.abs(rz) <= thresholds[rz >= 0 ? 'clockwise' : 'counterclockwise'].release
|
|
66
|
+
&& (!(active ? tiltMode(active.direction) : combinedEnabled) || tiltNeutral())
|
|
67
|
+
&& (Boolean(active) || !standalone || standaloneRest());
|
|
68
|
+
const allowSingle = (e) => e.direction !== 'push' && e.direction !== 'pull' || modes[e.direction] !== 'tilt';
|
|
69
|
+
const pendingStart = () => pending.timestamp - neutralMs - pending.durationMs;
|
|
70
|
+
const canResume = (t) => pending && tiltMode(pending.direction) && t <= pending.timestamp - neutralMs + tiltRelaxMs && t <= pendingStart() + tiltArmMs;
|
|
71
|
+
const deadline = (e) => e.timestamp + (tiltMode(e.direction) ? Math.max(doubleMs, tiltRelaxMs + tiltMinMs) : doubleWindow(e.direction));
|
|
72
|
+
function tiltDirection() {
|
|
73
|
+
if (Math.abs(rx) >= tiltActivation && Math.abs(rx) >= Math.abs(ry) * tiltDominance)
|
|
74
|
+
return rx > 0 ? 'rx+' : 'rx-';
|
|
75
|
+
if (Math.abs(ry) >= tiltActivation && Math.abs(ry) >= Math.abs(rx) * tiltDominance)
|
|
76
|
+
return ry > 0 ? 'ry+' : 'ry-';
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
38
79
|
function clock(t) {
|
|
39
80
|
if (!Number.isFinite(t) || t < last)
|
|
40
81
|
throw new RangeError('Gesture timestamps must be finite and monotonic.');
|
|
@@ -42,20 +83,36 @@ export function createGestures(configuration = defaultGestureTune) {
|
|
|
42
83
|
}
|
|
43
84
|
function tick(t) {
|
|
44
85
|
const events = [];
|
|
45
|
-
if (
|
|
86
|
+
if (active?.candidate && active.candidateAt !== undefined && t >= active.candidateAt + tiltMinMs
|
|
87
|
+
&& (!active.tilt || active.candidate === active.tilt || active.candidatePeak > active.tiltPeak * tiltDominance)) {
|
|
88
|
+
if (canResume(active.candidateAt) && pending.direction === active.direction) {
|
|
89
|
+
active.start = pendingStart();
|
|
90
|
+
pending = null;
|
|
91
|
+
}
|
|
92
|
+
active.tilt = active.candidate;
|
|
93
|
+
active.tiltPeak = Math.max(active.tiltPeak ?? 0, active.candidatePeak ?? 0);
|
|
94
|
+
}
|
|
95
|
+
if (phase === 'releasing' && releasedAt !== null && active && t >= releasedAt + dwell(active.direction)) {
|
|
46
96
|
const durationMs = releasedAt - active.start;
|
|
47
|
-
const completedAt = releasedAt +
|
|
48
|
-
if (
|
|
97
|
+
const completedAt = releasedAt + dwell(active.direction);
|
|
98
|
+
if (active.tilt && durationMs <= tiltMaxMs) {
|
|
99
|
+
// Combined gestures consume one excursion; repeats are separate actions, not tilt doubles.
|
|
100
|
+
if (pending && allowSingle(pending))
|
|
101
|
+
events.push({ ...pending, timestamp: Math.min(completedAt, deadline(pending)) });
|
|
102
|
+
pending = null;
|
|
103
|
+
events.push({ direction: active.direction, tilt: active.tilt, kind: 'single', timestamp: completedAt, durationMs });
|
|
104
|
+
}
|
|
105
|
+
else if (!active.borrowed && !active.attemptedTilt && durationMs >= (isTilt(active.direction) ? standaloneMin : minPulseMs) && durationMs <= (isTilt(active.direction) ? standaloneMax : maxPulseMs)) {
|
|
49
106
|
const pulse = { direction: active.direction, kind: 'single', timestamp: completedAt, durationMs };
|
|
50
|
-
if (pending && pending.direction === pulse.direction && completedAt - pending.timestamp <=
|
|
107
|
+
if (pending && pending.direction === pulse.direction && completedAt - pending.timestamp <= doubleWindow(pulse.direction)) {
|
|
51
108
|
events.push({ ...pulse, kind: 'double' });
|
|
52
109
|
pending = null;
|
|
53
110
|
}
|
|
54
111
|
else {
|
|
55
|
-
if (pending && singleMode === 'exclusive')
|
|
56
|
-
events.push({ ...pending, timestamp: Math.min(completedAt, pending
|
|
112
|
+
if (pending && singleMode === 'exclusive' && allowSingle(pending))
|
|
113
|
+
events.push({ ...pending, timestamp: Math.min(completedAt, deadline(pending)) });
|
|
57
114
|
pending = pulse;
|
|
58
|
-
if (singleMode === 'immediate')
|
|
115
|
+
if (singleMode === 'immediate' && allowSingle(pulse))
|
|
59
116
|
events.push(pulse);
|
|
60
117
|
}
|
|
61
118
|
}
|
|
@@ -63,36 +120,65 @@ export function createGestures(configuration = defaultGestureTune) {
|
|
|
63
120
|
releasedAt = null;
|
|
64
121
|
phase = 'neutral';
|
|
65
122
|
}
|
|
66
|
-
if (active && phase === 'active' && t - active.start > maxPulseMs) {
|
|
123
|
+
if (active && phase === 'active' && t - active.start > (isTilt(active.direction) ? standaloneMax : active.tilt || active.candidate ? tiltMaxMs : maxPulseMs)) {
|
|
67
124
|
phase = 'blocked';
|
|
68
125
|
active = null;
|
|
69
126
|
}
|
|
70
127
|
// A double is defined by completion-to-completion time, including neutral dwell.
|
|
71
|
-
if (pending && t > pending
|
|
72
|
-
if (singleMode === 'exclusive')
|
|
73
|
-
events.push({ ...pending, timestamp: pending
|
|
128
|
+
if (pending && t > deadline(pending)) {
|
|
129
|
+
if (singleMode === 'exclusive' && allowSingle(pending))
|
|
130
|
+
events.push({ ...pending, timestamp: deadline(pending) });
|
|
74
131
|
pending = null;
|
|
75
132
|
}
|
|
76
133
|
return events;
|
|
77
134
|
}
|
|
78
135
|
return {
|
|
79
136
|
get state() { return { phase, direction: active?.direction ?? null, pending: pending?.direction ?? null }; },
|
|
80
|
-
reset() { last = -Infinity; z = rz = 0; active = pending = null; releasedAt = null; phase = 'blocked'; },
|
|
137
|
+
reset() { last = -Infinity; z = rz = rx = ry = 0; active = pending = null; releasedAt = null; phase = 'blocked'; },
|
|
81
138
|
advance(t) { clock(t); return tick(t); },
|
|
82
139
|
update(input, t) {
|
|
83
|
-
if (!
|
|
140
|
+
if (![input.z, input.rz, ...(tiltEnabled ? [input.rx, input.ry] : [])].every(Number.isFinite))
|
|
84
141
|
throw new RangeError('Gesture axes must be finite.');
|
|
85
142
|
clock(t);
|
|
86
143
|
const events = tick(t);
|
|
144
|
+
const tiltWasBelowActivation = Math.max(Math.abs(rx), Math.abs(ry)) < tiltActivation;
|
|
87
145
|
z = Math.max(-1, Math.min(1, input.z));
|
|
88
146
|
rz = Math.max(-1, Math.min(1, input.rz));
|
|
147
|
+
if (tiltEnabled) {
|
|
148
|
+
rx = Math.max(-1, Math.min(1, input.rx));
|
|
149
|
+
ry = Math.max(-1, Math.min(1, input.ry));
|
|
150
|
+
}
|
|
89
151
|
if (phase === 'blocked') {
|
|
90
152
|
if (neutral())
|
|
91
153
|
phase = 'neutral';
|
|
92
154
|
return events;
|
|
93
155
|
}
|
|
156
|
+
const side = tiltEnabled ? tiltDirection() : undefined;
|
|
157
|
+
if (!active && side && canResume(t)) {
|
|
158
|
+
active = { direction: pending.direction, start: pendingStart(), borrowed: true, tiltEligible: true };
|
|
159
|
+
phase = 'active';
|
|
160
|
+
}
|
|
94
161
|
if (active) {
|
|
95
|
-
|
|
162
|
+
if (tiltMode(active.direction) && active.tiltEligible) {
|
|
163
|
+
if (Math.max(Math.abs(rx), Math.abs(ry)) >= tiltActivation)
|
|
164
|
+
active.attemptedTilt = true;
|
|
165
|
+
if (side && (active.tilt || active.candidate === side || t - active.start <= tiltArmMs)) {
|
|
166
|
+
const strength = Math.abs(side.startsWith('rx') ? rx : ry);
|
|
167
|
+
if (active.candidate !== side) {
|
|
168
|
+
active.candidate = side;
|
|
169
|
+
active.candidateAt = t;
|
|
170
|
+
active.candidatePeak = strength;
|
|
171
|
+
}
|
|
172
|
+
else
|
|
173
|
+
active.candidatePeak = Math.max(active.candidatePeak ?? 0, strength);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
active.candidate = undefined;
|
|
177
|
+
active.candidateAt = undefined;
|
|
178
|
+
active.candidatePeak = undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const signed = active.direction === 'push' ? z : active.direction === 'pull' ? -z : active.direction === 'clockwise' ? rz : active.direction === 'counterclockwise' ? -rz : (active.direction.startsWith('rx') ? rx : ry) * (active.direction.endsWith('+') ? 1 : -1);
|
|
96
182
|
if (neutral()) {
|
|
97
183
|
if (releasedAt === null)
|
|
98
184
|
releasedAt = t;
|
|
@@ -111,11 +197,30 @@ export function createGestures(configuration = defaultGestureTune) {
|
|
|
111
197
|
}
|
|
112
198
|
else {
|
|
113
199
|
const vertical = Math.abs(z) >= Math.abs(rz);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
200
|
+
let strong = vertical ? Math.abs(z) : Math.abs(rz);
|
|
201
|
+
let weak = vertical ? Math.abs(rz) : Math.abs(z);
|
|
202
|
+
let direction = vertical ? (z > 0 ? 'push' : 'pull') : (rz > 0 ? 'clockwise' : 'counterclockwise');
|
|
203
|
+
if (standalone) {
|
|
204
|
+
const axis = Math.abs(rx) >= Math.abs(ry) ? 'rx' : 'ry';
|
|
205
|
+
const sideStrength = Math.abs(axis === 'rx' ? rx : ry);
|
|
206
|
+
if (sideStrength >= (axis === 'rx' ? tx.activation : ty.activation)) {
|
|
207
|
+
if (sideStrength > strong) {
|
|
208
|
+
weak = Math.max(strong, Math.abs(axis === 'rx' ? ry : rx));
|
|
209
|
+
strong = sideStrength;
|
|
210
|
+
direction = (axis + ((axis === 'rx' ? rx : ry) > 0 ? '+' : '-'));
|
|
211
|
+
}
|
|
212
|
+
else
|
|
213
|
+
weak = Math.max(weak, sideStrength);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
117
216
|
if (strong >= thresholds[direction].activation && strong >= weak * dominance) {
|
|
118
|
-
active = { direction, start: t };
|
|
217
|
+
active = { direction, start: t, tiltEligible: tiltWasBelowActivation };
|
|
218
|
+
if (tiltMode(direction) && active.tiltEligible && side) {
|
|
219
|
+
active.candidate = side;
|
|
220
|
+
active.candidateAt = t;
|
|
221
|
+
active.candidatePeak = Math.abs(side.startsWith('rx') ? rx : ry);
|
|
222
|
+
active.attemptedTilt = true;
|
|
223
|
+
}
|
|
119
224
|
phase = 'active';
|
|
120
225
|
}
|
|
121
226
|
}
|
package/dist/graph.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { GestureDirection } from './gestures.js';
|
|
1
|
+
import type { PulseDirection as GestureDirection, TiltDirection } from './gestures.js';
|
|
2
|
+
import type { PressTiltAction } from './press-tilt-calibration.js';
|
|
3
|
+
import type { TiltCalibrationAction } from './tilt-calibration.js';
|
|
2
4
|
import type { GestureTune } from './tune.js';
|
|
3
5
|
import type { GestureRecording } from './recording.js';
|
|
4
6
|
import type { CalibrationAction } from './calibration.js';
|
|
5
7
|
export interface GestureGraphLane {
|
|
6
|
-
direction: GestureDirection;
|
|
8
|
+
direction: GestureDirection | TiltDirection;
|
|
7
9
|
label: string;
|
|
8
10
|
center: number;
|
|
9
11
|
low: number;
|
|
@@ -18,6 +20,7 @@ export interface GestureGraphLane {
|
|
|
18
20
|
start: number;
|
|
19
21
|
end: number;
|
|
20
22
|
kind: 'single' | 'double';
|
|
23
|
+
label?: string;
|
|
21
24
|
}[];
|
|
22
25
|
}
|
|
23
26
|
export interface GestureGraph {
|
|
@@ -32,6 +35,20 @@ export declare function createGestureGraph(recording: GestureRecording, tune?: G
|
|
|
32
35
|
actions?: readonly CalibrationAction[];
|
|
33
36
|
recordingIndex?: number;
|
|
34
37
|
}): GestureGraph;
|
|
38
|
+
/** Pressure and tilt in six separate lanes, with each inferred combination labeled on both. */
|
|
39
|
+
export declare function createPressTiltGraph(recording: GestureRecording, tune?: GestureTune, config?: {
|
|
40
|
+
start?: number;
|
|
41
|
+
end?: number;
|
|
42
|
+
actions?: readonly PressTiltAction[];
|
|
43
|
+
recordingIndex?: number;
|
|
44
|
+
}): GestureGraph;
|
|
45
|
+
/** Four standalone tilt lanes with independent rx/ry bands and single/double action spans. */
|
|
46
|
+
export declare function createTiltGraph(recording: GestureRecording, tune?: GestureTune, config?: {
|
|
47
|
+
start?: number;
|
|
48
|
+
end?: number;
|
|
49
|
+
actions?: readonly TiltCalibrationAction[];
|
|
50
|
+
recordingIndex?: number;
|
|
51
|
+
}): GestureGraph;
|
|
35
52
|
/** Standalone accessible SVG. No DOM, canvas, frameworks or global listeners. */
|
|
36
53
|
export declare function renderGestureGraphSvg(graph: GestureGraph, options?: {
|
|
37
54
|
width?: number;
|
package/dist/graph.js
CHANGED
|
@@ -24,7 +24,59 @@ export function createGestureGraph(recording, tune = defaultGestureTune, config
|
|
|
24
24
|
});
|
|
25
25
|
return { start, end, lanes };
|
|
26
26
|
}
|
|
27
|
+
/** Pressure and tilt in six separate lanes, with each inferred combination labeled on both. */
|
|
28
|
+
export function createPressTiltGraph(recording, tune = defaultGestureTune, config = {}) {
|
|
29
|
+
const graph = createGestureGraph(recording, tune, { start: config.start, end: config.end });
|
|
30
|
+
const actions = (config.actions ?? []).filter(a => a.recording === (config.recordingIndex ?? 0) && a.end >= graph.start && a.start <= graph.end);
|
|
31
|
+
graph.lanes = graph.lanes.filter(l => l.direction === 'push' || l.direction === 'pull');
|
|
32
|
+
for (const direction of ['rx+', 'rx-', 'ry+', 'ry-']) {
|
|
33
|
+
const axis = direction.startsWith('rx') ? 'rx' : 'ry', sign = direction.endsWith('+') ? 1 : -1;
|
|
34
|
+
const points = [];
|
|
35
|
+
let held = 0;
|
|
36
|
+
for (const row of recording.timeline) {
|
|
37
|
+
if (row.t > graph.end)
|
|
38
|
+
break;
|
|
39
|
+
if (row.type === 'input')
|
|
40
|
+
held = Math.max(0, row.input[axis] * sign);
|
|
41
|
+
else if (row.type === 'reset')
|
|
42
|
+
held = 0;
|
|
43
|
+
else
|
|
44
|
+
continue;
|
|
45
|
+
if (row.t < graph.start)
|
|
46
|
+
continue;
|
|
47
|
+
if (!points.length)
|
|
48
|
+
points.push({ t: graph.start, value: 0 });
|
|
49
|
+
points.push({ t: row.t, value: held });
|
|
50
|
+
}
|
|
51
|
+
// Seed the left edge with the held sample preceding the requested window.
|
|
52
|
+
let before = 0;
|
|
53
|
+
for (const row of recording.timeline) {
|
|
54
|
+
if (row.t >= graph.start)
|
|
55
|
+
break;
|
|
56
|
+
if (row.type === 'input')
|
|
57
|
+
before = Math.max(0, row.input[axis] * sign);
|
|
58
|
+
else if (row.type === 'reset')
|
|
59
|
+
before = 0;
|
|
60
|
+
}
|
|
61
|
+
if (!points.length)
|
|
62
|
+
points.push({ t: graph.start, value: before });
|
|
63
|
+
else
|
|
64
|
+
points[0].value = before;
|
|
65
|
+
points.push({ t: graph.end, value: held });
|
|
66
|
+
graph.lanes.push({ direction, label: `Tilt ${direction} (device axis)`, ...(tune.pressTilt ?? defaultGestureTune.pressTilt).force, points, actions: [] });
|
|
67
|
+
}
|
|
68
|
+
for (const lane of graph.lanes)
|
|
69
|
+
lane.actions = actions.filter(a => a.direction === lane.direction || a.tilt === lane.direction).map(a => ({ start: a.start, end: a.end, kind: 'single', label: `${a.direction} + ${a.tilt}` }));
|
|
70
|
+
return graph;
|
|
71
|
+
}
|
|
27
72
|
const escape = (value) => value.replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
73
|
+
/** Four standalone tilt lanes with independent rx/ry bands and single/double action spans. */
|
|
74
|
+
export function createTiltGraph(recording, tune = defaultGestureTune, config = {}) {
|
|
75
|
+
const graph = createPressTiltGraph(recording, tune, { start: config.start, end: config.end });
|
|
76
|
+
const bands = tune.standaloneTilt ?? defaultGestureTune.standaloneTilt;
|
|
77
|
+
graph.lanes = graph.lanes.filter(l => l.direction.startsWith('r')).map(l => ({ ...l, ...(l.direction.startsWith('rx') ? bands.rx : bands.ry), actions: (config.actions ?? []).filter(a => a.recording === (config.recordingIndex ?? 0) && a.direction === l.direction && a.end >= graph.start && a.start <= graph.end).map(a => ({ start: a.start, end: a.end, kind: a.kind })) }));
|
|
78
|
+
return graph;
|
|
79
|
+
}
|
|
28
80
|
/** Standalone accessible SVG. No DOM, canvas, frameworks or global listeners. */
|
|
29
81
|
export function renderGestureGraphSvg(graph, options = {}) {
|
|
30
82
|
const width = options.width ?? 960;
|
|
@@ -32,15 +84,15 @@ export function renderGestureGraphSvg(graph, options = {}) {
|
|
|
32
84
|
throw new RangeError('SVG width must be between 280 and 4000.');
|
|
33
85
|
const height = graph.lanes.length * 150 + 64, left = 45, right = 20, plotWidth = width - left - right;
|
|
34
86
|
const x = (t) => left + (Math.max(graph.start, Math.min(graph.end, t)) - graph.start) / (graph.end - graph.start) * plotWidth;
|
|
35
|
-
const svg = [`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" role="img" aria-label="${escape(options.title ?? 'Gesture recording:
|
|
87
|
+
const svg = [`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" role="img" aria-label="${escape(options.title ?? 'Gesture recording: independent input directions')}"><desc>Blue is input strength, green dashed is activation, gray dotted is release, pale blue is the typical peak range. Shaded time spans mark inferred actions. Stronger inputs remain accepted above the typical range.</desc><rect width="100%" height="100%" fill="white"/><g font-family="Segoe UI,Arial,sans-serif" font-size="12" fill="#283c55">`];
|
|
36
88
|
graph.lanes.forEach((lane, index) => {
|
|
37
89
|
const top = 30 + index * 150, y = (v) => top + 100 - Math.max(0, Math.min(1, v)) * 76;
|
|
38
90
|
svg.push(`<text x="${left}" y="${top}" font-size="15" font-weight="600">${escape(lane.label)}</text><text x="${width - right}" y="${top}" text-anchor="end" font-size="11">peak ${lane.center.toFixed(2)}</text><text x="${left}" y="${top + 19}">Activate ${lane.activation.toFixed(2)} · release ${lane.release.toFixed(2)}</text>`);
|
|
39
91
|
svg.push(`<rect x="${left}" y="${y(lane.high)}" width="${plotWidth}" height="${y(lane.low) - y(lane.high)}" fill="#edf3fc"/>`);
|
|
40
92
|
for (const a of lane.actions) {
|
|
41
|
-
svg.push(`<rect x="${x(a.start)}" y="${top + 24}" width="${Math.max(2, x(a.end) - x(a.start))}" height="76" fill="#cfdfef" opacity=".45"
|
|
42
|
-
if (x(a.end) - x(a.start) > 42)
|
|
43
|
-
svg.push(`<text x="${x(a.start) + 3}" y="${top + 116}" font-size="11">${a.kind}</text>`);
|
|
93
|
+
svg.push(`<rect x="${x(a.start)}" y="${top + 24}" width="${Math.max(2, x(a.end) - x(a.start))}" height="76" fill="#cfdfef" opacity=".45"><title>${escape(a.label ?? a.kind)}</title></rect>`);
|
|
94
|
+
if (x(a.end) - x(a.start) > (a.label ? 90 : 42))
|
|
95
|
+
svg.push(`<text x="${x(a.start) + 3}" y="${top + 116}" font-size="11">${escape(a.label ?? a.kind)}</text>`);
|
|
44
96
|
}
|
|
45
97
|
for (const [v, color, dash] of [[0, '#afbac8', ''], [lane.activation, '#237442', '6 4'], [lane.release, '#68778b', '2 4']])
|
|
46
98
|
svg.push(`<line x1="${left}" x2="${width - right}" y1="${y(v)}" y2="${y(v)}" stroke="${color}" stroke-dasharray="${dash}"/>`);
|
package/dist/index.d.ts
CHANGED
|
@@ -3,10 +3,14 @@ export type { InputState } from './input.js';
|
|
|
3
3
|
export { createPanZoom } from './motion.js';
|
|
4
4
|
export type { PanZoomController, PanZoomOptions, MotionDelta, ZoomInput } from './motion.js';
|
|
5
5
|
export { createGestures } from './gestures.js';
|
|
6
|
-
export type { GestureDirection, GestureOptions, GestureEvent, GestureRecognizer } from './gestures.js';
|
|
6
|
+
export type { GestureDirection, PulseDirection, TiltDirection, PressMode, GestureOptions, GestureEvent, GestureRecognizer } from './gestures.js';
|
|
7
7
|
export { createGestureTune, defaultGestureTune, gesturePresets } from './tune.js';
|
|
8
|
-
export type { GestureTune, GestureTuneData, ForceBand } from './tune.js';
|
|
8
|
+
export type { GestureTune, GestureTuneData, ForceBand, PressTiltTune, StandaloneTiltTune } from './tune.js';
|
|
9
9
|
export { createGestureRecorder, validateGestureRecording } from './recording.js';
|
|
10
10
|
export type { GestureRecorder, GestureRecording, RecordingEntry } from './recording.js';
|
|
11
11
|
export { calibrateGestures, gestureDirections } from './calibration.js';
|
|
12
12
|
export type { GestureCalibration, CalibrationAction, CalibrationPulse, CalibrationStats, GestureActionName } from './calibration.js';
|
|
13
|
+
export { calibratePressTilts, tiltDirections } from './press-tilt-calibration.js';
|
|
14
|
+
export type { PressTiltCalibration, PressTiltAction, PressTiltActionName } from './press-tilt-calibration.js';
|
|
15
|
+
export { calibrateTilts } from './tilt-calibration.js';
|
|
16
|
+
export type { TiltCalibration, TiltCalibrationAction, TiltActionName } from './tilt-calibration.js';
|
package/dist/index.js
CHANGED
|
@@ -4,3 +4,5 @@ export { createGestures } from './gestures.js';
|
|
|
4
4
|
export { createGestureTune, defaultGestureTune, gesturePresets } from './tune.js';
|
|
5
5
|
export { createGestureRecorder, validateGestureRecording } from './recording.js';
|
|
6
6
|
export { calibrateGestures, gestureDirections } from './calibration.js';
|
|
7
|
+
export { calibratePressTilts, tiltDirections } from './press-tilt-calibration.js';
|
|
8
|
+
export { calibrateTilts } from './tilt-calibration.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { TiltDirection } from './gestures.js';
|
|
2
|
+
import type { GestureRecording } from './recording.js';
|
|
3
|
+
import type { GestureTune } from './tune.js';
|
|
4
|
+
export declare const tiltDirections: readonly ['rx+', 'rx-', 'ry+', 'ry-'];
|
|
5
|
+
export type PressTiltActionName = `${'push' | 'pull'}.${TiltDirection}`;
|
|
6
|
+
export interface PressTiltAction {
|
|
7
|
+
direction: 'push' | 'pull';
|
|
8
|
+
tilt: TiltDirection;
|
|
9
|
+
kind: 'single';
|
|
10
|
+
recording: number;
|
|
11
|
+
start: number;
|
|
12
|
+
end: number;
|
|
13
|
+
pressurePeak: number;
|
|
14
|
+
tiltPeak: number;
|
|
15
|
+
pressureAtTiltPeak: number;
|
|
16
|
+
onsetDelayMs: number;
|
|
17
|
+
}
|
|
18
|
+
export interface PressTiltCalibration {
|
|
19
|
+
status: 'ready' | 'incomplete' | 'ambiguous';
|
|
20
|
+
tune: GestureTune | null;
|
|
21
|
+
counts: Record<PressTiltActionName, number>;
|
|
22
|
+
missing: PressTiltActionName[];
|
|
23
|
+
actions: PressTiltAction[];
|
|
24
|
+
issues: string[];
|
|
25
|
+
}
|
|
26
|
+
/** Infer combined single excursions from raw z/rx/ry. Preserves the base simple/double tune.
|
|
27
|
+
* Coverage is inferred evidence, not ground-truth labels or measured classification accuracy.
|
|
28
|
+
*/
|
|
29
|
+
export declare function calibratePressTilts(input: GestureRecording | readonly GestureRecording[], settings?: {
|
|
30
|
+
baseTune?: GestureTune;
|
|
31
|
+
minimumPerAction?: number;
|
|
32
|
+
}): PressTiltCalibration;
|