@fcannizzaro/streamdeck-react 0.1.11 → 0.1.13
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/README.md +11 -8
- package/dist/action.js +0 -1
- 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 +10 -0
- package/dist/bundler-shared.js +28 -1
- package/dist/devtools/bridge.d.ts +2 -2
- package/dist/devtools/bridge.js +7 -8
- package/dist/devtools/highlight.d.ts +1 -2
- package/dist/devtools/highlight.js +4 -3
- package/dist/devtools/types.d.ts +5 -5
- package/dist/font-inline.js +1 -1
- package/dist/google-font.d.ts +61 -0
- package/dist/google-font.js +124 -0
- package/dist/hooks/animation.d.ts +1 -1
- package/dist/hooks/animation.js +2 -2
- package/dist/hooks/events.js +1 -1
- package/dist/hooks/sdk.js +11 -11
- package/dist/hooks/utility.js +3 -2
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/plugin.js +102 -124
- package/dist/reconciler/vnode.d.ts +0 -2
- package/dist/reconciler/vnode.js +0 -1
- package/dist/render/cache.d.ts +5 -17
- package/dist/render/cache.js +7 -29
- package/dist/render/image-cache.d.ts +8 -7
- package/dist/render/image-cache.js +33 -17
- package/dist/render/metrics.d.ts +9 -10
- package/dist/render/metrics.js +36 -39
- package/dist/render/pipeline.d.ts +4 -14
- package/dist/render/pipeline.js +47 -111
- package/dist/render/png.d.ts +0 -9
- package/dist/render/png.js +5 -8
- package/dist/render/render-pool.d.ts +0 -2
- package/dist/render/render-pool.js +1 -12
- package/dist/rollup.d.ts +1 -1
- package/dist/rollup.js +3 -1
- package/dist/roots/registry.d.ts +5 -9
- package/dist/roots/registry.js +30 -47
- package/dist/roots/root.d.ts +7 -34
- package/dist/roots/root.js +23 -90
- package/dist/roots/touchstrip-root.d.ts +6 -32
- package/dist/roots/touchstrip-root.js +61 -181
- package/dist/types.d.ts +38 -20
- package/dist/vite.d.ts +1 -1
- package/dist/vite.js +3 -1
- package/package.json +14 -8
- package/dist/node_modules/.bun/xxhash-wasm@1.1.0/node_modules/xxhash-wasm/esm/xxhash-wasm.js +0 -3157
- package/dist/roots/flush-coordinator.d.ts +0 -18
- package/dist/roots/flush-coordinator.js +0 -38
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { clearDirtyFlags, createVContainer, isContainerDirty } from "../reconciler/vnode.js";
|
|
2
2
|
import { reconciler } from "../reconciler/renderer.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { buildTakumiChildren, measureTree, renderSegmentToDataUri, renderToRaw, sliceToDataUriAsync } from "../render/pipeline.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";
|
|
7
6
|
import { EventBus } from "../context/event-bus.js";
|
|
8
7
|
import { DeviceContext, EventBusContext, GlobalSettingsContext } from "../context/providers.js";
|
|
9
8
|
import { partialHasChanges, shallowEqualSettings } from "./settings-equality.js";
|
|
@@ -12,7 +11,7 @@ import { createElement } from "react";
|
|
|
12
11
|
//#region src/roots/touchstrip-root.ts
|
|
13
12
|
var SEGMENT_WIDTH = 200;
|
|
14
13
|
var SEGMENT_HEIGHT = 100;
|
|
15
|
-
var
|
|
14
|
+
var DEFAULT_TOUCH_STRIP_FPS = 30;
|
|
16
15
|
var TouchStripRoot = class TouchStripRoot {
|
|
17
16
|
eventBus = new EventBus();
|
|
18
17
|
container;
|
|
@@ -20,34 +19,15 @@ var TouchStripRoot = class TouchStripRoot {
|
|
|
20
19
|
columns = /* @__PURE__ */ new Map();
|
|
21
20
|
globalSettings;
|
|
22
21
|
setGlobalSettingsFn;
|
|
23
|
-
renderDebounceMs;
|
|
24
22
|
renderConfig;
|
|
25
23
|
deviceInfo;
|
|
26
24
|
disposed = false;
|
|
27
|
-
fps;
|
|
28
25
|
pluginWrapper;
|
|
26
|
+
renderDebounceMs = 17;
|
|
29
27
|
_renderCount = 0;
|
|
30
28
|
_lastRenderReport = 0;
|
|
31
|
-
static RENDER_WARN_THRESHOLD =
|
|
32
|
-
|
|
33
|
-
_pendingFlush = false;
|
|
34
|
-
_recentRenders = [];
|
|
35
|
-
_lastInteraction = 0;
|
|
36
|
-
static ANIMATION_WINDOW_MS = 100;
|
|
37
|
-
static ANIMATION_THRESHOLD = 2;
|
|
38
|
-
static INTERACTION_COOLDOWN_MS = 500;
|
|
39
|
-
static IDLE_THRESHOLD_MS = 2e3;
|
|
40
|
-
_lastFlushTime = 0;
|
|
41
|
-
/** Current render priority (lower = higher priority). Used by flush coordinator. */
|
|
42
|
-
get priority() {
|
|
43
|
-
const now = Date.now();
|
|
44
|
-
const cutoff = now - TouchStripRoot.ANIMATION_WINDOW_MS;
|
|
45
|
-
while (this._recentRenders.length > 0 && this._recentRenders[0] < cutoff) this._recentRenders.shift();
|
|
46
|
-
if (this._recentRenders.length > TouchStripRoot.ANIMATION_THRESHOLD) return 0;
|
|
47
|
-
if (now - this._lastInteraction < TouchStripRoot.INTERACTION_COOLDOWN_MS) return 1;
|
|
48
|
-
if (this._lastFlushTime > 0 && now - this._lastFlushTime > TouchStripRoot.IDLE_THRESHOLD_MS) return 3;
|
|
49
|
-
return 2;
|
|
50
|
-
}
|
|
29
|
+
static RENDER_WARN_THRESHOLD = 35;
|
|
30
|
+
_lastSegmentUriHash = 0;
|
|
51
31
|
/** Last rendered per-column data URIs. Used by devtools snapshots. */
|
|
52
32
|
lastSegmentUris = /* @__PURE__ */ new Map();
|
|
53
33
|
/**
|
|
@@ -93,14 +73,11 @@ var TouchStripRoot = class TouchStripRoot {
|
|
|
93
73
|
}
|
|
94
74
|
globalSettingsValue;
|
|
95
75
|
touchStripValue;
|
|
96
|
-
constructor(component, deviceInfo, initialGlobalSettings, renderConfig,
|
|
76
|
+
constructor(component, deviceInfo, initialGlobalSettings, renderConfig, onGlobalSettingsChange, pluginWrapper) {
|
|
97
77
|
this.component = component;
|
|
98
|
-
this.flushCoordinator = flushCoordinator;
|
|
99
78
|
this.deviceInfo = deviceInfo;
|
|
100
79
|
this.globalSettings = { ...initialGlobalSettings };
|
|
101
80
|
this.renderConfig = renderConfig;
|
|
102
|
-
this.fps = touchStripFPS ?? DEFAULT_TOUCHSTRIP_FPS;
|
|
103
|
-
this.renderDebounceMs = touchStripFPS != null ? Math.max(1, Math.round(1e3 / touchStripFPS)) : renderDebounceMs;
|
|
104
81
|
this.pluginWrapper = pluginWrapper;
|
|
105
82
|
this.setGlobalSettingsFn = (partial) => {
|
|
106
83
|
const hasChanges = partialHasChanges(this.globalSettings, partial);
|
|
@@ -125,8 +102,7 @@ var TouchStripRoot = class TouchStripRoot {
|
|
|
125
102
|
width: 0,
|
|
126
103
|
height: SEGMENT_HEIGHT,
|
|
127
104
|
columns: [],
|
|
128
|
-
segmentWidth: SEGMENT_WIDTH
|
|
129
|
-
fps: this.fps
|
|
105
|
+
segmentWidth: SEGMENT_WIDTH
|
|
130
106
|
};
|
|
131
107
|
this.container = createVContainer(() => {
|
|
132
108
|
this.flush();
|
|
@@ -138,7 +114,7 @@ var TouchStripRoot = class TouchStripRoot {
|
|
|
138
114
|
}, (err) => {
|
|
139
115
|
console.error("[@fcannizzaro/streamdeck-react] TouchStrip recoverable error:", err);
|
|
140
116
|
}, () => {});
|
|
141
|
-
this.eventBus.ownerId = `
|
|
117
|
+
this.eventBus.ownerId = `touchStrip:${deviceInfo.id}`;
|
|
142
118
|
}
|
|
143
119
|
addColumn(column, actionId, sdkAction) {
|
|
144
120
|
this.columns.set(column, {
|
|
@@ -166,8 +142,7 @@ var TouchStripRoot = class TouchStripRoot {
|
|
|
166
142
|
width: (sortedColumns.length > 0 ? sortedColumns[sortedColumns.length - 1] + 1 : 0) * SEGMENT_WIDTH,
|
|
167
143
|
height: SEGMENT_HEIGHT,
|
|
168
144
|
columns: sortedColumns,
|
|
169
|
-
segmentWidth: SEGMENT_WIDTH
|
|
170
|
-
fps: this.fps
|
|
145
|
+
segmentWidth: SEGMENT_WIDTH
|
|
171
146
|
};
|
|
172
147
|
}
|
|
173
148
|
render() {
|
|
@@ -184,59 +159,21 @@ var TouchStripRoot = class TouchStripRoot {
|
|
|
184
159
|
if (this.disposed) return;
|
|
185
160
|
this.render();
|
|
186
161
|
}
|
|
187
|
-
/** Record a user interaction for adaptive debounce. */
|
|
188
|
-
markInteraction() {
|
|
189
|
-
this._lastInteraction = Date.now();
|
|
190
|
-
}
|
|
191
|
-
get effectiveDebounceMs() {
|
|
192
|
-
const now = Date.now();
|
|
193
|
-
const cutoff = now - TouchStripRoot.ANIMATION_WINDOW_MS;
|
|
194
|
-
while (this._recentRenders.length > 0 && this._recentRenders[0] < cutoff) this._recentRenders.shift();
|
|
195
|
-
if (this._recentRenders.length > TouchStripRoot.ANIMATION_THRESHOLD) return 0;
|
|
196
|
-
if (now - this._lastInteraction < TouchStripRoot.INTERACTION_COOLDOWN_MS) return Math.min(this.renderDebounceMs, 16);
|
|
197
|
-
return this.renderDebounceMs;
|
|
198
|
-
}
|
|
199
162
|
async flush() {
|
|
200
163
|
if (this.disposed) return;
|
|
201
|
-
if (this.
|
|
202
|
-
this._pendingFlush = true;
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
this._recentRenders.push(Date.now());
|
|
206
|
-
const debounce = this.effectiveDebounceMs;
|
|
207
|
-
if (debounce > 0 && this.container.renderTimer !== null) clearTimeout(this.container.renderTimer);
|
|
208
|
-
if (debounce > 0) this.container.renderTimer = setTimeout(() => {
|
|
164
|
+
if (this.renderDebounceMs > 0) this.container.renderTimer = setTimeout(async () => {
|
|
209
165
|
this.container.renderTimer = null;
|
|
210
|
-
this.
|
|
211
|
-
},
|
|
212
|
-
else this.
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Submit this root for flushing. Routes through the coordinator
|
|
216
|
-
* (priority-ordered) when available, or flushes directly.
|
|
217
|
-
*/
|
|
218
|
-
submitFlush() {
|
|
219
|
-
if (this.disposed) return;
|
|
220
|
-
if (this.flushCoordinator) this.flushCoordinator.requestFlush(this);
|
|
221
|
-
else this.doFlush();
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* Execute the flush. Called by FlushCoordinator in priority order,
|
|
225
|
-
* or directly when no coordinator is present.
|
|
226
|
-
*/
|
|
227
|
-
async executeFlush() {
|
|
228
|
-
await this.doFlush();
|
|
166
|
+
await this.doFlush();
|
|
167
|
+
}, this.renderDebounceMs);
|
|
168
|
+
else await this.doFlush();
|
|
229
169
|
}
|
|
230
170
|
async doFlush() {
|
|
231
171
|
if (this.disposed || this.columns.size === 0) return;
|
|
232
|
-
this._rendering = true;
|
|
233
|
-
this._pendingFlush = false;
|
|
234
|
-
this._lastFlushTime = Date.now();
|
|
235
172
|
if (this.renderConfig.debug) {
|
|
236
173
|
this._renderCount++;
|
|
237
174
|
const now = Date.now();
|
|
238
175
|
if (now - this._lastRenderReport > 1e3) {
|
|
239
|
-
if (this._renderCount > TouchStripRoot.RENDER_WARN_THRESHOLD) console.warn(`[@fcannizzaro/streamdeck-react] TouchStrip rendered ${this._renderCount}x in 1s (
|
|
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)`);
|
|
240
177
|
this._renderCount = 0;
|
|
241
178
|
this._lastRenderReport = now;
|
|
242
179
|
}
|
|
@@ -244,121 +181,64 @@ var TouchStripRoot = class TouchStripRoot {
|
|
|
244
181
|
try {
|
|
245
182
|
const width = this.touchStripValue.width;
|
|
246
183
|
if (width === 0) return;
|
|
247
|
-
if (this.renderConfig.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (this.renderConfig.caching && this.renderConfig.touchstripCacheMaxBytes > 0) {
|
|
260
|
-
treeHash = computeTreeHash(this.container);
|
|
261
|
-
cacheKey = computeNativeTouchstripCacheKey(treeHash, width, SEGMENT_HEIGHT, this.renderConfig.devicePixelRatio, this.renderConfig.touchstripImageFormat, sortedColumns);
|
|
262
|
-
const cache = getTouchstripNativeCache(this.renderConfig.touchstripCacheMaxBytes);
|
|
263
|
-
const cached = cache.get(cacheKey);
|
|
264
|
-
if (cached !== void 0) {
|
|
265
|
-
metrics.recordCacheHit();
|
|
266
|
-
cacheHit = true;
|
|
267
|
-
for (const [column, uri] of cached) {
|
|
268
|
-
this.lastSegmentUris.set(column, uri);
|
|
269
|
-
if (!this.suppressHardwarePush) this.columns.get(column)?.sdkAction.setFeedback({ canvas: uri }).catch(() => {});
|
|
270
|
-
}
|
|
271
|
-
if (profiling) {
|
|
272
|
-
const tNow = performance.now();
|
|
273
|
-
const stats = measureTree(this.container.children);
|
|
274
|
-
this.renderConfig.onProfile({
|
|
275
|
-
vnodeToElementMs: 0,
|
|
276
|
-
fromJsxMs: 0,
|
|
277
|
-
takumiRenderMs: tNow - t0,
|
|
278
|
-
hashMs: 0,
|
|
279
|
-
base64Ms: 0,
|
|
280
|
-
totalMs: tNow - t0,
|
|
281
|
-
skipped: false,
|
|
282
|
-
cacheHit: true,
|
|
283
|
-
treeDepth: stats.depth,
|
|
284
|
-
nodeCount: stats.count,
|
|
285
|
-
cacheStats: cache.stats
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
clearDirtyFlags(this.container);
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
if (!cacheHit) {
|
|
292
|
-
const takumiChildren = buildTakumiChildren(this.container);
|
|
293
|
-
const segmentResults = [];
|
|
294
|
-
const renderPromises = [...this.columns.entries()].map(async ([column]) => {
|
|
295
|
-
const sliceUri = await renderSegmentToDataUri(this.container, width, SEGMENT_HEIGHT, column, SEGMENT_WIDTH, this.renderConfig.touchstripImageFormat, this.renderConfig, takumiChildren);
|
|
296
|
-
if (sliceUri != null) {
|
|
297
|
-
segmentResults.push([column, sliceUri]);
|
|
298
|
-
this.lastSegmentUris.set(column, sliceUri);
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
await Promise.all(renderPromises);
|
|
302
|
-
segmentResults.sort((a, b) => a[0] - b[0]);
|
|
303
|
-
let skipped = false;
|
|
304
|
-
if (this.renderConfig.caching) {
|
|
305
|
-
const uriHash = fnv1a(segmentResults.map(([col, uri]) => `${col}:${uri}`).join("\0"));
|
|
306
|
-
if (uriHash === this.container.lastSvgHash) {
|
|
307
|
-
metrics.recordHashDedup();
|
|
308
|
-
skipped = true;
|
|
309
|
-
} else this.container.lastSvgHash = uriHash;
|
|
310
|
-
}
|
|
311
|
-
if (!skipped && !this.suppressHardwarePush) for (const [column, uri] of segmentResults) this.columns.get(column)?.sdkAction.setFeedback({ canvas: uri }).catch(() => {});
|
|
312
|
-
const elapsedMs = (profiling ? performance.now() : 0) - t0;
|
|
313
|
-
metrics.recordRender(elapsedMs);
|
|
314
|
-
if (this.renderConfig.caching && this.renderConfig.touchstripCacheMaxBytes > 0) {
|
|
315
|
-
if (treeHash === void 0 || cacheKey === void 0) {
|
|
316
|
-
treeHash = computeTreeHash(this.container);
|
|
317
|
-
cacheKey = computeNativeTouchstripCacheKey(treeHash, width, SEGMENT_HEIGHT, this.renderConfig.devicePixelRatio, this.renderConfig.touchstripImageFormat, sortedColumns);
|
|
318
|
-
}
|
|
319
|
-
const cache = getTouchstripNativeCache(this.renderConfig.touchstripCacheMaxBytes);
|
|
320
|
-
let byteSize = 64;
|
|
321
|
-
for (const [, uri] of segmentResults) byteSize += uri.length * 2 + 16;
|
|
322
|
-
cache.set(cacheKey, segmentResults, byteSize);
|
|
323
|
-
}
|
|
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);
|
|
324
196
|
if (profiling) {
|
|
197
|
+
const tEnd = performance.now();
|
|
325
198
|
const stats = measureTree(this.container.children);
|
|
326
|
-
const nativeCache = this.renderConfig.touchstripCacheMaxBytes > 0 ? getTouchstripNativeCache(this.renderConfig.touchstripCacheMaxBytes) : null;
|
|
327
199
|
this.renderConfig.onProfile({
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
hashMs: 0,
|
|
200
|
+
vnodeConversionMs: 0,
|
|
201
|
+
takumiRenderMs: 0,
|
|
202
|
+
hashMs: tEnd - tPhase2,
|
|
332
203
|
base64Ms: 0,
|
|
333
|
-
totalMs:
|
|
334
|
-
skipped,
|
|
335
|
-
cacheHit:
|
|
204
|
+
totalMs: tEnd - tPhase2,
|
|
205
|
+
skipped: false,
|
|
206
|
+
cacheHit: true,
|
|
336
207
|
treeDepth: stats.depth,
|
|
337
208
|
nodeCount: stats.count,
|
|
338
|
-
cacheStats:
|
|
209
|
+
cacheStats: null
|
|
339
210
|
});
|
|
340
211
|
}
|
|
341
|
-
|
|
212
|
+
this.renderConfig.onRender?.(this.container, "");
|
|
213
|
+
return;
|
|
342
214
|
}
|
|
343
|
-
} else {
|
|
344
|
-
const result = await renderToRaw(this.container, width, SEGMENT_HEIGHT, this.renderConfig);
|
|
345
|
-
if (result === null || this.disposed) return;
|
|
346
|
-
const feedbackPromises = [...this.columns.entries()].map(async ([column, entry]) => {
|
|
347
|
-
const sliceUri = await sliceToDataUriAsync(result.buffer, result.width, result.height, column, SEGMENT_WIDTH, SEGMENT_HEIGHT);
|
|
348
|
-
this.lastSegmentUris.set(column, sliceUri);
|
|
349
|
-
if (!this.suppressHardwarePush) entry.sdkAction.setFeedback({ canvas: sliceUri }).catch(() => {});
|
|
350
|
-
});
|
|
351
|
-
await Promise.all(feedbackPromises);
|
|
352
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);
|
|
353
239
|
this.renderConfig.onRender?.(this.container, "");
|
|
354
240
|
} catch (err) {
|
|
355
241
|
console.error("[@fcannizzaro/streamdeck-react] TouchStrip render error:", err);
|
|
356
|
-
} finally {
|
|
357
|
-
this._rendering = false;
|
|
358
|
-
if (this._pendingFlush && !this.disposed) {
|
|
359
|
-
this._pendingFlush = false;
|
|
360
|
-
await this.doFlush();
|
|
361
|
-
}
|
|
362
242
|
}
|
|
363
243
|
}
|
|
364
244
|
updateGlobalSettings(settings) {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import { ComponentType, ReactNode } from 'react';
|
|
2
|
-
import { Action, Controller, Coordinates, DeviceType, DialAction, KeyAction, Size } from '@elgato/streamdeck';
|
|
3
2
|
import { JsonObject, JsonValue } from '@elgato/utils';
|
|
3
|
+
import { AdapterActionHandle, StreamDeckAdapter } from './adapter/types';
|
|
4
|
+
/** Controller surface type. */
|
|
5
|
+
export type Controller = "Keypad" | "Encoder";
|
|
6
|
+
/** Grid coordinates for a key or encoder on a device. */
|
|
7
|
+
export interface Coordinates {
|
|
8
|
+
column: number;
|
|
9
|
+
row: number;
|
|
10
|
+
}
|
|
11
|
+
/** Device grid size (number of key columns and rows). */
|
|
12
|
+
export interface Size {
|
|
13
|
+
columns: number;
|
|
14
|
+
rows: number;
|
|
15
|
+
}
|
|
4
16
|
export interface FontConfig {
|
|
5
17
|
name: string;
|
|
6
18
|
data: ArrayBuffer | Buffer;
|
|
@@ -61,11 +73,26 @@ export interface TouchStripLayout {
|
|
|
61
73
|
items: TouchStripLayoutItem[];
|
|
62
74
|
}
|
|
63
75
|
export type EncoderLayout = string | TouchStripLayout;
|
|
76
|
+
/** Takumi renderer backend selection. `"native-binding"` uses the native Rust NAPI addon (`@takumi-rs/core`). `"wasm"` uses the WebAssembly build (`@takumi-rs/wasm`), suitable for WebContainer and browser environments. */
|
|
77
|
+
export type TakumiBackend = "native-binding" | "wasm";
|
|
64
78
|
export interface PluginConfig {
|
|
79
|
+
/** Stream Deck adapter. Defaults to physicalDevice() (Elgato SDK). */
|
|
80
|
+
adapter?: StreamDeckAdapter;
|
|
65
81
|
fonts: FontConfig[];
|
|
66
82
|
actions: ActionDefinition[];
|
|
67
83
|
wrapper?: WrapperComponent;
|
|
68
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Takumi renderer backend.
|
|
86
|
+
*
|
|
87
|
+
* - `"native-binding"` — uses `@takumi-rs/core` (native Rust NAPI addon).
|
|
88
|
+
* Requires a platform-specific binary (e.g. `@takumi-rs/core-darwin-arm64`).
|
|
89
|
+
* - `"wasm"` — uses `@takumi-rs/wasm` (WebAssembly build).
|
|
90
|
+
* Requires `@takumi-rs/wasm` to be installed. WOFF fonts are not supported
|
|
91
|
+
* in this mode — use TTF/OTF only. Worker threads are force-disabled.
|
|
92
|
+
*
|
|
93
|
+
* @default "native-binding"
|
|
94
|
+
*/
|
|
95
|
+
takumi?: TakumiBackend;
|
|
69
96
|
imageFormat?: "png" | "webp";
|
|
70
97
|
caching?: boolean;
|
|
71
98
|
devicePixelRatio?: number;
|
|
@@ -76,12 +103,10 @@ export interface PluginConfig {
|
|
|
76
103
|
debug?: boolean;
|
|
77
104
|
/** Maximum image cache size in bytes for key/dial renders. Set to 0 to disable. @default 16777216 (16 MB) */
|
|
78
105
|
imageCacheMaxBytes?: number;
|
|
79
|
-
/** Maximum
|
|
80
|
-
|
|
81
|
-
/** Offload Takumi rendering to a worker thread. Set to false to disable. @default true */
|
|
106
|
+
/** Maximum TouchStrip raw buffer cache size in bytes. Set to 0 to disable. @default 8388608 (8 MB) */
|
|
107
|
+
touchStripCacheMaxBytes?: number;
|
|
108
|
+
/** Offload Takumi rendering to a worker thread. Set to false to disable. Automatically disabled when `takumi` is `"wasm"`. @default true */
|
|
82
109
|
useWorker?: boolean;
|
|
83
|
-
/** Image format for touchstrip segment encoding. `"webp"` renders each segment directly via Takumi (faster, no custom PNG encoder). `"png"` uses the raw+crop+deflate path. @default "webp" */
|
|
84
|
-
touchstripImageFormat?: "webp" | "png";
|
|
85
110
|
}
|
|
86
111
|
export interface Plugin {
|
|
87
112
|
connect(): Promise<void>;
|
|
@@ -112,10 +137,8 @@ export interface ActionConfig<S extends JsonObject = JsonObject> {
|
|
|
112
137
|
uuid: string;
|
|
113
138
|
key?: ComponentType;
|
|
114
139
|
dial?: ComponentType;
|
|
115
|
-
/** Full-strip
|
|
140
|
+
/** Full-strip TouchStrip component. When set, replaces per-encoder `dial` display with a single shared React tree that spans the entire touch strip. */
|
|
116
141
|
touchStrip?: ComponentType;
|
|
117
|
-
/** Target frame rate for the touchstrip animation loop and render pipeline. Controls both `useTick` cadence (via `useTouchStrip().fps`) and the render debounce. @default 60 */
|
|
118
|
-
touchStripFPS?: number;
|
|
119
142
|
/** Encoder feedback layout. Defaults to a full-width `pixmap` canvas layout. Custom layouts should include a `pixmap` item keyed as `canvas`. */
|
|
120
143
|
dialLayout?: EncoderLayout;
|
|
121
144
|
wrapper?: WrapperComponent;
|
|
@@ -127,8 +150,6 @@ export type ActionConfigInput<S extends JsonObject = JsonObject> = [keyof Manife
|
|
|
127
150
|
] ? ActionConfig<S> : {
|
|
128
151
|
[UUID in ActionUUID]: {
|
|
129
152
|
uuid: UUID;
|
|
130
|
-
/** Target frame rate for the touchstrip animation loop and render pipeline. Controls both `useTick` cadence (via `useTouchStrip().fps`) and the render debounce. @default 60 */
|
|
131
|
-
touchStripFPS?: number;
|
|
132
153
|
/** Encoder feedback layout. Defaults to a full-width `pixmap` canvas layout. Custom layouts should include a `pixmap` item keyed as `canvas`. */
|
|
133
154
|
dialLayout?: EncoderLayout;
|
|
134
155
|
wrapper?: WrapperComponent;
|
|
@@ -139,10 +160,8 @@ export interface ActionDefinition<S extends JsonObject = JsonObject> {
|
|
|
139
160
|
uuid: string;
|
|
140
161
|
key?: ComponentType;
|
|
141
162
|
dial?: ComponentType;
|
|
142
|
-
/** Full-strip
|
|
163
|
+
/** Full-strip TouchStrip component. When set, replaces per-encoder `dial` display with a single shared React tree that spans the entire touch strip. */
|
|
143
164
|
touchStrip?: ComponentType;
|
|
144
|
-
/** Target frame rate for the touchstrip animation loop and render pipeline. @default 60 */
|
|
145
|
-
touchStripFPS?: number;
|
|
146
165
|
/** Encoder feedback layout. Defaults to a full-width `pixmap` canvas layout. Custom layouts should include a `pixmap` item keyed as `canvas`. */
|
|
147
166
|
dialLayout?: EncoderLayout;
|
|
148
167
|
wrapper?: WrapperComponent;
|
|
@@ -150,7 +169,8 @@ export interface ActionDefinition<S extends JsonObject = JsonObject> {
|
|
|
150
169
|
}
|
|
151
170
|
export interface DeviceInfo {
|
|
152
171
|
id: string;
|
|
153
|
-
type
|
|
172
|
+
/** Numeric device type matching Elgato DeviceType enum values (e.g. 7 = StreamDeckPlus). */
|
|
173
|
+
type: number;
|
|
154
174
|
size: Size;
|
|
155
175
|
name: string;
|
|
156
176
|
}
|
|
@@ -167,8 +187,8 @@ export interface CanvasInfo {
|
|
|
167
187
|
type: "key" | "dial" | "touch";
|
|
168
188
|
}
|
|
169
189
|
export interface StreamDeckAccess {
|
|
170
|
-
action:
|
|
171
|
-
|
|
190
|
+
action: AdapterActionHandle;
|
|
191
|
+
adapter: StreamDeckAdapter;
|
|
172
192
|
}
|
|
173
193
|
export interface KeyDownPayload {
|
|
174
194
|
settings: JsonObject;
|
|
@@ -216,8 +236,6 @@ export interface TouchStripInfo {
|
|
|
216
236
|
columns: number[];
|
|
217
237
|
/** Width of each encoder segment in pixels (always 200). */
|
|
218
238
|
segmentWidth: number;
|
|
219
|
-
/** Target frame rate for the animation loop. Pass to `useTick` for matched cadence. */
|
|
220
|
-
fps: number;
|
|
221
239
|
}
|
|
222
240
|
export interface TouchStripTapPayload {
|
|
223
241
|
/** Absolute tap position across the full strip width. */
|
package/dist/vite.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Plugin } from 'vite';
|
|
2
2
|
import { StreamDeckTargetOptions } from './bundler-shared';
|
|
3
|
-
export type { StreamDeckPlatform, StreamDeckArch, StreamDeckTarget, StreamDeckTargetOptions, } from './bundler-shared';
|
|
3
|
+
export type { StreamDeckPlatform, StreamDeckArch, StreamDeckTarget, StreamDeckTargetOptions, TakumiBackend, } from './bundler-shared';
|
|
4
4
|
export interface StreamDeckReactOptions extends StreamDeckTargetOptions {
|
|
5
5
|
/**
|
|
6
6
|
* The plugin UUID used to restart the plugin after each build
|
package/dist/vite.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { NOOP_DEVTOOLS_CODE, NOOP_DEVTOOLS_ID, copyNativeBindings, isLibraryDevtoolsImport, shouldStripDevtools } from "./bundler-shared.js";
|
|
1
|
+
import { NOOP_DEVTOOLS_CODE, NOOP_DEVTOOLS_ID, TAKUMI_NATIVE_LOADER_CODE, TAKUMI_NATIVE_LOADER_ID, copyNativeBindings, isLibraryDevtoolsImport, shouldStripDevtools } from "./bundler-shared.js";
|
|
2
2
|
import { loadFont, resolveFontId } from "./font-inline.js";
|
|
3
3
|
import { generateManifestTypes } from "./manifest-codegen.js";
|
|
4
4
|
import { resolve } from "node:path";
|
|
@@ -47,10 +47,12 @@ function streamDeckReact(options = {}) {
|
|
|
47
47
|
},
|
|
48
48
|
resolveId(source, importer) {
|
|
49
49
|
if (stripDevtools && isLibraryDevtoolsImport(source, importer)) return NOOP_DEVTOOLS_ID;
|
|
50
|
+
if (options.takumi !== "wasm" && source === "@takumi-rs/core") return TAKUMI_NATIVE_LOADER_ID;
|
|
50
51
|
return resolveFontId(source, importer);
|
|
51
52
|
},
|
|
52
53
|
load(id) {
|
|
53
54
|
if (id === "\0streamdeck-react:noop-devtools") return NOOP_DEVTOOLS_CODE;
|
|
55
|
+
if (id === "\0streamdeck-react:takumi-native") return TAKUMI_NATIVE_LOADER_CODE;
|
|
54
56
|
return loadFont(id);
|
|
55
57
|
},
|
|
56
58
|
writeBundle() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fcannizzaro/streamdeck-react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Build Stream Deck plugins with React — render components directly to keys, dials, and touch screens",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"elgato",
|
|
@@ -60,29 +60,35 @@
|
|
|
60
60
|
"test": "bun test ./src"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@
|
|
64
|
-
"@takumi-rs/
|
|
65
|
-
"
|
|
66
|
-
"react-reconciler": "^0.33.0",
|
|
67
|
-
"xxhash-wasm": "^1.1.0"
|
|
63
|
+
"@takumi-rs/core": "^0.73.1",
|
|
64
|
+
"@takumi-rs/helpers": "^0.73.1",
|
|
65
|
+
"react-reconciler": "^0.33.0"
|
|
68
66
|
},
|
|
69
67
|
"devDependencies": {
|
|
70
|
-
"@happy-dom/global-registrator": "^20.8.
|
|
68
|
+
"@happy-dom/global-registrator": "^20.8.4",
|
|
71
69
|
"@types/react": "^19.2.14",
|
|
72
70
|
"@types/react-dom": "^19.2.3",
|
|
73
71
|
"@types/react-reconciler": "^0.33.0",
|
|
74
72
|
"react-dom": "^19.2.4",
|
|
75
73
|
"rollup": "^4.59.0",
|
|
76
|
-
"vite": "8.0.0
|
|
74
|
+
"vite": "8.0.0",
|
|
77
75
|
"vite-plugin-dts": "^4.5.4"
|
|
78
76
|
},
|
|
79
77
|
"peerDependencies": {
|
|
78
|
+
"@elgato/streamdeck": "^2.0.2",
|
|
79
|
+
"@takumi-rs/wasm": "^0.71.7",
|
|
80
80
|
"react": "^18.0.0 || ^19.0.0",
|
|
81
81
|
"rollup": "^4.0.0",
|
|
82
82
|
"typescript": "^5",
|
|
83
83
|
"vite": "^7.0.0 || ^8.0.0"
|
|
84
84
|
},
|
|
85
85
|
"peerDependenciesMeta": {
|
|
86
|
+
"@elgato/streamdeck": {
|
|
87
|
+
"optional": true
|
|
88
|
+
},
|
|
89
|
+
"@takumi-rs/wasm": {
|
|
90
|
+
"optional": true
|
|
91
|
+
},
|
|
86
92
|
"rollup": {
|
|
87
93
|
"optional": true
|
|
88
94
|
},
|