@glassly/engine 0.3.2-dev.595 → 0.3.2-dev.596
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 +6 -9
- package/build/services/GestureDotsService.d.ts.map +1 -1
- package/build/services/GestureDotsService.js +56 -45
- package/build/services/GestureDotsService.js.map +1 -1
- package/package.json +7 -7
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/services/GestureDotsService.ts +53 -43
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export const ENGINE_RELEASE_METADATA = Object.freeze({
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"familyBaseVersion": "0.3.2",
|
|
4
|
-
"releaseIdentity": "0.3.2-dev.
|
|
5
|
-
"releaseSetId": "glassly-0.3.2-dev.
|
|
6
|
-
"sourceCommit": "
|
|
4
|
+
"releaseIdentity": "0.3.2-dev.596",
|
|
5
|
+
"releaseSetId": "glassly-0.3.2-dev.596",
|
|
6
|
+
"sourceCommit": "4bbd4945a62883e5f384569b0f5c56a51e5c81cd",
|
|
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.2\",\n \"releaseIdentity\": \"0.3.2-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.2\",\n \"releaseIdentity\": \"0.3.2-dev.596\",\n \"releaseSetId\": \"glassly-0.3.2-dev.596\",\n \"sourceCommit\": \"4bbd4945a62883e5f384569b0f5c56a51e5c81cd\",\n \"otaManifestUrl\": null,\n \"otaManifestSha256\": null\n})\n"]}
|
|
@@ -2,15 +2,10 @@ declare class GestureDotsService {
|
|
|
2
2
|
private started;
|
|
3
3
|
private flash;
|
|
4
4
|
private flashTimer;
|
|
5
|
-
/**
|
|
6
|
-
private
|
|
7
|
-
/**
|
|
8
|
-
private
|
|
9
|
-
/**
|
|
10
|
-
* The CFW's tap_then_hold fired during this press (discrete recognition
|
|
11
|
-
* event; the tap_end that follows on release is what clears it).
|
|
12
|
-
*/
|
|
13
|
-
private holdRecognized;
|
|
5
|
+
/** Raw controller holds survive hold recognition/dial events until their own release. */
|
|
6
|
+
private presses;
|
|
7
|
+
/** Avoid sending identical images for successive phases of the same press. */
|
|
8
|
+
private renderedGlyph;
|
|
14
9
|
/** Whether the indicator scene currently has anything on it (needs a clear when the flash ends). */
|
|
15
10
|
private visible;
|
|
16
11
|
private unsubs;
|
|
@@ -28,6 +23,8 @@ declare class GestureDotsService {
|
|
|
28
23
|
*/
|
|
29
24
|
noteGesture(gestureName: string, claimed?: boolean, fingerIndex?: number): void;
|
|
30
25
|
private applyGesture;
|
|
26
|
+
private clearFlash;
|
|
27
|
+
private startFlash;
|
|
31
28
|
/** Debug setting only; no payloads or retained history. Correlate seq with G2PERF. */
|
|
32
29
|
private trace;
|
|
33
30
|
/**
|
|
@@ -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":"AAyCA,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,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;IAOZ,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;IAgDpB,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"}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Gesture dots — a debug indicator that flashes a small glyph top-right on the
|
|
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
|
-
* (cleared after a short pulse), a bullseye
|
|
5
|
+
* (cleared after a short pulse), a bullseye for a recognized long press.
|
|
6
6
|
* The Neural Band's middle finger (index 1) draws the same shapes hollow. Extracted from
|
|
7
7
|
* the CFW dashboard service; it renders through
|
|
8
8
|
* the ordinary scene pipeline, so it works on any positioning device (stock
|
|
@@ -24,8 +24,6 @@ const DOTS_APP_ID = "system.gesture-dots";
|
|
|
24
24
|
const GESTURE_FLASH_MS = 700;
|
|
25
25
|
const MARGIN_X = 16;
|
|
26
26
|
const MARGIN_TOP = 10;
|
|
27
|
-
// Off: tap_then_hold opens the side menu now.
|
|
28
|
-
const HOLD_GLYPH_ENABLED = false;
|
|
29
27
|
/** Gestures that flash a glyph. */
|
|
30
28
|
const FLASH_GLYPHS = {
|
|
31
29
|
single_tap: "dot",
|
|
@@ -42,15 +40,10 @@ class GestureDotsService {
|
|
|
42
40
|
started = false;
|
|
43
41
|
flash = null;
|
|
44
42
|
flashTimer = null;
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* The CFW's tap_then_hold fired during this press (discrete recognition
|
|
51
|
-
* event; the tap_end that follows on release is what clears it).
|
|
52
|
-
*/
|
|
53
|
-
holdRecognized = false;
|
|
43
|
+
/** Raw controller holds survive hold recognition/dial events until their own release. */
|
|
44
|
+
presses = new Map();
|
|
45
|
+
/** Avoid sending identical images for successive phases of the same press. */
|
|
46
|
+
renderedGlyph = null;
|
|
54
47
|
/** Whether the indicator scene currently has anything on it (needs a clear when the flash ends). */
|
|
55
48
|
visible = false;
|
|
56
49
|
unsubs = [];
|
|
@@ -86,14 +79,13 @@ class GestureDotsService {
|
|
|
86
79
|
BgTimer.clearTimeout(this.flashTimer);
|
|
87
80
|
this.flashTimer = null;
|
|
88
81
|
this.flash = null;
|
|
89
|
-
this.
|
|
90
|
-
this.heldMiddle = false;
|
|
91
|
-
this.holdRecognized = false;
|
|
82
|
+
this.presses.clear();
|
|
92
83
|
}
|
|
93
84
|
/** Another surface owns the display; forget the indicator without painting over it. */
|
|
94
85
|
relinquish() {
|
|
95
86
|
this.resetGesture();
|
|
96
87
|
this.visible = false;
|
|
88
|
+
this.renderedGlyph = null;
|
|
97
89
|
sceneRenderer.clearApp(DOTS_APP_ID);
|
|
98
90
|
this.trace("relinquish");
|
|
99
91
|
}
|
|
@@ -122,49 +114,63 @@ class GestureDotsService {
|
|
|
122
114
|
this.render();
|
|
123
115
|
return;
|
|
124
116
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
117
|
+
const finger = fingerIndex ?? 0;
|
|
118
|
+
const press = this.presses.get(finger);
|
|
119
|
+
if (gestureName === "touch_down" || gestureName === "tap_start") {
|
|
120
|
+
if (press) {
|
|
121
|
+
// Both reports describe one press. Never restart its visual feedback.
|
|
122
|
+
if (gestureName === "touch_down")
|
|
123
|
+
press.raw = true;
|
|
132
124
|
return;
|
|
133
|
-
|
|
134
|
-
this.
|
|
135
|
-
|
|
136
|
-
this.holdRecognized = false;
|
|
125
|
+
}
|
|
126
|
+
this.clearFlash();
|
|
127
|
+
this.presses.set(finger, { raw: gestureName === "touch_down", glyph: finger === 1 ? "ring" : "dot" });
|
|
137
128
|
this.render();
|
|
138
129
|
return;
|
|
139
130
|
}
|
|
140
|
-
if (gestureName === "
|
|
141
|
-
|
|
142
|
-
|
|
131
|
+
if (gestureName === "tap_end") {
|
|
132
|
+
if (!press)
|
|
133
|
+
return;
|
|
134
|
+
this.presses.delete(finger);
|
|
135
|
+
// The band's resolved tap arrives after release (double-tap window).
|
|
136
|
+
// Keep the glyph lit across that gap so one pinch never flashes twice.
|
|
137
|
+
if (press.raw && this.presses.size === 0)
|
|
138
|
+
this.startFlash(press.glyph);
|
|
143
139
|
this.render();
|
|
144
140
|
return;
|
|
145
141
|
}
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (released)
|
|
155
|
-
this.render();
|
|
142
|
+
const shape = FLASH_GLYPHS[gestureName];
|
|
143
|
+
const glyph = shape && finger === 1 ? MIDDLE_FINGER_GLYPHS[shape] ?? shape : shape;
|
|
144
|
+
if (press?.raw && (!glyph || gestureName === "long_press")) {
|
|
145
|
+
// Pinch-and-turn reports do not release a finger. A recognized hold
|
|
146
|
+
// stays visible until tap_end instead of expiring halfway through it.
|
|
147
|
+
if (glyph)
|
|
148
|
+
press.glyph = glyph;
|
|
149
|
+
this.render();
|
|
156
150
|
return;
|
|
157
151
|
}
|
|
158
|
-
|
|
152
|
+
// Completed taps/swipes become one continuous pulse, even if the device
|
|
153
|
+
// omits tap_end. Older tap_start-only devices retain the same fallback.
|
|
154
|
+
this.presses.delete(finger);
|
|
155
|
+
if (glyph)
|
|
156
|
+
this.startFlash(glyph);
|
|
157
|
+
this.render();
|
|
158
|
+
}
|
|
159
|
+
clearFlash() {
|
|
159
160
|
if (this.flashTimer !== null)
|
|
160
161
|
BgTimer.clearTimeout(this.flashTimer);
|
|
162
|
+
this.flashTimer = null;
|
|
163
|
+
this.flash = null;
|
|
164
|
+
}
|
|
165
|
+
startFlash(glyph) {
|
|
166
|
+
this.clearFlash();
|
|
167
|
+
this.flash = glyph;
|
|
161
168
|
this.flashTimer = BgTimer.setTimeout(() => {
|
|
162
169
|
this.flashTimer = null;
|
|
163
170
|
this.flash = null;
|
|
164
171
|
this.render();
|
|
165
172
|
this.trace("flash-ended");
|
|
166
173
|
}, GESTURE_FLASH_MS);
|
|
167
|
-
this.render();
|
|
168
174
|
}
|
|
169
175
|
/** Debug setting only; no payloads or retained history. Correlate seq with G2PERF. */
|
|
170
176
|
trace(reason, details = {}) {
|
|
@@ -174,7 +180,7 @@ class GestureDotsService {
|
|
|
174
180
|
JSON.stringify({
|
|
175
181
|
t: Date.now(),
|
|
176
182
|
reason,
|
|
177
|
-
held: this.
|
|
183
|
+
held: this.presses.size > 0,
|
|
178
184
|
flash: this.flash,
|
|
179
185
|
overlay: useDisplayStore.getState().overlayOwner,
|
|
180
186
|
active: localDisplayManager.hasActiveDisplay(),
|
|
@@ -197,16 +203,20 @@ class GestureDotsService {
|
|
|
197
203
|
return;
|
|
198
204
|
if (localDisplayManager.hasActiveDisplay() || useDisplayStore.getState().overlayOwner !== null)
|
|
199
205
|
return;
|
|
200
|
-
const shown =
|
|
206
|
+
const shown = Array.from(this.presses.values()).at(-1)?.glyph ?? this.flash;
|
|
201
207
|
if (!this.enabled()) {
|
|
202
208
|
if (this.visible) {
|
|
203
209
|
this.visible = false;
|
|
210
|
+
this.renderedGlyph = null;
|
|
204
211
|
sceneRenderer.emitScene(DOTS_APP_ID, "main", []);
|
|
205
212
|
}
|
|
206
213
|
return;
|
|
207
214
|
}
|
|
208
215
|
if (!shown && !this.visible)
|
|
209
216
|
return;
|
|
217
|
+
const glyph = shown ?? "blank";
|
|
218
|
+
if (glyph === this.renderedGlyph)
|
|
219
|
+
return;
|
|
210
220
|
const elements = [
|
|
211
221
|
{
|
|
212
222
|
type: "image",
|
|
@@ -218,13 +228,14 @@ class GestureDotsService {
|
|
|
218
228
|
h: GESTURE_DOT_GLYPH_SIZE,
|
|
219
229
|
anchor: "top-right",
|
|
220
230
|
},
|
|
221
|
-
data: GESTURE_DOT_GLYPHS[
|
|
231
|
+
data: GESTURE_DOT_GLYPHS[glyph],
|
|
222
232
|
},
|
|
223
233
|
];
|
|
224
234
|
this.visible = true;
|
|
235
|
+
this.renderedGlyph = glyph;
|
|
225
236
|
const result = sceneRenderer.emitScene(DOTS_APP_ID, "main", elements);
|
|
226
237
|
this.trace("render", {
|
|
227
|
-
glyph
|
|
238
|
+
glyph,
|
|
228
239
|
...(result?.kind === "scene" ? { seq: result.seq, sent: result.sent } : {}),
|
|
229
240
|
});
|
|
230
241
|
}
|
|
@@ -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 while a CFW tap-then-hold is held.\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// Off: tap_then_hold opens the side menu now.\nconst HOLD_GLYPH_ENABLED = false\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 /** Finger currently on the touch bar (tap_start..tap_end; the CFW forwards holds as these edges). */\n private held = false\n /** The band's middle finger is down (its edges carry index 1); any other finger draws the disc. */\n private heldMiddle = false\n /**\n * The CFW's tap_then_hold fired during this press (discrete recognition\n * event; the tap_end that follows on release is what clears it).\n */\n private holdRecognized = false\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.held = false\n this.heldMiddle = false\n this.holdRecognized = false\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 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 // Tap edges show a steady circle for as long as the finger is down (on\n // CFW these arrive for holds: ET_LONG/ET_REL forwarded as\n // tap_start/tap_end), with no timer — tap_end clears it. The ring's\n // touch_down is the same finger-down edge; the ring's tap_end releases it.\n if (gestureName === \"tap_start\" || gestureName === \"touch_down\" || gestureName === \"tap_end\") {\n const held = gestureName !== \"tap_end\"\n if (this.held === held) return\n this.held = held\n this.heldMiddle = held && fingerIndex === 1\n if (!held) this.holdRecognized = false\n this.render()\n return\n }\n if (gestureName === \"tap_then_hold\" && HOLD_GLYPH_ENABLED) {\n this.held = true\n this.holdRecognized = true\n this.render()\n return\n }\n // Any other gesture implies the finger is up, even if tap_end was lost.\n const released = this.held\n this.held = false\n this.heldMiddle = false\n this.holdRecognized = false\n const shape = FLASH_GLYPHS[gestureName] ?? null\n const flash = shape && fingerIndex === 1 ? MIDDLE_FINGER_GLYPHS[shape] ?? shape : shape\n if (!flash) {\n if (released) this.render()\n return\n }\n this.flash = flash\n if (this.flashTimer !== null) BgTimer.clearTimeout(this.flashTimer)\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 this.render()\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.held,\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 = this.flash ?? (this.held ? (this.holdRecognized ? \"hold\" : this.heldMiddle ? \"ring\" : \"dot\") : null)\n if (!this.enabled()) {\n if (this.visible) {\n this.visible = false\n sceneRenderer.emitScene(DOTS_APP_ID, \"main\", [])\n }\n return\n }\n if (!shown && !this.visible) 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[shown ?? \"blank\"],\n },\n ]\n this.visible = true\n const result = sceneRenderer.emitScene(DOTS_APP_ID, \"main\", elements)\n this.trace(\"render\", {\n glyph: shown ?? \"blank\",\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 * 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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glassly/engine",
|
|
3
|
-
"version": "0.3.2-dev.
|
|
3
|
+
"version": "0.3.2-dev.596",
|
|
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.2-dev.
|
|
101
|
-
"@glassly/cloud-client": "0.3.2-dev.
|
|
102
|
-
"@glassly/cloud-protocol": "0.3.2-dev.
|
|
103
|
-
"@glassly/crust": "0.3.2-dev.
|
|
104
|
-
"@glassly/miniapp": "0.3.2-dev.
|
|
100
|
+
"@glassly/bluetooth-sdk": "0.3.2-dev.596",
|
|
101
|
+
"@glassly/cloud-client": "0.3.2-dev.596",
|
|
102
|
+
"@glassly/cloud-protocol": "0.3.2-dev.596",
|
|
103
|
+
"@glassly/crust": "0.3.2-dev.596",
|
|
104
|
+
"@glassly/miniapp": "0.3.2-dev.596",
|
|
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.2-dev.
|
|
110
|
+
"@glassly/llm-providers": "0.3.2-dev.596"
|
|
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.2",
|
|
15
|
-
"releaseIdentity": "0.3.2-dev.
|
|
16
|
-
"releaseSetId": "glassly-0.3.2-dev.
|
|
17
|
-
"sourceCommit": "
|
|
15
|
+
"releaseIdentity": "0.3.2-dev.596",
|
|
16
|
+
"releaseSetId": "glassly-0.3.2-dev.596",
|
|
17
|
+
"sourceCommit": "4bbd4945a62883e5f384569b0f5c56a51e5c81cd",
|
|
18
18
|
"otaManifestUrl": null,
|
|
19
19
|
"otaManifestSha256": null
|
|
20
20
|
})
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Gesture dots — a debug indicator that flashes a small glyph top-right on the
|
|
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
|
-
* (cleared after a short pulse), a bullseye
|
|
5
|
+
* (cleared after a short pulse), a bullseye for a recognized long press.
|
|
6
6
|
* The Neural Band's middle finger (index 1) draws the same shapes hollow. Extracted from
|
|
7
7
|
* the CFW dashboard service; it renders through
|
|
8
8
|
* the ordinary scene pipeline, so it works on any positioning device (stock
|
|
@@ -26,8 +26,6 @@ const DOTS_APP_ID = "system.gesture-dots"
|
|
|
26
26
|
const GESTURE_FLASH_MS = 700
|
|
27
27
|
const MARGIN_X = 16
|
|
28
28
|
const MARGIN_TOP = 10
|
|
29
|
-
// Off: tap_then_hold opens the side menu now.
|
|
30
|
-
const HOLD_GLYPH_ENABLED = false
|
|
31
29
|
/** Gestures that flash a glyph. */
|
|
32
30
|
const FLASH_GLYPHS: Record<string, GestureDotGlyph> = {
|
|
33
31
|
single_tap: "dot",
|
|
@@ -45,15 +43,10 @@ class GestureDotsService {
|
|
|
45
43
|
private started = false
|
|
46
44
|
private flash: GestureDotGlyph | null = null
|
|
47
45
|
private flashTimer: number | null = null
|
|
48
|
-
/**
|
|
49
|
-
private
|
|
50
|
-
/**
|
|
51
|
-
private
|
|
52
|
-
/**
|
|
53
|
-
* The CFW's tap_then_hold fired during this press (discrete recognition
|
|
54
|
-
* event; the tap_end that follows on release is what clears it).
|
|
55
|
-
*/
|
|
56
|
-
private holdRecognized = false
|
|
46
|
+
/** Raw controller holds survive hold recognition/dial events until their own release. */
|
|
47
|
+
private presses = new Map<number, {raw: boolean; glyph: GestureDotGlyph}>()
|
|
48
|
+
/** Avoid sending identical images for successive phases of the same press. */
|
|
49
|
+
private renderedGlyph: GestureDotGlyph | null = null
|
|
57
50
|
/** Whether the indicator scene currently has anything on it (needs a clear when the flash ends). */
|
|
58
51
|
private visible = false
|
|
59
52
|
private unsubs: Array<() => void> = []
|
|
@@ -90,15 +83,14 @@ class GestureDotsService {
|
|
|
90
83
|
if (this.flashTimer !== null) BgTimer.clearTimeout(this.flashTimer)
|
|
91
84
|
this.flashTimer = null
|
|
92
85
|
this.flash = null
|
|
93
|
-
this.
|
|
94
|
-
this.heldMiddle = false
|
|
95
|
-
this.holdRecognized = false
|
|
86
|
+
this.presses.clear()
|
|
96
87
|
}
|
|
97
88
|
|
|
98
89
|
/** Another surface owns the display; forget the indicator without painting over it. */
|
|
99
90
|
private relinquish(): void {
|
|
100
91
|
this.resetGesture()
|
|
101
92
|
this.visible = false
|
|
93
|
+
this.renderedGlyph = null
|
|
102
94
|
sceneRenderer.clearApp(DOTS_APP_ID)
|
|
103
95
|
this.trace("relinquish")
|
|
104
96
|
}
|
|
@@ -129,45 +121,59 @@ class GestureDotsService {
|
|
|
129
121
|
this.render()
|
|
130
122
|
return
|
|
131
123
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
this.
|
|
141
|
-
|
|
124
|
+
const finger = fingerIndex ?? 0
|
|
125
|
+
const press = this.presses.get(finger)
|
|
126
|
+
if (gestureName === "touch_down" || gestureName === "tap_start") {
|
|
127
|
+
if (press) {
|
|
128
|
+
// Both reports describe one press. Never restart its visual feedback.
|
|
129
|
+
if (gestureName === "touch_down") press.raw = true
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
this.clearFlash()
|
|
133
|
+
this.presses.set(finger, {raw: gestureName === "touch_down", glyph: finger === 1 ? "ring" : "dot"})
|
|
142
134
|
this.render()
|
|
143
135
|
return
|
|
144
136
|
}
|
|
145
|
-
if (gestureName === "
|
|
146
|
-
|
|
147
|
-
this.
|
|
137
|
+
if (gestureName === "tap_end") {
|
|
138
|
+
if (!press) return
|
|
139
|
+
this.presses.delete(finger)
|
|
140
|
+
// The band's resolved tap arrives after release (double-tap window).
|
|
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)
|
|
148
143
|
this.render()
|
|
149
144
|
return
|
|
150
145
|
}
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (!flash) {
|
|
159
|
-
if (released) this.render()
|
|
146
|
+
const shape = FLASH_GLYPHS[gestureName]
|
|
147
|
+
const glyph = shape && finger === 1 ? MIDDLE_FINGER_GLYPHS[shape] ?? shape : shape
|
|
148
|
+
if (press?.raw && (!glyph || gestureName === "long_press")) {
|
|
149
|
+
// Pinch-and-turn reports do not release a finger. A recognized hold
|
|
150
|
+
// stays visible until tap_end instead of expiring halfway through it.
|
|
151
|
+
if (glyph) press.glyph = glyph
|
|
152
|
+
this.render()
|
|
160
153
|
return
|
|
161
154
|
}
|
|
162
|
-
|
|
155
|
+
// Completed taps/swipes become one continuous pulse, even if the device
|
|
156
|
+
// omits tap_end. Older tap_start-only devices retain the same fallback.
|
|
157
|
+
this.presses.delete(finger)
|
|
158
|
+
if (glyph) this.startFlash(glyph)
|
|
159
|
+
this.render()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private clearFlash(): void {
|
|
163
163
|
if (this.flashTimer !== null) BgTimer.clearTimeout(this.flashTimer)
|
|
164
|
+
this.flashTimer = null
|
|
165
|
+
this.flash = null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private startFlash(glyph: GestureDotGlyph): void {
|
|
169
|
+
this.clearFlash()
|
|
170
|
+
this.flash = glyph
|
|
164
171
|
this.flashTimer = BgTimer.setTimeout(() => {
|
|
165
172
|
this.flashTimer = null
|
|
166
173
|
this.flash = null
|
|
167
174
|
this.render()
|
|
168
175
|
this.trace("flash-ended")
|
|
169
176
|
}, GESTURE_FLASH_MS)
|
|
170
|
-
this.render()
|
|
171
177
|
}
|
|
172
178
|
|
|
173
179
|
/** Debug setting only; no payloads or retained history. Correlate seq with G2PERF. */
|
|
@@ -178,7 +184,7 @@ class GestureDotsService {
|
|
|
178
184
|
JSON.stringify({
|
|
179
185
|
t: Date.now(),
|
|
180
186
|
reason,
|
|
181
|
-
held: this.
|
|
187
|
+
held: this.presses.size > 0,
|
|
182
188
|
flash: this.flash,
|
|
183
189
|
overlay: useDisplayStore.getState().overlayOwner,
|
|
184
190
|
active: localDisplayManager.hasActiveDisplay(),
|
|
@@ -201,15 +207,18 @@ class GestureDotsService {
|
|
|
201
207
|
private render(): void {
|
|
202
208
|
if (!this.started) return
|
|
203
209
|
if (localDisplayManager.hasActiveDisplay() || useDisplayStore.getState().overlayOwner !== null) return
|
|
204
|
-
const shown =
|
|
210
|
+
const shown = Array.from(this.presses.values()).at(-1)?.glyph ?? this.flash
|
|
205
211
|
if (!this.enabled()) {
|
|
206
212
|
if (this.visible) {
|
|
207
213
|
this.visible = false
|
|
214
|
+
this.renderedGlyph = null
|
|
208
215
|
sceneRenderer.emitScene(DOTS_APP_ID, "main", [])
|
|
209
216
|
}
|
|
210
217
|
return
|
|
211
218
|
}
|
|
212
219
|
if (!shown && !this.visible) return
|
|
220
|
+
const glyph = shown ?? "blank"
|
|
221
|
+
if (glyph === this.renderedGlyph) return
|
|
213
222
|
const elements: SceneElementInput[] = [
|
|
214
223
|
{
|
|
215
224
|
type: "image",
|
|
@@ -221,13 +230,14 @@ class GestureDotsService {
|
|
|
221
230
|
h: GESTURE_DOT_GLYPH_SIZE,
|
|
222
231
|
anchor: "top-right",
|
|
223
232
|
},
|
|
224
|
-
data: GESTURE_DOT_GLYPHS[
|
|
233
|
+
data: GESTURE_DOT_GLYPHS[glyph],
|
|
225
234
|
},
|
|
226
235
|
]
|
|
227
236
|
this.visible = true
|
|
237
|
+
this.renderedGlyph = glyph
|
|
228
238
|
const result = sceneRenderer.emitScene(DOTS_APP_ID, "main", elements)
|
|
229
239
|
this.trace("render", {
|
|
230
|
-
glyph
|
|
240
|
+
glyph,
|
|
231
241
|
...(result?.kind === "scene" ? {seq: result.seq, sent: result.sent} : {}),
|
|
232
242
|
})
|
|
233
243
|
}
|