@fcannizzaro/streamdeck-react 0.1.10 → 0.1.12

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.
Files changed (76) hide show
  1. package/LICENSE +190 -21
  2. package/README.md +2 -0
  3. package/dist/action.d.ts +2 -2
  4. package/dist/action.js +1 -2
  5. package/dist/adapter/index.d.ts +2 -0
  6. package/dist/adapter/physical-device.d.ts +2 -0
  7. package/dist/adapter/physical-device.js +153 -0
  8. package/dist/adapter/types.d.ts +127 -0
  9. package/dist/bundler-shared.d.ts +11 -0
  10. package/dist/bundler-shared.js +11 -0
  11. package/dist/context/event-bus.d.ts +1 -1
  12. package/dist/context/event-bus.js +1 -1
  13. package/dist/context/touchstrip-context.d.ts +2 -0
  14. package/dist/context/touchstrip-context.js +5 -0
  15. package/dist/devtools/bridge.d.ts +35 -7
  16. package/dist/devtools/bridge.js +152 -46
  17. package/dist/devtools/highlight.d.ts +5 -0
  18. package/dist/devtools/highlight.js +107 -57
  19. package/dist/devtools/index.js +6 -0
  20. package/dist/devtools/observers/lifecycle.d.ts +4 -4
  21. package/dist/devtools/server.d.ts +6 -1
  22. package/dist/devtools/server.js +6 -1
  23. package/dist/devtools/types.d.ts +50 -6
  24. package/dist/font-inline.d.ts +5 -1
  25. package/dist/font-inline.js +8 -3
  26. package/dist/hooks/animation.d.ts +154 -0
  27. package/dist/hooks/animation.js +381 -0
  28. package/dist/hooks/events.js +2 -6
  29. package/dist/hooks/sdk.js +11 -11
  30. package/dist/hooks/touchstrip.d.ts +6 -0
  31. package/dist/hooks/touchstrip.js +37 -0
  32. package/dist/hooks/utility.js +3 -2
  33. package/dist/index.d.ts +9 -2
  34. package/dist/index.js +4 -2
  35. package/dist/manifest-codegen.d.ts +38 -0
  36. package/dist/manifest-codegen.js +110 -0
  37. package/dist/plugin.js +86 -106
  38. package/dist/reconciler/host-config.js +19 -1
  39. package/dist/reconciler/vnode.d.ts +26 -2
  40. package/dist/reconciler/vnode.js +40 -10
  41. package/dist/render/buffer-pool.d.ts +19 -0
  42. package/dist/render/buffer-pool.js +51 -0
  43. package/dist/render/cache.d.ts +29 -0
  44. package/dist/render/cache.js +137 -5
  45. package/dist/render/image-cache.d.ts +54 -0
  46. package/dist/render/image-cache.js +144 -0
  47. package/dist/render/metrics.d.ts +57 -0
  48. package/dist/render/metrics.js +98 -0
  49. package/dist/render/pipeline.d.ts +36 -1
  50. package/dist/render/pipeline.js +304 -34
  51. package/dist/render/png.d.ts +1 -1
  52. package/dist/render/png.js +26 -11
  53. package/dist/render/render-pool.d.ts +24 -0
  54. package/dist/render/render-pool.js +130 -0
  55. package/dist/render/svg.d.ts +7 -0
  56. package/dist/render/svg.js +139 -0
  57. package/dist/render/worker.d.ts +1 -0
  58. package/dist/rollup.d.ts +23 -9
  59. package/dist/rollup.js +24 -9
  60. package/dist/roots/registry.d.ts +9 -11
  61. package/dist/roots/registry.js +39 -42
  62. package/dist/roots/root.d.ts +9 -6
  63. package/dist/roots/root.js +52 -29
  64. package/dist/roots/settings-equality.d.ts +5 -0
  65. package/dist/roots/settings-equality.js +24 -0
  66. package/dist/roots/{touchbar-root.d.ts → touchstrip-root.d.ts} +30 -8
  67. package/dist/roots/touchstrip-root.js +263 -0
  68. package/dist/types.d.ts +73 -23
  69. package/dist/vite.d.ts +22 -8
  70. package/dist/vite.js +24 -8
  71. package/package.json +7 -4
  72. package/dist/context/touchbar-context.d.ts +0 -2
  73. package/dist/context/touchbar-context.js +0 -5
  74. package/dist/hooks/touchbar.d.ts +0 -6
  75. package/dist/hooks/touchbar.js +0 -37
  76. package/dist/roots/touchbar-root.js +0 -175
@@ -1,8 +1,8 @@
1
1
  import { ReactRoot } from '../roots/root';
2
- import { TouchBarRoot } from '../roots/touchbar-root';
2
+ import { TouchStripRoot } from '../roots/touchstrip-root';
3
3
  import { CanvasInfo, DeviceInfo } from '../types';
4
4
  import { VContainer } from '../reconciler/vnode';
5
- import { RenderConfig } from '../render/pipeline';
5
+ import { RenderConfig, RenderProfile } from '../render/pipeline';
6
6
  import { RegistryObserver } from './observers/lifecycle';
7
7
  import { DevtoolsServer } from './server';
8
8
  import { SnapshotMessage } from './types';
@@ -14,12 +14,14 @@ export declare class DevtoolsBridge implements RegistryObserver {
14
14
  private networkRing;
15
15
  private eventRing;
16
16
  private actions;
17
- private touchBars;
17
+ private touchStrips;
18
18
  private lastRenderSent;
19
19
  private pendingTrailing;
20
20
  private eventBusOwners;
21
21
  private highlightedActionId;
22
22
  private highlightedNodeId;
23
+ private _lastProfile;
24
+ private _metricsTimer;
23
25
  constructor(server: DevtoolsServer, devtoolsName: string, renderConfig: RenderConfig);
24
26
  onRootCreated(actionId: string, root: ReactRoot, meta: {
25
27
  actionUuid: string;
@@ -32,22 +34,48 @@ export declare class DevtoolsBridge implements RegistryObserver {
32
34
  };
33
35
  }): void;
34
36
  onRootDestroyed(actionId: string): void;
35
- onTouchBarCreated(deviceId: string, root: TouchBarRoot, deviceInfo: DeviceInfo): void;
36
- onTouchBarColumnChanged(deviceId: string, columns: number[], actionMap: Map<number, string>): void;
37
- onTouchBarDestroyed(deviceId: string): void;
37
+ onTouchStripCreated(deviceId: string, root: TouchStripRoot, deviceInfo: DeviceInfo): void;
38
+ onTouchStripColumnChanged(deviceId: string, columns: number[], actionMap: Map<number, string>): void;
39
+ onTouchStripDestroyed(deviceId: string): void;
38
40
  onDispatch(actionId: string, event: string, payload: unknown): void;
39
41
  onEventBusEmit(bus: object, event: string, payload: unknown): void;
40
42
  onConsole(level: string, args: unknown[], stack: string | undefined): void;
43
+ /**
44
+ * Stash the render profile for the next onRender call.
45
+ * In the pipeline, onProfile fires synchronously before onRender,
46
+ * so the stash is always consumed immediately.
47
+ */
48
+ onProfile(profile: RenderProfile): void;
49
+ private static readonly METRICS_INTERVAL_MS;
50
+ /** Start periodic metrics emission to connected devtools clients. */
51
+ startMetricsEmitter(): void;
52
+ /** Stop periodic metrics emission. */
53
+ stopMetricsEmitter(): void;
41
54
  onFetchRequest(id: string, method: string, url: string, headers: Record<string, string>, body?: string): void;
42
55
  onFetchResponse(id: string, status: number, statusText: string, headers: Record<string, string>, body: string | undefined, durationMs: number): void;
43
56
  onFetchError(id: string, error: string, durationMs: number): void;
44
57
  onRender(container: VContainer, dataUri: string): void;
45
58
  private throttledRender;
46
59
  private emitRender;
47
- private emitTouchBarRender;
60
+ /** Convert internal RenderProfile to wire-protocol ProfileData. */
61
+ private toProfileData;
62
+ private emitTouchStripRender;
63
+ private static readonly TB_PREFIX;
48
64
  private handleHighlight;
65
+ /**
66
+ * Restore a highlighted action or TouchStrip to its normal state.
67
+ * Un-suppresses hardware pushes and restores the original image(s).
68
+ */
69
+ private restoreHighlight;
49
70
  private applyHighlight;
71
+ private applyTouchStripHighlight;
50
72
  /** Broadcast highlight render image (or null to clear) to devtools UI. */
51
73
  private broadcastHighlightRender;
74
+ /**
75
+ * Clear highlight URIs for the given actionId.
76
+ * For TouchStrip IDs, clears all per-segment keys (touchStrip:*:seg:N).
77
+ * For regular actions, clears the single actionId key.
78
+ */
79
+ private broadcastHighlightClear;
52
80
  buildSnapshot(): SnapshotMessage;
53
81
  }
@@ -1,6 +1,7 @@
1
+ import { metrics } from "../render/metrics.js";
1
2
  import { serializeValue } from "./serialization/value.js";
2
3
  import { serializeVNode } from "./serialization/vnode.js";
3
- import { renderWithHighlight } from "./highlight.js";
4
+ import { renderTouchStripWithHighlight, renderWithHighlight } from "./highlight.js";
4
5
  //#region src/devtools/bridge.ts
5
6
  var RingBuffer = class {
6
7
  items;
@@ -27,7 +28,7 @@ var NETWORK_RING_SIZE = 100;
27
28
  var EVENT_RING_SIZE = 200;
28
29
  var consoleIdCounter = 0;
29
30
  var eventIdCounter = 0;
30
- var DevtoolsBridge = class {
31
+ var DevtoolsBridge = class DevtoolsBridge {
31
32
  server;
32
33
  devtoolsName;
33
34
  renderConfig;
@@ -35,12 +36,14 @@ var DevtoolsBridge = class {
35
36
  networkRing = new RingBuffer(NETWORK_RING_SIZE);
36
37
  eventRing = new RingBuffer(EVENT_RING_SIZE);
37
38
  actions = /* @__PURE__ */ new Map();
38
- touchBars = /* @__PURE__ */ new Map();
39
+ touchStrips = /* @__PURE__ */ new Map();
39
40
  lastRenderSent = /* @__PURE__ */ new Map();
40
41
  pendingTrailing = /* @__PURE__ */ new Map();
41
42
  eventBusOwners = /* @__PURE__ */ new Map();
42
43
  highlightedActionId = null;
43
44
  highlightedNodeId = null;
45
+ _lastProfile = null;
46
+ _metricsTimer = null;
44
47
  constructor(server, devtoolsName, renderConfig) {
45
48
  this.server = server;
46
49
  this.devtoolsName = devtoolsName;
@@ -131,25 +134,25 @@ var DevtoolsBridge = class {
131
134
  this.pendingTrailing.delete(actionId);
132
135
  }
133
136
  }
134
- onTouchBarCreated(deviceId, root, deviceInfo) {
135
- this.touchBars.set(deviceId, {
137
+ onTouchStripCreated(deviceId, root, deviceInfo) {
138
+ this.touchStrips.set(deviceId, {
136
139
  root,
137
140
  deviceInfo,
138
141
  columns: /* @__PURE__ */ new Map()
139
142
  });
140
143
  this.eventBusOwners.set(root.eventBus, {
141
- actionId: `touchbar:${deviceId}`,
144
+ actionId: `touchStrip:${deviceId}`,
142
145
  uuid: ""
143
146
  });
144
147
  }
145
- onTouchBarColumnChanged(deviceId, columns, actionMap) {
146
- const tb = this.touchBars.get(deviceId);
148
+ onTouchStripColumnChanged(deviceId, columns, actionMap) {
149
+ const tb = this.touchStrips.get(deviceId);
147
150
  if (tb) tb.columns = new Map(actionMap);
148
151
  }
149
- onTouchBarDestroyed(deviceId) {
150
- const tb = this.touchBars.get(deviceId);
152
+ onTouchStripDestroyed(deviceId) {
153
+ const tb = this.touchStrips.get(deviceId);
151
154
  if (tb) this.eventBusOwners.delete(tb.root.eventBus);
152
- this.touchBars.delete(deviceId);
155
+ this.touchStrips.delete(deviceId);
153
156
  }
154
157
  onDispatch(actionId, event, payload) {
155
158
  if (!this.server.hasClients()) return;
@@ -207,6 +210,37 @@ var DevtoolsBridge = class {
207
210
  this.consoleRing.push(msg);
208
211
  this.server.broadcast(msg);
209
212
  }
213
+ /**
214
+ * Stash the render profile for the next onRender call.
215
+ * In the pipeline, onProfile fires synchronously before onRender,
216
+ * so the stash is always consumed immediately.
217
+ */
218
+ onProfile(profile) {
219
+ this._lastProfile = profile;
220
+ }
221
+ static METRICS_INTERVAL_MS = 3e3;
222
+ /** Start periodic metrics emission to connected devtools clients. */
223
+ startMetricsEmitter() {
224
+ if (this._metricsTimer) return;
225
+ this._metricsTimer = setInterval(() => {
226
+ if (!this.server.hasClients()) return;
227
+ const snapshot = metrics.snapshot();
228
+ const msg = {
229
+ type: "metrics",
230
+ ts: Date.now(),
231
+ metrics: snapshot
232
+ };
233
+ this.server.broadcast(msg);
234
+ }, DevtoolsBridge.METRICS_INTERVAL_MS);
235
+ if (typeof this._metricsTimer === "object" && "unref" in this._metricsTimer) this._metricsTimer.unref();
236
+ }
237
+ /** Stop periodic metrics emission. */
238
+ stopMetricsEmitter() {
239
+ if (this._metricsTimer) {
240
+ clearInterval(this._metricsTimer);
241
+ this._metricsTimer = null;
242
+ }
243
+ }
210
244
  onFetchRequest(id, method, url, headers, body) {
211
245
  const msg = {
212
246
  type: "network:request",
@@ -247,6 +281,8 @@ var DevtoolsBridge = class {
247
281
  }
248
282
  onRender(container, dataUri) {
249
283
  if (!this.server.hasClients()) return;
284
+ const profile = this._lastProfile;
285
+ this._lastProfile = null;
250
286
  let actionId = null;
251
287
  let meta = null;
252
288
  for (const [id, m] of this.actions) if (m.root.vcontainer === container) {
@@ -256,30 +292,32 @@ var DevtoolsBridge = class {
256
292
  }
257
293
  if (actionId && meta) {
258
294
  meta.root.lastDataUri = dataUri;
259
- this.throttledRender(actionId, container, dataUri, meta);
295
+ this.throttledRender(actionId, container, dataUri, meta, profile);
260
296
  if (this.highlightedActionId === actionId && this.highlightedNodeId !== null) this.applyHighlight(actionId, this.highlightedNodeId, meta).catch(() => {});
261
297
  return;
262
298
  }
263
- for (const [deviceId, tb] of this.touchBars) if (tb.root.vcontainer === container) {
264
- this.emitTouchBarRender(deviceId, tb);
299
+ for (const [deviceId, tb] of this.touchStrips) if (tb.root.vcontainer === container) {
300
+ this.emitTouchStripRender(deviceId, tb, profile);
301
+ const tbActionId = `${DevtoolsBridge.TB_PREFIX}${deviceId}`;
302
+ if (this.highlightedActionId === tbActionId && this.highlightedNodeId !== null) this.applyTouchStripHighlight(tbActionId, deviceId, this.highlightedNodeId, tb).catch(() => {});
265
303
  return;
266
304
  }
267
305
  }
268
- throttledRender(actionId, container, dataUri, meta) {
306
+ throttledRender(actionId, container, dataUri, meta, profile) {
269
307
  const now = Date.now();
270
308
  const elapsed = now - (this.lastRenderSent.get(actionId) ?? 0);
271
309
  const pending = this.pendingTrailing.get(actionId);
272
310
  if (pending) clearTimeout(pending);
273
311
  if (elapsed >= RENDER_THROTTLE_MS) {
274
- this.emitRender(actionId, container, dataUri, meta, now);
312
+ this.emitRender(actionId, container, dataUri, meta, now, profile);
275
313
  this.lastRenderSent.set(actionId, now);
276
314
  } else this.pendingTrailing.set(actionId, setTimeout(() => {
277
- this.emitRender(actionId, container, dataUri, meta, Date.now());
315
+ this.emitRender(actionId, container, dataUri, meta, Date.now(), profile);
278
316
  this.lastRenderSent.set(actionId, Date.now());
279
317
  this.pendingTrailing.delete(actionId);
280
318
  }, RENDER_THROTTLE_MS - elapsed));
281
319
  }
282
- emitRender(actionId, container, dataUri, meta, ts) {
320
+ emitRender(actionId, container, dataUri, meta, ts, profile) {
283
321
  const tree = serializeVNode(container);
284
322
  const msg = {
285
323
  type: "render",
@@ -293,11 +331,26 @@ var DevtoolsBridge = class {
293
331
  },
294
332
  tree,
295
333
  dataUri,
296
- renderMs: 0
334
+ renderMs: profile?.totalMs ?? 0,
335
+ ...profile ? { profile: this.toProfileData(profile) } : {}
297
336
  };
298
337
  this.server.broadcast(msg);
299
338
  }
300
- emitTouchBarRender(deviceId, tb) {
339
+ /** Convert internal RenderProfile to wire-protocol ProfileData. */
340
+ toProfileData(profile) {
341
+ return {
342
+ vnodeConversionMs: profile.vnodeConversionMs,
343
+ takumiRenderMs: profile.takumiRenderMs,
344
+ hashMs: profile.hashMs,
345
+ base64Ms: profile.base64Ms,
346
+ totalMs: profile.totalMs,
347
+ skipped: profile.skipped,
348
+ cacheHit: profile.cacheHit,
349
+ treeDepth: profile.treeDepth,
350
+ nodeCount: profile.nodeCount
351
+ };
352
+ }
353
+ emitTouchStripRender(deviceId, tb, profile) {
301
354
  const tree = serializeVNode(tb.root.vcontainer);
302
355
  const segments = [];
303
356
  for (const [column, actionId] of tb.columns) {
@@ -309,7 +362,7 @@ var DevtoolsBridge = class {
309
362
  });
310
363
  }
311
364
  const msg = {
312
- type: "render:touchbar",
365
+ type: "render:touchStrip",
313
366
  ts: Date.now(),
314
367
  deviceId,
315
368
  canvas: {
@@ -318,46 +371,72 @@ var DevtoolsBridge = class {
318
371
  },
319
372
  tree,
320
373
  segments,
321
- renderMs: 0
374
+ renderMs: profile?.totalMs ?? 0,
375
+ ...profile ? { profile: this.toProfileData(profile) } : {}
322
376
  };
323
377
  this.server.broadcast(msg);
324
378
  }
379
+ static TB_PREFIX = "touchStrip:";
325
380
  async handleHighlight(actionId, nodeId) {
326
381
  try {
327
382
  const prevId = this.highlightedActionId;
328
383
  this.highlightedActionId = actionId;
329
384
  this.highlightedNodeId = nodeId;
330
385
  if (prevId && prevId !== actionId) {
331
- const prevMeta = this.actions.get(prevId);
332
- if (prevMeta) {
333
- prevMeta.root.suppressHardwarePush = false;
334
- if (prevMeta.root.lastDataUri) await prevMeta.root.pushImage(prevMeta.root.lastDataUri).catch(() => {});
335
- }
336
- this.broadcastHighlightRender(prevId, null);
386
+ await this.restoreHighlight(prevId);
387
+ this.broadcastHighlightClear(prevId);
337
388
  }
338
389
  if (!actionId || nodeId === null) {
339
390
  if (actionId && prevId === actionId) {
340
- const meta = this.actions.get(actionId);
341
- if (meta) {
342
- meta.root.suppressHardwarePush = false;
343
- if (meta.root.lastDataUri) await meta.root.pushImage(meta.root.lastDataUri).catch(() => {});
344
- }
345
- this.broadcastHighlightRender(actionId, null);
391
+ await this.restoreHighlight(actionId);
392
+ this.broadcastHighlightClear(actionId);
346
393
  }
347
394
  this.highlightedActionId = null;
348
395
  this.highlightedNodeId = null;
349
396
  return;
350
397
  }
351
- const meta = this.actions.get(actionId);
352
- if (!meta) {
353
- this.highlightedActionId = null;
354
- this.highlightedNodeId = null;
355
- return;
398
+ if (actionId.startsWith(DevtoolsBridge.TB_PREFIX)) {
399
+ const deviceId = actionId.slice(DevtoolsBridge.TB_PREFIX.length);
400
+ const tb = this.touchStrips.get(deviceId);
401
+ if (!tb) {
402
+ this.highlightedActionId = null;
403
+ this.highlightedNodeId = null;
404
+ return;
405
+ }
406
+ tb.root.suppressHardwarePush = true;
407
+ await this.applyTouchStripHighlight(actionId, deviceId, nodeId, tb);
408
+ } else {
409
+ const meta = this.actions.get(actionId);
410
+ if (!meta) {
411
+ this.highlightedActionId = null;
412
+ this.highlightedNodeId = null;
413
+ return;
414
+ }
415
+ meta.root.suppressHardwarePush = true;
416
+ await this.applyHighlight(actionId, nodeId, meta);
356
417
  }
357
- meta.root.suppressHardwarePush = true;
358
- await this.applyHighlight(actionId, nodeId, meta);
359
418
  } catch {}
360
419
  }
420
+ /**
421
+ * Restore a highlighted action or TouchStrip to its normal state.
422
+ * Un-suppresses hardware pushes and restores the original image(s).
423
+ */
424
+ async restoreHighlight(id) {
425
+ if (id.startsWith(DevtoolsBridge.TB_PREFIX)) {
426
+ const deviceId = id.slice(DevtoolsBridge.TB_PREFIX.length);
427
+ const tb = this.touchStrips.get(deviceId);
428
+ if (tb) {
429
+ tb.root.suppressHardwarePush = false;
430
+ await tb.root.pushSegmentImages(tb.root.lastSegmentUris);
431
+ }
432
+ } else {
433
+ const prevMeta = this.actions.get(id);
434
+ if (prevMeta) {
435
+ prevMeta.root.suppressHardwarePush = false;
436
+ if (prevMeta.root.lastDataUri) await prevMeta.root.pushImage(prevMeta.root.lastDataUri).catch(() => {});
437
+ }
438
+ }
439
+ }
361
440
  async applyHighlight(actionId, nodeId, meta) {
362
441
  try {
363
442
  const uri = await renderWithHighlight(meta.root.vcontainer, meta.canvas.width, meta.canvas.height, this.renderConfig, nodeId);
@@ -367,6 +446,20 @@ var DevtoolsBridge = class {
367
446
  }
368
447
  } catch {}
369
448
  }
449
+ async applyTouchStripHighlight(actionId, deviceId, nodeId, tb) {
450
+ try {
451
+ const columns = tb.root.columnNumbers;
452
+ if (columns.length === 0) return;
453
+ const segmentWidth = 200;
454
+ const segmentHeight = 100;
455
+ const fullWidth = (Math.max(...columns) + 1) * segmentWidth;
456
+ const result = await renderTouchStripWithHighlight(tb.root.vcontainer, fullWidth, segmentHeight, columns, segmentWidth, this.renderConfig, nodeId);
457
+ if (result && this.highlightedActionId === actionId && this.highlightedNodeId === nodeId) {
458
+ await tb.root.pushSegmentImages(result.segmentUris);
459
+ for (const [col, uri] of result.segmentUris) this.broadcastHighlightRender(`${actionId}:seg:${col}`, uri);
460
+ }
461
+ } catch {}
462
+ }
370
463
  /** Broadcast highlight render image (or null to clear) to devtools UI. */
371
464
  broadcastHighlightRender(actionId, dataUri) {
372
465
  const msg = {
@@ -377,6 +470,18 @@ var DevtoolsBridge = class {
377
470
  };
378
471
  this.server.broadcast(msg);
379
472
  }
473
+ /**
474
+ * Clear highlight URIs for the given actionId.
475
+ * For TouchStrip IDs, clears all per-segment keys (touchStrip:*:seg:N).
476
+ * For regular actions, clears the single actionId key.
477
+ */
478
+ broadcastHighlightClear(id) {
479
+ if (id.startsWith(DevtoolsBridge.TB_PREFIX)) {
480
+ const deviceId = id.slice(DevtoolsBridge.TB_PREFIX.length);
481
+ const tb = this.touchStrips.get(deviceId);
482
+ if (tb) for (const col of tb.root.columnNumbers) this.broadcastHighlightRender(`${id}:seg:${col}`, null);
483
+ } else this.broadcastHighlightRender(id, null);
484
+ }
380
485
  buildSnapshot() {
381
486
  const actions = [];
382
487
  for (const [actionId, meta] of this.actions) {
@@ -402,8 +507,8 @@ var DevtoolsBridge = class {
402
507
  dataUri: meta.root.lastDataUri
403
508
  });
404
509
  }
405
- const touchBars = [];
406
- for (const [deviceId, tb] of this.touchBars) {
510
+ const touchStrips = [];
511
+ for (const [deviceId, tb] of this.touchStrips) {
407
512
  let tree = null;
408
513
  try {
409
514
  tree = serializeVNode(tb.root.vcontainer);
@@ -414,7 +519,7 @@ var DevtoolsBridge = class {
414
519
  actionId,
415
520
  dataUri: tb.root.lastSegmentUris.get(column) ?? null
416
521
  });
417
- touchBars.push({
522
+ touchStrips.push({
418
523
  deviceId,
419
524
  deviceName: tb.deviceInfo.name,
420
525
  canvas: {
@@ -429,10 +534,11 @@ var DevtoolsBridge = class {
429
534
  type: "snapshot",
430
535
  ts: Date.now(),
431
536
  actions,
432
- touchBars,
537
+ touchStrips,
433
538
  recentConsole: this.consoleRing.toArray(),
434
539
  recentNetwork: this.networkRing.toArray(),
435
- recentEvents: this.eventRing.toArray()
540
+ recentEvents: this.eventRing.toArray(),
541
+ metrics: metrics.snapshot()
436
542
  };
437
543
  }
438
544
  };
@@ -1,3 +1,8 @@
1
1
  import { VContainer } from '../reconciler/vnode';
2
2
  import { RenderConfig } from '../render/pipeline';
3
3
  export declare function renderWithHighlight(container: VContainer, width: number, height: number, config: RenderConfig, targetNid: number): Promise<string | null>;
4
+ export interface TouchStripHighlightResult {
5
+ /** Per-column data URIs for ALL segments. */
6
+ segmentUris: Map<number, string>;
7
+ }
8
+ export declare function renderTouchStripWithHighlight(container: VContainer, fullWidth: number, segmentHeight: number, columns: number[], segmentWidth: number, config: RenderConfig, targetNid: number): Promise<TouchStripHighlightResult | null>;
@@ -1,92 +1,142 @@
1
- import { vnodeToElement } from "../reconciler/vnode.js";
2
- import { bufferToDataUri } from "../render/pipeline.js";
3
- import { createElement } from "react";
4
- import { fromJsx } from "@takumi-rs/helpers/jsx";
1
+ import { bufferToDataUri, buildTakumiRoot } from "../render/pipeline.js";
5
2
  //#region src/devtools/highlight.ts
6
- var HIGHLIGHT_BORDER_COLOR = "rgba(111, 168, 220, 0.85)";
7
- var HIGHLIGHT_BORDER_WIDTH = 2;
8
3
  var HIGHLIGHT_BG = "rgba(111, 168, 220, 0.66)";
9
- var HIGHLIGHT_BOX_SHADOW = `inset 0 0 0 ${HIGHLIGHT_BORDER_WIDTH}px ${HIGHLIGHT_BORDER_COLOR}`;
4
+ var HIGHLIGHT_BOX_SHADOW = `inset 0 0 0 2px rgba(111, 168, 220, 0.85)`;
10
5
  async function renderWithHighlight(container, width, height, config, targetNid) {
11
6
  if (container.children.length === 0) return null;
12
7
  const effectiveNid = resolveTargetNid(container, targetNid);
13
- const rootChildren = container.children.map(vnodeToElement);
14
- const { node, stylesheets } = await fromJsx(createElement("div", { style: {
15
- display: "flex",
16
- position: "relative",
17
- width: "100%",
18
- height: "100%"
19
- } }, ...rootChildren));
20
- const renderOpts = {
8
+ const rootNode = buildTakumiRoot(container);
9
+ const counter = { value: 0 };
10
+ counter.value++;
11
+ const target = findTargetTakumiNode(container.children, rootNode.children ?? [], effectiveNid, counter);
12
+ if (!target) return null;
13
+ injectHighlightOverlay(target);
14
+ return bufferToDataUri(await config.renderer.render(rootNode, {
21
15
  width,
22
16
  height,
23
- stylesheets,
17
+ format: config.imageFormat,
24
18
  devicePixelRatio: config.devicePixelRatio
25
- };
26
- const measured = await config.renderer.measure(node, renderOpts);
19
+ }), config.imageFormat);
20
+ }
21
+ var TOUCHSTRIP_HIGHLIGHT_FORMAT = "png";
22
+ async function renderTouchStripWithHighlight(container, fullWidth, segmentHeight, columns, segmentWidth, config, targetNid) {
23
+ if (container.children.length === 0) return null;
24
+ const effectiveNid = resolveTargetNid(container, targetNid);
25
+ const rootNode = buildTakumiRoot(container);
27
26
  const counter = { value: 0 };
28
27
  counter.value++;
29
- const bounds = findTargetBounds(container.children, measured.children, effectiveNid, counter, measured.transform[4], measured.transform[5]);
30
- if (!bounds) return null;
31
- const overlay = createElement("div", { style: {
32
- position: "absolute",
33
- top: bounds.y,
34
- left: bounds.x,
35
- width: bounds.width,
36
- height: bounds.height,
37
- backgroundColor: HIGHLIGHT_BG,
38
- boxShadow: HIGHLIGHT_BOX_SHADOW
39
- } });
40
- const { node: hlNode, stylesheets: hlStylesheets } = await fromJsx(createElement("div", { style: {
41
- display: "flex",
42
- position: "relative",
43
- width: "100%",
44
- height: "100%"
45
- } }, ...rootChildren, overlay));
46
- return bufferToDataUri(await config.renderer.render(hlNode, {
47
- ...renderOpts,
48
- format: config.imageFormat,
49
- stylesheets: hlStylesheets
50
- }), config.imageFormat);
28
+ const target = findTargetTakumiNode(container.children, rootNode.children ?? [], effectiveNid, counter);
29
+ if (!target) return null;
30
+ injectHighlightOverlay(target);
31
+ const segmentUris = /* @__PURE__ */ new Map();
32
+ const segmentPromises = columns.map(async (column) => {
33
+ const clipNode = {
34
+ type: "container",
35
+ style: {
36
+ width: segmentWidth,
37
+ height: segmentHeight,
38
+ overflow: "hidden"
39
+ },
40
+ children: [{
41
+ type: "container",
42
+ style: {
43
+ display: "flex",
44
+ width: fullWidth,
45
+ height: segmentHeight,
46
+ marginLeft: -(column * segmentWidth)
47
+ },
48
+ children: rootNode.children
49
+ }]
50
+ };
51
+ const segBuffer = await config.renderer.render(clipNode, {
52
+ width: segmentWidth,
53
+ height: segmentHeight,
54
+ format: TOUCHSTRIP_HIGHLIGHT_FORMAT,
55
+ devicePixelRatio: config.devicePixelRatio
56
+ });
57
+ segmentUris.set(column, bufferToDataUri(segBuffer, TOUCHSTRIP_HIGHLIGHT_FORMAT));
58
+ });
59
+ await Promise.all(segmentPromises);
60
+ return { segmentUris };
51
61
  }
52
- function findTargetBounds(vnodes, measuredChildren, targetNid, counter, parentX, parentY) {
53
- let measuredIdx = 0;
62
+ function injectHighlightOverlay(target) {
63
+ const node = target.node;
64
+ const overlay = {
65
+ type: "container",
66
+ style: {
67
+ position: "absolute",
68
+ top: 0,
69
+ left: 0,
70
+ width: "100%",
71
+ height: "100%",
72
+ backgroundColor: HIGHLIGHT_BG,
73
+ boxShadow: HIGHLIGHT_BOX_SHADOW
74
+ }
75
+ };
76
+ if (node.type === "container") {
77
+ node.style = {
78
+ ...node.style ?? {},
79
+ position: "relative"
80
+ };
81
+ if (!node.children) node.children = [];
82
+ node.children.push(overlay);
83
+ } else {
84
+ const wrapper = {
85
+ type: "container",
86
+ style: { position: "relative" },
87
+ children: [target.node, overlay]
88
+ };
89
+ target.parentChildren[target.indexInParent] = wrapper;
90
+ }
91
+ }
92
+ function findTargetTakumiNode(vnodes, takumiNodes, targetNid, counter) {
93
+ let idx = 0;
54
94
  for (const vnode of vnodes) {
55
95
  const nid = counter.value++;
56
- if (vnode.type === "#text") continue;
57
- const measured = measuredChildren[measuredIdx++];
58
- if (!measured) continue;
59
- const absX = parentX + measured.transform[4];
60
- const absY = parentY + measured.transform[5];
96
+ const takumiNode = takumiNodes[idx];
97
+ idx++;
98
+ if (!takumiNode) continue;
61
99
  if (nid === targetNid) return {
62
- x: absX,
63
- y: absY,
64
- width: measured.width,
65
- height: measured.height
100
+ node: takumiNode,
101
+ parentChildren: takumiNodes,
102
+ indexInParent: idx - 1
66
103
  };
67
- const found = findTargetBounds(vnode.children, measured.children, targetNid, counter, absX, absY);
104
+ if (vnode.type === "#text" || vnode.type === "img") continue;
105
+ if (vnode.type === "svg") {
106
+ advanceCounterThroughSubtree(vnode.children, counter);
107
+ continue;
108
+ }
109
+ const childTakumiNodes = takumiNode.children ?? [];
110
+ const found = findTargetTakumiNode(vnode.children, childTakumiNodes, targetNid, counter);
68
111
  if (found) return found;
69
112
  }
70
113
  return null;
71
114
  }
115
+ function advanceCounterThroughSubtree(vnodes, counter) {
116
+ for (const node of vnodes) {
117
+ counter.value++;
118
+ advanceCounterThroughSubtree(node.children, counter);
119
+ }
120
+ }
72
121
  function resolveTargetNid(container, targetNid) {
73
122
  const counter = { value: 0 };
74
123
  counter.value++;
75
124
  for (const child of container.children) {
76
- const resolved = resolveInSubtree(child, targetNid, counter, 0);
125
+ const resolved = resolveInSubtree(child, targetNid, counter, 0, false);
77
126
  if (resolved !== null) return resolved;
78
127
  }
79
128
  return targetNid;
80
129
  }
81
- function resolveInSubtree(node, targetNid, counter, parentNid) {
130
+ function resolveInSubtree(node, targetNid, counter, parentNid, insideSvg) {
82
131
  const nid = counter.value++;
83
- if (nid === targetNid) return node.type === "#text" ? parentNid : nid;
132
+ if (nid === targetNid) return node.type === "#text" || insideSvg ? parentNid : nid;
84
133
  if (node.type === "#text") return null;
134
+ const childInsideSvg = insideSvg || node.type === "svg";
85
135
  for (const child of node.children) {
86
- const resolved = resolveInSubtree(child, targetNid, counter, nid);
136
+ const resolved = resolveInSubtree(child, targetNid, counter, nid, childInsideSvg);
87
137
  if (resolved !== null) return resolved;
88
138
  }
89
139
  return null;
90
140
  }
91
141
  //#endregion
92
- export { renderWithHighlight };
142
+ export { renderTouchStripWithHighlight, renderWithHighlight };
@@ -11,6 +11,9 @@ function startDevtoolsServer(config) {
11
11
  config.renderConfig.onRender = (container, dataUri) => {
12
12
  bridge.onRender(container, dataUri);
13
13
  };
14
+ config.renderConfig.onProfile = (profile) => {
15
+ bridge.onProfile(profile);
16
+ };
14
17
  EventBus.devtoolsObserver = (bus, event, payload) => {
15
18
  bridge.onEventBusEmit(bus, event, payload);
16
19
  };
@@ -23,12 +26,15 @@ function startDevtoolsServer(config) {
23
26
  onError: (id, error, dur) => bridge.onFetchError(id, error, dur)
24
27
  });
25
28
  server.start();
29
+ bridge.startMetricsEmitter();
26
30
  process.on("exit", () => {
31
+ bridge.stopMetricsEmitter();
27
32
  restoreConsole();
28
33
  restoreFetch();
29
34
  EventBus.devtoolsObserver = null;
30
35
  config.registry.observer = null;
31
36
  config.renderConfig.onRender = void 0;
37
+ config.renderConfig.onProfile = void 0;
32
38
  server.stop();
33
39
  });
34
40
  }