@glassly/engine 0.1.0-dev.118 → 0.1.0-dev.120
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/LocalDisplayManager.d.ts +4 -0
- package/build/services/LocalDisplayManager.d.ts.map +1 -1
- package/build/services/LocalDisplayManager.js +26 -1
- package/build/services/LocalDisplayManager.js.map +1 -1
- package/build/services/SceneRenderer.d.ts +32 -2
- package/build/services/SceneRenderer.d.ts.map +1 -1
- package/build/services/SceneRenderer.js +88 -5
- package/build/services/SceneRenderer.js.map +1 -1
- package/build/utils/display/scene/store.d.ts +1 -1
- package/build/utils/display/scene/store.d.ts.map +1 -1
- package/build/utils/display/scene/store.js +0 -0
- package/build/utils/display/scene/store.js.map +1 -1
- package/build/utils/display/scene/types.d.ts +7 -0
- package/build/utils/display/scene/types.d.ts.map +1 -1
- package/build/utils/display/scene/types.js.map +1 -1
- package/package.json +7 -7
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/services/LocalDisplayManager.ts +28 -1
- package/src/services/SceneRenderer.ts +114 -8
- package/src/utils/display/scene/store.ts +0 -0
- package/src/utils/display/scene/types.ts +0 -0
|
@@ -18,6 +18,7 @@ import {SETTINGS, useSettingsStore} from "../stores/settings"
|
|
|
18
18
|
import {useGlassesStore} from "../stores/glasses"
|
|
19
19
|
import {DeviceTypes} from "../types"
|
|
20
20
|
import {getEffectiveCapabilities} from "./effectiveCapabilities"
|
|
21
|
+
import {BgTimer} from "../utils/timers"
|
|
21
22
|
import {
|
|
22
23
|
degradeScene,
|
|
23
24
|
diffScene,
|
|
@@ -37,16 +38,47 @@ const LOG_TAG = "SCENE_RENDERER"
|
|
|
37
38
|
export type LegacyLayout = {layoutType: string; [key: string]: unknown}
|
|
38
39
|
|
|
39
40
|
export type SceneEmitResult =
|
|
40
|
-
/**
|
|
41
|
-
|
|
41
|
+
/**
|
|
42
|
+
* A SceneFrame was committed. `sent` is false when the frame stayed
|
|
43
|
+
* phone-side (dashboard covering main). `settleMs` is how long the device
|
|
44
|
+
* keeps animating after it has the frame: the longest transition among the
|
|
45
|
+
* elements this frame changed, 0 on devices that jump.
|
|
46
|
+
*/
|
|
47
|
+
| {kind: "scene"; degraded: boolean; dropped: string[]; seq: number; sent: boolean; settleMs: number}
|
|
42
48
|
/** Degrade path: caller routes this legacy layout through the existing pipeline (null layout ⇒ clear). */
|
|
43
49
|
| {kind: "legacy"; layout: LegacyLayout | null; degraded: boolean; dropped: string[]}
|
|
44
50
|
/** No display capabilities on the connected device. */
|
|
45
51
|
| {kind: "no-display"}
|
|
46
52
|
|
|
53
|
+
/** How a frame ended up, as reported by native (or inferred from a later frame's report). */
|
|
54
|
+
export interface ScenePresentation {
|
|
55
|
+
/** The device had the frame's content (this frame or a later one that folded it in). */
|
|
56
|
+
presented: boolean
|
|
57
|
+
/** A later frame's report settled this one. */
|
|
58
|
+
superseded: boolean
|
|
59
|
+
reason?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Longest tween the G2 CFW encodes (255 frames at ~30 fps); nothing waits past it. */
|
|
63
|
+
const MAX_SETTLE_MS = 255 * 33
|
|
64
|
+
|
|
65
|
+
/** Longest transition among the elements that actually change on the device this frame. */
|
|
66
|
+
function settleTimeOf(elements: FrameElement[]): number {
|
|
67
|
+
let ms = 0
|
|
68
|
+
for (const el of elements) {
|
|
69
|
+
if (el.change !== "updated" && el.change !== "moved") continue
|
|
70
|
+
const t = el.transition?.durationMs ?? 0
|
|
71
|
+
if (t > ms) ms = t
|
|
72
|
+
}
|
|
73
|
+
return Math.min(ms, MAX_SETTLE_MS)
|
|
74
|
+
}
|
|
75
|
+
|
|
47
76
|
class SceneRenderer {
|
|
48
77
|
private static instance: SceneRenderer | null = null
|
|
49
78
|
private store = new SceneStore()
|
|
79
|
+
private seqCounter = 0
|
|
80
|
+
private pending = new Map<number, {appId: string; resolve: (p: ScenePresentation) => void}>()
|
|
81
|
+
private presentedSub: {remove: () => void} | null = null
|
|
50
82
|
|
|
51
83
|
public static getInstance(): SceneRenderer {
|
|
52
84
|
if (!SceneRenderer.instance) {
|
|
@@ -226,12 +258,20 @@ class SceneRenderer {
|
|
|
226
258
|
appId,
|
|
227
259
|
view,
|
|
228
260
|
sceneEpoch: this.store.currentEpoch(appId, view),
|
|
261
|
+
seq: ++this.seqCounter,
|
|
229
262
|
elements: diffed,
|
|
230
263
|
removed,
|
|
231
264
|
canvas,
|
|
232
265
|
}
|
|
233
|
-
this.sendFrame(frame)
|
|
234
|
-
return {
|
|
266
|
+
const sent = this.sendFrame(frame)
|
|
267
|
+
return {
|
|
268
|
+
kind: "scene",
|
|
269
|
+
degraded: processed.degraded,
|
|
270
|
+
dropped: processed.dropped,
|
|
271
|
+
seq: frame.seq,
|
|
272
|
+
sent,
|
|
273
|
+
settleMs: caps.animation ? settleTimeOf(diffed) : 0,
|
|
274
|
+
}
|
|
235
275
|
}
|
|
236
276
|
|
|
237
277
|
/**
|
|
@@ -240,15 +280,72 @@ class SceneRenderer {
|
|
|
240
280
|
* Returns false when nothing is retained.
|
|
241
281
|
*/
|
|
242
282
|
public replayApp(appId: string, view: "main" | "dashboard"): boolean {
|
|
243
|
-
const frame = this.store.buildReplayFrame(appId, view)
|
|
283
|
+
const frame = this.store.buildReplayFrame(appId, view, ++this.seqCounter)
|
|
244
284
|
if (!frame) return false
|
|
245
285
|
this.sendFrame(frame)
|
|
246
286
|
return true
|
|
247
287
|
}
|
|
248
288
|
|
|
249
|
-
/** Forget an app's retained scenes (app stop). */
|
|
289
|
+
/** Forget an app's retained scenes (app stop). Frames still in flight will never show. */
|
|
250
290
|
public clearApp(appId: string): void {
|
|
251
291
|
this.store.clear(appId)
|
|
292
|
+
this.settlePending((p) => p.appId === appId, {presented: false, superseded: true, reason: "cleared"})
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ===========================================================================
|
|
296
|
+
// Presentation — native reports when the device has a frame
|
|
297
|
+
// ===========================================================================
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Resolves once native reports frame `seq` on the device, or a later frame
|
|
301
|
+
* that folded it in. Never rejects; `timeoutMs` bounds a frame the device
|
|
302
|
+
* never acknowledges (link stalls, page rebuilding).
|
|
303
|
+
*/
|
|
304
|
+
public whenPresented(seq: number, timeoutMs: number, appId: string): Promise<ScenePresentation> {
|
|
305
|
+
this.ensurePresentedListener()
|
|
306
|
+
return new Promise((resolve) => {
|
|
307
|
+
const timer = BgTimer.setTimeout(() => {
|
|
308
|
+
if (this.pending.delete(seq)) resolve({presented: false, superseded: false, reason: "no presentation report"})
|
|
309
|
+
}, timeoutMs)
|
|
310
|
+
this.pending.set(seq, {
|
|
311
|
+
appId,
|
|
312
|
+
resolve: (p) => {
|
|
313
|
+
BgTimer.clearTimeout(timer)
|
|
314
|
+
resolve(p)
|
|
315
|
+
},
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private ensurePresentedListener(): void {
|
|
321
|
+
if (this.presentedSub) return
|
|
322
|
+
this.presentedSub = BluetoothSdk.addListener("scene_frame_presented", (event) => {
|
|
323
|
+
this.onFramePresented(event.seq, event.appId, event.presented, event.reason)
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** A report for `seq` settles it and every earlier frame (folded in by the native pacer). */
|
|
328
|
+
private onFramePresented(seq: number, appId: string, presented: boolean, reason?: string): void {
|
|
329
|
+
this.settlePending(
|
|
330
|
+
(p, s) => s <= seq,
|
|
331
|
+
(p, s) =>
|
|
332
|
+
s === seq
|
|
333
|
+
? {presented, superseded: false, reason}
|
|
334
|
+
: p.appId === appId
|
|
335
|
+
? {presented, superseded: true, reason}
|
|
336
|
+
: {presented: false, superseded: true, reason: `superseded by ${appId}`},
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
private settlePending(
|
|
341
|
+
match: (p: {appId: string}, seq: number) => boolean,
|
|
342
|
+
outcome: ScenePresentation | ((p: {appId: string}, seq: number) => ScenePresentation),
|
|
343
|
+
): void {
|
|
344
|
+
for (const [seq, p] of [...this.pending]) {
|
|
345
|
+
if (!match(p, seq)) continue
|
|
346
|
+
this.pending.delete(seq)
|
|
347
|
+
p.resolve(typeof outcome === "function" ? outcome(p, seq) : outcome)
|
|
348
|
+
}
|
|
252
349
|
}
|
|
253
350
|
|
|
254
351
|
/**
|
|
@@ -271,13 +368,16 @@ class SceneRenderer {
|
|
|
271
368
|
*/
|
|
272
369
|
public markViewStale(view: "main" | "dashboard"): void {
|
|
273
370
|
this.store.markViewStale(view)
|
|
371
|
+
// Whatever was in flight for the view got wiped with it.
|
|
372
|
+
this.settlePending(() => true, {presented: false, superseded: true, reason: "cleared"})
|
|
274
373
|
}
|
|
275
374
|
|
|
276
|
-
|
|
375
|
+
/** Returns false when the frame stayed phone-side (no presentation report will follow). */
|
|
376
|
+
private sendFrame(frame: SceneFrame): boolean {
|
|
277
377
|
// Single native view: only the visible surface's frames go out. Suppressed
|
|
278
378
|
// main frames stay committed; setDashboardActive(false) replays them.
|
|
279
379
|
const dashboardActive = useDisplayStore.getState().dashboardActive
|
|
280
|
-
if (dashboardActive !== (frame.view === "dashboard")) return
|
|
380
|
+
if (dashboardActive !== (frame.view === "dashboard")) return false
|
|
281
381
|
try {
|
|
282
382
|
// Frames carry the canvas they were laid out against; only a frame from
|
|
283
383
|
// a path that predates the tag falls back to the current capabilities.
|
|
@@ -302,8 +402,10 @@ class SceneRenderer {
|
|
|
302
402
|
elements: frame.elements,
|
|
303
403
|
},
|
|
304
404
|
})
|
|
405
|
+
return true
|
|
305
406
|
} catch (err) {
|
|
306
407
|
console.error(`${LOG_TAG}: native scene send failed:`, err)
|
|
408
|
+
return false
|
|
307
409
|
}
|
|
308
410
|
}
|
|
309
411
|
|
|
@@ -313,6 +415,10 @@ class SceneRenderer {
|
|
|
313
415
|
|
|
314
416
|
public _resetForTest(): void {
|
|
315
417
|
this.store.clearAll()
|
|
418
|
+
this.settlePending(() => true, {presented: false, superseded: false, reason: "reset"})
|
|
419
|
+
this.presentedSub?.remove()
|
|
420
|
+
this.presentedSub = null
|
|
421
|
+
this.seqCounter = 0
|
|
316
422
|
}
|
|
317
423
|
|
|
318
424
|
public _lastFrameForTest(appId: string, view: string): FrameElement[] {
|
|
Binary file
|
|
Binary file
|