@glassly/engine 0.1.0-dev.117 → 0.1.0-dev.119

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.
@@ -17,11 +17,29 @@ import { SETTINGS, useSettingsStore } from "../stores/settings";
17
17
  import { useGlassesStore } from "../stores/glasses";
18
18
  import { DeviceTypes } from "../types";
19
19
  import { getEffectiveCapabilities } from "./effectiveCapabilities";
20
+ import { BgTimer } from "../utils/timers";
20
21
  import { degradeScene, diffScene, processScene, resolveAnchors, SceneStore, } from "../utils/display/scene";
21
22
  const LOG_TAG = "SCENE_RENDERER";
23
+ /** Longest tween the G2 CFW encodes (255 frames at ~30 fps); nothing waits past it. */
24
+ const MAX_SETTLE_MS = 255 * 33;
25
+ /** Longest transition among the elements that actually change on the device this frame. */
26
+ function settleTimeOf(elements) {
27
+ let ms = 0;
28
+ for (const el of elements) {
29
+ if (el.change !== "updated" && el.change !== "moved")
30
+ continue;
31
+ const t = el.transition?.durationMs ?? 0;
32
+ if (t > ms)
33
+ ms = t;
34
+ }
35
+ return Math.min(ms, MAX_SETTLE_MS);
36
+ }
22
37
  class SceneRenderer {
23
38
  static instance = null;
24
39
  store = new SceneStore();
40
+ seqCounter = 0;
41
+ pending = new Map();
42
+ presentedSub = null;
25
43
  static getInstance() {
26
44
  if (!SceneRenderer.instance) {
27
45
  SceneRenderer.instance = new SceneRenderer();
@@ -186,12 +204,20 @@ class SceneRenderer {
186
204
  appId,
187
205
  view,
188
206
  sceneEpoch: this.store.currentEpoch(appId, view),
207
+ seq: ++this.seqCounter,
189
208
  elements: diffed,
190
209
  removed,
191
210
  canvas,
192
211
  };
193
- this.sendFrame(frame);
194
- return { kind: "scene", degraded: processed.degraded, dropped: processed.dropped };
212
+ const sent = this.sendFrame(frame);
213
+ return {
214
+ kind: "scene",
215
+ degraded: processed.degraded,
216
+ dropped: processed.dropped,
217
+ seq: frame.seq,
218
+ sent,
219
+ settleMs: caps.animation ? settleTimeOf(diffed) : 0,
220
+ };
195
221
  }
196
222
  /**
197
223
  * Re-emit an app's retained scene (reconnect / arbitration restore). Replay
@@ -199,15 +225,63 @@ class SceneRenderer {
199
225
  * Returns false when nothing is retained.
200
226
  */
201
227
  replayApp(appId, view) {
202
- const frame = this.store.buildReplayFrame(appId, view);
228
+ const frame = this.store.buildReplayFrame(appId, view, ++this.seqCounter);
203
229
  if (!frame)
204
230
  return false;
205
231
  this.sendFrame(frame);
206
232
  return true;
207
233
  }
208
- /** Forget an app's retained scenes (app stop). */
234
+ /** Forget an app's retained scenes (app stop). Frames still in flight will never show. */
209
235
  clearApp(appId) {
210
236
  this.store.clear(appId);
237
+ this.settlePending((p) => p.appId === appId, { presented: false, superseded: true, reason: "cleared" });
238
+ }
239
+ // ===========================================================================
240
+ // Presentation — native reports when the device has a frame
241
+ // ===========================================================================
242
+ /**
243
+ * Resolves once native reports frame `seq` on the device, or a later frame
244
+ * that folded it in. Never rejects; `timeoutMs` bounds a frame the device
245
+ * never acknowledges (link stalls, page rebuilding).
246
+ */
247
+ whenPresented(seq, timeoutMs, appId) {
248
+ this.ensurePresentedListener();
249
+ return new Promise((resolve) => {
250
+ const timer = BgTimer.setTimeout(() => {
251
+ if (this.pending.delete(seq))
252
+ resolve({ presented: false, superseded: false, reason: "no presentation report" });
253
+ }, timeoutMs);
254
+ this.pending.set(seq, {
255
+ appId,
256
+ resolve: (p) => {
257
+ BgTimer.clearTimeout(timer);
258
+ resolve(p);
259
+ },
260
+ });
261
+ });
262
+ }
263
+ ensurePresentedListener() {
264
+ if (this.presentedSub)
265
+ return;
266
+ this.presentedSub = BluetoothSdk.addListener("scene_frame_presented", (event) => {
267
+ this.onFramePresented(event.seq, event.appId, event.presented, event.reason);
268
+ });
269
+ }
270
+ /** A report for `seq` settles it and every earlier frame (folded in by the native pacer). */
271
+ onFramePresented(seq, appId, presented, reason) {
272
+ this.settlePending((p, s) => s <= seq, (p, s) => s === seq
273
+ ? { presented, superseded: false, reason }
274
+ : p.appId === appId
275
+ ? { presented, superseded: true, reason }
276
+ : { presented: false, superseded: true, reason: `superseded by ${appId}` });
277
+ }
278
+ settlePending(match, outcome) {
279
+ for (const [seq, p] of [...this.pending]) {
280
+ if (!match(p, seq))
281
+ continue;
282
+ this.pending.delete(seq);
283
+ p.resolve(typeof outcome === "function" ? outcome(p, seq) : outcome);
284
+ }
211
285
  }
212
286
  /**
213
287
  * Reset the diff baseline for (app, view) without touching the device.
@@ -228,13 +302,16 @@ class SceneRenderer {
228
302
  */
229
303
  markViewStale(view) {
230
304
  this.store.markViewStale(view);
305
+ // Whatever was in flight for the view got wiped with it.
306
+ this.settlePending(() => true, { presented: false, superseded: true, reason: "cleared" });
231
307
  }
308
+ /** Returns false when the frame stayed phone-side (no presentation report will follow). */
232
309
  sendFrame(frame) {
233
310
  // Single native view: only the visible surface's frames go out. Suppressed
234
311
  // main frames stay committed; setDashboardActive(false) replays them.
235
312
  const dashboardActive = useDisplayStore.getState().dashboardActive;
236
313
  if (dashboardActive !== (frame.view === "dashboard"))
237
- return;
314
+ return false;
238
315
  try {
239
316
  // Frames carry the canvas they were laid out against; only a frame from
240
317
  // a path that predates the tag falls back to the current capabilities.
@@ -259,9 +336,11 @@ class SceneRenderer {
259
336
  elements: frame.elements,
260
337
  },
261
338
  });
339
+ return true;
262
340
  }
263
341
  catch (err) {
264
342
  console.error(`${LOG_TAG}: native scene send failed:`, err);
343
+ return false;
265
344
  }
266
345
  }
267
346
  // ===========================================================================
@@ -269,6 +348,10 @@ class SceneRenderer {
269
348
  // ===========================================================================
270
349
  _resetForTest() {
271
350
  this.store.clearAll();
351
+ this.settlePending(() => true, { presented: false, superseded: false, reason: "reset" });
352
+ this.presentedSub?.remove();
353
+ this.presentedSub = null;
354
+ this.seqCounter = 0;
272
355
  }
273
356
  _lastFrameForTest(appId, view) {
274
357
  return this.store.lastFrame(appId, view);
@@ -1 +1 @@
1
- {"version":3,"file":"SceneRenderer.js","sourceRoot":"","sources":["../../src/services/SceneRenderer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,gBAAgB,MAAM,oBAAoB,CAAA;AACjD,OAAO,YAAY,MAAM,iCAAiC,CAAA;AAC1D,OAAO,EAAC,eAAe,EAAC,MAAM,mBAAmB,CAAA;AACjD,OAAO,EAAC,QAAQ,EAAE,gBAAgB,EAAC,MAAM,oBAAoB,CAAA;AAC7D,OAAO,EAAC,eAAe,EAAC,MAAM,mBAAmB,CAAA;AACjD,OAAO,EAAC,WAAW,EAAC,MAAM,UAAU,CAAA;AACpC,OAAO,EAAC,wBAAwB,EAAC,MAAM,yBAAyB,CAAA;AAChE,OAAO,EACL,YAAY,EACZ,SAAS,EACT,YAAY,EACZ,cAAc,EACd,UAAU,GAMX,MAAM,wBAAwB,CAAA;AAE/B,MAAM,OAAO,GAAG,gBAAgB,CAAA;AAahC,MAAM,aAAa;IACT,MAAM,CAAC,QAAQ,GAAyB,IAAI,CAAA;IAC5C,KAAK,GAAG,IAAI,UAAU,EAAE,CAAA;IAEzB,MAAM,CAAC,WAAW;QACvB,IAAI,CAAC,aAAa,CAAC,QAAQ,EAAE,CAAC;YAC5B,aAAa,CAAC,QAAQ,GAAG,IAAI,aAAa,EAAE,CAAA;QAC9C,CAAC;QACD,OAAO,aAAa,CAAC,QAAQ,CAAA;IAC/B,CAAC;IAED,8EAA8E;IAC9E,eAAe;IACf,8EAA8E;IAE9E;;;OAGG;IACI,mBAAmB;QACxB,wEAAwE;QACxE,+CAA+C;QAC/C,MAAM,KAAK,GACR,gBAAgB,CAAC,QAAQ,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,gBAAgB,CAAC,GAAG,CAAwB;YAC5F,eAAe,CAAC,QAAQ,EAAE,CAAC,WAAkC,CAAA;QAChE,MAAM,OAAO,GAAG,wBAAwB,CAAC,CAAC,KAAK,IAAI,WAAW,CAAC,IAAI,CAAgB,CAAC,EAAE,OAAO,CAAA;QAC7F,IAAI,CAAC,OAAO;YAAE,OAAO,IAAI,CAAA;QACzB,OAAO;YACL,KAAK,EAAE,OAAO,CAAC,KAAK,IAAI,OAAO,CAAC,UAAU,EAAE,KAAK,IAAI,gBAAgB,CAAC,UAAU,EAAE,CAAC,cAAc;YACjG,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,UAAU,EAAE,MAAM,IAAI,gBAAgB,CAAC,UAAU,EAAE,CAAC,eAAe,IAAI,GAAG;YAC5G,WAAW,EAAE,OAAO,CAAC,WAAW,KAAK,IAAI;YACzC,eAAe,EAAE,OAAO,CAAC,eAAe,IAAI,CAAC;YAC7C,gBAAgB,EAAE,OAAO,CAAC,gBAAgB,IAAI,CAAC;YAC/C,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,EAAE;YAC5B,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,KAAK;YACrC,eAAe,EAAE,OAAO,CAAC,eAAe,IAAI,CAAC;YAC7C,aAAa,EAAE,OAAO,CAAC,aAAa,IAAI,KAAK;SAC9C,CAAA;IACH,CAAC;IAED,8EAA8E;IAC9E,2EAA2E;IAC3E,8EAA8E;IAE9E;;;;OAIG;IACI,mBAAmB,CAAC,MAAoB,EAAE,IAA8B;QAC7E,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAA;QACpB,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAA;QACrB,MAAM,SAAS,GAAG,MAAM,CAAC,SAAuC,CAAA;QAEhE,QAAQ,MAAM,CAAC,UAAU,EAAE,CAAC;YAC1B,KAAK,WAAW;gBACd,OAAO;oBACL;wBACE,IAAI,EAAE,MAAM;wBACZ,EAAE,EAAE,YAAY;wBAChB,GAAG,EAAE,EAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAC;wBAC7B,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;wBAC/B,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAC,KAAK,EAAE,EAAC,SAAS,EAAC,EAAC,CAAC,CAAC,CAAC,EAAE,CAAC;qBAC3C;iBACF,CAAA;YACH,KAAK,kBAAkB,CAAC,CAAC,CAAC;gBACxB,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;gBAC9B,OAAO;oBACL;wBACE,IAAI,EAAE,MAAM;wBACZ,EAAE,EAAE,YAAY;wBAChB,GAAG,EAAE,EAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,EAAC;wBAChC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC;wBAClC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAC,KAAK,EAAE,EAAC,SAAS,EAAC,EAAC,CAAC,CAAC,CAAC,EAAE,CAAC;qBAC3C;oBACD;wBACE,IAAI,EAAE,MAAM;wBACZ,EAAE,EAAE,aAAa;wBACjB,GAAG,EAAE,EAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,EAAE,CAAC,EAAC;wBACvC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,UAAU,IAAI,EAAE,CAAC;wBACrC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAC,KAAK,EAAE,EAAC,SAAS,EAAC,EAAC,CAAC,CAAC,CAAC,EAAE,CAAC;qBAC3C;iBACF,CAAA;YACH,CAAC;YACD,KAAK,gBAAgB,CAAC,CAAC,CAAC;gBACtB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;gBAC9C,OAAO;oBACL,EAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,EAAE,GAAG,EAAE,EAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAC,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC,EAAC;oBACvG;wBACE,IAAI,EAAE,MAAM;wBACZ,EAAE,EAAE,YAAY;wBAChB,GAAG,EAAE,EAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,MAAM,EAAC;wBAC3C,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;wBAC/B,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,EAAC,KAAK,EAAE,EAAC,SAAS,EAAC,EAAC,CAAC,CAAC,CAAC,EAAE,CAAC;qBAC3C;iBACF,CAAA;YACH,CAAC;YACD,KAAK,aAAa,CAAC,CAAC,CAAC;gBACnB,mEAAmE;gBACnE,qEAAqE;gBACrE,sEAAsE;gBACtE,MAAM,CAAC,GAAI,MAAM,CAAC,KAA4B,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;gBAClE,MAAM,CAAC,GAAI,MAAM,CAAC,MAA6B,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC,CAAA;gBACnE,OAAO;oBACL;wBACE,IAAI,EAAE,OAAO;wBACb,EAAE,EAAE,WAAW;wBACf,GAAG,EAAE;4BACH,CAAC,EAAG,MAAM,CAAC,CAAwB,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;4BAC3E,CAAC,EAAG,MAAM,CAAC,CAAwB,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;4BAC3E,CAAC;4BACD,CAAC;yBACF;wBACD,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;qBAChC;iBACF,CAAA;YACH,CAAC;YACD,KAAK,iBAAiB,CAAC,CAAC,CAAC;gBACvB,MAAM,MAAM,GAAI,MAAM,CAAC,WAAkC,IAAI,CAAC,CAAA;gBAC9D,MAAM,MAAM,GAAI,MAAM,CAAC,YAAmC,IAAI,CAAC,CAAA;gBAC/D,OAAO;oBACL;wBACE,IAAI,EAAE,MAAM;wBACZ,EAAE,EAAE,WAAW;wBACf,GAAG,EAAE;4BACH,CAAC,EAAG,MAAM,CAAC,CAAwB,IAAI,CAAC;4BACxC,CAAC,EAAG,MAAM,CAAC,CAAwB,IAAI,CAAC;4BACxC,CAAC,EAAG,MAAM,CAAC,KAA4B,IAAI,CAAC;4BAC5C,CAAC,EAAG,MAAM,CAAC,MAA6B,IAAI,CAAC;yBAC9C;wBACD,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;wBAC/B,GAAG,CAAC,MAAM,IAAI,MAAM,CAAC,CAAC,CAAC,EAAC,KAAK,EAAE,EAAC,MAAM,EAAE,MAAM,EAAC,EAAC,CAAC,CAAC,CAAC,EAAE,CAAC;qBACvD;iBACF,CAAA;YACH,CAAC;YACD;gBACE,OAAO,IAAI,CAAA;QACf,CAAC;IACH,CAAC;IAED,8EAA8E;IAC9E,OAAO;IACP,8EAA8E;IAE9E;;;;;OAKG;IACI,SAAS,CAAC,KAAa,EAAE,IAA0B,EAAE,QAA6B;QACvF,MAAM,IAAI,GAAG,IAAI,CAAC,mBAAmB,EAAE,CAAA;QACvC,IAAI,CAAC,IAAI;YAAE,OAAO,EAAC,IAAI,EAAE,YAAY,EAAC,CAAA;QAEtC,sEAAsE;QACtE,0EAA0E;QAC1E,yEAAyE;QACzE,QAAQ,GAAG,cAAc,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;QAE5D,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YACtB,MAAM,EAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAC,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAA;YAC1D,OAAO,EAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAC,CAAA;QACpD,CAAC;QAED,MAAM,SAAS,GAAG,YAAY,CAAC,QAAQ,EAAE,IAAI,EAAE,gBAAgB,CAAC,UAAU,EAAE,CAAC,CAAA;QAC7E,0EAA0E;QAC1E,yEAAyE;QACzE,8BAA8B;QAC9B,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;QAClG,MAAM,EAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAC,GAAG,SAAS,CAC3C,SAAS,EACT,SAAS,CAAC,QAAQ,EAClB,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,KAAK,EAAE,IAAI,CAAC,CACxC,CAAA;QACD,MAAM,MAAM,GAAG,EAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAC,CAAA;QACvD,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;QAE9C,MAAM,KAAK,GAAe;YACxB,KAAK;YACL,IAAI;YACJ,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC;YAChD,QAAQ,EAAE,MAAM;YAChB,OAAO;YACP,MAAM;SACP,CAAA;QACD,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;QACrB,OAAO,EAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,EAAC,CAAA;IAClF,CAAC;IAED;;;;OAIG;IACI,SAAS,CAAC,KAAa,EAAE,IAA0B;QACxD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;QACtD,IAAI,CAAC,KAAK;YAAE,OAAO,KAAK,CAAA;QACxB,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;QACrB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,kDAAkD;IAC3C,QAAQ,CAAC,KAAa;QAC3B,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;IACzB,CAAC;IAED;;;;;;;OAOG;IACI,aAAa,CAAC,KAAa,EAAE,IAA0B;QAC5D,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;IACpC,CAAC;IAED;;;;;OAKG;IACI,aAAa,CAAC,IAA0B;QAC7C,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,CAAA;IAChC,CAAC;IAEO,SAAS,CAAC,KAAiB;QACjC,2EAA2E;QAC3E,sEAAsE;QACtE,MAAM,eAAe,GAAG,eAAe,CAAC,QAAQ,EAAE,CAAC,eAAe,CAAA;QAClE,IAAI,eAAe,KAAK,CAAC,KAAK,CAAC,IAAI,KAAK,WAAW,CAAC;YAAE,OAAM;QAC5D,IAAI,CAAC;YACH,wEAAwE;YACxE,uEAAuE;YACvE,MAAM,IAAI,GAAG,IAAI,CAAC,mBAAmB,EAAE,CAAA;YACvC,IAAI,CAAC,KAAK,CAAC,MAAM,IAAI,IAAI;gBAAE,KAAK,GAAG,EAAC,GAAG,KAAK,EAAE,MAAM,EAAE,EAAC,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAC,EAAC,CAAA;YAC/F,uEAAuE;YACvE,mDAAmD;YACnD,KAAK,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,YAAY,CAAC,EAAC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;gBAC9F,OAAO,CAAC,KAAK,CAAC,GAAG,OAAO,6BAA6B,EAAE,GAAG,CAAC,CAAA;YAC7D,CAAC,CAAC,CAAA;YAEF,yEAAyE;YACzE,uEAAuE;YACvE,gEAAgE;YAChE,mEAAmE;YACnE,eAAe,CAAC,QAAQ,EAAE,CAAC,eAAe,CAAC;gBACzC,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,MAAM,EAAE;oBACN,UAAU,EAAE,OAAO;oBACnB,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK;oBAC1B,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM;oBAC5B,QAAQ,EAAE,KAAK,CAAC,QAAQ;iBACzB;aACF,CAAC,CAAA;QACJ,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,GAAG,OAAO,6BAA6B,EAAE,GAAG,CAAC,CAAA;QAC7D,CAAC;IACH,CAAC;IAED,8EAA8E;IAC9E,aAAa;IACb,8EAA8E;IAEvE,aAAa;QAClB,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAA;IACvB,CAAC;IAEM,iBAAiB,CAAC,KAAa,EAAE,IAAY;QAClD,OAAO,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IAC1C,CAAC;;AAGH,MAAM,aAAa,GAAG,aAAa,CAAC,WAAW,EAAE,CAAA;AACjD,eAAe,aAAa,CAAA;AAC5B,OAAO,EAAC,aAAa,EAAC,CAAA","sourcesContent":["/**\n * SceneRenderer — the device-aware boundary of the scene display pipeline.\n *\n * LocalDisplayManager owns WHO may draw (arbitration/boot/expiry); this service\n * owns WHAT gets sent for a scene: capabilities lookup, legacy-layout→scene\n * conversion (sugar and old bundles ride the same pipeline), process → diff →\n * SceneFrame emission, retained-scene replay, and the degrade branch for\n * devices that can't position elements.\n *\n * Everything device-specific here is DATA (capability fields, display profile);\n * the code paths are generic. Spec: notes/miniapp-display-render-implementation-spec.md\n */\n\nimport displayProcessor from \"./DisplayProcessor\"\nimport BluetoothSdk from \"@glassly/bluetooth-sdk/internal\"\nimport {useDisplayStore} from \"../stores/display\"\nimport {SETTINGS, useSettingsStore} from \"../stores/settings\"\nimport {useGlassesStore} from \"../stores/glasses\"\nimport {DeviceTypes} from \"../types\"\nimport {getEffectiveCapabilities} from \"./effectiveCapabilities\"\nimport {\n degradeScene,\n diffScene,\n processScene,\n resolveAnchors,\n SceneStore,\n type FrameElement,\n type SceneBreakMode,\n type SceneDisplayCapabilities,\n type SceneElementInput,\n type SceneFrame,\n} from \"../utils/display/scene\"\n\nconst LOG_TAG = \"SCENE_RENDERER\"\n\n/** Legacy layout shape (matches DisplayPayload.layout). */\nexport type LegacyLayout = {layoutType: string; [key: string]: unknown}\n\nexport type SceneEmitResult =\n /** A SceneFrame went to native. */\n | {kind: \"scene\"; degraded: boolean; dropped: string[]}\n /** Degrade path: caller routes this legacy layout through the existing pipeline (null layout ⇒ clear). */\n | {kind: \"legacy\"; layout: LegacyLayout | null; degraded: boolean; dropped: string[]}\n /** No display capabilities on the connected device. */\n | {kind: \"no-display\"}\n\nclass SceneRenderer {\n private static instance: SceneRenderer | null = null\n private store = new SceneStore()\n\n public static getInstance(): SceneRenderer {\n if (!SceneRenderer.instance) {\n SceneRenderer.instance = new SceneRenderer()\n }\n return SceneRenderer.instance\n }\n\n // ===========================================================================\n // Capabilities\n // ===========================================================================\n\n /**\n * Scene capabilities of the currently-selected device, or null when it has no\n * display block. Read on demand — same model source the CONNECT_ACK uses.\n */\n public currentCapabilities(): SceneDisplayCapabilities | null {\n // dev read these via the (since-removed) settings/glassesStatus runtime\n // hooks; engine reads its own stores directly.\n const model =\n (useSettingsStore.getState().getSetting(SETTINGS.default_wearable.key) as string | undefined) ??\n (useGlassesStore.getState().deviceModel as string | undefined)\n const display = getEffectiveCapabilities((model ?? DeviceTypes.NONE) as DeviceTypes)?.display\n if (!display) return null\n return {\n width: display.width ?? display.resolution?.width ?? displayProcessor.getProfile().displayWidthPx,\n height: display.height ?? display.resolution?.height ?? displayProcessor.getProfile().displayHeightPx ?? 288,\n canPosition: display.canPosition === true,\n maxTextElements: display.maxTextElements ?? 6,\n maxImageElements: display.maxImageElements ?? 0,\n maxImagePx: display.maxImagePx,\n shapes: display.shapes ?? [],\n animation: display.animation ?? false,\n intensityLevels: display.intensityLevels ?? 2,\n partialUpdate: display.partialUpdate ?? false,\n }\n }\n\n // ===========================================================================\n // Legacy layout → scene conversion (sugar + old bundles on the scene path)\n // ===========================================================================\n\n /**\n * Convert a legacy layout into scene elements for a positioning device.\n * Returns null for layouts that must stay on the legacy path (dashboard_card,\n * clear_view, unknown types). Sugar semantics: full-canvas / split boxes.\n */\n public convertLegacyLayout(layout: LegacyLayout, caps: SceneDisplayCapabilities): SceneElementInput[] | null {\n const W = caps.width\n const H = caps.height\n const breakMode = layout.breakMode as SceneBreakMode | undefined\n\n switch (layout.layoutType) {\n case \"text_wall\":\n return [\n {\n type: \"text\",\n id: \"sugar:wall\",\n box: {x: 0, y: 0, w: W, h: H},\n text: String(layout.text ?? \"\"),\n ...(breakMode ? {style: {breakMode}} : {}),\n },\n ]\n case \"double_text_wall\": {\n const half = Math.floor(W / 2)\n return [\n {\n type: \"text\",\n id: \"sugar:left\",\n box: {x: 0, y: 0, w: half, h: H},\n text: String(layout.topText ?? \"\"),\n ...(breakMode ? {style: {breakMode}} : {}),\n },\n {\n type: \"text\",\n id: \"sugar:right\",\n box: {x: half, y: 0, w: W - half, h: H},\n text: String(layout.bottomText ?? \"\"),\n ...(breakMode ? {style: {breakMode}} : {}),\n },\n ]\n }\n case \"reference_card\": {\n const titleH = Math.max(24, Math.floor(H / 5))\n return [\n {type: \"text\", id: \"sugar:title\", box: {x: 0, y: 0, w: W, h: titleH}, text: String(layout.title ?? \"\")},\n {\n type: \"text\",\n id: \"sugar:body\",\n box: {x: 0, y: titleH, w: W, h: H - titleH},\n text: String(layout.text ?? \"\"),\n ...(breakMode ? {style: {breakMode}} : {}),\n },\n ]\n }\n case \"bitmap_view\": {\n // Dimensionless legacy bitmaps historically landed in the device's\n // default ~200×100 container (G2 seeded 200×100 at 188,44) — NOT the\n // full canvas, which would tile a small image across the whole frame.\n const w = (layout.width as number | undefined) ?? Math.min(200, W)\n const h = (layout.height as number | undefined) ?? Math.min(100, H)\n return [\n {\n type: \"image\",\n id: \"sugar:bmp\",\n box: {\n x: (layout.x as number | undefined) ?? Math.max(0, Math.round((W - w) / 2)),\n y: (layout.y as number | undefined) ?? Math.max(0, Math.round((H - h) / 4)),\n w,\n h,\n },\n data: String(layout.data ?? \"\"),\n },\n ]\n }\n case \"positioned_text\": {\n const border = (layout.borderWidth as number | undefined) ?? 0\n const radius = (layout.borderRadius as number | undefined) ?? 0\n return [\n {\n type: \"text\",\n id: \"sugar:pos\",\n box: {\n x: (layout.x as number | undefined) ?? 0,\n y: (layout.y as number | undefined) ?? 0,\n w: (layout.width as number | undefined) ?? W,\n h: (layout.height as number | undefined) ?? H,\n },\n text: String(layout.text ?? \"\"),\n ...(border || radius ? {style: {border, radius}} : {}),\n },\n ]\n }\n default:\n return null\n }\n }\n\n // ===========================================================================\n // Emit\n // ===========================================================================\n\n /**\n * Process + diff + send a scene for (app, view). On non-positioning devices\n * this returns the degraded legacy layout instead — the caller routes it\n * through the existing DisplayProcessor path (which wraps it, keeping G1/Z100\n * output byte-identical to today).\n */\n public emitScene(appId: string, view: \"main\" | \"dashboard\", elements: SceneElementInput[]): SceneEmitResult {\n const caps = this.currentCapabilities()\n if (!caps) return {kind: \"no-display\"}\n\n // Corner-anchored boxes resolve against the REAL canvas before either\n // branch: processScene clamps concrete coords, and degradeScene's reading\n // order sorts by resolved y (a bottom-anchored y:0 must not sort first).\n elements = resolveAnchors(elements, caps.width, caps.height)\n\n if (!caps.canPosition) {\n const {layout, degraded, dropped} = degradeScene(elements)\n return {kind: \"legacy\", layout, degraded, dropped}\n }\n\n const processed = processScene(elements, caps, displayProcessor.getProfile())\n // A stale baseline means the glasses were cleared since the last commit —\n // diff from empty so everything repaints (the retained frame stays valid\n // as the replay source only).\n const prevFrame = this.store.isBaselineStale(appId, view) ? [] : this.store.lastFrame(appId, view)\n const {elements: diffed, removed} = diffScene(\n prevFrame,\n processed.elements,\n this.store.nextSyntheticId(appId, view),\n )\n const canvas = {width: caps.width, height: caps.height}\n this.store.commit(appId, view, diffed, canvas)\n\n const frame: SceneFrame = {\n appId,\n view,\n sceneEpoch: this.store.currentEpoch(appId, view),\n elements: diffed,\n removed,\n canvas,\n }\n this.sendFrame(frame)\n return {kind: \"scene\", degraded: processed.degraded, dropped: processed.dropped}\n }\n\n /**\n * Re-emit an app's retained scene (reconnect / arbitration restore). Replay\n * is always create-based — the device rebuilds from scratch (spec §4).\n * Returns false when nothing is retained.\n */\n public replayApp(appId: string, view: \"main\" | \"dashboard\"): boolean {\n const frame = this.store.buildReplayFrame(appId, view)\n if (!frame) return false\n this.sendFrame(frame)\n return true\n }\n\n /** Forget an app's retained scenes (app stop). */\n public clearApp(appId: string): void {\n this.store.clear(appId)\n }\n\n /**\n * Reset the diff baseline for (app, view) without touching the device.\n * Used when a legacy layout bypasses the scene path (clear_view /\n * dashboard_card / unknown layoutTypes go to native raw): after that, the\n * retained frame no longer matches the glasses, so the next render() must\n * diff from empty — otherwise \"unchanged\" elements would be skipped and\n * never repainted.\n */\n public resetBaseline(appId: string, view: \"main\" | \"dashboard\"): void {\n this.store.commit(appId, view, [])\n }\n\n /**\n * A system-level clear wiped the view (durationMs expiry, unmount fallback,\n * boot-window expiry): mark every app's baseline for that view stale. Unlike\n * resetBaseline this KEEPS the retained frames — they remain the\n * restore-as-replay source; only incremental diffing restarts from empty.\n */\n public markViewStale(view: \"main\" | \"dashboard\"): void {\n this.store.markViewStale(view)\n }\n\n private sendFrame(frame: SceneFrame): void {\n // Single native view: only the visible surface's frames go out. Suppressed\n // main frames stay committed; setDashboardActive(false) replays them.\n const dashboardActive = useDisplayStore.getState().dashboardActive\n if (dashboardActive !== (frame.view === \"dashboard\")) return\n try {\n // Frames carry the canvas they were laid out against; only a frame from\n // a path that predates the tag falls back to the current capabilities.\n const caps = this.currentCapabilities()\n if (!frame.canvas && caps) frame = {...frame, canvas: {width: caps.width, height: caps.height}}\n // sendDisplayEvent hook -> direct BluetoothSdk.displayEvent (the exact\n // wiring the deleted MantleManager hook provided).\n void Promise.resolve(BluetoothSdk.displayEvent({view: frame.view, scene: frame})).catch((err) => {\n console.error(`${LOG_TAG}: native scene send failed:`, err)\n })\n\n // Keep the phone-side glasses mirror on the same frame as native. Legacy\n // layouts update this store in LocalDisplayManager.sendToNative; scene\n // frames bypass that method, so without an explicit write every\n // positioning device (including G2) leaves the mirror blank/stale.\n useDisplayStore.getState().setDisplayEvent({\n view: frame.view,\n layout: {\n layoutType: \"scene\",\n width: frame.canvas?.width,\n height: frame.canvas?.height,\n elements: frame.elements,\n },\n })\n } catch (err) {\n console.error(`${LOG_TAG}: native scene send failed:`, err)\n }\n }\n\n // ===========================================================================\n // Test hooks\n // ===========================================================================\n\n public _resetForTest(): void {\n this.store.clearAll()\n }\n\n public _lastFrameForTest(appId: string, view: string): FrameElement[] {\n return this.store.lastFrame(appId, view)\n }\n}\n\nconst sceneRenderer = SceneRenderer.getInstance()\nexport default sceneRenderer\nexport {SceneRenderer}\n"]}
1
+ {"version":3,"file":"SceneRenderer.js","sourceRoot":"","sources":["../../src/services/SceneRenderer.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SceneRenderer — the device-aware boundary of the scene display pipeline.\n *\n * LocalDisplayManager owns WHO may draw (arbitration/boot/expiry); this service\n * owns WHAT gets sent for a scene: capabilities lookup, legacy-layout→scene\n * conversion (sugar and old bundles ride the same pipeline), process → diff →\n * SceneFrame emission, retained-scene replay, and the degrade branch for\n * devices that can't position elements.\n *\n * Everything device-specific here is DATA (capability fields, display profile);\n * the code paths are generic. Spec: notes/miniapp-display-render-implementation-spec.md\n */\n\nimport displayProcessor from \"./DisplayProcessor\"\nimport BluetoothSdk from \"@glassly/bluetooth-sdk/internal\"\nimport {useDisplayStore} from \"../stores/display\"\nimport {SETTINGS, useSettingsStore} from \"../stores/settings\"\nimport {useGlassesStore} from \"../stores/glasses\"\nimport {DeviceTypes} from \"../types\"\nimport {getEffectiveCapabilities} from \"./effectiveCapabilities\"\nimport {BgTimer} from \"../utils/timers\"\nimport {\n degradeScene,\n diffScene,\n processScene,\n resolveAnchors,\n SceneStore,\n type FrameElement,\n type SceneBreakMode,\n type SceneDisplayCapabilities,\n type SceneElementInput,\n type SceneFrame,\n} from \"../utils/display/scene\"\n\nconst LOG_TAG = \"SCENE_RENDERER\"\n\n/** Legacy layout shape (matches DisplayPayload.layout). */\nexport type LegacyLayout = {layoutType: string; [key: string]: unknown}\n\nexport type SceneEmitResult =\n /**\n * A SceneFrame was committed. `sent` is false when the frame stayed\n * phone-side (dashboard covering main). `settleMs` is how long the device\n * keeps animating after it has the frame: the longest transition among the\n * elements this frame changed, 0 on devices that jump.\n */\n | {kind: \"scene\"; degraded: boolean; dropped: string[]; seq: number; sent: boolean; settleMs: number}\n /** Degrade path: caller routes this legacy layout through the existing pipeline (null layout ⇒ clear). */\n | {kind: \"legacy\"; layout: LegacyLayout | null; degraded: boolean; dropped: string[]}\n /** No display capabilities on the connected device. */\n | {kind: \"no-display\"}\n\n/** How a frame ended up, as reported by native (or inferred from a later frame's report). */\nexport interface ScenePresentation {\n /** The device had the frame's content (this frame or a later one that folded it in). */\n presented: boolean\n /** A later frame's report settled this one. */\n superseded: boolean\n reason?: string\n}\n\n/** Longest tween the G2 CFW encodes (255 frames at ~30 fps); nothing waits past it. */\nconst MAX_SETTLE_MS = 255 * 33\n\n/** Longest transition among the elements that actually change on the device this frame. */\nfunction settleTimeOf(elements: FrameElement[]): number {\n let ms = 0\n for (const el of elements) {\n if (el.change !== \"updated\" && el.change !== \"moved\") continue\n const t = el.transition?.durationMs ?? 0\n if (t > ms) ms = t\n }\n return Math.min(ms, MAX_SETTLE_MS)\n}\n\nclass SceneRenderer {\n private static instance: SceneRenderer | null = null\n private store = new SceneStore()\n private seqCounter = 0\n private pending = new Map<number, {appId: string; resolve: (p: ScenePresentation) => void}>()\n private presentedSub: {remove: () => void} | null = null\n\n public static getInstance(): SceneRenderer {\n if (!SceneRenderer.instance) {\n SceneRenderer.instance = new SceneRenderer()\n }\n return SceneRenderer.instance\n }\n\n // ===========================================================================\n // Capabilities\n // ===========================================================================\n\n /**\n * Scene capabilities of the currently-selected device, or null when it has no\n * display block. Read on demand — same model source the CONNECT_ACK uses.\n */\n public currentCapabilities(): SceneDisplayCapabilities | null {\n // dev read these via the (since-removed) settings/glassesStatus runtime\n // hooks; engine reads its own stores directly.\n const model =\n (useSettingsStore.getState().getSetting(SETTINGS.default_wearable.key) as string | undefined) ??\n (useGlassesStore.getState().deviceModel as string | undefined)\n const display = getEffectiveCapabilities((model ?? DeviceTypes.NONE) as DeviceTypes)?.display\n if (!display) return null\n return {\n width: display.width ?? display.resolution?.width ?? displayProcessor.getProfile().displayWidthPx,\n height: display.height ?? display.resolution?.height ?? displayProcessor.getProfile().displayHeightPx ?? 288,\n canPosition: display.canPosition === true,\n maxTextElements: display.maxTextElements ?? 6,\n maxImageElements: display.maxImageElements ?? 0,\n maxImagePx: display.maxImagePx,\n shapes: display.shapes ?? [],\n animation: display.animation ?? false,\n intensityLevels: display.intensityLevels ?? 2,\n partialUpdate: display.partialUpdate ?? false,\n }\n }\n\n // ===========================================================================\n // Legacy layout → scene conversion (sugar + old bundles on the scene path)\n // ===========================================================================\n\n /**\n * Convert a legacy layout into scene elements for a positioning device.\n * Returns null for layouts that must stay on the legacy path (dashboard_card,\n * clear_view, unknown types). Sugar semantics: full-canvas / split boxes.\n */\n public convertLegacyLayout(layout: LegacyLayout, caps: SceneDisplayCapabilities): SceneElementInput[] | null {\n const W = caps.width\n const H = caps.height\n const breakMode = layout.breakMode as SceneBreakMode | undefined\n\n switch (layout.layoutType) {\n case \"text_wall\":\n return [\n {\n type: \"text\",\n id: \"sugar:wall\",\n box: {x: 0, y: 0, w: W, h: H},\n text: String(layout.text ?? \"\"),\n ...(breakMode ? {style: {breakMode}} : {}),\n },\n ]\n case \"double_text_wall\": {\n const half = Math.floor(W / 2)\n return [\n {\n type: \"text\",\n id: \"sugar:left\",\n box: {x: 0, y: 0, w: half, h: H},\n text: String(layout.topText ?? \"\"),\n ...(breakMode ? {style: {breakMode}} : {}),\n },\n {\n type: \"text\",\n id: \"sugar:right\",\n box: {x: half, y: 0, w: W - half, h: H},\n text: String(layout.bottomText ?? \"\"),\n ...(breakMode ? {style: {breakMode}} : {}),\n },\n ]\n }\n case \"reference_card\": {\n const titleH = Math.max(24, Math.floor(H / 5))\n return [\n {type: \"text\", id: \"sugar:title\", box: {x: 0, y: 0, w: W, h: titleH}, text: String(layout.title ?? \"\")},\n {\n type: \"text\",\n id: \"sugar:body\",\n box: {x: 0, y: titleH, w: W, h: H - titleH},\n text: String(layout.text ?? \"\"),\n ...(breakMode ? {style: {breakMode}} : {}),\n },\n ]\n }\n case \"bitmap_view\": {\n // Dimensionless legacy bitmaps historically landed in the device's\n // default ~200×100 container (G2 seeded 200×100 at 188,44) — NOT the\n // full canvas, which would tile a small image across the whole frame.\n const w = (layout.width as number | undefined) ?? Math.min(200, W)\n const h = (layout.height as number | undefined) ?? Math.min(100, H)\n return [\n {\n type: \"image\",\n id: \"sugar:bmp\",\n box: {\n x: (layout.x as number | undefined) ?? Math.max(0, Math.round((W - w) / 2)),\n y: (layout.y as number | undefined) ?? Math.max(0, Math.round((H - h) / 4)),\n w,\n h,\n },\n data: String(layout.data ?? \"\"),\n },\n ]\n }\n case \"positioned_text\": {\n const border = (layout.borderWidth as number | undefined) ?? 0\n const radius = (layout.borderRadius as number | undefined) ?? 0\n return [\n {\n type: \"text\",\n id: \"sugar:pos\",\n box: {\n x: (layout.x as number | undefined) ?? 0,\n y: (layout.y as number | undefined) ?? 0,\n w: (layout.width as number | undefined) ?? W,\n h: (layout.height as number | undefined) ?? H,\n },\n text: String(layout.text ?? \"\"),\n ...(border || radius ? {style: {border, radius}} : {}),\n },\n ]\n }\n default:\n return null\n }\n }\n\n // ===========================================================================\n // Emit\n // ===========================================================================\n\n /**\n * Process + diff + send a scene for (app, view). On non-positioning devices\n * this returns the degraded legacy layout instead — the caller routes it\n * through the existing DisplayProcessor path (which wraps it, keeping G1/Z100\n * output byte-identical to today).\n */\n public emitScene(appId: string, view: \"main\" | \"dashboard\", elements: SceneElementInput[]): SceneEmitResult {\n const caps = this.currentCapabilities()\n if (!caps) return {kind: \"no-display\"}\n\n // Corner-anchored boxes resolve against the REAL canvas before either\n // branch: processScene clamps concrete coords, and degradeScene's reading\n // order sorts by resolved y (a bottom-anchored y:0 must not sort first).\n elements = resolveAnchors(elements, caps.width, caps.height)\n\n if (!caps.canPosition) {\n const {layout, degraded, dropped} = degradeScene(elements)\n return {kind: \"legacy\", layout, degraded, dropped}\n }\n\n const processed = processScene(elements, caps, displayProcessor.getProfile())\n // A stale baseline means the glasses were cleared since the last commit —\n // diff from empty so everything repaints (the retained frame stays valid\n // as the replay source only).\n const prevFrame = this.store.isBaselineStale(appId, view) ? [] : this.store.lastFrame(appId, view)\n const {elements: diffed, removed} = diffScene(\n prevFrame,\n processed.elements,\n this.store.nextSyntheticId(appId, view),\n )\n const canvas = {width: caps.width, height: caps.height}\n this.store.commit(appId, view, diffed, canvas)\n\n const frame: SceneFrame = {\n appId,\n view,\n sceneEpoch: this.store.currentEpoch(appId, view),\n seq: ++this.seqCounter,\n elements: diffed,\n removed,\n canvas,\n }\n const sent = this.sendFrame(frame)\n return {\n kind: \"scene\",\n degraded: processed.degraded,\n dropped: processed.dropped,\n seq: frame.seq,\n sent,\n settleMs: caps.animation ? settleTimeOf(diffed) : 0,\n }\n }\n\n /**\n * Re-emit an app's retained scene (reconnect / arbitration restore). Replay\n * is always create-based — the device rebuilds from scratch (spec §4).\n * Returns false when nothing is retained.\n */\n public replayApp(appId: string, view: \"main\" | \"dashboard\"): boolean {\n const frame = this.store.buildReplayFrame(appId, view, ++this.seqCounter)\n if (!frame) return false\n this.sendFrame(frame)\n return true\n }\n\n /** Forget an app's retained scenes (app stop). Frames still in flight will never show. */\n public clearApp(appId: string): void {\n this.store.clear(appId)\n this.settlePending((p) => p.appId === appId, {presented: false, superseded: true, reason: \"cleared\"})\n }\n\n // ===========================================================================\n // Presentation — native reports when the device has a frame\n // ===========================================================================\n\n /**\n * Resolves once native reports frame `seq` on the device, or a later frame\n * that folded it in. Never rejects; `timeoutMs` bounds a frame the device\n * never acknowledges (link stalls, page rebuilding).\n */\n public whenPresented(seq: number, timeoutMs: number, appId: string): Promise<ScenePresentation> {\n this.ensurePresentedListener()\n return new Promise((resolve) => {\n const timer = BgTimer.setTimeout(() => {\n if (this.pending.delete(seq)) resolve({presented: false, superseded: false, reason: \"no presentation report\"})\n }, timeoutMs)\n this.pending.set(seq, {\n appId,\n resolve: (p) => {\n BgTimer.clearTimeout(timer)\n resolve(p)\n },\n })\n })\n }\n\n private ensurePresentedListener(): void {\n if (this.presentedSub) return\n this.presentedSub = BluetoothSdk.addListener(\"scene_frame_presented\", (event) => {\n this.onFramePresented(event.seq, event.appId, event.presented, event.reason)\n })\n }\n\n /** A report for `seq` settles it and every earlier frame (folded in by the native pacer). */\n private onFramePresented(seq: number, appId: string, presented: boolean, reason?: string): void {\n this.settlePending(\n (p, s) => s <= seq,\n (p, s) =>\n s === seq\n ? {presented, superseded: false, reason}\n : p.appId === appId\n ? {presented, superseded: true, reason}\n : {presented: false, superseded: true, reason: `superseded by ${appId}`},\n )\n }\n\n private settlePending(\n match: (p: {appId: string}, seq: number) => boolean,\n outcome: ScenePresentation | ((p: {appId: string}, seq: number) => ScenePresentation),\n ): void {\n for (const [seq, p] of [...this.pending]) {\n if (!match(p, seq)) continue\n this.pending.delete(seq)\n p.resolve(typeof outcome === \"function\" ? outcome(p, seq) : outcome)\n }\n }\n\n /**\n * Reset the diff baseline for (app, view) without touching the device.\n * Used when a legacy layout bypasses the scene path (clear_view /\n * dashboard_card / unknown layoutTypes go to native raw): after that, the\n * retained frame no longer matches the glasses, so the next render() must\n * diff from empty — otherwise \"unchanged\" elements would be skipped and\n * never repainted.\n */\n public resetBaseline(appId: string, view: \"main\" | \"dashboard\"): void {\n this.store.commit(appId, view, [])\n }\n\n /**\n * A system-level clear wiped the view (durationMs expiry, unmount fallback,\n * boot-window expiry): mark every app's baseline for that view stale. Unlike\n * resetBaseline this KEEPS the retained frames — they remain the\n * restore-as-replay source; only incremental diffing restarts from empty.\n */\n public markViewStale(view: \"main\" | \"dashboard\"): void {\n this.store.markViewStale(view)\n // Whatever was in flight for the view got wiped with it.\n this.settlePending(() => true, {presented: false, superseded: true, reason: \"cleared\"})\n }\n\n /** Returns false when the frame stayed phone-side (no presentation report will follow). */\n private sendFrame(frame: SceneFrame): boolean {\n // Single native view: only the visible surface's frames go out. Suppressed\n // main frames stay committed; setDashboardActive(false) replays them.\n const dashboardActive = useDisplayStore.getState().dashboardActive\n if (dashboardActive !== (frame.view === \"dashboard\")) return false\n try {\n // Frames carry the canvas they were laid out against; only a frame from\n // a path that predates the tag falls back to the current capabilities.\n const caps = this.currentCapabilities()\n if (!frame.canvas && caps) frame = {...frame, canvas: {width: caps.width, height: caps.height}}\n // sendDisplayEvent hook -> direct BluetoothSdk.displayEvent (the exact\n // wiring the deleted MantleManager hook provided).\n void Promise.resolve(BluetoothSdk.displayEvent({view: frame.view, scene: frame})).catch((err) => {\n console.error(`${LOG_TAG}: native scene send failed:`, err)\n })\n\n // Keep the phone-side glasses mirror on the same frame as native. Legacy\n // layouts update this store in LocalDisplayManager.sendToNative; scene\n // frames bypass that method, so without an explicit write every\n // positioning device (including G2) leaves the mirror blank/stale.\n useDisplayStore.getState().setDisplayEvent({\n view: frame.view,\n layout: {\n layoutType: \"scene\",\n width: frame.canvas?.width,\n height: frame.canvas?.height,\n elements: frame.elements,\n },\n })\n return true\n } catch (err) {\n console.error(`${LOG_TAG}: native scene send failed:`, err)\n return false\n }\n }\n\n // ===========================================================================\n // Test hooks\n // ===========================================================================\n\n public _resetForTest(): void {\n this.store.clearAll()\n this.settlePending(() => true, {presented: false, superseded: false, reason: \"reset\"})\n this.presentedSub?.remove()\n this.presentedSub = null\n this.seqCounter = 0\n }\n\n public _lastFrameForTest(appId: string, view: string): FrameElement[] {\n return this.store.lastFrame(appId, view)\n }\n}\n\nconst sceneRenderer = SceneRenderer.getInstance()\nexport default sceneRenderer\nexport {SceneRenderer}\n"]}
@@ -40,7 +40,7 @@ export declare class SceneStore {
40
40
  * "created" (replay is always create-based — updates to dead firmware ids
41
41
  * drop silently on the wire; spec §4). Returns null if nothing is retained.
42
42
  */
43
- buildReplayFrame(appId: string, view: "main" | "dashboard"): SceneFrame | null;
43
+ buildReplayFrame(appId: string, view: "main" | "dashboard", seq: number): SceneFrame | null;
44
44
  /** Forget an app's scene (app stop / clear). */
45
45
  clear(appId: string, view?: string): void;
46
46
  clearAll(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../../src/utils/display/scene/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,YAAY,EAAE,UAAU,EAAC,MAAM,SAAS,CAAA;AAiBrD,qBAAa,UAAU;IACrB,OAAO,CAAC,OAAO,CAA2B;IAE1C,OAAO,CAAC,GAAG;IAIX,OAAO,CAAC,KAAK;IAUb,0DAA0D;IAC1D,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,EAAE;IAItD,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAK9C,qFAAqF;IACrF,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,MAAM;IAK1D,4EAA4E;IAC5E,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,EAAE,MAAM,CAAC,EAAE,UAAU,CAAC,QAAQ,CAAC,GAAG,IAAI;IAOlG,kFAAkF;IAClF,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAIrD;;;;;OAKG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAOjC,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM;IAIjD;;;;OAIG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM;IAO9C;;;;OAIG;IACH,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,GAAG,IAAI;IAgB9E,gDAAgD;IAChD,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;IAUzC,QAAQ,IAAI,IAAI;CAGjB"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../../src/utils/display/scene/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAC,YAAY,EAAE,UAAU,EAAC,MAAM,SAAS,CAAA;AAiBrD,qBAAa,UAAU;IACrB,OAAO,CAAC,OAAO,CAA2B;IAE1C,OAAO,CAAC,GAAG;IAIX,OAAO,CAAC,KAAK;IAUb,0DAA0D;IAC1D,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,YAAY,EAAE;IAItD,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAK9C,qFAAqF;IACrF,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,MAAM;IAK1D,4EAA4E;IAC5E,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,EAAE,MAAM,CAAC,EAAE,UAAU,CAAC,QAAQ,CAAC,GAAG,IAAI;IAOlG,kFAAkF;IAClF,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO;IAIrD;;;;;OAKG;IACH,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAOjC,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM;IAIjD;;;;OAIG;IACH,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM;IAO9C;;;;OAIG;IACH,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI;IAiB3F,gDAAgD;IAChD,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;IAUzC,QAAQ,IAAI,IAAI;CAGjB"}
Binary file
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../../../../src/utils/display/scene/store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SceneStore — the host's retained scene state, keyed (appId, view). Holds the\n * last POST-PROCESSED frame sent to the device (the diff baseline), the scene\n * epoch, and the synthetic-id counter. Recovery = replay from here; the device\n * never needs an app's cooperation to rebuild (design doc §3.4.7).\n *\n * Duration/expiry stays in LocalDisplayManager (it already owns those timers);\n * this store is pure retained state.\n */\n\nimport type {FrameElement, SceneFrame} from \"./types\"\n\ninterface Entry {\n lastFrame: FrameElement[] | null\n /** Canvas `lastFrame` was laid out against — replays must carry it, not whatever the device reports now. */\n canvas: SceneFrame[\"canvas\"]\n epoch: number\n idCounter: number\n /**\n * The glasses no longer show `lastFrame` (a system clear wiped the view) but\n * the frame is retained as the replay source. The next incremental diff must\n * run against an EMPTY baseline — otherwise \"unchanged\" elements are skipped\n * and never repainted onto the blank screen.\n */\n baselineStale: boolean\n}\n\nexport class SceneStore {\n private entries = new Map<string, Entry>()\n\n private key(appId: string, view: string): string {\n return `${appId}\u0000${view}`\n }\n\n private entry(appId: string, view: string): Entry {\n const k = this.key(appId, view)\n let e = this.entries.get(k)\n if (!e) {\n e = {lastFrame: null, canvas: undefined, epoch: 0, idCounter: 0, baselineStale: false}\n this.entries.set(k, e)\n }\n return e\n }\n\n /** The last frame sent for (app, view), or [] if none. */\n lastFrame(appId: string, view: string): FrameElement[] {\n return this.entry(appId, view).lastFrame ?? []\n }\n\n hasScene(appId: string, view: string): boolean {\n const e = this.entries.get(this.key(appId, view))\n return !!e?.lastFrame && e.lastFrame.length > 0\n }\n\n /** Mint a synthetic element id — \"~\" prefix keeps it out of the app-id namespace. */\n nextSyntheticId(appId: string, view: string): () => string {\n const e = this.entry(appId, view)\n return () => `~${++e.idCounter}`\n }\n\n /** Record the frame that was actually sent (post-processing, post-diff). */\n commit(appId: string, view: string, elements: FrameElement[], canvas?: SceneFrame[\"canvas\"]): void {\n const e = this.entry(appId, view)\n e.lastFrame = elements\n e.canvas = canvas\n e.baselineStale = false\n }\n\n /** True when the retained frame is a replay source only — not a diff baseline. */\n isBaselineStale(appId: string, view: string): boolean {\n return this.entries.get(this.key(appId, view))?.baselineStale ?? false\n }\n\n /**\n * The glasses view was cleared (durationMs expiry, unmount fallback, boot\n * expiry): every app's retained frame for that view is now replay-only.\n * Frames are kept — restore-as-replay repaints create-based and doesn't\n * consult the baseline.\n */\n markViewStale(view: string): void {\n // Key separator is NUL (see key()) — invisible in dumps, so match explicitly.\n for (const [k, e] of this.entries) {\n if (k.endsWith(`\\u0000${view}`)) e.baselineStale = true\n }\n }\n\n currentEpoch(appId: string, view: string): number {\n return this.entry(appId, view).epoch\n }\n\n /**\n * Bump the epoch — device switch, reconnect, app switch, scene↔legacy\n * transition. A bumped epoch bypasses native dedup and tells the device the\n * next frame rebuilds from scratch, so the diff baseline resets too.\n */\n bumpEpoch(appId: string, view: string): number {\n const e = this.entry(appId, view)\n e.epoch++\n e.lastFrame = null\n return e.epoch\n }\n\n /**\n * Build a replay frame from retained state: the whole scene, all elements\n * \"created\" (replay is always create-based — updates to dead firmware ids\n * drop silently on the wire; spec §4). Returns null if nothing is retained.\n */\n buildReplayFrame(appId: string, view: \"main\" | \"dashboard\"): SceneFrame | null {\n const e = this.entries.get(this.key(appId, view))\n if (!e?.lastFrame || e.lastFrame.length === 0) return null\n const elements = e.lastFrame.map((el) => ({...el, change: \"created\" as const}))\n e.epoch++\n return {\n appId,\n view,\n sceneEpoch: e.epoch,\n replay: true,\n elements,\n removed: [],\n ...(e.canvas ? {canvas: e.canvas} : {}),\n }\n }\n\n /** Forget an app's scene (app stop / clear). */\n clear(appId: string, view?: string): void {\n if (view !== undefined) {\n this.entries.delete(this.key(appId, view))\n return\n }\n for (const k of [...this.entries.keys()]) {\n if (k.startsWith(`${appId}\u0000`)) this.entries.delete(k)\n }\n }\n\n clearAll(): void {\n this.entries.clear()\n }\n}\n"]}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../../../src/utils/display/scene/store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * SceneStore — the host's retained scene state, keyed (appId, view). Holds the\n * last POST-PROCESSED frame sent to the device (the diff baseline), the scene\n * epoch, and the synthetic-id counter. Recovery = replay from here; the device\n * never needs an app's cooperation to rebuild (design doc §3.4.7).\n *\n * Duration/expiry stays in LocalDisplayManager (it already owns those timers);\n * this store is pure retained state.\n */\n\nimport type {FrameElement, SceneFrame} from \"./types\"\n\ninterface Entry {\n lastFrame: FrameElement[] | null\n /** Canvas `lastFrame` was laid out against — replays must carry it, not whatever the device reports now. */\n canvas: SceneFrame[\"canvas\"]\n epoch: number\n idCounter: number\n /**\n * The glasses no longer show `lastFrame` (a system clear wiped the view) but\n * the frame is retained as the replay source. The next incremental diff must\n * run against an EMPTY baseline — otherwise \"unchanged\" elements are skipped\n * and never repainted onto the blank screen.\n */\n baselineStale: boolean\n}\n\nexport class SceneStore {\n private entries = new Map<string, Entry>()\n\n private key(appId: string, view: string): string {\n return `${appId}\u0000${view}`\n }\n\n private entry(appId: string, view: string): Entry {\n const k = this.key(appId, view)\n let e = this.entries.get(k)\n if (!e) {\n e = {lastFrame: null, canvas: undefined, epoch: 0, idCounter: 0, baselineStale: false}\n this.entries.set(k, e)\n }\n return e\n }\n\n /** The last frame sent for (app, view), or [] if none. */\n lastFrame(appId: string, view: string): FrameElement[] {\n return this.entry(appId, view).lastFrame ?? []\n }\n\n hasScene(appId: string, view: string): boolean {\n const e = this.entries.get(this.key(appId, view))\n return !!e?.lastFrame && e.lastFrame.length > 0\n }\n\n /** Mint a synthetic element id — \"~\" prefix keeps it out of the app-id namespace. */\n nextSyntheticId(appId: string, view: string): () => string {\n const e = this.entry(appId, view)\n return () => `~${++e.idCounter}`\n }\n\n /** Record the frame that was actually sent (post-processing, post-diff). */\n commit(appId: string, view: string, elements: FrameElement[], canvas?: SceneFrame[\"canvas\"]): void {\n const e = this.entry(appId, view)\n e.lastFrame = elements\n e.canvas = canvas\n e.baselineStale = false\n }\n\n /** True when the retained frame is a replay source only — not a diff baseline. */\n isBaselineStale(appId: string, view: string): boolean {\n return this.entries.get(this.key(appId, view))?.baselineStale ?? false\n }\n\n /**\n * The glasses view was cleared (durationMs expiry, unmount fallback, boot\n * expiry): every app's retained frame for that view is now replay-only.\n * Frames are kept — restore-as-replay repaints create-based and doesn't\n * consult the baseline.\n */\n markViewStale(view: string): void {\n // Key separator is NUL (see key()) — invisible in dumps, so match explicitly.\n for (const [k, e] of this.entries) {\n if (k.endsWith(`\\u0000${view}`)) e.baselineStale = true\n }\n }\n\n currentEpoch(appId: string, view: string): number {\n return this.entry(appId, view).epoch\n }\n\n /**\n * Bump the epoch — device switch, reconnect, app switch, scene↔legacy\n * transition. A bumped epoch bypasses native dedup and tells the device the\n * next frame rebuilds from scratch, so the diff baseline resets too.\n */\n bumpEpoch(appId: string, view: string): number {\n const e = this.entry(appId, view)\n e.epoch++\n e.lastFrame = null\n return e.epoch\n }\n\n /**\n * Build a replay frame from retained state: the whole scene, all elements\n * \"created\" (replay is always create-based — updates to dead firmware ids\n * drop silently on the wire; spec §4). Returns null if nothing is retained.\n */\n buildReplayFrame(appId: string, view: \"main\" | \"dashboard\", seq: number): SceneFrame | null {\n const e = this.entries.get(this.key(appId, view))\n if (!e?.lastFrame || e.lastFrame.length === 0) return null\n const elements = e.lastFrame.map((el) => ({...el, change: \"created\" as const}))\n e.epoch++\n return {\n appId,\n view,\n sceneEpoch: e.epoch,\n seq,\n replay: true,\n elements,\n removed: [],\n ...(e.canvas ? {canvas: e.canvas} : {}),\n }\n }\n\n /** Forget an app's scene (app stop / clear). */\n clear(appId: string, view?: string): void {\n if (view !== undefined) {\n this.entries.delete(this.key(appId, view))\n return\n }\n for (const k of [...this.entries.keys()]) {\n if (k.startsWith(`${appId}\u0000`)) this.entries.delete(k)\n }\n }\n\n clearAll(): void {\n this.entries.clear()\n }\n}\n"]}
@@ -184,6 +184,13 @@ export interface SceneFrame {
184
184
  view: "main" | "dashboard";
185
185
  /** Bumps on replay / app switch / reconnect; a bumped epoch bypasses native dedup. */
186
186
  sceneEpoch: number;
187
+ /**
188
+ * Host-wide monotonic frame number. Native reports it back in
189
+ * `scene_frame_presented` once the device has the frame; a report for N
190
+ * also covers every earlier frame (the native pacer folds unsent frames
191
+ * into the next one, so their content is on the glasses by then).
192
+ */
193
+ seq: number;
187
194
  /** True when the device is expected to rebuild from scratch (all elements arrive "created"). */
188
195
  replay?: boolean;
189
196
  elements: FrameElement[];
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/utils/display/scene/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/utils/display/scene/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../src/utils/display/scene/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA8IH,0GAA0G;AAC1G,MAAM,CAAC,MAAM,iBAAiB,GAAgC,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAA;AAEpG,6CAA6C;AAC7C,MAAM,CAAC,MAAM,iBAAiB,GAAsC;IAClE,IAAI,EAAE,CAAC,CAAC,CAAC;IACT,QAAQ,EAAE,CAAC,CAAC,CAAC;IACb,IAAI,EAAE,CAAC,CAAC,CAAC;IACT,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;CACf,CAAA;AAyED,4FAA4F;AAC5F,MAAM,UAAU,WAAW,CAAC,GAAG,KAAsC;IACnE,IAAI,CAAC,GAAG,UAAU,CAAA;IAClB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,CAAC,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QACjD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAClC,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;YACpB,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,UAAU,CAAC,CAAA;QAC9B,CAAC;QACD,CAAC,IAAI,IAAI,CAAA;QACT,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,UAAU,CAAC,CAAA;IAC9B,CAAC;IACD,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;AAC/B,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,CAAW,EAAE,CAAW;IACjD,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AACjE,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,kBAAkB,CAAC,EAQlC;IACC,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,IAAI,EAAE,CAAA;IAC5B,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC;SAChC,IAAI,EAAE;SACN,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAK,KAAiC,CAAC,CAAC,CAAC,EAAE,CAAC;SAC3D,IAAI,CAAC,GAAG,CAAC,CAAA;IACZ,MAAM,SAAS,GAAG,EAAE,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAClE,MAAM,QAAQ,GAAG,EAAE,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,QAAQ,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;IACjH,OAAO,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAA;AAC9E,CAAC","sourcesContent":["/**\n * Scene pipeline types — the host half of the miniapp `render()` display API.\n *\n * A miniapp describes a full frame (\"scene\") of positioned elements; the host\n * clamps/budgets/wraps it, diffs it against the last frame sent to the device,\n * and ships ONE annotated SceneFrame over the bridge. SGCs either walk the\n * per-element annotations (G2, Mentra Display) or serialize the whole frame\n * (full-frame devices). Spec: notes/miniapp-display-render-implementation-spec.md\n */\n\nexport interface SceneBox {\n x: number\n y: number\n w: number\n h: number\n}\n\n/** Canvas corner a box's x/y are measured from (insets grow inward). */\nexport type SceneAnchor = \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\"\n\n/**\n * A box as it arrives from the miniapp SDK: optionally corner-anchored.\n * `resolveAnchors` turns these into plain top-left SceneBoxes against the\n * device's real canvas before any processing/degrading — anchors never reach\n * FrameElement, the differ, or the wire.\n */\nexport interface SceneBoxInput extends SceneBox {\n anchor?: SceneAnchor\n}\n\nexport type SceneBreakMode = \"character\" | \"character-no-hyphen\" | \"word\" | \"strict-word\"\n\nexport interface SceneTextStyle {\n /** Border width in px (0/absent = none). */\n border?: number\n /** Border corner radius in px. */\n radius?: number\n /** What happens to text that doesn't fit the box after wrapping. Default \"clip\". */\n overflow?: \"clip\" | \"ellipsis\"\n /** Line-break policy for wrapping. */\n breakMode?: SceneBreakMode\n /**\n * \"mono\": fixed-pitch at `fontSize` px, lines taken as given (no host wrap).\n * \"builtin\": drawn by the glasses' own 20 px font engine (Glassly CFW G2);\n * lines taken as given, other devices fall back to the default face.\n */\n font?: \"default\" | \"mono\" | \"builtin\"\n fontSize?: number\n /** Gray level 0..15 for device-drawn text. */\n color?: number\n}\n\nexport interface SceneRectStyle {\n border?: number\n radius?: number\n /** Solid fill instead of an outline. */\n fill?: boolean\n /** Gray level 0..15 (15 = brightest). Default 15. */\n color?: number\n}\n\n/** Closed shapes (circle, triangle, quad): outline of `border` px, or filled. */\nexport interface SceneShapeStyle {\n border?: number\n fill?: boolean\n color?: number\n}\n\n/** Open strokes (line, bezier, arc). */\nexport interface SceneStrokeStyle {\n width?: number\n color?: number\n}\n\nexport interface ScenePoint {\n x: number\n y: number\n}\n\nexport type SceneEasing = \"linear\" | \"ease\" | \"ease-in\" | \"ease-out\" | \"ease-in-out\"\n\n/** Animate geometry changes of an element with a stable id (devices with `animation`). */\nexport interface SceneTransition {\n durationMs: number\n easing?: SceneEasing\n}\n\nexport type SceneShape = \"rect\" | \"circle\" | \"arc\" | \"pie\" | \"line\" | \"triangle\" | \"quad\" | \"bezier\"\n\n/** A scene element as it arrives from the miniapp SDK (pre-processing). */\nexport type SceneElementInput =\n | {type: \"text\"; id?: string; box: SceneBoxInput; text: string; style?: SceneTextStyle; transition?: SceneTransition}\n | {type: \"image\"; id?: string; box: SceneBoxInput; data: string; transition?: SceneTransition}\n | {type: \"rect\"; id?: string; box: SceneBoxInput; style?: SceneRectStyle; transition?: SceneTransition}\n | {type: \"circle\"; id?: string; box: SceneBoxInput; style?: SceneShapeStyle; transition?: SceneTransition}\n | {\n type: \"arc\"\n id?: string\n box: SceneBoxInput\n startDeg: number\n endDeg: number\n style?: SceneStrokeStyle\n transition?: SceneTransition\n }\n | {\n type: \"pie\"\n id?: string\n box: SceneBoxInput\n startDeg: number\n endDeg: number\n style?: {color?: number}\n transition?: SceneTransition\n }\n | {\n type: \"line\"\n id?: string\n points: ScenePoint[]\n box?: SceneBoxInput\n style?: SceneStrokeStyle\n transition?: SceneTransition\n }\n | {\n type: \"triangle\"\n id?: string\n points: ScenePoint[]\n box?: SceneBoxInput\n style?: SceneShapeStyle\n transition?: SceneTransition\n }\n | {\n type: \"quad\"\n id?: string\n points: ScenePoint[]\n box?: SceneBoxInput\n style?: SceneShapeStyle\n transition?: SceneTransition\n }\n | {\n type: \"bezier\"\n id?: string\n points: ScenePoint[]\n box?: SceneBoxInput\n style?: SceneStrokeStyle\n transition?: SceneTransition\n }\n\nexport type SceneElementType = SceneElementInput[\"type\"]\n\nexport type SceneElementStyle = SceneTextStyle | SceneRectStyle | SceneShapeStyle | SceneStrokeStyle | {color?: number}\n\n/** Elements positioned by a point list; the host derives `box` (their bounding box) before processing. */\nexport const POINT_SHAPE_TYPES: readonly SceneElementType[] = [\"line\", \"triangle\", \"quad\", \"bezier\"]\n\n/** Point counts each point shape accepts. */\nexport const POINT_SHAPE_ARITY: Record<string, readonly number[]> = {\n line: [2],\n triangle: [3],\n quad: [4],\n bezier: [3, 4],\n}\n\nexport type SceneChange = \"created\" | \"updated\" | \"moved\" | \"unchanged\"\n\n/**\n * A processed element inside a SceneFrame: id definitely assigned, box clamped,\n * text pre-wrapped (newlines inserted), annotated with what changed vs the last\n * frame sent to this device.\n */\nexport interface FrameElement {\n id: string\n type: SceneElementType\n box: SceneBox\n text?: string\n /** Image pixels, base64 (PNG documented; SGCs decode → re-encode to wire format and scale to box). */\n data?: string\n style?: SceneElementStyle\n /** Point shapes: the points, in canvas pixels; `box` is their bounding box. */\n points?: ScenePoint[]\n startDeg?: number\n endDeg?: number\n transition?: SceneTransition\n change: SceneChange\n /** Hash of the element's content (text/data + style) — lets SGCs skip BLE re-upload without retaining pixels. */\n contentHash: string\n}\n\n/** The unit that crosses the JS→native bridge: one whole frame per render(). */\nexport interface SceneFrame {\n appId: string\n view: \"main\" | \"dashboard\"\n /** Bumps on replay / app switch / reconnect; a bumped epoch bypasses native dedup. */\n sceneEpoch: number\n /** True when the device is expected to rebuild from scratch (all elements arrive \"created\"). */\n replay?: boolean\n elements: FrameElement[]\n /** Element ids present in the previous frame but absent now — host-computed; apps never send removes. */\n removed: string[]\n /** Canvas the boxes were clamped against — lets a full-frame SGC place a legacy-sized frame correctly. */\n canvas?: {width: number; height: number}\n}\n\n/**\n * Typed display capabilities block (host-side mirror of the SDK's\n * DisplayCapabilities). All limits are DATA — the pipeline acts on them\n * generically; no device names appear in pipeline code.\n */\nexport interface SceneDisplayCapabilities {\n /** Public drawable canvas in px (the device's safe area, not the physical panel). */\n width: number\n height: number\n /** False ⇒ scenes degrade to legacy text layouts host-side; only sugar-shaped content renders. */\n canPosition: boolean\n /** Text-element budget. Rects share this pool on container-based devices. */\n maxTextElements: number\n maxImageElements: number\n /** Max per-image dimensions the device accepts (box-level check), if limited. */\n maxImagePx?: {width: number; height: number}\n shapes: SceneShape[]\n /** True ⇒ element transitions animate on the device. */\n animation: boolean\n intensityLevels: number\n partialUpdate: boolean\n}\n\n/** Result of processing a scene — feeds the awaitable render() result. */\nexport interface SceneProcessResult {\n frame: SceneFrame\n degraded: boolean\n /** Ids (or synthesized ids) of elements dropped by budget/bounds/image limits. */\n dropped: string[]\n}\n\n/** Cheap stable string hash (FNV-1a, 32-bit) — content identity for diff + native dedup. */\nexport function contentHash(...parts: (string | number | undefined)[]): string {\n let h = 0x811c9dc5\n for (const part of parts) {\n const s = part === undefined ? \"\u0000\" : String(part)\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i)\n h = Math.imul(h, 0x01000193)\n }\n h ^= 0x1f\n h = Math.imul(h, 0x01000193)\n }\n return (h >>> 0).toString(36)\n}\n\nexport function boxesEqual(a: SceneBox, b: SceneBox): boolean {\n return a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h\n}\n\n/** Content identity of an element (text/data/points/angles + style), excluding its box. */\nexport function elementContentHash(el: {\n type: SceneElementType\n text?: string\n data?: string\n style?: SceneElementStyle\n points?: ScenePoint[]\n startDeg?: number\n endDeg?: number\n}): string {\n const style = el.style ?? {}\n const styleKey = Object.keys(style)\n .sort()\n .map((k) => `${k}=${(style as Record<string, unknown>)[k]}`)\n .join(\",\")\n const pointsKey = el.points?.map((p) => `${p.x},${p.y}`).join(\";\")\n const angleKey = el.startDeg !== undefined || el.endDeg !== undefined ? `${el.startDeg}-${el.endDeg}` : undefined\n return contentHash(el.type, el.text, el.data, styleKey, pointsKey, angleKey)\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../src/utils/display/scene/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA8IH,0GAA0G;AAC1G,MAAM,CAAC,MAAM,iBAAiB,GAAgC,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAA;AAEpG,6CAA6C;AAC7C,MAAM,CAAC,MAAM,iBAAiB,GAAsC;IAClE,IAAI,EAAE,CAAC,CAAC,CAAC;IACT,QAAQ,EAAE,CAAC,CAAC,CAAC;IACb,IAAI,EAAE,CAAC,CAAC,CAAC;IACT,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC;CACf,CAAA;AAgFD,4FAA4F;AAC5F,MAAM,UAAU,WAAW,CAAC,GAAG,KAAsC;IACnE,IAAI,CAAC,GAAG,UAAU,CAAA;IAClB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,CAAC,GAAG,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;QACjD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YAClC,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;YACpB,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,UAAU,CAAC,CAAA;QAC9B,CAAC;QACD,CAAC,IAAI,IAAI,CAAA;QACT,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,EAAE,UAAU,CAAC,CAAA;IAC9B,CAAC;IACD,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;AAC/B,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,CAAW,EAAE,CAAW;IACjD,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AACjE,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,kBAAkB,CAAC,EAQlC;IACC,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,IAAI,EAAE,CAAA;IAC5B,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC;SAChC,IAAI,EAAE;SACN,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAK,KAAiC,CAAC,CAAC,CAAC,EAAE,CAAC;SAC3D,IAAI,CAAC,GAAG,CAAC,CAAA;IACZ,MAAM,SAAS,GAAG,EAAE,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAClE,MAAM,QAAQ,GAAG,EAAE,CAAC,QAAQ,KAAK,SAAS,IAAI,EAAE,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,QAAQ,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;IACjH,OAAO,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAA;AAC9E,CAAC","sourcesContent":["/**\n * Scene pipeline types — the host half of the miniapp `render()` display API.\n *\n * A miniapp describes a full frame (\"scene\") of positioned elements; the host\n * clamps/budgets/wraps it, diffs it against the last frame sent to the device,\n * and ships ONE annotated SceneFrame over the bridge. SGCs either walk the\n * per-element annotations (G2, Mentra Display) or serialize the whole frame\n * (full-frame devices). Spec: notes/miniapp-display-render-implementation-spec.md\n */\n\nexport interface SceneBox {\n x: number\n y: number\n w: number\n h: number\n}\n\n/** Canvas corner a box's x/y are measured from (insets grow inward). */\nexport type SceneAnchor = \"top-left\" | \"top-right\" | \"bottom-left\" | \"bottom-right\"\n\n/**\n * A box as it arrives from the miniapp SDK: optionally corner-anchored.\n * `resolveAnchors` turns these into plain top-left SceneBoxes against the\n * device's real canvas before any processing/degrading — anchors never reach\n * FrameElement, the differ, or the wire.\n */\nexport interface SceneBoxInput extends SceneBox {\n anchor?: SceneAnchor\n}\n\nexport type SceneBreakMode = \"character\" | \"character-no-hyphen\" | \"word\" | \"strict-word\"\n\nexport interface SceneTextStyle {\n /** Border width in px (0/absent = none). */\n border?: number\n /** Border corner radius in px. */\n radius?: number\n /** What happens to text that doesn't fit the box after wrapping. Default \"clip\". */\n overflow?: \"clip\" | \"ellipsis\"\n /** Line-break policy for wrapping. */\n breakMode?: SceneBreakMode\n /**\n * \"mono\": fixed-pitch at `fontSize` px, lines taken as given (no host wrap).\n * \"builtin\": drawn by the glasses' own 20 px font engine (Glassly CFW G2);\n * lines taken as given, other devices fall back to the default face.\n */\n font?: \"default\" | \"mono\" | \"builtin\"\n fontSize?: number\n /** Gray level 0..15 for device-drawn text. */\n color?: number\n}\n\nexport interface SceneRectStyle {\n border?: number\n radius?: number\n /** Solid fill instead of an outline. */\n fill?: boolean\n /** Gray level 0..15 (15 = brightest). Default 15. */\n color?: number\n}\n\n/** Closed shapes (circle, triangle, quad): outline of `border` px, or filled. */\nexport interface SceneShapeStyle {\n border?: number\n fill?: boolean\n color?: number\n}\n\n/** Open strokes (line, bezier, arc). */\nexport interface SceneStrokeStyle {\n width?: number\n color?: number\n}\n\nexport interface ScenePoint {\n x: number\n y: number\n}\n\nexport type SceneEasing = \"linear\" | \"ease\" | \"ease-in\" | \"ease-out\" | \"ease-in-out\"\n\n/** Animate geometry changes of an element with a stable id (devices with `animation`). */\nexport interface SceneTransition {\n durationMs: number\n easing?: SceneEasing\n}\n\nexport type SceneShape = \"rect\" | \"circle\" | \"arc\" | \"pie\" | \"line\" | \"triangle\" | \"quad\" | \"bezier\"\n\n/** A scene element as it arrives from the miniapp SDK (pre-processing). */\nexport type SceneElementInput =\n | {type: \"text\"; id?: string; box: SceneBoxInput; text: string; style?: SceneTextStyle; transition?: SceneTransition}\n | {type: \"image\"; id?: string; box: SceneBoxInput; data: string; transition?: SceneTransition}\n | {type: \"rect\"; id?: string; box: SceneBoxInput; style?: SceneRectStyle; transition?: SceneTransition}\n | {type: \"circle\"; id?: string; box: SceneBoxInput; style?: SceneShapeStyle; transition?: SceneTransition}\n | {\n type: \"arc\"\n id?: string\n box: SceneBoxInput\n startDeg: number\n endDeg: number\n style?: SceneStrokeStyle\n transition?: SceneTransition\n }\n | {\n type: \"pie\"\n id?: string\n box: SceneBoxInput\n startDeg: number\n endDeg: number\n style?: {color?: number}\n transition?: SceneTransition\n }\n | {\n type: \"line\"\n id?: string\n points: ScenePoint[]\n box?: SceneBoxInput\n style?: SceneStrokeStyle\n transition?: SceneTransition\n }\n | {\n type: \"triangle\"\n id?: string\n points: ScenePoint[]\n box?: SceneBoxInput\n style?: SceneShapeStyle\n transition?: SceneTransition\n }\n | {\n type: \"quad\"\n id?: string\n points: ScenePoint[]\n box?: SceneBoxInput\n style?: SceneShapeStyle\n transition?: SceneTransition\n }\n | {\n type: \"bezier\"\n id?: string\n points: ScenePoint[]\n box?: SceneBoxInput\n style?: SceneStrokeStyle\n transition?: SceneTransition\n }\n\nexport type SceneElementType = SceneElementInput[\"type\"]\n\nexport type SceneElementStyle = SceneTextStyle | SceneRectStyle | SceneShapeStyle | SceneStrokeStyle | {color?: number}\n\n/** Elements positioned by a point list; the host derives `box` (their bounding box) before processing. */\nexport const POINT_SHAPE_TYPES: readonly SceneElementType[] = [\"line\", \"triangle\", \"quad\", \"bezier\"]\n\n/** Point counts each point shape accepts. */\nexport const POINT_SHAPE_ARITY: Record<string, readonly number[]> = {\n line: [2],\n triangle: [3],\n quad: [4],\n bezier: [3, 4],\n}\n\nexport type SceneChange = \"created\" | \"updated\" | \"moved\" | \"unchanged\"\n\n/**\n * A processed element inside a SceneFrame: id definitely assigned, box clamped,\n * text pre-wrapped (newlines inserted), annotated with what changed vs the last\n * frame sent to this device.\n */\nexport interface FrameElement {\n id: string\n type: SceneElementType\n box: SceneBox\n text?: string\n /** Image pixels, base64 (PNG documented; SGCs decode → re-encode to wire format and scale to box). */\n data?: string\n style?: SceneElementStyle\n /** Point shapes: the points, in canvas pixels; `box` is their bounding box. */\n points?: ScenePoint[]\n startDeg?: number\n endDeg?: number\n transition?: SceneTransition\n change: SceneChange\n /** Hash of the element's content (text/data + style) — lets SGCs skip BLE re-upload without retaining pixels. */\n contentHash: string\n}\n\n/** The unit that crosses the JS→native bridge: one whole frame per render(). */\nexport interface SceneFrame {\n appId: string\n view: \"main\" | \"dashboard\"\n /** Bumps on replay / app switch / reconnect; a bumped epoch bypasses native dedup. */\n sceneEpoch: number\n /**\n * Host-wide monotonic frame number. Native reports it back in\n * `scene_frame_presented` once the device has the frame; a report for N\n * also covers every earlier frame (the native pacer folds unsent frames\n * into the next one, so their content is on the glasses by then).\n */\n seq: number\n /** True when the device is expected to rebuild from scratch (all elements arrive \"created\"). */\n replay?: boolean\n elements: FrameElement[]\n /** Element ids present in the previous frame but absent now — host-computed; apps never send removes. */\n removed: string[]\n /** Canvas the boxes were clamped against — lets a full-frame SGC place a legacy-sized frame correctly. */\n canvas?: {width: number; height: number}\n}\n\n/**\n * Typed display capabilities block (host-side mirror of the SDK's\n * DisplayCapabilities). All limits are DATA — the pipeline acts on them\n * generically; no device names appear in pipeline code.\n */\nexport interface SceneDisplayCapabilities {\n /** Public drawable canvas in px (the device's safe area, not the physical panel). */\n width: number\n height: number\n /** False ⇒ scenes degrade to legacy text layouts host-side; only sugar-shaped content renders. */\n canPosition: boolean\n /** Text-element budget. Rects share this pool on container-based devices. */\n maxTextElements: number\n maxImageElements: number\n /** Max per-image dimensions the device accepts (box-level check), if limited. */\n maxImagePx?: {width: number; height: number}\n shapes: SceneShape[]\n /** True ⇒ element transitions animate on the device. */\n animation: boolean\n intensityLevels: number\n partialUpdate: boolean\n}\n\n/** Result of processing a scene — feeds the awaitable render() result. */\nexport interface SceneProcessResult {\n frame: SceneFrame\n degraded: boolean\n /** Ids (or synthesized ids) of elements dropped by budget/bounds/image limits. */\n dropped: string[]\n}\n\n/** Cheap stable string hash (FNV-1a, 32-bit) — content identity for diff + native dedup. */\nexport function contentHash(...parts: (string | number | undefined)[]): string {\n let h = 0x811c9dc5\n for (const part of parts) {\n const s = part === undefined ? \"\u0000\" : String(part)\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i)\n h = Math.imul(h, 0x01000193)\n }\n h ^= 0x1f\n h = Math.imul(h, 0x01000193)\n }\n return (h >>> 0).toString(36)\n}\n\nexport function boxesEqual(a: SceneBox, b: SceneBox): boolean {\n return a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h\n}\n\n/** Content identity of an element (text/data/points/angles + style), excluding its box. */\nexport function elementContentHash(el: {\n type: SceneElementType\n text?: string\n data?: string\n style?: SceneElementStyle\n points?: ScenePoint[]\n startDeg?: number\n endDeg?: number\n}): string {\n const style = el.style ?? {}\n const styleKey = Object.keys(style)\n .sort()\n .map((k) => `${k}=${(style as Record<string, unknown>)[k]}`)\n .join(\",\")\n const pointsKey = el.points?.map((p) => `${p.x},${p.y}`).join(\";\")\n const angleKey = el.startDeg !== undefined || el.endDeg !== undefined ? `${el.startDeg}-${el.endDeg}` : undefined\n return contentHash(el.type, el.text, el.data, styleKey, pointsKey, angleKey)\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassly/engine",
3
- "version": "0.1.0-dev.117",
3
+ "version": "0.1.0-dev.119",
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.1.0-dev.117",
101
- "@glassly/cloud-client": "0.1.0-dev.117",
102
- "@glassly/cloud-protocol": "0.1.0-dev.117",
103
- "@glassly/crust": "0.1.0-dev.117",
104
- "@glassly/miniapp": "0.1.0-dev.117",
100
+ "@glassly/bluetooth-sdk": "0.1.0-dev.119",
101
+ "@glassly/cloud-client": "0.1.0-dev.119",
102
+ "@glassly/cloud-protocol": "0.1.0-dev.119",
103
+ "@glassly/crust": "0.1.0-dev.119",
104
+ "@glassly/miniapp": "0.1.0-dev.119",
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.1.0-dev.117"
110
+ "@glassly/llm-providers": "0.1.0-dev.119"
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.1.0",
15
- "releaseIdentity": "0.1.0-dev.117",
16
- "releaseSetId": "glassly-0.1.0-dev.117",
17
- "sourceCommit": "8897d176be739fa9b410be5716281f6fec52f299",
15
+ "releaseIdentity": "0.1.0-dev.119",
16
+ "releaseSetId": "glassly-0.1.0-dev.119",
17
+ "sourceCommit": "95cb68c1d59d209b9d984b0960bc1524d57fd262",
18
18
  "otaManifestUrl": null,
19
19
  "otaManifestSha256": null
20
20
  })
@@ -53,6 +53,10 @@ export interface DisplayRequestResult {
53
53
  status: "displayed" | "blocked"
54
54
  degraded?: boolean
55
55
  dropped?: string[]
56
+ /** Scene frames: the glasses had the frame (and finished its transitions) before this resolved. */
57
+ presented?: boolean
58
+ /** A newer frame from the same app settled this one. */
59
+ superseded?: boolean
56
60
  reason?: string
57
61
  }
58
62
 
@@ -99,6 +103,10 @@ const SYSTEM_BOOT_PKG = "system.boot"
99
103
  // Sentinel package for transient OS notices (quota exhausted, etc.).
100
104
  const SYSTEM_NOTICE_PKG = "system.notice"
101
105
  const SYSTEM_NOTICE_DURATION_MS = 6_000
106
+ // A scene frame native never reports on (page rebuilding, link stalled)
107
+ // resolves presented:false after this — long enough for a full-frame BLE
108
+ // image transfer with retries, short enough that an awaiting app isn't stuck.
109
+ const PRESENTATION_TIMEOUT_MS = 3_000
102
110
 
103
111
  /** "Render nothing" — an empty scene or an explicit clear_view layout. */
104
112
  function isClearPayload(payload: DisplayPayload): boolean {
@@ -717,7 +725,26 @@ class LocalDisplayManager {
717
725
  this.handleExpiry(packageName)
718
726
  }, delay)
719
727
  }
720
- resolve?.({status: "displayed", degraded: result.degraded, dropped: result.dropped})
728
+ if (!resolve) return
729
+ const base = {status: "displayed" as const, degraded: result.degraded, dropped: result.dropped}
730
+ if (!result.sent) {
731
+ resolve({...base, presented: false, reason: "dashboard active"})
732
+ return
733
+ }
734
+ // Awaited render(): settle once the glasses have the frame and its
735
+ // transitions have played out. The timeout keeps a frame the device
736
+ // never reports on from hanging the app.
737
+ void sceneRenderer.whenPresented(result.seq, PRESENTATION_TIMEOUT_MS, packageName).then((p) => {
738
+ const finish = () =>
739
+ resolve({
740
+ ...base,
741
+ presented: p.presented,
742
+ ...(p.superseded ? {superseded: true} : {}),
743
+ ...(p.reason !== undefined ? {reason: p.reason} : {}),
744
+ })
745
+ if (p.presented && result.settleMs > 0) BgTimer.setTimeout(finish, result.settleMs)
746
+ else finish()
747
+ })
721
748
  }
722
749
 
723
750
  private sendToNative(packageName: string, rawEvent: Record<string, unknown>, expiresAt: number | null): void {