@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,9 @@
1
- import { createVContainer } from "../reconciler/vnode.js";
1
+ import { clearDirtyFlags, createVContainer } from "../reconciler/vnode.js";
2
2
  import { reconciler } from "../reconciler/renderer.js";
3
3
  import { renderToDataUri } from "../render/pipeline.js";
4
4
  import { EventBus } from "../context/event-bus.js";
5
5
  import { ActionContext, CanvasContext, DeviceContext, EventBusContext, GlobalSettingsContext, SettingsContext, StreamDeckContext } from "../context/providers.js";
6
+ import { partialHasChanges, shallowEqualSettings } from "./settings-equality.js";
6
7
  import { createElement } from "react";
7
8
  //#region src/roots/root.ts
8
9
  var DEFAULT_DIAL_LAYOUT = {
@@ -18,7 +19,7 @@ var DEFAULT_DIAL_LAYOUT = {
18
19
  ]
19
20
  }]
20
21
  };
21
- var ReactRoot = class {
22
+ var ReactRoot = class ReactRoot {
22
23
  eventBus = new EventBus();
23
24
  container;
24
25
  fiberRoot;
@@ -26,13 +27,16 @@ var ReactRoot = class {
26
27
  globalSettings;
27
28
  setSettingsFn;
28
29
  setGlobalSettingsFn;
29
- renderDebounceMs;
30
+ renderDebounceMs = 17;
30
31
  renderConfig;
31
32
  canvas;
32
33
  resolvedDialLayout;
33
- sdkAction;
34
- sdkInstance;
34
+ action;
35
+ adapter;
35
36
  disposed = false;
37
+ _renderCount = 0;
38
+ _lastRenderReport = 0;
39
+ static RENDER_WARN_THRESHOLD = 30;
36
40
  /** Last data URI successfully sent to hardware. Used by devtools snapshots. */
37
41
  lastDataUri = null;
38
42
  /**
@@ -45,16 +49,12 @@ var ReactRoot = class {
45
49
  /** Push an arbitrary data URI to the hardware. Used by devtools highlight overlay. */
46
50
  async pushImage(dataUri) {
47
51
  if (this.disposed) return;
48
- if (this.canvas.type === "key") {
49
- if ("setImage" in this.sdkAction) await this.sdkAction.setImage(dataUri);
50
- } else if (this.canvas.type === "dial") {
51
- if ("setFeedback" in this.sdkAction) await this.sdkAction.setFeedback({
52
- canvas: dataUri,
53
- title: ""
54
- });
55
- } else if (this.canvas.type === "touch") {
56
- if ("setFeedback" in this.sdkAction) await this.sdkAction.setFeedback({ canvas: dataUri });
57
- }
52
+ if (this.canvas.type === "key") await this.action.setImage(dataUri);
53
+ else if (this.canvas.type === "dial") await this.action.setFeedback({
54
+ canvas: dataUri,
55
+ title: ""
56
+ });
57
+ else if (this.canvas.type === "touch") await this.action.setFeedback({ canvas: dataUri });
58
58
  }
59
59
  /** Exposes the VContainer for devtools inspection. */
60
60
  get vcontainer() {
@@ -63,7 +63,7 @@ var ReactRoot = class {
63
63
  streamDeckValue;
64
64
  settingsValue;
65
65
  globalSettingsValue;
66
- constructor(component, actionInfo, deviceInfo, canvas, initialSettings, initialGlobalSettings, sdkAction, sdkInstance, renderConfig, renderDebounceMs, onSettingsChange, onGlobalSettingsChange, pluginWrapper, actionWrapper, dialLayout) {
66
+ constructor(component, actionInfo, deviceInfo, canvas, initialSettings, initialGlobalSettings, action, adapter, renderConfig, onSettingsChange, onGlobalSettingsChange, pluginWrapper, actionWrapper, dialLayout) {
67
67
  this.component = component;
68
68
  this.actionInfo = actionInfo;
69
69
  this.deviceInfo = deviceInfo;
@@ -72,38 +72,43 @@ var ReactRoot = class {
72
72
  this.canvas = canvas;
73
73
  this.settings = { ...initialSettings };
74
74
  this.globalSettings = { ...initialGlobalSettings };
75
- this.sdkAction = sdkAction;
76
- this.sdkInstance = sdkInstance;
75
+ this.action = action;
76
+ this.adapter = adapter;
77
77
  this.renderConfig = renderConfig;
78
- this.renderDebounceMs = renderDebounceMs;
79
78
  this.resolvedDialLayout = resolveDialLayout(dialLayout);
80
79
  this.setSettingsFn = (partial) => {
81
- this.settings = {
80
+ const hasChanges = partialHasChanges(this.settings, partial);
81
+ const nextSettings = hasChanges ? {
82
82
  ...this.settings,
83
83
  ...partial
84
- };
84
+ } : this.settings;
85
+ onSettingsChange(nextSettings);
86
+ if (!hasChanges) return;
87
+ this.settings = nextSettings;
85
88
  this.settingsValue = {
86
89
  settings: this.settings,
87
90
  setSettings: this.setSettingsFn
88
91
  };
89
- onSettingsChange(this.settings);
90
92
  this.scheduleRerender();
91
93
  };
92
94
  this.setGlobalSettingsFn = (partial) => {
93
- this.globalSettings = {
95
+ const hasChanges = partialHasChanges(this.globalSettings, partial);
96
+ const nextSettings = hasChanges ? {
94
97
  ...this.globalSettings,
95
98
  ...partial
96
- };
99
+ } : this.globalSettings;
100
+ onGlobalSettingsChange(nextSettings);
101
+ if (!hasChanges) return;
102
+ this.globalSettings = nextSettings;
97
103
  this.globalSettingsValue = {
98
104
  settings: this.globalSettings,
99
105
  setSettings: this.setGlobalSettingsFn
100
106
  };
101
- onGlobalSettingsChange(this.globalSettings);
102
107
  this.scheduleRerender();
103
108
  };
104
109
  this.streamDeckValue = {
105
- action: this.sdkAction,
106
- sdk: this.sdkInstance
110
+ action: this.action,
111
+ adapter: this.adapter
107
112
  };
108
113
  this.settingsValue = {
109
114
  settings: this.settings,
@@ -124,7 +129,8 @@ var ReactRoot = class {
124
129
  console.error("[@fcannizzaro/streamdeck-react] Recoverable error:", err);
125
130
  }, () => {});
126
131
  if (canvas.type === "dial" || canvas.type === "touch") {
127
- if ("setFeedbackLayout" in this.sdkAction) this.sdkAction.setFeedbackLayout(this.resolvedDialLayout);
132
+ const layout = this.resolvedDialLayout;
133
+ this.action.setFeedbackLayout(typeof layout === "string" ? layout : layout);
128
134
  }
129
135
  this.eventBus.ownerId = actionInfo.id;
130
136
  this.eventBus.ownerUuid = actionInfo.uuid;
@@ -155,16 +161,32 @@ var ReactRoot = class {
155
161
  }
156
162
  async doFlush() {
157
163
  if (this.disposed) return;
164
+ if (this.renderConfig.debug) {
165
+ this._renderCount++;
166
+ const now = Date.now();
167
+ if (now - this._lastRenderReport > 1e3) {
168
+ if (this._renderCount > ReactRoot.RENDER_WARN_THRESHOLD) console.warn(`[@fcannizzaro/streamdeck-react] Action ${this.actionInfo.id} rendered ${this._renderCount}x in 1s`);
169
+ this._renderCount = 0;
170
+ this._lastRenderReport = now;
171
+ }
172
+ }
158
173
  try {
159
174
  const dataUri = await renderToDataUri(this.container, this.canvas.width, this.canvas.height, this.renderConfig);
175
+ clearDirtyFlags(this.container);
160
176
  if (dataUri === null || this.disposed) return;
161
177
  this.lastDataUri = dataUri;
162
- if (!this.suppressHardwarePush) await this.pushImage(dataUri);
178
+ if (!this.suppressHardwarePush) this.pushImage(dataUri).catch((err) => {
179
+ console.error("[@fcannizzaro/streamdeck-react] Hardware push error:", err);
180
+ });
163
181
  } catch (err) {
164
182
  console.error("[@fcannizzaro/streamdeck-react] Render error:", err);
165
183
  }
166
184
  }
167
185
  updateSettings(settings) {
186
+ if (shallowEqualSettings(this.settings, settings)) {
187
+ this.eventBus.emit("settingsChanged", settings);
188
+ return;
189
+ }
168
190
  this.settings = { ...settings };
169
191
  this.settingsValue = {
170
192
  settings: this.settings,
@@ -174,6 +196,7 @@ var ReactRoot = class {
174
196
  this.scheduleRerender();
175
197
  }
176
198
  updateGlobalSettings(settings) {
199
+ if (shallowEqualSettings(this.globalSettings, settings)) return;
177
200
  this.globalSettings = { ...settings };
178
201
  this.globalSettingsValue = {
179
202
  settings: this.globalSettings,
@@ -0,0 +1,5 @@
1
+ import { JsonObject } from '@elgato/utils';
2
+ /** Returns true when applying `partial` would change at least one key in `current`. */
3
+ export declare function partialHasChanges(current: JsonObject, partial: JsonObject): boolean;
4
+ /** Shallow equality for full settings objects. */
5
+ export declare function shallowEqualSettings(a: JsonObject, b: JsonObject): boolean;
@@ -0,0 +1,24 @@
1
+ //#region src/roots/settings-equality.ts
2
+ /** Returns true when applying `partial` would change at least one key in `current`. */
3
+ function partialHasChanges(current, partial) {
4
+ const keys = Object.keys(partial);
5
+ for (let i = 0; i < keys.length; i++) {
6
+ const key = keys[i];
7
+ if (!Object.is(current[key], partial[key])) return true;
8
+ }
9
+ return false;
10
+ }
11
+ /** Shallow equality for full settings objects. */
12
+ function shallowEqualSettings(a, b) {
13
+ const aKeys = Object.keys(a);
14
+ const bKeys = Object.keys(b);
15
+ if (aKeys.length !== bKeys.length) return false;
16
+ for (let i = 0; i < aKeys.length; i++) {
17
+ const key = aKeys[i];
18
+ if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
19
+ if (!Object.is(a[key], b[key])) return false;
20
+ }
21
+ return true;
22
+ }
23
+ //#endregion
24
+ export { partialHasChanges, shallowEqualSettings };
@@ -3,9 +3,9 @@ import { VContainer } from '../reconciler/vnode';
3
3
  import { RenderConfig } from '../render/pipeline';
4
4
  import { EventBus } from '../context/event-bus';
5
5
  import { DeviceInfo, WrapperComponent } from '../types';
6
- import { DialAction } from '@elgato/streamdeck';
6
+ import { AdapterActionHandle } from '../adapter/types';
7
7
  import { JsonObject } from '@elgato/utils';
8
- export declare class TouchBarRoot {
8
+ export declare class TouchStripRoot {
9
9
  private component;
10
10
  readonly eventBus: EventBus;
11
11
  private container;
@@ -13,14 +13,36 @@ export declare class TouchBarRoot {
13
13
  private columns;
14
14
  private globalSettings;
15
15
  private setGlobalSettingsFn;
16
- private renderDebounceMs;
17
16
  private renderConfig;
18
17
  private deviceInfo;
19
18
  private disposed;
20
- private fps;
21
19
  private pluginWrapper?;
20
+ private readonly renderDebounceMs;
21
+ private _renderCount;
22
+ private _lastRenderReport;
23
+ private static readonly RENDER_WARN_THRESHOLD;
24
+ private _lastSegmentUriHash;
22
25
  /** Last rendered per-column data URIs. Used by devtools snapshots. */
23
26
  lastSegmentUris: Map<number, string>;
27
+ /**
28
+ * When true, doFlush skips pushing rendered segments to hardware.
29
+ * Set by the devtools bridge while a highlight overlay is active so
30
+ * that rapid re-renders don't overwrite the highlight on the device.
31
+ * The highlight path calls pushSegmentImages() directly (bypassing
32
+ * this flag).
33
+ *
34
+ * Mirrors ReactRoot.suppressHardwarePush — same pattern, different
35
+ * granularity (per-segment instead of single image).
36
+ */
37
+ suppressHardwarePush: boolean;
38
+ /**
39
+ * Push per-column data URIs to the physical Stream Deck touch strip.
40
+ * Used by the devtools highlight overlay to bypass suppressHardwarePush.
41
+ *
42
+ * @param uris Map of column → data URI to push to hardware.
43
+ * Columns not present in the map are left unchanged.
44
+ */
45
+ pushSegmentImages(uris: Map<number, string>): Promise<void>;
24
46
  /** Exposes the VContainer for devtools inspection. */
25
47
  get vcontainer(): VContainer;
26
48
  /** Sorted column numbers for devtools observer. */
@@ -28,13 +50,13 @@ export declare class TouchBarRoot {
28
50
  /** Column → actionId map for devtools observer. */
29
51
  get columnActionMap(): Map<number, string>;
30
52
  private globalSettingsValue;
31
- private touchBarValue;
32
- constructor(component: ComponentType, deviceInfo: DeviceInfo, initialGlobalSettings: JsonObject, renderConfig: RenderConfig, renderDebounceMs: number, onGlobalSettingsChange: (settings: JsonObject) => Promise<void>, pluginWrapper?: WrapperComponent, touchBarFPS?: number);
33
- addColumn(column: number, actionId: string, sdkAction: DialAction): void;
53
+ private touchStripValue;
54
+ constructor(component: ComponentType, deviceInfo: DeviceInfo, initialGlobalSettings: JsonObject, renderConfig: RenderConfig, onGlobalSettingsChange: (settings: JsonObject) => Promise<void>, pluginWrapper?: WrapperComponent);
55
+ addColumn(column: number, actionId: string, sdkAction: AdapterActionHandle): void;
34
56
  removeColumn(column: number): void;
35
57
  get isEmpty(): boolean;
36
58
  findColumnByActionId(actionId: string): number | undefined;
37
- private updateTouchBarInfo;
59
+ private updateTouchStripInfo;
38
60
  private render;
39
61
  private buildTree;
40
62
  private scheduleRerender;
@@ -0,0 +1,263 @@
1
+ import { clearDirtyFlags, createVContainer, isContainerDirty } from "../reconciler/vnode.js";
2
+ import { reconciler } from "../reconciler/renderer.js";
3
+ import { computeTouchStripSegmentCacheKey, computeTreeHash, fnv1a } from "../render/cache.js";
4
+ import { getTouchStripSegmentCache } from "../render/image-cache.js";
5
+ import { measureTree, renderToRaw, sliceToDataUri } from "../render/pipeline.js";
6
+ import { EventBus } from "../context/event-bus.js";
7
+ import { DeviceContext, EventBusContext, GlobalSettingsContext } from "../context/providers.js";
8
+ import { partialHasChanges, shallowEqualSettings } from "./settings-equality.js";
9
+ import { TouchStripContext } from "../context/touchstrip-context.js";
10
+ import { createElement } from "react";
11
+ //#region src/roots/touchstrip-root.ts
12
+ var SEGMENT_WIDTH = 200;
13
+ var SEGMENT_HEIGHT = 100;
14
+ var DEFAULT_TOUCH_STRIP_FPS = 30;
15
+ var TouchStripRoot = class TouchStripRoot {
16
+ eventBus = new EventBus();
17
+ container;
18
+ fiberRoot;
19
+ columns = /* @__PURE__ */ new Map();
20
+ globalSettings;
21
+ setGlobalSettingsFn;
22
+ renderConfig;
23
+ deviceInfo;
24
+ disposed = false;
25
+ pluginWrapper;
26
+ renderDebounceMs = 17;
27
+ _renderCount = 0;
28
+ _lastRenderReport = 0;
29
+ static RENDER_WARN_THRESHOLD = 35;
30
+ _lastSegmentUriHash = 0;
31
+ /** Last rendered per-column data URIs. Used by devtools snapshots. */
32
+ lastSegmentUris = /* @__PURE__ */ new Map();
33
+ /**
34
+ * When true, doFlush skips pushing rendered segments to hardware.
35
+ * Set by the devtools bridge while a highlight overlay is active so
36
+ * that rapid re-renders don't overwrite the highlight on the device.
37
+ * The highlight path calls pushSegmentImages() directly (bypassing
38
+ * this flag).
39
+ *
40
+ * Mirrors ReactRoot.suppressHardwarePush — same pattern, different
41
+ * granularity (per-segment instead of single image).
42
+ */
43
+ suppressHardwarePush = false;
44
+ /**
45
+ * Push per-column data URIs to the physical Stream Deck touch strip.
46
+ * Used by the devtools highlight overlay to bypass suppressHardwarePush.
47
+ *
48
+ * @param uris Map of column → data URI to push to hardware.
49
+ * Columns not present in the map are left unchanged.
50
+ */
51
+ async pushSegmentImages(uris) {
52
+ if (this.disposed) return;
53
+ const promises = [];
54
+ for (const [column, uri] of uris) {
55
+ const entry = this.columns.get(column);
56
+ if (entry) promises.push(entry.sdkAction.setFeedback({ canvas: uri }).catch(() => {}));
57
+ }
58
+ await Promise.all(promises);
59
+ }
60
+ /** Exposes the VContainer for devtools inspection. */
61
+ get vcontainer() {
62
+ return this.container;
63
+ }
64
+ /** Sorted column numbers for devtools observer. */
65
+ get columnNumbers() {
66
+ return [...this.columns.keys()].sort((a, b) => a - b);
67
+ }
68
+ /** Column → actionId map for devtools observer. */
69
+ get columnActionMap() {
70
+ const map = /* @__PURE__ */ new Map();
71
+ for (const [col, entry] of this.columns) map.set(col, entry.actionId);
72
+ return map;
73
+ }
74
+ globalSettingsValue;
75
+ touchStripValue;
76
+ constructor(component, deviceInfo, initialGlobalSettings, renderConfig, onGlobalSettingsChange, pluginWrapper) {
77
+ this.component = component;
78
+ this.deviceInfo = deviceInfo;
79
+ this.globalSettings = { ...initialGlobalSettings };
80
+ this.renderConfig = renderConfig;
81
+ this.pluginWrapper = pluginWrapper;
82
+ this.setGlobalSettingsFn = (partial) => {
83
+ const hasChanges = partialHasChanges(this.globalSettings, partial);
84
+ const nextSettings = hasChanges ? {
85
+ ...this.globalSettings,
86
+ ...partial
87
+ } : this.globalSettings;
88
+ onGlobalSettingsChange(nextSettings);
89
+ if (!hasChanges) return;
90
+ this.globalSettings = nextSettings;
91
+ this.globalSettingsValue = {
92
+ settings: this.globalSettings,
93
+ setSettings: this.setGlobalSettingsFn
94
+ };
95
+ this.scheduleRerender();
96
+ };
97
+ this.globalSettingsValue = {
98
+ settings: this.globalSettings,
99
+ setSettings: this.setGlobalSettingsFn
100
+ };
101
+ this.touchStripValue = {
102
+ width: 0,
103
+ height: SEGMENT_HEIGHT,
104
+ columns: [],
105
+ segmentWidth: SEGMENT_WIDTH
106
+ };
107
+ this.container = createVContainer(() => {
108
+ this.flush();
109
+ });
110
+ this.fiberRoot = reconciler.createContainer(this.container, 0, null, false, null, "", (err) => {
111
+ console.error("[@fcannizzaro/streamdeck-react] TouchStrip uncaught error:", err);
112
+ }, (err) => {
113
+ console.error("[@fcannizzaro/streamdeck-react] TouchStrip caught error:", err);
114
+ }, (err) => {
115
+ console.error("[@fcannizzaro/streamdeck-react] TouchStrip recoverable error:", err);
116
+ }, () => {});
117
+ this.eventBus.ownerId = `touchStrip:${deviceInfo.id}`;
118
+ }
119
+ addColumn(column, actionId, sdkAction) {
120
+ this.columns.set(column, {
121
+ actionId,
122
+ sdkAction
123
+ });
124
+ this.updateTouchStripInfo();
125
+ this.scheduleRerender();
126
+ }
127
+ removeColumn(column) {
128
+ this.columns.delete(column);
129
+ if (this.columns.size === 0) return;
130
+ this.updateTouchStripInfo();
131
+ this.scheduleRerender();
132
+ }
133
+ get isEmpty() {
134
+ return this.columns.size === 0;
135
+ }
136
+ findColumnByActionId(actionId) {
137
+ for (const [column, entry] of this.columns) if (entry.actionId === actionId) return column;
138
+ }
139
+ updateTouchStripInfo() {
140
+ const sortedColumns = [...this.columns.keys()].sort((a, b) => a - b);
141
+ this.touchStripValue = {
142
+ width: (sortedColumns.length > 0 ? sortedColumns[sortedColumns.length - 1] + 1 : 0) * SEGMENT_WIDTH,
143
+ height: SEGMENT_HEIGHT,
144
+ columns: sortedColumns,
145
+ segmentWidth: SEGMENT_WIDTH
146
+ };
147
+ }
148
+ render() {
149
+ if (this.disposed) return;
150
+ const element = this.buildTree();
151
+ reconciler.updateContainer(element, this.fiberRoot, null, () => {});
152
+ }
153
+ buildTree() {
154
+ let child = createElement(this.component);
155
+ if (this.pluginWrapper) child = createElement(this.pluginWrapper, null, child);
156
+ return createElement(TouchStripContext.Provider, { value: this.touchStripValue }, createElement(DeviceContext.Provider, { value: this.deviceInfo }, createElement(EventBusContext.Provider, { value: this.eventBus }, createElement(GlobalSettingsContext.Provider, { value: this.globalSettingsValue }, child))));
157
+ }
158
+ scheduleRerender() {
159
+ if (this.disposed) return;
160
+ this.render();
161
+ }
162
+ async flush() {
163
+ if (this.disposed) return;
164
+ if (this.renderDebounceMs > 0) this.container.renderTimer = setTimeout(async () => {
165
+ this.container.renderTimer = null;
166
+ await this.doFlush();
167
+ }, this.renderDebounceMs);
168
+ else await this.doFlush();
169
+ }
170
+ async doFlush() {
171
+ if (this.disposed || this.columns.size === 0) return;
172
+ if (this.renderConfig.debug) {
173
+ this._renderCount++;
174
+ const now = Date.now();
175
+ if (now - this._lastRenderReport > 1e3) {
176
+ if (this._renderCount > TouchStripRoot.RENDER_WARN_THRESHOLD) console.warn(`[@fcannizzaro/streamdeck-react] TouchStrip rendered ${this._renderCount}x in 1s (max ${DEFAULT_TOUCH_STRIP_FPS}fps)`);
177
+ this._renderCount = 0;
178
+ this._lastRenderReport = now;
179
+ }
180
+ }
181
+ try {
182
+ const width = this.touchStripValue.width;
183
+ if (width === 0) return;
184
+ if (this.renderConfig.caching && !isContainerDirty(this.container)) return;
185
+ const sortedColumns = [...this.columns.keys()].sort((a, b) => a - b);
186
+ let cacheKey;
187
+ const profiling = this.renderConfig.onProfile != null;
188
+ const tPhase2 = profiling ? performance.now() : 0;
189
+ if (this.renderConfig.caching && this.renderConfig.touchStripCacheMaxBytes > 0) {
190
+ cacheKey = computeTouchStripSegmentCacheKey(computeTreeHash(this.container), width, SEGMENT_HEIGHT, this.renderConfig.devicePixelRatio, sortedColumns);
191
+ const cached = getTouchStripSegmentCache(this.renderConfig.touchStripCacheMaxBytes).get(cacheKey);
192
+ if (cached !== void 0) {
193
+ for (const [column, uri] of cached) this.lastSegmentUris.set(column, uri);
194
+ if (!this.suppressHardwarePush) for (const [column, uri] of cached) this.columns.get(column)?.sdkAction.setFeedback({ canvas: uri }).catch(() => {});
195
+ clearDirtyFlags(this.container);
196
+ if (profiling) {
197
+ const tEnd = performance.now();
198
+ const stats = measureTree(this.container.children);
199
+ this.renderConfig.onProfile({
200
+ vnodeConversionMs: 0,
201
+ takumiRenderMs: 0,
202
+ hashMs: tEnd - tPhase2,
203
+ base64Ms: 0,
204
+ totalMs: tEnd - tPhase2,
205
+ skipped: false,
206
+ cacheHit: true,
207
+ treeDepth: stats.depth,
208
+ nodeCount: stats.count,
209
+ cacheStats: null
210
+ });
211
+ }
212
+ this.renderConfig.onRender?.(this.container, "");
213
+ return;
214
+ }
215
+ }
216
+ const result = await renderToRaw(this.container, width, SEGMENT_HEIGHT, this.renderConfig);
217
+ if (result === null || this.disposed) return;
218
+ const segmentResults = [];
219
+ for (const [column] of this.columns) {
220
+ const sliceUri = sliceToDataUri(result.buffer, result.width, result.height, column, SEGMENT_WIDTH, SEGMENT_HEIGHT);
221
+ segmentResults.push([column, sliceUri]);
222
+ this.lastSegmentUris.set(column, sliceUri);
223
+ }
224
+ segmentResults.sort((a, b) => a[0] - b[0]);
225
+ let skipped = false;
226
+ if (this.renderConfig.caching) {
227
+ const uriHash = fnv1a(segmentResults.map(([col, uri]) => `${col}:${uri}`).join("\0"));
228
+ if (uriHash === this._lastSegmentUriHash) skipped = true;
229
+ else this._lastSegmentUriHash = uriHash;
230
+ }
231
+ if (!skipped && !this.suppressHardwarePush) for (const [column, uri] of segmentResults) this.columns.get(column)?.sdkAction.setFeedback({ canvas: uri }).catch(() => {});
232
+ if (cacheKey !== void 0 && this.renderConfig.caching && this.renderConfig.touchStripCacheMaxBytes > 0) {
233
+ const cache = getTouchStripSegmentCache(this.renderConfig.touchStripCacheMaxBytes);
234
+ let byteSize = 64;
235
+ for (const [, uri] of segmentResults) byteSize += uri.length * 2 + 16;
236
+ cache.set(cacheKey, segmentResults, byteSize);
237
+ }
238
+ clearDirtyFlags(this.container);
239
+ this.renderConfig.onRender?.(this.container, "");
240
+ } catch (err) {
241
+ console.error("[@fcannizzaro/streamdeck-react] TouchStrip render error:", err);
242
+ }
243
+ }
244
+ updateGlobalSettings(settings) {
245
+ if (shallowEqualSettings(this.globalSettings, settings)) return;
246
+ this.globalSettings = { ...settings };
247
+ this.globalSettingsValue = {
248
+ settings: this.globalSettings,
249
+ setSettings: this.setGlobalSettingsFn
250
+ };
251
+ this.scheduleRerender();
252
+ }
253
+ unmount() {
254
+ this.disposed = true;
255
+ if (this.container.renderTimer !== null) clearTimeout(this.container.renderTimer);
256
+ this.eventBus.emit("willDisappear", void 0);
257
+ reconciler.updateContainer(null, this.fiberRoot, null, () => {});
258
+ this.eventBus.removeAllListeners();
259
+ this.columns.clear();
260
+ }
261
+ };
262
+ //#endregion
263
+ export { TouchStripRoot };