@glassly/engine 0.3.4-dev.676 → 0.3.4-dev.679

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.
@@ -1,9 +1,9 @@
1
1
  export const ENGINE_RELEASE_METADATA = Object.freeze({
2
2
  "schemaVersion": 1,
3
3
  "familyBaseVersion": "0.3.4",
4
- "releaseIdentity": "0.3.4-dev.676",
5
- "releaseSetId": "glassly-0.3.4-dev.676",
6
- "sourceCommit": "9066b8df01df6448c03c1765e1dbc11965bcaf6c",
4
+ "releaseIdentity": "0.3.4-dev.679",
5
+ "releaseSetId": "glassly-0.3.4-dev.679",
6
+ "sourceCommit": "a20f7318a933f9f959e73303c1babb564155352f",
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.4\",\n \"releaseIdentity\": \"0.3.4-dev.676\",\n \"releaseSetId\": \"glassly-0.3.4-dev.676\",\n \"sourceCommit\": \"9066b8df01df6448c03c1765e1dbc11965bcaf6c\",\n \"otaManifestUrl\": null,\n \"otaManifestSha256\": null\n})\n"]}
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.4\",\n \"releaseIdentity\": \"0.3.4-dev.679\",\n \"releaseSetId\": \"glassly-0.3.4-dev.679\",\n \"sourceCommit\": \"a20f7318a933f9f959e73303c1babb564155352f\",\n \"otaManifestUrl\": null,\n \"otaManifestSha256\": null\n})\n"]}
@@ -26,6 +26,16 @@ declare class GestureDotsService {
26
26
  */
27
27
  noteGesture(gestureName: string, claimed?: boolean, fingerIndex?: number, controllerId?: string): void;
28
28
  private applyGesture;
29
+ /**
30
+ * The press this edge belongs to. Glasses relaying a ring tag its edges
31
+ * unevenly (a touch_down carries the ring's id while the firmware leaves the
32
+ * release's source unspecified, or the reverse), so an edge that misses its
33
+ * exact key joins the one outstanding press of that finger instead of
34
+ * leaving it stuck.
35
+ */
36
+ private pressKey;
37
+ /** The finger's latest press edge under any attribution (see pressKey). */
38
+ private lastEdgeFor;
29
39
  private clearFlash;
30
40
  private startFlash;
31
41
  /** Debug setting only; no payloads or retained history. Correlate seq with G2PERF. */
@@ -1 +1 @@
1
- {"version":3,"file":"GestureDotsService.d.ts","sourceRoot":"","sources":["../../src/services/GestureDotsService.ts"],"names":[],"mappings":"AA2DA,cAAM,kBAAkB;IACtB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,UAAU,CAAsB;IACxC,6GAA6G;IAC7G,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;;;;;;OAMG;IACH,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,UAAQ,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI;IAMpG,OAAO,CAAC,YAAY;IAuDpB,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"}
1
+ {"version":3,"file":"GestureDotsService.d.ts","sourceRoot":"","sources":["../../src/services/GestureDotsService.ts"],"names":[],"mappings":"AA+DA,cAAM,kBAAkB;IACtB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,KAAK,CAA+B;IAC5C,OAAO,CAAC,UAAU,CAAsB;IACxC,6GAA6G;IAC7G,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;;;;;;OAMG;IACH,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,UAAQ,EAAE,WAAW,CAAC,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI;IAMpG,OAAO,CAAC,YAAY;IAuDpB;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ;IAWhB,2EAA2E;IAC3E,OAAO,CAAC,WAAW;IAQnB,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"}
@@ -47,11 +47,14 @@ const MIDDLE_FINGER_GLYPHS = {
47
47
  };
48
48
  /** `glyph` as `finger` draws it: the middle finger's shapes are hollow. */
49
49
  function fingerGlyph(glyph, finger) {
50
- return finger === 1 ? (MIDDLE_FINGER_GLYPHS[glyph] ?? glyph) : glyph;
50
+ return finger === 1 ? MIDDLE_FINGER_GLYPHS[glyph] ?? glyph : glyph;
51
51
  }
52
52
  function fingerKey(controllerId, finger) {
53
53
  return `${controllerId ?? ""}:${finger}`;
54
54
  }
55
+ function fingerOf(key) {
56
+ return Number(key.slice(key.lastIndexOf(":") + 1));
57
+ }
55
58
  class GestureDotsService {
56
59
  started = false;
57
60
  flash = null;
@@ -135,7 +138,7 @@ class GestureDotsService {
135
138
  return;
136
139
  }
137
140
  const finger = fingerIndex ?? 0;
138
- const key = fingerKey(controllerId, finger);
141
+ const key = this.pressKey(controllerId, finger);
139
142
  const press = this.presses.get(key);
140
143
  if (gestureName === "touch_down" || gestureName === "tap_start") {
141
144
  if (gestureName === "touch_down")
@@ -176,7 +179,7 @@ class GestureDotsService {
176
179
  this.render();
177
180
  return;
178
181
  }
179
- const lastEdgeAt = this.lastEdgeAt.get(key);
182
+ const lastEdgeAt = this.lastEdgeFor(finger);
180
183
  if (!press && shownByPress && lastEdgeAt !== undefined && Date.now() - lastEdgeAt < EDGE_REPORT_GRACE_MS)
181
184
  return;
182
185
  // Completed taps/swipes become one continuous pulse, even if the device
@@ -186,6 +189,33 @@ class GestureDotsService {
186
189
  this.startFlash(glyph);
187
190
  this.render();
188
191
  }
192
+ /**
193
+ * The press this edge belongs to. Glasses relaying a ring tag its edges
194
+ * unevenly (a touch_down carries the ring's id while the firmware leaves the
195
+ * release's source unspecified, or the reverse), so an edge that misses its
196
+ * exact key joins the one outstanding press of that finger instead of
197
+ * leaving it stuck.
198
+ */
199
+ pressKey(controllerId, finger) {
200
+ const exact = fingerKey(controllerId, finger);
201
+ if (this.presses.has(exact))
202
+ return exact;
203
+ if (controllerId !== undefined) {
204
+ const unattributed = fingerKey(undefined, finger);
205
+ return this.presses.has(unattributed) ? unattributed : exact;
206
+ }
207
+ const candidates = [...this.presses.keys()].filter((key) => fingerOf(key) === finger);
208
+ return candidates.length === 1 ? candidates[0] : exact;
209
+ }
210
+ /** The finger's latest press edge under any attribution (see pressKey). */
211
+ lastEdgeFor(finger) {
212
+ let latest;
213
+ for (const [key, at] of this.lastEdgeAt) {
214
+ if (fingerOf(key) === finger && (latest === undefined || at > latest))
215
+ latest = at;
216
+ }
217
+ return latest;
218
+ }
189
219
  clearFlash() {
190
220
  if (this.flashTimer !== null)
191
221
  BgTimer.clearTimeout(this.flashTimer);
@@ -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 * 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\n * hollow: a ring for its tap, the bullseye without its center for its hold. 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>> = {\n dot: \"ring\",\n double: \"doubleRing\",\n hold: \"holdRing\",\n}\n\n/** `glyph` as `finger` draws it: the middle finger's shapes are hollow. */\nfunction fingerGlyph(glyph: GestureDotGlyph, finger: number): GestureDotGlyph {\n return finger === 1 ? (MIDDLE_FINGER_GLYPHS[glyph] ?? glyph) : glyph\n}\n\nfunction fingerKey(controllerId: string | undefined, finger: number): string {\n return `${controllerId ?? \"\"}:${finger}`\n}\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. Keyed by fingerKey. */\n private presses = new Map<string, {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<string, 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 * `controllerId` keeps two controllers' fingers apart.\n */\n noteGesture(gestureName: string, claimed = false, fingerIndex?: number, controllerId?: string): void {\n if (!this.started || !this.enabled()) return\n this.applyGesture(gestureName, claimed, fingerIndex, controllerId)\n this.trace(\"input\", {gestureName, claimed})\n }\n\n private applyGesture(gestureName: string, claimed: boolean, fingerIndex?: number, controllerId?: string): 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 key = fingerKey(controllerId, finger)\n const press = this.presses.get(key)\n if (gestureName === \"touch_down\" || gestureName === \"tap_start\") {\n if (gestureName === \"touch_down\") this.lastEdgeAt.set(key, 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 = fingerGlyph(\"hold\", finger)\n this.render()\n return\n }\n this.clearFlash()\n this.presses.set(key, {raw: gestureName === \"touch_down\", glyph: fingerGlyph(\"dot\", finger)})\n this.render()\n return\n }\n if (gestureName === \"tap_end\") {\n if (!press) return\n this.presses.delete(key)\n if (press.raw) this.lastEdgeAt.set(key, Date.now())\n this.render()\n return\n }\n const shape = FLASH_GLYPHS[gestureName]\n const glyph = shape ? fingerGlyph(shape, finger) : 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 = fingerGlyph(\"hold\", finger)\n this.render()\n return\n }\n const lastEdgeAt = this.lastEdgeAt.get(key)\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(key)\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\n * hollow: a ring for its tap, the bullseye without its center for its hold. 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>> = {\n dot: \"ring\",\n double: \"doubleRing\",\n hold: \"holdRing\",\n}\n\n/** `glyph` as `finger` draws it: the middle finger's shapes are hollow. */\nfunction fingerGlyph(glyph: GestureDotGlyph, finger: number): GestureDotGlyph {\n return finger === 1 ? MIDDLE_FINGER_GLYPHS[glyph] ?? glyph : glyph\n}\n\nfunction fingerKey(controllerId: string | undefined, finger: number): string {\n return `${controllerId ?? \"\"}:${finger}`\n}\n\nfunction fingerOf(key: string): number {\n return Number(key.slice(key.lastIndexOf(\":\") + 1))\n}\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. Keyed by fingerKey. */\n private presses = new Map<string, {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<string, 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 * `controllerId` keeps two controllers' fingers apart.\n */\n noteGesture(gestureName: string, claimed = false, fingerIndex?: number, controllerId?: string): void {\n if (!this.started || !this.enabled()) return\n this.applyGesture(gestureName, claimed, fingerIndex, controllerId)\n this.trace(\"input\", {gestureName, claimed})\n }\n\n private applyGesture(gestureName: string, claimed: boolean, fingerIndex?: number, controllerId?: string): 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 key = this.pressKey(controllerId, finger)\n const press = this.presses.get(key)\n if (gestureName === \"touch_down\" || gestureName === \"tap_start\") {\n if (gestureName === \"touch_down\") this.lastEdgeAt.set(key, 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 = fingerGlyph(\"hold\", finger)\n this.render()\n return\n }\n this.clearFlash()\n this.presses.set(key, {raw: gestureName === \"touch_down\", glyph: fingerGlyph(\"dot\", finger)})\n this.render()\n return\n }\n if (gestureName === \"tap_end\") {\n if (!press) return\n this.presses.delete(key)\n if (press.raw) this.lastEdgeAt.set(key, Date.now())\n this.render()\n return\n }\n const shape = FLASH_GLYPHS[gestureName]\n const glyph = shape ? fingerGlyph(shape, finger) : 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 = fingerGlyph(\"hold\", finger)\n this.render()\n return\n }\n const lastEdgeAt = this.lastEdgeFor(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(key)\n if (glyph) this.startFlash(glyph)\n this.render()\n }\n\n /**\n * The press this edge belongs to. Glasses relaying a ring tag its edges\n * unevenly (a touch_down carries the ring's id while the firmware leaves the\n * release's source unspecified, or the reverse), so an edge that misses its\n * exact key joins the one outstanding press of that finger instead of\n * leaving it stuck.\n */\n private pressKey(controllerId: string | undefined, finger: number): string {\n const exact = fingerKey(controllerId, finger)\n if (this.presses.has(exact)) return exact\n if (controllerId !== undefined) {\n const unattributed = fingerKey(undefined, finger)\n return this.presses.has(unattributed) ? unattributed : exact\n }\n const candidates = [...this.presses.keys()].filter((key) => fingerOf(key) === finger)\n return candidates.length === 1 ? candidates[0] : exact\n }\n\n /** The finger's latest press edge under any attribution (see pressKey). */\n private lastEdgeFor(finger: number): number | undefined {\n let latest: number | undefined\n for (const [key, at] of this.lastEdgeAt) {\n if (fingerOf(key) === finger && (latest === undefined || at > latest)) latest = at\n }\n return latest\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"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassly/engine",
3
- "version": "0.3.4-dev.676",
3
+ "version": "0.3.4-dev.679",
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.4-dev.676",
101
- "@glassly/cloud-client": "0.3.4-dev.676",
102
- "@glassly/cloud-protocol": "0.3.4-dev.676",
103
- "@glassly/crust": "0.3.4-dev.676",
104
- "@glassly/miniapp": "0.3.4-dev.676",
100
+ "@glassly/bluetooth-sdk": "0.3.4-dev.679",
101
+ "@glassly/cloud-client": "0.3.4-dev.679",
102
+ "@glassly/cloud-protocol": "0.3.4-dev.679",
103
+ "@glassly/crust": "0.3.4-dev.679",
104
+ "@glassly/miniapp": "0.3.4-dev.679",
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.4-dev.676",
110
+ "@glassly/llm-providers": "0.3.4-dev.679",
111
111
  "@huggingface/jinja": "^0.5.9",
112
112
  "jsonrepair": "^3.12.0"
113
113
  },
@@ -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.4",
15
- "releaseIdentity": "0.3.4-dev.676",
16
- "releaseSetId": "glassly-0.3.4-dev.676",
17
- "sourceCommit": "9066b8df01df6448c03c1765e1dbc11965bcaf6c",
15
+ "releaseIdentity": "0.3.4-dev.679",
16
+ "releaseSetId": "glassly-0.3.4-dev.679",
17
+ "sourceCommit": "a20f7318a933f9f959e73303c1babb564155352f",
18
18
  "otaManifestUrl": null,
19
19
  "otaManifestSha256": null
20
20
  })
@@ -50,13 +50,17 @@ const MIDDLE_FINGER_GLYPHS: Partial<Record<GestureDotGlyph, GestureDotGlyph>> =
50
50
 
51
51
  /** `glyph` as `finger` draws it: the middle finger's shapes are hollow. */
52
52
  function fingerGlyph(glyph: GestureDotGlyph, finger: number): GestureDotGlyph {
53
- return finger === 1 ? (MIDDLE_FINGER_GLYPHS[glyph] ?? glyph) : glyph
53
+ return finger === 1 ? MIDDLE_FINGER_GLYPHS[glyph] ?? glyph : glyph
54
54
  }
55
55
 
56
56
  function fingerKey(controllerId: string | undefined, finger: number): string {
57
57
  return `${controllerId ?? ""}:${finger}`
58
58
  }
59
59
 
60
+ function fingerOf(key: string): number {
61
+ return Number(key.slice(key.lastIndexOf(":") + 1))
62
+ }
63
+
60
64
  class GestureDotsService {
61
65
  private started = false
62
66
  private flash: GestureDotGlyph | null = null
@@ -144,7 +148,7 @@ class GestureDotsService {
144
148
  return
145
149
  }
146
150
  const finger = fingerIndex ?? 0
147
- const key = fingerKey(controllerId, finger)
151
+ const key = this.pressKey(controllerId, finger)
148
152
  const press = this.presses.get(key)
149
153
  if (gestureName === "touch_down" || gestureName === "tap_start") {
150
154
  if (gestureName === "touch_down") this.lastEdgeAt.set(key, Date.now())
@@ -179,7 +183,7 @@ class GestureDotsService {
179
183
  this.render()
180
184
  return
181
185
  }
182
- const lastEdgeAt = this.lastEdgeAt.get(key)
186
+ const lastEdgeAt = this.lastEdgeFor(finger)
183
187
  if (!press && shownByPress && lastEdgeAt !== undefined && Date.now() - lastEdgeAt < EDGE_REPORT_GRACE_MS) return
184
188
  // Completed taps/swipes become one continuous pulse, even if the device
185
189
  // omits tap_end. Older tap_start-only devices retain the same fallback.
@@ -188,6 +192,33 @@ class GestureDotsService {
188
192
  this.render()
189
193
  }
190
194
 
195
+ /**
196
+ * The press this edge belongs to. Glasses relaying a ring tag its edges
197
+ * unevenly (a touch_down carries the ring's id while the firmware leaves the
198
+ * release's source unspecified, or the reverse), so an edge that misses its
199
+ * exact key joins the one outstanding press of that finger instead of
200
+ * leaving it stuck.
201
+ */
202
+ private pressKey(controllerId: string | undefined, finger: number): string {
203
+ const exact = fingerKey(controllerId, finger)
204
+ if (this.presses.has(exact)) return exact
205
+ if (controllerId !== undefined) {
206
+ const unattributed = fingerKey(undefined, finger)
207
+ return this.presses.has(unattributed) ? unattributed : exact
208
+ }
209
+ const candidates = [...this.presses.keys()].filter((key) => fingerOf(key) === finger)
210
+ return candidates.length === 1 ? candidates[0] : exact
211
+ }
212
+
213
+ /** The finger's latest press edge under any attribution (see pressKey). */
214
+ private lastEdgeFor(finger: number): number | undefined {
215
+ let latest: number | undefined
216
+ for (const [key, at] of this.lastEdgeAt) {
217
+ if (fingerOf(key) === finger && (latest === undefined || at > latest)) latest = at
218
+ }
219
+ return latest
220
+ }
221
+
191
222
  private clearFlash(): void {
192
223
  if (this.flashTimer !== null) BgTimer.clearTimeout(this.flashTimer)
193
224
  this.flashTimer = null