@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.
- package/LICENSE +190 -21
- package/README.md +2 -0
- package/dist/action.d.ts +2 -2
- package/dist/action.js +1 -2
- package/dist/adapter/index.d.ts +2 -0
- package/dist/adapter/physical-device.d.ts +2 -0
- package/dist/adapter/physical-device.js +153 -0
- package/dist/adapter/types.d.ts +127 -0
- package/dist/bundler-shared.d.ts +11 -0
- package/dist/bundler-shared.js +11 -0
- package/dist/context/event-bus.d.ts +1 -1
- package/dist/context/event-bus.js +1 -1
- package/dist/context/touchstrip-context.d.ts +2 -0
- package/dist/context/touchstrip-context.js +5 -0
- package/dist/devtools/bridge.d.ts +35 -7
- package/dist/devtools/bridge.js +152 -46
- package/dist/devtools/highlight.d.ts +5 -0
- package/dist/devtools/highlight.js +107 -57
- package/dist/devtools/index.js +6 -0
- package/dist/devtools/observers/lifecycle.d.ts +4 -4
- package/dist/devtools/server.d.ts +6 -1
- package/dist/devtools/server.js +6 -1
- package/dist/devtools/types.d.ts +50 -6
- package/dist/font-inline.d.ts +5 -1
- package/dist/font-inline.js +8 -3
- package/dist/hooks/animation.d.ts +154 -0
- package/dist/hooks/animation.js +381 -0
- package/dist/hooks/events.js +2 -6
- package/dist/hooks/sdk.js +11 -11
- package/dist/hooks/touchstrip.d.ts +6 -0
- package/dist/hooks/touchstrip.js +37 -0
- package/dist/hooks/utility.js +3 -2
- package/dist/index.d.ts +9 -2
- package/dist/index.js +4 -2
- package/dist/manifest-codegen.d.ts +38 -0
- package/dist/manifest-codegen.js +110 -0
- package/dist/plugin.js +86 -106
- package/dist/reconciler/host-config.js +19 -1
- package/dist/reconciler/vnode.d.ts +26 -2
- package/dist/reconciler/vnode.js +40 -10
- package/dist/render/buffer-pool.d.ts +19 -0
- package/dist/render/buffer-pool.js +51 -0
- package/dist/render/cache.d.ts +29 -0
- package/dist/render/cache.js +137 -5
- package/dist/render/image-cache.d.ts +54 -0
- package/dist/render/image-cache.js +144 -0
- package/dist/render/metrics.d.ts +57 -0
- package/dist/render/metrics.js +98 -0
- package/dist/render/pipeline.d.ts +36 -1
- package/dist/render/pipeline.js +304 -34
- package/dist/render/png.d.ts +1 -1
- package/dist/render/png.js +26 -11
- package/dist/render/render-pool.d.ts +24 -0
- package/dist/render/render-pool.js +130 -0
- package/dist/render/svg.d.ts +7 -0
- package/dist/render/svg.js +139 -0
- package/dist/render/worker.d.ts +1 -0
- package/dist/rollup.d.ts +23 -9
- package/dist/rollup.js +24 -9
- package/dist/roots/registry.d.ts +9 -11
- package/dist/roots/registry.js +39 -42
- package/dist/roots/root.d.ts +9 -6
- package/dist/roots/root.js +52 -29
- package/dist/roots/settings-equality.d.ts +5 -0
- package/dist/roots/settings-equality.js +24 -0
- package/dist/roots/{touchbar-root.d.ts → touchstrip-root.d.ts} +30 -8
- package/dist/roots/touchstrip-root.js +263 -0
- package/dist/types.d.ts +73 -23
- package/dist/vite.d.ts +22 -8
- package/dist/vite.js +24 -8
- package/package.json +7 -4
- package/dist/context/touchbar-context.d.ts +0 -2
- package/dist/context/touchbar-context.js +0 -5
- package/dist/hooks/touchbar.d.ts +0 -6
- package/dist/hooks/touchbar.js +0 -37
- package/dist/roots/touchbar-root.js +0 -175
package/dist/roots/root.js
CHANGED
|
@@ -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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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,
|
|
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.
|
|
76
|
-
this.
|
|
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.
|
|
106
|
-
|
|
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
|
-
|
|
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)
|
|
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 {
|
|
6
|
+
import { AdapterActionHandle } from '../adapter/types';
|
|
7
7
|
import { JsonObject } from '@elgato/utils';
|
|
8
|
-
export declare class
|
|
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
|
|
32
|
-
constructor(component: ComponentType, deviceInfo: DeviceInfo, initialGlobalSettings: JsonObject, renderConfig: RenderConfig,
|
|
33
|
-
addColumn(column: number, actionId: string, sdkAction:
|
|
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
|
|
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 };
|