@glassly/engine 0.3.3-dev.621 → 0.3.3-dev.622
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/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/GestureDotsService.d.ts +2 -0
- package/build/services/GestureDotsService.d.ts.map +1 -1
- package/build/services/GestureDotsService.js +25 -9
- package/build/services/GestureDotsService.js.map +1 -1
- package/build/services/GlassesTouchInput.d.ts.map +1 -1
- package/build/services/GlassesTouchInput.js +4 -2
- package/build/services/GlassesTouchInput.js.map +1 -1
- package/package.json +7 -7
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/services/GestureDotsService.ts +20 -7
- package/src/services/GlassesTouchInput.ts +4 -2
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const ENGINE_RELEASE_METADATA = Object.freeze({
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"familyBaseVersion": "0.3.3",
|
|
4
|
-
"releaseIdentity": "0.3.3-dev.
|
|
5
|
-
"releaseSetId": "glassly-0.3.3-dev.
|
|
6
|
-
"sourceCommit": "
|
|
4
|
+
"releaseIdentity": "0.3.3-dev.622",
|
|
5
|
+
"releaseSetId": "glassly-0.3.3-dev.622",
|
|
6
|
+
"sourceCommit": "4f871d45d1403ad2c0dde01b58cf893f1a0e6ff8",
|
|
7
7
|
"otaManifestUrl": null,
|
|
8
8
|
"otaManifestSha256": null
|
|
9
9
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,uBAAuB,GAAoC,MAAM,CAAC,MAAM,CAAC;IACpF,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,eAAe;IAClC,cAAc,EAAE,uBAAuB;IACvC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,IAAI;IACtB,mBAAmB,EAAE,IAAI;CAC1B,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface EngineReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"0.3.3\",\n \"releaseIdentity\": \"0.3.3-dev.
|
|
1
|
+
{"version":3,"file":"releaseMetadata.js","sourceRoot":"","sources":["../../src/generated/releaseMetadata.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,uBAAuB,GAAoC,MAAM,CAAC,MAAM,CAAC;IACpF,eAAe,EAAE,CAAC;IAClB,mBAAmB,EAAE,OAAO;IAC5B,iBAAiB,EAAE,eAAe;IAClC,cAAc,EAAE,uBAAuB;IACvC,cAAc,EAAE,0CAA0C;IAC1D,gBAAgB,EAAE,IAAI;IACtB,mBAAmB,EAAE,IAAI;CAC1B,CAAC,CAAA","sourcesContent":["/** Generated by release CI. Do not edit in a release checkout. */\nexport interface EngineReleaseMetadata {\n schemaVersion: 1\n familyBaseVersion: string | null\n releaseIdentity: string | null\n releaseSetId: string | null\n sourceCommit: string | null\n otaManifestUrl: string | null\n otaManifestSha256: string | null\n}\n\nexport const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({\n \"schemaVersion\": 1,\n \"familyBaseVersion\": \"0.3.3\",\n \"releaseIdentity\": \"0.3.3-dev.622\",\n \"releaseSetId\": \"glassly-0.3.3-dev.622\",\n \"sourceCommit\": \"4f871d45d1403ad2c0dde01b58cf893f1a0e6ff8\",\n \"otaManifestUrl\": null,\n \"otaManifestSha256\": null\n})\n"]}
|
|
@@ -4,6 +4,8 @@ declare class GestureDotsService {
|
|
|
4
4
|
private flashTimer;
|
|
5
5
|
/** Raw controller holds survive hold recognition/dial events until their own release. */
|
|
6
6
|
private presses;
|
|
7
|
+
/** When each finger last sent a press edge (touch_down, or tap_end after one). */
|
|
8
|
+
private lastEdgeAt;
|
|
7
9
|
/** Avoid sending identical images for successive phases of the same press. */
|
|
8
10
|
private renderedGlyph;
|
|
9
11
|
/** Whether the indicator scene currently has anything on it (needs a clear when the flash ends). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GestureDotsService.d.ts","sourceRoot":"","sources":["../../src/services/GestureDotsService.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"GestureDotsService.d.ts","sourceRoot":"","sources":["../../src/services/GestureDotsService.ts"],"names":[],"mappings":"AA6CA,cAAM,kBAAkB;IACtB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,UAAU,CAAsB;IACxC,yFAAyF;IACzF,OAAO,CAAC,OAAO,CAA4D;IAC3E,kFAAkF;IAClF,OAAO,CAAC,UAAU,CAA4B;IAC9C,8EAA8E;IAC9E,OAAO,CAAC,aAAa,CAA+B;IACpD,oGAAoG;IACpG,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,MAAM,CAAwB;IAEtC,KAAK,IAAI,IAAI;IAqBb,IAAI,IAAI,IAAI;IAQZ,OAAO,CAAC,YAAY;IAOpB,uFAAuF;IACvF,OAAO,CAAC,UAAU;IAQlB,OAAO,CAAC,OAAO;IAIf;;;;;OAKG;IACH,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,UAAQ,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI;IAM7E,OAAO,CAAC,YAAY;IAsDpB,OAAO,CAAC,UAAU;IAMlB,OAAO,CAAC,UAAU;IAWlB,sFAAsF;IACtF,OAAO,CAAC,KAAK;IAgBb;;;;;;;;;;OAUG;IACH,OAAO,CAAC,MAAM;CAqCf;AAED,QAAA,MAAM,kBAAkB,oBAA2B,CAAA;AACnD,eAAe,kBAAkB,CAAA;AACjC,OAAO,EAAC,kBAAkB,EAAC,CAAA"}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* glasses when a touch gesture arrives, while nothing else is on the display:
|
|
4
4
|
* a filled circle for a single tap, two for a double tap, an arrow for a swipe
|
|
5
5
|
* (cleared after a short pulse), a bullseye for a recognized long press.
|
|
6
|
-
*
|
|
6
|
+
* Controllers that report press edges (touch_down … tap_end) show the press
|
|
7
|
+
* itself instead: a dot while pressed, the bullseye once held, cleared at
|
|
8
|
+
* release. The Neural Band's middle finger (index 1) draws the same shapes hollow. Extracted from
|
|
7
9
|
* the CFW dashboard service; it renders through
|
|
8
10
|
* the ordinary scene pipeline, so it works on any positioning device (stock
|
|
9
11
|
* G2 included), and is gated by the gesture_dots debug setting.
|
|
@@ -22,6 +24,8 @@ import sceneRenderer from "./SceneRenderer";
|
|
|
22
24
|
const DOTS_APP_ID = "system.gesture-dots";
|
|
23
25
|
/** How long a gesture stays on the indicator. */
|
|
24
26
|
const GESTURE_FLASH_MS = 700;
|
|
27
|
+
/** A press-edge device's tap and hold reports trail its last edge by its double-tap window. */
|
|
28
|
+
const EDGE_REPORT_GRACE_MS = 1000;
|
|
25
29
|
const MARGIN_X = 16;
|
|
26
30
|
const MARGIN_TOP = 10;
|
|
27
31
|
/** Gestures that flash a glyph. */
|
|
@@ -42,6 +46,8 @@ class GestureDotsService {
|
|
|
42
46
|
flashTimer = null;
|
|
43
47
|
/** Raw controller holds survive hold recognition/dial events until their own release. */
|
|
44
48
|
presses = new Map();
|
|
49
|
+
/** When each finger last sent a press edge (touch_down, or tap_end after one). */
|
|
50
|
+
lastEdgeAt = new Map();
|
|
45
51
|
/** Avoid sending identical images for successive phases of the same press. */
|
|
46
52
|
renderedGlyph = null;
|
|
47
53
|
/** Whether the indicator scene currently has anything on it (needs a clear when the flash ends). */
|
|
@@ -72,6 +78,7 @@ class GestureDotsService {
|
|
|
72
78
|
unsub();
|
|
73
79
|
this.unsubs = [];
|
|
74
80
|
this.relinquish();
|
|
81
|
+
this.lastEdgeAt.clear();
|
|
75
82
|
this.started = false;
|
|
76
83
|
}
|
|
77
84
|
resetGesture() {
|
|
@@ -117,10 +124,15 @@ class GestureDotsService {
|
|
|
117
124
|
const finger = fingerIndex ?? 0;
|
|
118
125
|
const press = this.presses.get(finger);
|
|
119
126
|
if (gestureName === "touch_down" || gestureName === "tap_start") {
|
|
127
|
+
if (gestureName === "touch_down")
|
|
128
|
+
this.lastEdgeAt.set(finger, Date.now());
|
|
120
129
|
if (press) {
|
|
121
|
-
// Both reports describe one press
|
|
130
|
+
// Both reports describe one press: after touch_down, tap_start means it is held.
|
|
122
131
|
if (gestureName === "touch_down")
|
|
123
132
|
press.raw = true;
|
|
133
|
+
else if (press.raw)
|
|
134
|
+
press.glyph = "hold";
|
|
135
|
+
this.render();
|
|
124
136
|
return;
|
|
125
137
|
}
|
|
126
138
|
this.clearFlash();
|
|
@@ -132,23 +144,27 @@ class GestureDotsService {
|
|
|
132
144
|
if (!press)
|
|
133
145
|
return;
|
|
134
146
|
this.presses.delete(finger);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (press.raw && this.presses.size === 0)
|
|
138
|
-
this.startFlash(press.glyph);
|
|
147
|
+
if (press.raw)
|
|
148
|
+
this.lastEdgeAt.set(finger, Date.now());
|
|
139
149
|
this.render();
|
|
140
150
|
return;
|
|
141
151
|
}
|
|
142
152
|
const shape = FLASH_GLYPHS[gestureName];
|
|
143
153
|
const glyph = shape && finger === 1 ? MIDDLE_FINGER_GLYPHS[shape] ?? shape : shape;
|
|
144
|
-
|
|
154
|
+
// The press already showed its own tap and hold; the device's report of
|
|
155
|
+
// them follows after its double-tap window, often after release.
|
|
156
|
+
const shownByPress = gestureName === "single_tap" || gestureName === "long_press";
|
|
157
|
+
if (press?.raw && (!glyph || shownByPress)) {
|
|
145
158
|
// Pinch-and-turn reports do not release a finger. A recognized hold
|
|
146
159
|
// stays visible until tap_end instead of expiring halfway through it.
|
|
147
|
-
if (
|
|
148
|
-
press.glyph =
|
|
160
|
+
if (gestureName === "long_press")
|
|
161
|
+
press.glyph = "hold";
|
|
149
162
|
this.render();
|
|
150
163
|
return;
|
|
151
164
|
}
|
|
165
|
+
const lastEdgeAt = this.lastEdgeAt.get(finger);
|
|
166
|
+
if (!press && shownByPress && lastEdgeAt !== undefined && Date.now() - lastEdgeAt < EDGE_REPORT_GRACE_MS)
|
|
167
|
+
return;
|
|
152
168
|
// Completed taps/swipes become one continuous pulse, even if the device
|
|
153
169
|
// omits tap_end. Older tap_start-only devices retain the same fallback.
|
|
154
170
|
this.presses.delete(finger);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GestureDotsService.js","sourceRoot":"","sources":["../../src/services/GestureDotsService.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Gesture dots — a debug indicator that flashes a small glyph top-right on the\n * glasses when a touch gesture arrives, while nothing else is on the display:\n * a filled circle for a single tap, two for a double tap, an arrow for a swipe\n * (cleared after a short pulse), a bullseye for a recognized long press.\n * The Neural Band's middle finger (index 1) draws the same shapes hollow. Extracted from\n * the CFW dashboard service; it renders through\n * the ordinary scene pipeline, so it works on any positioning device (stock\n * G2 included), and is gated by the gesture_dots debug setting.\n *\n * The glyphs are pre-baked images (not text): the stock firmware's builtin\n * font has no ●/↑/↓, and a single image element with a stable id and box maps\n * to ONE re-used firmware image container — flash and idle swap its bytes\n * (data-only update, no page rebuild) instead of creating/removing containers.\n */\nimport {useDisplayStore} from \"../stores/display\"\nimport {SETTINGS, useSettingsStore} from \"../stores/settings\"\nimport type {SceneElementInput} from \"../utils/display/scene\"\nimport {BgTimer} from \"../utils/timers\"\nimport {GESTURE_DOT_GLYPHS, GESTURE_DOT_GLYPH_SIZE, type GestureDotGlyph} from \"./gestureDotGlyphs\"\nimport localDisplayManager from \"./LocalDisplayManager\"\nimport sceneRenderer from \"./SceneRenderer\"\n\nconst DOTS_APP_ID = \"system.gesture-dots\"\n/** How long a gesture stays on the indicator. */\nconst GESTURE_FLASH_MS = 700\nconst MARGIN_X = 16\nconst MARGIN_TOP = 10\n/** Gestures that flash a glyph. */\nconst FLASH_GLYPHS: Record<string, GestureDotGlyph> = {\n single_tap: \"dot\",\n double_tap: \"double\",\n long_press: \"hold\",\n swipe_up: \"up\",\n swipe_down: \"down\",\n swipe_left: \"left\",\n swipe_right: \"right\",\n}\n/** The band's middle finger draws the filled shapes hollow. */\nconst MIDDLE_FINGER_GLYPHS: Partial<Record<GestureDotGlyph, GestureDotGlyph>> = {dot: \"ring\", double: \"doubleRing\"}\n\nclass GestureDotsService {\n private started = false\n private flash: GestureDotGlyph | null = null\n private flashTimer: number | null = null\n /** Raw controller holds survive hold recognition/dial events until their own release. */\n private presses = new Map<number, {raw: boolean; glyph: GestureDotGlyph}>()\n /** Avoid sending identical images for successive phases of the same press. */\n private renderedGlyph: GestureDotGlyph | null = null\n /** Whether the indicator scene currently has anything on it (needs a clear when the flash ends). */\n private visible = false\n private unsubs: Array<() => void> = []\n\n start(): void {\n if (this.started) return\n this.started = true\n this.unsubs.push(\n // The indicator owns the screen only while it is empty: drop our\n // retained state when something else takes over, and clear any\n // leftover flash when the display empties again.\n localDisplayManager.onActiveDisplayChange((active) => {\n if (active) this.relinquish()\n else this.render()\n }),\n // Overlays do not change the retained main display's occupancy.\n useDisplayStore.subscribe(\n (state) => state.overlayOwner,\n (owner) => {\n if (owner !== null) this.relinquish()\n },\n ),\n )\n }\n\n stop(): void {\n for (const unsub of this.unsubs) unsub()\n this.unsubs = []\n this.relinquish()\n this.started = false\n }\n\n private resetGesture(): void {\n if (this.flashTimer !== null) BgTimer.clearTimeout(this.flashTimer)\n this.flashTimer = null\n this.flash = null\n this.presses.clear()\n }\n\n /** Another surface owns the display; forget the indicator without painting over it. */\n private relinquish(): void {\n this.resetGesture()\n this.visible = false\n this.renderedGlyph = null\n sceneRenderer.clearApp(DOTS_APP_ID)\n this.trace(\"relinquish\")\n }\n\n private enabled(): boolean {\n return useSettingsStore.getState().getSetting(SETTINGS.gesture_dots.key) === true\n }\n\n /**\n * Flash a gesture on the indicator. Other gesture names are ignored.\n * Claimed gestures cancel any earlier press/flash without starting a new\n * one: the claimant may also consume the release edge. `fingerIndex` is the\n * Neural Band's finger on its pinches and press edges (1 = middle).\n */\n noteGesture(gestureName: string, claimed = false, fingerIndex?: number): void {\n if (!this.started || !this.enabled()) return\n this.applyGesture(gestureName, claimed, fingerIndex)\n this.trace(\"input\", {gestureName, claimed})\n }\n\n private applyGesture(gestureName: string, claimed: boolean, fingerIndex?: number): void {\n if (localDisplayManager.hasActiveDisplay() || useDisplayStore.getState().overlayOwner !== null) {\n this.relinquish()\n return\n }\n if (claimed) {\n this.resetGesture()\n this.render()\n return\n }\n const finger = fingerIndex ?? 0\n const press = this.presses.get(finger)\n if (gestureName === \"touch_down\" || gestureName === \"tap_start\") {\n if (press) {\n // Both reports describe one press. Never restart its visual feedback.\n if (gestureName === \"touch_down\") press.raw = true\n return\n }\n this.clearFlash()\n this.presses.set(finger, {raw: gestureName === \"touch_down\", glyph: finger === 1 ? \"ring\" : \"dot\"})\n this.render()\n return\n }\n if (gestureName === \"tap_end\") {\n if (!press) return\n this.presses.delete(finger)\n // The band's resolved tap arrives after release (double-tap window).\n // Keep the glyph lit across that gap so one pinch never flashes twice.\n if (press.raw && this.presses.size === 0) this.startFlash(press.glyph)\n this.render()\n return\n }\n const shape = FLASH_GLYPHS[gestureName]\n const glyph = shape && finger === 1 ? MIDDLE_FINGER_GLYPHS[shape] ?? shape : shape\n if (press?.raw && (!glyph || gestureName === \"long_press\")) {\n // Pinch-and-turn reports do not release a finger. A recognized hold\n // stays visible until tap_end instead of expiring halfway through it.\n if (glyph) press.glyph = glyph\n this.render()\n return\n }\n // Completed taps/swipes become one continuous pulse, even if the device\n // omits tap_end. Older tap_start-only devices retain the same fallback.\n this.presses.delete(finger)\n if (glyph) this.startFlash(glyph)\n this.render()\n }\n\n private clearFlash(): void {\n if (this.flashTimer !== null) BgTimer.clearTimeout(this.flashTimer)\n this.flashTimer = null\n this.flash = null\n }\n\n private startFlash(glyph: GestureDotGlyph): void {\n this.clearFlash()\n this.flash = glyph\n this.flashTimer = BgTimer.setTimeout(() => {\n this.flashTimer = null\n this.flash = null\n this.render()\n this.trace(\"flash-ended\")\n }, GESTURE_FLASH_MS)\n }\n\n /** Debug setting only; no payloads or retained history. Correlate seq with G2PERF. */\n private trace(reason: string, details: Record<string, unknown> = {}): void {\n if (!this.enabled()) return\n console.log(\n \"GESTUREDOTS \" +\n JSON.stringify({\n t: Date.now(),\n reason,\n held: this.presses.size > 0,\n flash: this.flash,\n overlay: useDisplayStore.getState().overlayOwner,\n active: localDisplayManager.hasActiveDisplay(),\n ...details,\n }),\n )\n }\n\n /**\n * Paint or clear the indicator. Nothing is drawn while anything else is on\n * the glasses (miniapp frame or system screen) or when no gesture was just\n * received. The top-right anchor resolves against the device's real canvas,\n * so no size math here.\n *\n * Between flashes the element STAYS in the scene with the blank glyph:\n * removing it would tear down the firmware image container and rebuild the\n * page on every flash (a rebuild storm on stock firmware). Only disabling\n * the setting removes the element outright.\n */\n private render(): void {\n if (!this.started) return\n if (localDisplayManager.hasActiveDisplay() || useDisplayStore.getState().overlayOwner !== null) return\n const shown = Array.from(this.presses.values()).at(-1)?.glyph ?? this.flash\n if (!this.enabled()) {\n if (this.visible) {\n this.visible = false\n this.renderedGlyph = null\n sceneRenderer.emitScene(DOTS_APP_ID, \"main\", [])\n }\n return\n }\n if (!shown && !this.visible) return\n const glyph = shown ?? \"blank\"\n if (glyph === this.renderedGlyph) return\n const elements: SceneElementInput[] = [\n {\n type: \"image\",\n id: \"gesture-dot\",\n box: {\n x: MARGIN_X,\n y: MARGIN_TOP,\n w: GESTURE_DOT_GLYPH_SIZE,\n h: GESTURE_DOT_GLYPH_SIZE,\n anchor: \"top-right\",\n },\n data: GESTURE_DOT_GLYPHS[glyph],\n },\n ]\n this.visible = true\n this.renderedGlyph = glyph\n const result = sceneRenderer.emitScene(DOTS_APP_ID, \"main\", elements)\n this.trace(\"render\", {\n glyph,\n ...(result?.kind === \"scene\" ? {seq: result.seq, sent: result.sent} : {}),\n })\n }\n}\n\nconst gestureDotsService = new GestureDotsService()\nexport default gestureDotsService\nexport {GestureDotsService}\n"]}
|
|
1
|
+
{"version":3,"file":"GestureDotsService.js","sourceRoot":"","sources":["../../src/services/GestureDotsService.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Gesture dots — a debug indicator that flashes a small glyph top-right on the\n * glasses when a touch gesture arrives, while nothing else is on the display:\n * a filled circle for a single tap, two for a double tap, an arrow for a swipe\n * (cleared after a short pulse), a bullseye for a recognized long press.\n * Controllers that report press edges (touch_down … tap_end) show the press\n * itself instead: a dot while pressed, the bullseye once held, cleared at\n * release. The Neural Band's middle finger (index 1) draws the same shapes hollow. Extracted from\n * the CFW dashboard service; it renders through\n * the ordinary scene pipeline, so it works on any positioning device (stock\n * G2 included), and is gated by the gesture_dots debug setting.\n *\n * The glyphs are pre-baked images (not text): the stock firmware's builtin\n * font has no ●/↑/↓, and a single image element with a stable id and box maps\n * to ONE re-used firmware image container — flash and idle swap its bytes\n * (data-only update, no page rebuild) instead of creating/removing containers.\n */\nimport {useDisplayStore} from \"../stores/display\"\nimport {SETTINGS, useSettingsStore} from \"../stores/settings\"\nimport type {SceneElementInput} from \"../utils/display/scene\"\nimport {BgTimer} from \"../utils/timers\"\nimport {GESTURE_DOT_GLYPHS, GESTURE_DOT_GLYPH_SIZE, type GestureDotGlyph} from \"./gestureDotGlyphs\"\nimport localDisplayManager from \"./LocalDisplayManager\"\nimport sceneRenderer from \"./SceneRenderer\"\n\nconst DOTS_APP_ID = \"system.gesture-dots\"\n/** How long a gesture stays on the indicator. */\nconst GESTURE_FLASH_MS = 700\n/** A press-edge device's tap and hold reports trail its last edge by its double-tap window. */\nconst EDGE_REPORT_GRACE_MS = 1000\nconst MARGIN_X = 16\nconst MARGIN_TOP = 10\n/** Gestures that flash a glyph. */\nconst FLASH_GLYPHS: Record<string, GestureDotGlyph> = {\n single_tap: \"dot\",\n double_tap: \"double\",\n long_press: \"hold\",\n swipe_up: \"up\",\n swipe_down: \"down\",\n swipe_left: \"left\",\n swipe_right: \"right\",\n}\n/** The band's middle finger draws the filled shapes hollow. */\nconst MIDDLE_FINGER_GLYPHS: Partial<Record<GestureDotGlyph, GestureDotGlyph>> = {dot: \"ring\", double: \"doubleRing\"}\n\nclass GestureDotsService {\n private started = false\n private flash: GestureDotGlyph | null = null\n private flashTimer: number | null = null\n /** Raw controller holds survive hold recognition/dial events until their own release. */\n private presses = new Map<number, {raw: boolean; glyph: GestureDotGlyph}>()\n /** When each finger last sent a press edge (touch_down, or tap_end after one). */\n private lastEdgeAt = new Map<number, number>()\n /** Avoid sending identical images for successive phases of the same press. */\n private renderedGlyph: GestureDotGlyph | null = null\n /** Whether the indicator scene currently has anything on it (needs a clear when the flash ends). */\n private visible = false\n private unsubs: Array<() => void> = []\n\n start(): void {\n if (this.started) return\n this.started = true\n this.unsubs.push(\n // The indicator owns the screen only while it is empty: drop our\n // retained state when something else takes over, and clear any\n // leftover flash when the display empties again.\n localDisplayManager.onActiveDisplayChange((active) => {\n if (active) this.relinquish()\n else this.render()\n }),\n // Overlays do not change the retained main display's occupancy.\n useDisplayStore.subscribe(\n (state) => state.overlayOwner,\n (owner) => {\n if (owner !== null) this.relinquish()\n },\n ),\n )\n }\n\n stop(): void {\n for (const unsub of this.unsubs) unsub()\n this.unsubs = []\n this.relinquish()\n this.lastEdgeAt.clear()\n this.started = false\n }\n\n private resetGesture(): void {\n if (this.flashTimer !== null) BgTimer.clearTimeout(this.flashTimer)\n this.flashTimer = null\n this.flash = null\n this.presses.clear()\n }\n\n /** Another surface owns the display; forget the indicator without painting over it. */\n private relinquish(): void {\n this.resetGesture()\n this.visible = false\n this.renderedGlyph = null\n sceneRenderer.clearApp(DOTS_APP_ID)\n this.trace(\"relinquish\")\n }\n\n private enabled(): boolean {\n return useSettingsStore.getState().getSetting(SETTINGS.gesture_dots.key) === true\n }\n\n /**\n * Flash a gesture on the indicator. Other gesture names are ignored.\n * Claimed gestures cancel any earlier press/flash without starting a new\n * one: the claimant may also consume the release edge. `fingerIndex` is the\n * Neural Band's finger on its pinches and press edges (1 = middle).\n */\n noteGesture(gestureName: string, claimed = false, fingerIndex?: number): void {\n if (!this.started || !this.enabled()) return\n this.applyGesture(gestureName, claimed, fingerIndex)\n this.trace(\"input\", {gestureName, claimed})\n }\n\n private applyGesture(gestureName: string, claimed: boolean, fingerIndex?: number): void {\n if (localDisplayManager.hasActiveDisplay() || useDisplayStore.getState().overlayOwner !== null) {\n this.relinquish()\n return\n }\n if (claimed) {\n this.resetGesture()\n this.render()\n return\n }\n const finger = fingerIndex ?? 0\n const press = this.presses.get(finger)\n if (gestureName === \"touch_down\" || gestureName === \"tap_start\") {\n if (gestureName === \"touch_down\") this.lastEdgeAt.set(finger, Date.now())\n if (press) {\n // Both reports describe one press: after touch_down, tap_start means it is held.\n if (gestureName === \"touch_down\") press.raw = true\n else if (press.raw) press.glyph = \"hold\"\n this.render()\n return\n }\n this.clearFlash()\n this.presses.set(finger, {raw: gestureName === \"touch_down\", glyph: finger === 1 ? \"ring\" : \"dot\"})\n this.render()\n return\n }\n if (gestureName === \"tap_end\") {\n if (!press) return\n this.presses.delete(finger)\n if (press.raw) this.lastEdgeAt.set(finger, Date.now())\n this.render()\n return\n }\n const shape = FLASH_GLYPHS[gestureName]\n const glyph = shape && finger === 1 ? MIDDLE_FINGER_GLYPHS[shape] ?? shape : shape\n // The press already showed its own tap and hold; the device's report of\n // them follows after its double-tap window, often after release.\n const shownByPress = gestureName === \"single_tap\" || gestureName === \"long_press\"\n if (press?.raw && (!glyph || shownByPress)) {\n // Pinch-and-turn reports do not release a finger. A recognized hold\n // stays visible until tap_end instead of expiring halfway through it.\n if (gestureName === \"long_press\") press.glyph = \"hold\"\n this.render()\n return\n }\n const lastEdgeAt = this.lastEdgeAt.get(finger)\n if (!press && shownByPress && lastEdgeAt !== undefined && Date.now() - lastEdgeAt < EDGE_REPORT_GRACE_MS) return\n // Completed taps/swipes become one continuous pulse, even if the device\n // omits tap_end. Older tap_start-only devices retain the same fallback.\n this.presses.delete(finger)\n if (glyph) this.startFlash(glyph)\n this.render()\n }\n\n private clearFlash(): void {\n if (this.flashTimer !== null) BgTimer.clearTimeout(this.flashTimer)\n this.flashTimer = null\n this.flash = null\n }\n\n private startFlash(glyph: GestureDotGlyph): void {\n this.clearFlash()\n this.flash = glyph\n this.flashTimer = BgTimer.setTimeout(() => {\n this.flashTimer = null\n this.flash = null\n this.render()\n this.trace(\"flash-ended\")\n }, GESTURE_FLASH_MS)\n }\n\n /** Debug setting only; no payloads or retained history. Correlate seq with G2PERF. */\n private trace(reason: string, details: Record<string, unknown> = {}): void {\n if (!this.enabled()) return\n console.log(\n \"GESTUREDOTS \" +\n JSON.stringify({\n t: Date.now(),\n reason,\n held: this.presses.size > 0,\n flash: this.flash,\n overlay: useDisplayStore.getState().overlayOwner,\n active: localDisplayManager.hasActiveDisplay(),\n ...details,\n }),\n )\n }\n\n /**\n * Paint or clear the indicator. Nothing is drawn while anything else is on\n * the glasses (miniapp frame or system screen) or when no gesture was just\n * received. The top-right anchor resolves against the device's real canvas,\n * so no size math here.\n *\n * Between flashes the element STAYS in the scene with the blank glyph:\n * removing it would tear down the firmware image container and rebuild the\n * page on every flash (a rebuild storm on stock firmware). Only disabling\n * the setting removes the element outright.\n */\n private render(): void {\n if (!this.started) return\n if (localDisplayManager.hasActiveDisplay() || useDisplayStore.getState().overlayOwner !== null) return\n const shown = Array.from(this.presses.values()).at(-1)?.glyph ?? this.flash\n if (!this.enabled()) {\n if (this.visible) {\n this.visible = false\n this.renderedGlyph = null\n sceneRenderer.emitScene(DOTS_APP_ID, \"main\", [])\n }\n return\n }\n if (!shown && !this.visible) return\n const glyph = shown ?? \"blank\"\n if (glyph === this.renderedGlyph) return\n const elements: SceneElementInput[] = [\n {\n type: \"image\",\n id: \"gesture-dot\",\n box: {\n x: MARGIN_X,\n y: MARGIN_TOP,\n w: GESTURE_DOT_GLYPH_SIZE,\n h: GESTURE_DOT_GLYPH_SIZE,\n anchor: \"top-right\",\n },\n data: GESTURE_DOT_GLYPHS[glyph],\n },\n ]\n this.visible = true\n this.renderedGlyph = glyph\n const result = sceneRenderer.emitScene(DOTS_APP_ID, \"main\", elements)\n this.trace(\"render\", {\n glyph,\n ...(result?.kind === \"scene\" ? {seq: result.seq, sent: result.sent} : {}),\n })\n }\n}\n\nconst gestureDotsService = new GestureDotsService()\nexport default gestureDotsService\nexport {GestureDotsService}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GlassesTouchInput.d.ts","sourceRoot":"","sources":["../../src/services/GlassesTouchInput.ts"],"names":[],"mappings":"AACA,OAAO,EAAe,KAAK,UAAU,EAAC,MAAM,wBAAwB,CAAA;
|
|
1
|
+
{"version":3,"file":"GlassesTouchInput.d.ts","sourceRoot":"","sources":["../../src/services/GlassesTouchInput.ts"],"names":[],"mappings":"AACA,OAAO,EAAe,KAAK,UAAU,EAAC,MAAM,wBAAwB,CAAA;AAyCpE;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAqD1D;AAED,wBAAgB,eAAe,IAAI,IAAI,CAGtC;AAED,iFAAiF;AACjF,wBAAgB,WAAW,CACzB,WAAW,EACP,YAAY,GACZ,WAAW,GACX,YAAY,GACZ,SAAS,GACT,eAAe,GACf,YAAY,GACZ,UAAU,GACV,YAAY,GACZ,YAAY,GACZ,aAAa,GACb,YAAY,GACZ,SAAS,GACT,WAAW,GACd,IAAI,CAQN"}
|
|
@@ -6,6 +6,7 @@ import { useGlassesStore } from "../stores/glasses";
|
|
|
6
6
|
import { BgTimer } from "../utils/timers";
|
|
7
7
|
import dashboardOpenTrace from "./debug/DashboardOpenTrace";
|
|
8
8
|
import gestureDotsService from "./GestureDotsService";
|
|
9
|
+
import { isGlassesConnected } from "./GlassesReadiness";
|
|
9
10
|
import localDisplayManager from "./LocalDisplayManager";
|
|
10
11
|
import localMiniappRuntime from "./LocalMiniappRuntime";
|
|
11
12
|
import { MiniappExitGesture } from "./MiniappExitGesture";
|
|
@@ -85,8 +86,9 @@ export function dispatchTouchEvent(event) {
|
|
|
85
86
|
// Nobody claimed a double tap: the glasses' own dashboard. On eligible
|
|
86
87
|
// Glassly CFW glasses the dashboard miniapp claims it while it runs
|
|
87
88
|
// (ADR-0006); stock firmware keeps its native dashboard (ADR-0002). The
|
|
88
|
-
// modal nav UI owns the screen; showDashboard would fight it.
|
|
89
|
-
|
|
89
|
+
// modal nav UI owns the screen; showDashboard would fight it. Without
|
|
90
|
+
// glasses there is no dashboard to open, and the mirror keeps its frame.
|
|
91
|
+
if (osDoubleTap && !nativeNavigationService.isActive && isGlassesConnected(useGlassesStore.getState().connection)) {
|
|
90
92
|
useDisplayStore.getState().setDisplayEvent({ layout: { layoutType: "unavailable", reason: "firmware-dashboard" } });
|
|
91
93
|
void Promise.resolve(BluetoothSdk.showDashboard()).catch((err) => {
|
|
92
94
|
console.error("DeviceEventRouter: showDashboard failed:", err);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GlassesTouchInput.js","sourceRoot":"","sources":["../../src/services/GlassesTouchInput.ts"],"names":[],"mappings":"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","sourcesContent":["import {useDisplayStore} from \"../stores/display\"\nimport {DeviceModels, type TouchEvent} from \"@glassly/bluetooth-sdk\"\nimport BluetoothSdk from \"@glassly/bluetooth-sdk/internal\"\n\nimport {useAppStatusStore} from \"../stores/apps\"\nimport {useGlassesStore} from \"../stores/glasses\"\nimport {BgTimer} from \"../utils/timers\"\nimport dashboardOpenTrace from \"./debug/DashboardOpenTrace\"\nimport gestureDotsService from \"./GestureDotsService\"\nimport localDisplayManager from \"./LocalDisplayManager\"\nimport localMiniappRuntime from \"./LocalMiniappRuntime\"\nimport {MiniappExitGesture} from \"./MiniappExitGesture\"\nimport nativeNavigationService from \"./NativeNavigationService\"\nimport {isOverlayApp} from \"./pages/overlayApps\"\n\nconst miniappExitGesture = new MiniappExitGesture({\n getState: () => useAppStatusStore.getState(),\n getVisiblePackageName: () => localDisplayManager.getVisibleMiniappPackageName(),\n beginExitNotice: (packageName, text) => localDisplayManager.beginMiniappExit(packageName, text),\n finishExitNotice: (packageName) => localDisplayManager.finishMiniappExit(packageName),\n cancelExitNotice: (packageName) => localDisplayManager.cancelMiniappExit(packageName),\n setTimeout: (callback, delay) => BgTimer.setTimeout(callback, delay),\n clearTimeout: (timerId) => BgTimer.clearTimeout(timerId),\n})\n\n// Raw ring reports include every scroll step, whether the CFW forwards them\n// (deviceModel G2) or the ring sends them to the phone itself (deviceModel R1);\n// both tag source 2. Rate-limit scroll actions separately from press/release\n// recognition, using native receipt timestamps so a JS backlog does not\n// collapse otherwise separate gestures.\nconst RING_SCROLL_INTERVAL_MS = 100\nlet lastRingScrollAt: number | null = null\n\nfunction shouldForwardScroll(event: TouchEvent): boolean {\n if (event.source !== 2 || (event.gestureName !== \"swipe_up\" && event.gestureName !== \"swipe_down\")) return true\n const elapsed = lastRingScrollAt === null ? null : event.timestamp - lastRingScrollAt\n if (elapsed !== null && elapsed >= 0 && elapsed < RING_SCROLL_INTERVAL_MS) return false\n lastRingScrollAt = event.timestamp\n return true\n}\n\n/**\n * Route both real glasses input and phone mirror controls through the same\n * policy. A claimed gesture goes to the app holding the claim, alone; an\n * unclaimed one goes to every subscriber and then the engine's default. The\n * one rule ahead of the claims: an unclaimed double tap on a miniapp whose\n * frame is visibly on the glasses leaves that app, whoever else claimed the\n * gesture — the dashboard's claim is for the empty screen.\n */\nexport function dispatchTouchEvent(event: TouchEvent): void {\n const gesture = event.gestureName\n const claimant = localMiniappRuntime.gestureClaimant(gesture)\n // The Neural Band's middle finger (index 1) shares the tap names but is left\n // to miniapps: the engine's own double-tap behaviors are the index finger's.\n const osDoubleTap = gesture === \"double_tap\" && event.index !== 1\n\n if (osDoubleTap) {\n // A visible main miniapp opts out of OS exit by registering double tap (the\n // terminal's back-to-menu). The latest claimant still receives it. Otherwise\n // the tap means \"leave the app\". The controller shows the bounded OS\n // notice first, then uses the normal\n // store stop path (and closes its phone overlay).\n const visible = localDisplayManager.getVisibleMiniappPackageName()\n if (\n visible !== null &&\n !localMiniappRuntime.hasGestureClaim(visible, gesture) &&\n miniappExitGesture.tryExitVisibleMiniapp()\n ) {\n gestureDotsService.noteGesture(gesture, true)\n return\n }\n }\n\n if (claimant !== null) {\n // An overlay app claims the double tap on an empty screen to open the dashboard.\n if (gesture === \"double_tap\" && isOverlayApp(claimant)) dashboardOpenTrace.trigger(\"double-tap\", event.timestamp)\n // The press may have started before this app claimed its release edge.\n gestureDotsService.noteGesture(gesture, true, event.index)\n if (!shouldForwardScroll(event)) return\n localMiniappRuntime.forwardEvent(\"touch_event\", event, undefined, claimant)\n return\n }\n\n // Gesture-dots debug indicator (any device, gated by its setting). It\n // only paints while the screen is otherwise empty. Process even suppressed\n // scroll repeats here so their completion still clears a held dot.\n gestureDotsService.noteGesture(gesture, false, event.index)\n\n if (!shouldForwardScroll(event)) return\n localMiniappRuntime.forwardEvent(\"touch_event\", event)\n\n // Nobody claimed a double tap: the glasses' own dashboard. On eligible\n // Glassly CFW glasses the dashboard miniapp claims it while it runs\n // (ADR-0006); stock firmware keeps its native dashboard (ADR-0002). The\n // modal nav UI owns the screen; showDashboard would fight it.\n if (osDoubleTap && !nativeNavigationService.isActive) {\n useDisplayStore.getState().setDisplayEvent({layout: {layoutType: \"unavailable\", reason: \"firmware-dashboard\"}})\n void Promise.resolve(BluetoothSdk.showDashboard()).catch((err) => {\n console.error(\"DeviceEventRouter: showDashboard failed:\", err)\n })\n }\n}\n\nexport function resetTouchInput(): void {\n miniappExitGesture.reset()\n lastRingScrollAt = null\n}\n\n/** Phone mirror input uses the same dashboard/miniapp policy as real glasses. */\nexport function sendGesture(\n gestureName:\n | \"single_tap\"\n | \"tap_start\"\n | \"double_tap\"\n | \"tap_end\"\n | \"tap_then_hold\"\n | \"touch_down\"\n | \"swipe_up\"\n | \"swipe_down\"\n | \"swipe_left\"\n | \"swipe_right\"\n | \"long_press\"\n | \"dial_up\"\n | \"dial_down\",\n): void {\n const model = useGlassesStore.getState().deviceModel\n dispatchTouchEvent({\n type: \"touch_event\",\n gestureName,\n deviceModel: Object.values(DeviceModels).find((candidate) => candidate === model) ?? DeviceModels.Simulated,\n timestamp: Date.now(),\n })\n}\n"]}
|
|
1
|
+
{"version":3,"file":"GlassesTouchInput.js","sourceRoot":"","sources":["../../src/services/GlassesTouchInput.ts"],"names":[],"mappings":"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","sourcesContent":["import {useDisplayStore} from \"../stores/display\"\nimport {DeviceModels, type TouchEvent} from \"@glassly/bluetooth-sdk\"\nimport BluetoothSdk from \"@glassly/bluetooth-sdk/internal\"\n\nimport {useAppStatusStore} from \"../stores/apps\"\nimport {useGlassesStore} from \"../stores/glasses\"\nimport {BgTimer} from \"../utils/timers\"\nimport dashboardOpenTrace from \"./debug/DashboardOpenTrace\"\nimport gestureDotsService from \"./GestureDotsService\"\nimport {isGlassesConnected} from \"./GlassesReadiness\"\nimport localDisplayManager from \"./LocalDisplayManager\"\nimport localMiniappRuntime from \"./LocalMiniappRuntime\"\nimport {MiniappExitGesture} from \"./MiniappExitGesture\"\nimport nativeNavigationService from \"./NativeNavigationService\"\nimport {isOverlayApp} from \"./pages/overlayApps\"\n\nconst miniappExitGesture = new MiniappExitGesture({\n getState: () => useAppStatusStore.getState(),\n getVisiblePackageName: () => localDisplayManager.getVisibleMiniappPackageName(),\n beginExitNotice: (packageName, text) => localDisplayManager.beginMiniappExit(packageName, text),\n finishExitNotice: (packageName) => localDisplayManager.finishMiniappExit(packageName),\n cancelExitNotice: (packageName) => localDisplayManager.cancelMiniappExit(packageName),\n setTimeout: (callback, delay) => BgTimer.setTimeout(callback, delay),\n clearTimeout: (timerId) => BgTimer.clearTimeout(timerId),\n})\n\n// Raw ring reports include every scroll step, whether the CFW forwards them\n// (deviceModel G2) or the ring sends them to the phone itself (deviceModel R1);\n// both tag source 2. Rate-limit scroll actions separately from press/release\n// recognition, using native receipt timestamps so a JS backlog does not\n// collapse otherwise separate gestures.\nconst RING_SCROLL_INTERVAL_MS = 100\nlet lastRingScrollAt: number | null = null\n\nfunction shouldForwardScroll(event: TouchEvent): boolean {\n if (event.source !== 2 || (event.gestureName !== \"swipe_up\" && event.gestureName !== \"swipe_down\")) return true\n const elapsed = lastRingScrollAt === null ? null : event.timestamp - lastRingScrollAt\n if (elapsed !== null && elapsed >= 0 && elapsed < RING_SCROLL_INTERVAL_MS) return false\n lastRingScrollAt = event.timestamp\n return true\n}\n\n/**\n * Route both real glasses input and phone mirror controls through the same\n * policy. A claimed gesture goes to the app holding the claim, alone; an\n * unclaimed one goes to every subscriber and then the engine's default. The\n * one rule ahead of the claims: an unclaimed double tap on a miniapp whose\n * frame is visibly on the glasses leaves that app, whoever else claimed the\n * gesture — the dashboard's claim is for the empty screen.\n */\nexport function dispatchTouchEvent(event: TouchEvent): void {\n const gesture = event.gestureName\n const claimant = localMiniappRuntime.gestureClaimant(gesture)\n // The Neural Band's middle finger (index 1) shares the tap names but is left\n // to miniapps: the engine's own double-tap behaviors are the index finger's.\n const osDoubleTap = gesture === \"double_tap\" && event.index !== 1\n\n if (osDoubleTap) {\n // A visible main miniapp opts out of OS exit by registering double tap (the\n // terminal's back-to-menu). The latest claimant still receives it. Otherwise\n // the tap means \"leave the app\". The controller shows the bounded OS\n // notice first, then uses the normal\n // store stop path (and closes its phone overlay).\n const visible = localDisplayManager.getVisibleMiniappPackageName()\n if (\n visible !== null &&\n !localMiniappRuntime.hasGestureClaim(visible, gesture) &&\n miniappExitGesture.tryExitVisibleMiniapp()\n ) {\n gestureDotsService.noteGesture(gesture, true)\n return\n }\n }\n\n if (claimant !== null) {\n // An overlay app claims the double tap on an empty screen to open the dashboard.\n if (gesture === \"double_tap\" && isOverlayApp(claimant)) dashboardOpenTrace.trigger(\"double-tap\", event.timestamp)\n // The press may have started before this app claimed its release edge.\n gestureDotsService.noteGesture(gesture, true, event.index)\n if (!shouldForwardScroll(event)) return\n localMiniappRuntime.forwardEvent(\"touch_event\", event, undefined, claimant)\n return\n }\n\n // Gesture-dots debug indicator (any device, gated by its setting). It\n // only paints while the screen is otherwise empty. Process even suppressed\n // scroll repeats here so their completion still clears a held dot.\n gestureDotsService.noteGesture(gesture, false, event.index)\n\n if (!shouldForwardScroll(event)) return\n localMiniappRuntime.forwardEvent(\"touch_event\", event)\n\n // Nobody claimed a double tap: the glasses' own dashboard. On eligible\n // Glassly CFW glasses the dashboard miniapp claims it while it runs\n // (ADR-0006); stock firmware keeps its native dashboard (ADR-0002). The\n // modal nav UI owns the screen; showDashboard would fight it. Without\n // glasses there is no dashboard to open, and the mirror keeps its frame.\n if (osDoubleTap && !nativeNavigationService.isActive && isGlassesConnected(useGlassesStore.getState().connection)) {\n useDisplayStore.getState().setDisplayEvent({layout: {layoutType: \"unavailable\", reason: \"firmware-dashboard\"}})\n void Promise.resolve(BluetoothSdk.showDashboard()).catch((err) => {\n console.error(\"DeviceEventRouter: showDashboard failed:\", err)\n })\n }\n}\n\nexport function resetTouchInput(): void {\n miniappExitGesture.reset()\n lastRingScrollAt = null\n}\n\n/** Phone mirror input uses the same dashboard/miniapp policy as real glasses. */\nexport function sendGesture(\n gestureName:\n | \"single_tap\"\n | \"tap_start\"\n | \"double_tap\"\n | \"tap_end\"\n | \"tap_then_hold\"\n | \"touch_down\"\n | \"swipe_up\"\n | \"swipe_down\"\n | \"swipe_left\"\n | \"swipe_right\"\n | \"long_press\"\n | \"dial_up\"\n | \"dial_down\",\n): void {\n const model = useGlassesStore.getState().deviceModel\n dispatchTouchEvent({\n type: \"touch_event\",\n gestureName,\n deviceModel: Object.values(DeviceModels).find((candidate) => candidate === model) ?? DeviceModels.Simulated,\n timestamp: Date.now(),\n })\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glassly/engine",
|
|
3
|
-
"version": "0.3.3-dev.
|
|
3
|
+
"version": "0.3.3-dev.622",
|
|
4
4
|
"description": "Glassly Engine — miniapp registry, WebView bridge, and miniapp store",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -97,17 +97,17 @@
|
|
|
97
97
|
"registry": "https://registry.npmjs.org/"
|
|
98
98
|
},
|
|
99
99
|
"dependencies": {
|
|
100
|
-
"@glassly/bluetooth-sdk": "0.3.3-dev.
|
|
101
|
-
"@glassly/cloud-client": "0.3.3-dev.
|
|
102
|
-
"@glassly/cloud-protocol": "0.3.3-dev.
|
|
103
|
-
"@glassly/crust": "0.3.3-dev.
|
|
104
|
-
"@glassly/miniapp": "0.3.3-dev.
|
|
100
|
+
"@glassly/bluetooth-sdk": "0.3.3-dev.622",
|
|
101
|
+
"@glassly/cloud-client": "0.3.3-dev.622",
|
|
102
|
+
"@glassly/cloud-protocol": "0.3.3-dev.622",
|
|
103
|
+
"@glassly/crust": "0.3.3-dev.622",
|
|
104
|
+
"@glassly/miniapp": "0.3.3-dev.622",
|
|
105
105
|
"buffer": "^6.0.3",
|
|
106
106
|
"events": "^3.3.0",
|
|
107
107
|
"react-native-marked": "8.1.1",
|
|
108
108
|
"semver": "^7.7.4",
|
|
109
109
|
"typesafe-ts": "^1.7.1",
|
|
110
|
-
"@glassly/llm-providers": "0.3.3-dev.
|
|
110
|
+
"@glassly/llm-providers": "0.3.3-dev.622"
|
|
111
111
|
},
|
|
112
112
|
"devDependencies": {
|
|
113
113
|
"@glassly/llm-providers": "workspace:*",
|
|
@@ -12,9 +12,9 @@ export interface EngineReleaseMetadata {
|
|
|
12
12
|
export const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({
|
|
13
13
|
"schemaVersion": 1,
|
|
14
14
|
"familyBaseVersion": "0.3.3",
|
|
15
|
-
"releaseIdentity": "0.3.3-dev.
|
|
16
|
-
"releaseSetId": "glassly-0.3.3-dev.
|
|
17
|
-
"sourceCommit": "
|
|
15
|
+
"releaseIdentity": "0.3.3-dev.622",
|
|
16
|
+
"releaseSetId": "glassly-0.3.3-dev.622",
|
|
17
|
+
"sourceCommit": "4f871d45d1403ad2c0dde01b58cf893f1a0e6ff8",
|
|
18
18
|
"otaManifestUrl": null,
|
|
19
19
|
"otaManifestSha256": null
|
|
20
20
|
})
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* glasses when a touch gesture arrives, while nothing else is on the display:
|
|
4
4
|
* a filled circle for a single tap, two for a double tap, an arrow for a swipe
|
|
5
5
|
* (cleared after a short pulse), a bullseye for a recognized long press.
|
|
6
|
-
*
|
|
6
|
+
* Controllers that report press edges (touch_down … tap_end) show the press
|
|
7
|
+
* itself instead: a dot while pressed, the bullseye once held, cleared at
|
|
8
|
+
* release. The Neural Band's middle finger (index 1) draws the same shapes hollow. Extracted from
|
|
7
9
|
* the CFW dashboard service; it renders through
|
|
8
10
|
* the ordinary scene pipeline, so it works on any positioning device (stock
|
|
9
11
|
* G2 included), and is gated by the gesture_dots debug setting.
|
|
@@ -24,6 +26,8 @@ import sceneRenderer from "./SceneRenderer"
|
|
|
24
26
|
const DOTS_APP_ID = "system.gesture-dots"
|
|
25
27
|
/** How long a gesture stays on the indicator. */
|
|
26
28
|
const GESTURE_FLASH_MS = 700
|
|
29
|
+
/** A press-edge device's tap and hold reports trail its last edge by its double-tap window. */
|
|
30
|
+
const EDGE_REPORT_GRACE_MS = 1000
|
|
27
31
|
const MARGIN_X = 16
|
|
28
32
|
const MARGIN_TOP = 10
|
|
29
33
|
/** Gestures that flash a glyph. */
|
|
@@ -45,6 +49,8 @@ class GestureDotsService {
|
|
|
45
49
|
private flashTimer: number | null = null
|
|
46
50
|
/** Raw controller holds survive hold recognition/dial events until their own release. */
|
|
47
51
|
private presses = new Map<number, {raw: boolean; glyph: GestureDotGlyph}>()
|
|
52
|
+
/** When each finger last sent a press edge (touch_down, or tap_end after one). */
|
|
53
|
+
private lastEdgeAt = new Map<number, number>()
|
|
48
54
|
/** Avoid sending identical images for successive phases of the same press. */
|
|
49
55
|
private renderedGlyph: GestureDotGlyph | null = null
|
|
50
56
|
/** Whether the indicator scene currently has anything on it (needs a clear when the flash ends). */
|
|
@@ -76,6 +82,7 @@ class GestureDotsService {
|
|
|
76
82
|
for (const unsub of this.unsubs) unsub()
|
|
77
83
|
this.unsubs = []
|
|
78
84
|
this.relinquish()
|
|
85
|
+
this.lastEdgeAt.clear()
|
|
79
86
|
this.started = false
|
|
80
87
|
}
|
|
81
88
|
|
|
@@ -124,9 +131,12 @@ class GestureDotsService {
|
|
|
124
131
|
const finger = fingerIndex ?? 0
|
|
125
132
|
const press = this.presses.get(finger)
|
|
126
133
|
if (gestureName === "touch_down" || gestureName === "tap_start") {
|
|
134
|
+
if (gestureName === "touch_down") this.lastEdgeAt.set(finger, Date.now())
|
|
127
135
|
if (press) {
|
|
128
|
-
// Both reports describe one press
|
|
136
|
+
// Both reports describe one press: after touch_down, tap_start means it is held.
|
|
129
137
|
if (gestureName === "touch_down") press.raw = true
|
|
138
|
+
else if (press.raw) press.glyph = "hold"
|
|
139
|
+
this.render()
|
|
130
140
|
return
|
|
131
141
|
}
|
|
132
142
|
this.clearFlash()
|
|
@@ -137,21 +147,24 @@ class GestureDotsService {
|
|
|
137
147
|
if (gestureName === "tap_end") {
|
|
138
148
|
if (!press) return
|
|
139
149
|
this.presses.delete(finger)
|
|
140
|
-
|
|
141
|
-
// Keep the glyph lit across that gap so one pinch never flashes twice.
|
|
142
|
-
if (press.raw && this.presses.size === 0) this.startFlash(press.glyph)
|
|
150
|
+
if (press.raw) this.lastEdgeAt.set(finger, Date.now())
|
|
143
151
|
this.render()
|
|
144
152
|
return
|
|
145
153
|
}
|
|
146
154
|
const shape = FLASH_GLYPHS[gestureName]
|
|
147
155
|
const glyph = shape && finger === 1 ? MIDDLE_FINGER_GLYPHS[shape] ?? shape : shape
|
|
148
|
-
|
|
156
|
+
// The press already showed its own tap and hold; the device's report of
|
|
157
|
+
// them follows after its double-tap window, often after release.
|
|
158
|
+
const shownByPress = gestureName === "single_tap" || gestureName === "long_press"
|
|
159
|
+
if (press?.raw && (!glyph || shownByPress)) {
|
|
149
160
|
// Pinch-and-turn reports do not release a finger. A recognized hold
|
|
150
161
|
// stays visible until tap_end instead of expiring halfway through it.
|
|
151
|
-
if (
|
|
162
|
+
if (gestureName === "long_press") press.glyph = "hold"
|
|
152
163
|
this.render()
|
|
153
164
|
return
|
|
154
165
|
}
|
|
166
|
+
const lastEdgeAt = this.lastEdgeAt.get(finger)
|
|
167
|
+
if (!press && shownByPress && lastEdgeAt !== undefined && Date.now() - lastEdgeAt < EDGE_REPORT_GRACE_MS) return
|
|
155
168
|
// Completed taps/swipes become one continuous pulse, even if the device
|
|
156
169
|
// omits tap_end. Older tap_start-only devices retain the same fallback.
|
|
157
170
|
this.presses.delete(finger)
|
|
@@ -7,6 +7,7 @@ import {useGlassesStore} from "../stores/glasses"
|
|
|
7
7
|
import {BgTimer} from "../utils/timers"
|
|
8
8
|
import dashboardOpenTrace from "./debug/DashboardOpenTrace"
|
|
9
9
|
import gestureDotsService from "./GestureDotsService"
|
|
10
|
+
import {isGlassesConnected} from "./GlassesReadiness"
|
|
10
11
|
import localDisplayManager from "./LocalDisplayManager"
|
|
11
12
|
import localMiniappRuntime from "./LocalMiniappRuntime"
|
|
12
13
|
import {MiniappExitGesture} from "./MiniappExitGesture"
|
|
@@ -92,8 +93,9 @@ export function dispatchTouchEvent(event: TouchEvent): void {
|
|
|
92
93
|
// Nobody claimed a double tap: the glasses' own dashboard. On eligible
|
|
93
94
|
// Glassly CFW glasses the dashboard miniapp claims it while it runs
|
|
94
95
|
// (ADR-0006); stock firmware keeps its native dashboard (ADR-0002). The
|
|
95
|
-
// modal nav UI owns the screen; showDashboard would fight it.
|
|
96
|
-
|
|
96
|
+
// modal nav UI owns the screen; showDashboard would fight it. Without
|
|
97
|
+
// glasses there is no dashboard to open, and the mirror keeps its frame.
|
|
98
|
+
if (osDoubleTap && !nativeNavigationService.isActive && isGlassesConnected(useGlassesStore.getState().connection)) {
|
|
97
99
|
useDisplayStore.getState().setDisplayEvent({layout: {layoutType: "unavailable", reason: "firmware-dashboard"}})
|
|
98
100
|
void Promise.resolve(BluetoothSdk.showDashboard()).catch((err) => {
|
|
99
101
|
console.error("DeviceEventRouter: showDashboard failed:", err)
|