@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/render/pipeline.js
CHANGED
|
@@ -1,62 +1,329 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { fnv1a } from "./cache.js";
|
|
1
|
+
import { isContainerDirty } from "../reconciler/vnode.js";
|
|
2
|
+
import { computeCacheKey, computeTreeHash, fnv1a } from "./cache.js";
|
|
3
|
+
import { getBufferPool } from "./buffer-pool.js";
|
|
3
4
|
import { encodePng } from "./png.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { getImageCache, getTouchStripCache } from "./image-cache.js";
|
|
6
|
+
import { metrics } from "./metrics.js";
|
|
7
|
+
import { serializeSvgTree } from "./svg.js";
|
|
6
8
|
//#region src/render/pipeline.ts
|
|
9
|
+
var ROOT_STYLE = {
|
|
10
|
+
display: "flex",
|
|
11
|
+
width: "100%",
|
|
12
|
+
height: "100%"
|
|
13
|
+
};
|
|
14
|
+
/** Warn threshold for consecutive identical renders in debug mode. */
|
|
15
|
+
var DUP_RENDER_WARN_THRESHOLD = 3;
|
|
16
|
+
/** Maximum recommended VNode tree depth. Warn in debug mode when exceeded. */
|
|
17
|
+
var MAX_DEPTH_WARN = 25;
|
|
18
|
+
/** Whether we've already warned about tree depth (avoid log spam). */
|
|
19
|
+
var depthWarned = false;
|
|
20
|
+
var SKIP_PROPS = new Set([
|
|
21
|
+
"children",
|
|
22
|
+
"className",
|
|
23
|
+
"src",
|
|
24
|
+
"tw"
|
|
25
|
+
]);
|
|
26
|
+
function copyPropsToNode(target, props) {
|
|
27
|
+
for (const key of Object.keys(props)) if (!SKIP_PROPS.has(key)) target[key] = props[key];
|
|
28
|
+
}
|
|
29
|
+
function vnodeToTakumiNode(node, depth = 0) {
|
|
30
|
+
if (!depthWarned && depth > MAX_DEPTH_WARN) {
|
|
31
|
+
depthWarned = true;
|
|
32
|
+
console.warn(`[@fcannizzaro/streamdeck-react] VNode tree depth ${depth} exceeds recommended limit ${MAX_DEPTH_WARN}. Deep nesting increases render cost.`);
|
|
33
|
+
}
|
|
34
|
+
if (node.type === "#text") return {
|
|
35
|
+
type: "text",
|
|
36
|
+
text: node.text ?? ""
|
|
37
|
+
};
|
|
38
|
+
const props = node.props;
|
|
39
|
+
const rawTw = typeof props.tw === "string" ? props.tw : void 0;
|
|
40
|
+
const className = props.className;
|
|
41
|
+
let tw = rawTw;
|
|
42
|
+
if (typeof className === "string" && className.length > 0) tw = tw ? tw + " " + className : className;
|
|
43
|
+
if (node.type === "img" && typeof props.src === "string") {
|
|
44
|
+
const result = {
|
|
45
|
+
type: "image",
|
|
46
|
+
src: props.src
|
|
47
|
+
};
|
|
48
|
+
if (tw) result.tw = tw;
|
|
49
|
+
copyPropsToNode(result, props);
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
if (node.type === "svg") {
|
|
53
|
+
const result = {
|
|
54
|
+
type: "image",
|
|
55
|
+
src: serializeSvgTree(node),
|
|
56
|
+
tagName: "svg"
|
|
57
|
+
};
|
|
58
|
+
const width = typeof props.width === "number" ? props.width : void 0;
|
|
59
|
+
const height = typeof props.height === "number" ? props.height : void 0;
|
|
60
|
+
if (width != null) result.width = width;
|
|
61
|
+
if (height != null) result.height = height;
|
|
62
|
+
if (tw) result.tw = tw;
|
|
63
|
+
if (props.style) result.style = props.style;
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
const result = { type: "container" };
|
|
67
|
+
if (tw) result.tw = tw;
|
|
68
|
+
copyPropsToNode(result, props);
|
|
69
|
+
if (node.children.length > 0) result.children = node.children.map((child) => vnodeToTakumiNode(child, depth + 1));
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
/** Build the root Takumi container wrapping the VNode children. */
|
|
73
|
+
function buildTakumiRoot(container) {
|
|
74
|
+
return {
|
|
75
|
+
type: "container",
|
|
76
|
+
style: ROOT_STYLE,
|
|
77
|
+
children: container.children.map(vnodeToTakumiNode)
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function measureTree(nodes) {
|
|
81
|
+
let maxDepth = 0;
|
|
82
|
+
let count = 0;
|
|
83
|
+
function walk(children, depth) {
|
|
84
|
+
for (const child of children) {
|
|
85
|
+
count++;
|
|
86
|
+
if (depth > maxDepth) maxDepth = depth;
|
|
87
|
+
walk(child.children, depth + 1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
walk(nodes, 1);
|
|
91
|
+
return {
|
|
92
|
+
depth: maxDepth,
|
|
93
|
+
count
|
|
94
|
+
};
|
|
95
|
+
}
|
|
7
96
|
function bufferToDataUri(buffer, format) {
|
|
8
97
|
return `data:image/${format};base64,${(Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer)).toString("base64")}`;
|
|
9
98
|
}
|
|
99
|
+
function emitProfile(config, times, opts) {
|
|
100
|
+
const stats = measureTree(opts.container.children);
|
|
101
|
+
const cache = config.imageCacheMaxBytes > 0 ? getImageCache(config.imageCacheMaxBytes) : null;
|
|
102
|
+
config.onProfile({
|
|
103
|
+
vnodeConversionMs: times.t1 - times.t0,
|
|
104
|
+
takumiRenderMs: times.t2 - times.t1,
|
|
105
|
+
hashMs: times.t3 - times.t2,
|
|
106
|
+
base64Ms: 0,
|
|
107
|
+
totalMs: times.t3 - times.t0,
|
|
108
|
+
skipped: opts.skipped,
|
|
109
|
+
cacheHit: opts.cacheHit,
|
|
110
|
+
treeDepth: stats.depth,
|
|
111
|
+
nodeCount: stats.count,
|
|
112
|
+
cacheStats: cache?.stats ?? null
|
|
113
|
+
});
|
|
114
|
+
}
|
|
10
115
|
async function renderToDataUri(container, width, height, config) {
|
|
11
116
|
if (container.children.length === 0) return null;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
117
|
+
metrics.recordFlush();
|
|
118
|
+
if (config.caching && !isContainerDirty(container)) {
|
|
119
|
+
metrics.recordDirtySkip();
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const profiling = config.onProfile != null;
|
|
123
|
+
const t0 = profiling ? performance.now() : 0;
|
|
124
|
+
let t1 = t0;
|
|
125
|
+
let t2 = t0;
|
|
126
|
+
let treeHash;
|
|
127
|
+
let cacheKey;
|
|
128
|
+
if (config.caching && config.imageCacheMaxBytes > 0) {
|
|
129
|
+
treeHash = computeTreeHash(container);
|
|
130
|
+
cacheKey = computeCacheKey(treeHash, width, height, config.devicePixelRatio, config.imageFormat);
|
|
131
|
+
const cached = getImageCache(config.imageCacheMaxBytes).get(cacheKey);
|
|
132
|
+
if (cached !== void 0) {
|
|
133
|
+
metrics.recordCacheHit();
|
|
134
|
+
if (profiling) {
|
|
135
|
+
const tNow = performance.now();
|
|
136
|
+
emitProfile(config, {
|
|
137
|
+
t0,
|
|
138
|
+
t1: tNow,
|
|
139
|
+
t2: tNow,
|
|
140
|
+
t3: tNow
|
|
141
|
+
}, {
|
|
142
|
+
skipped: false,
|
|
143
|
+
cacheHit: true,
|
|
144
|
+
container
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
container._dupCount = 0;
|
|
148
|
+
config.onRender?.(container, cached);
|
|
149
|
+
return cached;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
let buffer;
|
|
153
|
+
if (config.renderPool?.isAvailable) {
|
|
154
|
+
buffer = await config.renderPool.render(container.children, width, height, config.imageFormat, config.devicePixelRatio);
|
|
155
|
+
t2 = profiling ? performance.now() : 0;
|
|
156
|
+
t1 = t0;
|
|
157
|
+
} else {
|
|
158
|
+
const rootNode = buildTakumiRoot(container);
|
|
159
|
+
t1 = profiling ? performance.now() : 0;
|
|
160
|
+
buffer = await config.renderer.render(rootNode, {
|
|
161
|
+
width,
|
|
162
|
+
height,
|
|
163
|
+
format: config.imageFormat,
|
|
164
|
+
devicePixelRatio: config.devicePixelRatio
|
|
165
|
+
});
|
|
166
|
+
t2 = profiling ? performance.now() : 0;
|
|
167
|
+
}
|
|
24
168
|
if (config.caching) {
|
|
25
169
|
const hash = fnv1a(buffer);
|
|
26
|
-
if (hash === container.lastSvgHash)
|
|
170
|
+
if (hash === container.lastSvgHash) {
|
|
171
|
+
metrics.recordHashDedup();
|
|
172
|
+
if (config.debug) {
|
|
173
|
+
container._dupCount++;
|
|
174
|
+
if (container._dupCount > DUP_RENDER_WARN_THRESHOLD) console.warn(`[@fcannizzaro/streamdeck-react] ${container._dupCount} consecutive identical renders — component likely re-rendering without visual change`);
|
|
175
|
+
}
|
|
176
|
+
if (profiling) {
|
|
177
|
+
const tEnd = performance.now();
|
|
178
|
+
emitProfile(config, {
|
|
179
|
+
t0,
|
|
180
|
+
t1,
|
|
181
|
+
t2,
|
|
182
|
+
t3: tEnd
|
|
183
|
+
}, {
|
|
184
|
+
skipped: true,
|
|
185
|
+
cacheHit: false,
|
|
186
|
+
container
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
27
191
|
container.lastSvgHash = hash;
|
|
192
|
+
container._dupCount = 0;
|
|
28
193
|
}
|
|
29
194
|
const dataUri = bufferToDataUri(buffer, config.imageFormat);
|
|
195
|
+
if (config.caching && config.imageCacheMaxBytes > 0) {
|
|
196
|
+
if (treeHash === void 0 || cacheKey === void 0) {
|
|
197
|
+
treeHash = computeTreeHash(container);
|
|
198
|
+
cacheKey = computeCacheKey(treeHash, width, height, config.devicePixelRatio, config.imageFormat);
|
|
199
|
+
}
|
|
200
|
+
getImageCache(config.imageCacheMaxBytes).set(cacheKey, dataUri, dataUri.length * 2 + 64);
|
|
201
|
+
}
|
|
202
|
+
metrics.recordRender(t2 - t0);
|
|
203
|
+
if (profiling) {
|
|
204
|
+
const tEnd = performance.now();
|
|
205
|
+
emitProfile(config, {
|
|
206
|
+
t0,
|
|
207
|
+
t1,
|
|
208
|
+
t2,
|
|
209
|
+
t3: tEnd
|
|
210
|
+
}, {
|
|
211
|
+
skipped: false,
|
|
212
|
+
cacheHit: false,
|
|
213
|
+
container
|
|
214
|
+
});
|
|
215
|
+
}
|
|
30
216
|
config.onRender?.(container, dataUri);
|
|
31
217
|
return dataUri;
|
|
32
218
|
}
|
|
33
219
|
async function renderToRaw(container, width, height, config) {
|
|
34
220
|
if (container.children.length === 0) return null;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
221
|
+
metrics.recordFlush();
|
|
222
|
+
if (config.caching && !isContainerDirty(container)) {
|
|
223
|
+
metrics.recordDirtySkip();
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
const profiling = config.onProfile != null;
|
|
227
|
+
const t0 = profiling ? performance.now() : 0;
|
|
228
|
+
let t1 = t0;
|
|
229
|
+
let t2 = t0;
|
|
230
|
+
let treeHash;
|
|
231
|
+
let cacheKey;
|
|
232
|
+
if (config.caching && config.touchStripCacheMaxBytes > 0) {
|
|
233
|
+
treeHash = computeTreeHash(container);
|
|
234
|
+
cacheKey = computeCacheKey(treeHash, width, height, config.devicePixelRatio, "raw");
|
|
235
|
+
const cached = getTouchStripCache(config.touchStripCacheMaxBytes).get(cacheKey);
|
|
236
|
+
if (cached !== void 0) {
|
|
237
|
+
metrics.recordCacheHit();
|
|
238
|
+
if (profiling) {
|
|
239
|
+
const tNow = performance.now();
|
|
240
|
+
emitProfile(config, {
|
|
241
|
+
t0,
|
|
242
|
+
t1: tNow,
|
|
243
|
+
t2: tNow,
|
|
244
|
+
t3: tNow
|
|
245
|
+
}, {
|
|
246
|
+
skipped: false,
|
|
247
|
+
cacheHit: true,
|
|
248
|
+
container
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
buffer: cached,
|
|
253
|
+
width,
|
|
254
|
+
height
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
let buffer;
|
|
259
|
+
if (config.renderPool?.isAvailable) {
|
|
260
|
+
buffer = await config.renderPool.render(container.children, width, height, "raw", config.devicePixelRatio);
|
|
261
|
+
t2 = profiling ? performance.now() : 0;
|
|
262
|
+
t1 = t0;
|
|
263
|
+
} else {
|
|
264
|
+
const rootNode = buildTakumiRoot(container);
|
|
265
|
+
t1 = profiling ? performance.now() : 0;
|
|
266
|
+
buffer = await config.renderer.render(rootNode, {
|
|
267
|
+
width,
|
|
268
|
+
height,
|
|
269
|
+
format: "raw",
|
|
270
|
+
devicePixelRatio: config.devicePixelRatio
|
|
271
|
+
});
|
|
272
|
+
t2 = profiling ? performance.now() : 0;
|
|
273
|
+
}
|
|
47
274
|
if (config.caching) {
|
|
48
275
|
const hash = fnv1a(buffer);
|
|
49
|
-
if (hash === container.lastSvgHash)
|
|
276
|
+
if (hash === container.lastSvgHash) {
|
|
277
|
+
metrics.recordHashDedup();
|
|
278
|
+
if (profiling) {
|
|
279
|
+
const tEnd = performance.now();
|
|
280
|
+
emitProfile(config, {
|
|
281
|
+
t0,
|
|
282
|
+
t1,
|
|
283
|
+
t2,
|
|
284
|
+
t3: tEnd
|
|
285
|
+
}, {
|
|
286
|
+
skipped: true,
|
|
287
|
+
cacheHit: false,
|
|
288
|
+
container
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
50
293
|
container.lastSvgHash = hash;
|
|
51
294
|
}
|
|
295
|
+
const buf = Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
|
|
296
|
+
if (config.caching && config.touchStripCacheMaxBytes > 0) {
|
|
297
|
+
if (treeHash === void 0 || cacheKey === void 0) {
|
|
298
|
+
treeHash = computeTreeHash(container);
|
|
299
|
+
cacheKey = computeCacheKey(treeHash, width, height, config.devicePixelRatio, "raw");
|
|
300
|
+
}
|
|
301
|
+
getTouchStripCache(config.touchStripCacheMaxBytes).set(cacheKey, buf, buf.byteLength + 64);
|
|
302
|
+
}
|
|
303
|
+
metrics.recordRender(t2 - t0);
|
|
304
|
+
if (profiling) {
|
|
305
|
+
const tEnd = performance.now();
|
|
306
|
+
emitProfile(config, {
|
|
307
|
+
t0,
|
|
308
|
+
t1,
|
|
309
|
+
t2,
|
|
310
|
+
t3: tEnd
|
|
311
|
+
}, {
|
|
312
|
+
skipped: false,
|
|
313
|
+
cacheHit: false,
|
|
314
|
+
container
|
|
315
|
+
});
|
|
316
|
+
}
|
|
52
317
|
return {
|
|
53
|
-
buffer:
|
|
318
|
+
buffer: buf,
|
|
54
319
|
width,
|
|
55
320
|
height
|
|
56
321
|
};
|
|
57
322
|
}
|
|
58
323
|
function cropSlice(raw, fullWidth, column, segmentWidth, segmentHeight) {
|
|
59
|
-
const
|
|
324
|
+
const pool = getBufferPool();
|
|
325
|
+
const sliceSize = segmentWidth * segmentHeight * 4;
|
|
326
|
+
const slice = pool.acquire(sliceSize);
|
|
60
327
|
const srcRowBytes = fullWidth * 4;
|
|
61
328
|
const dstRowBytes = segmentWidth * 4;
|
|
62
329
|
const xOffset = column * segmentWidth * 4;
|
|
@@ -68,7 +335,10 @@ function cropSlice(raw, fullWidth, column, segmentWidth, segmentHeight) {
|
|
|
68
335
|
return slice;
|
|
69
336
|
}
|
|
70
337
|
function sliceToDataUri(raw, fullWidth, fullHeight, column, segmentWidth, segmentHeight) {
|
|
71
|
-
|
|
338
|
+
const cropped = cropSlice(raw, fullWidth, column, segmentWidth, segmentHeight);
|
|
339
|
+
const png = encodePng(segmentWidth, segmentHeight, cropped);
|
|
340
|
+
getBufferPool().release(cropped);
|
|
341
|
+
return bufferToDataUri(png, "png");
|
|
72
342
|
}
|
|
73
343
|
//#endregion
|
|
74
|
-
export { bufferToDataUri, renderToDataUri, renderToRaw, sliceToDataUri };
|
|
344
|
+
export { bufferToDataUri, buildTakumiRoot, measureTree, renderToDataUri, renderToRaw, sliceToDataUri };
|
package/dist/render/png.d.ts
CHANGED
package/dist/render/png.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getBufferPool } from "./buffer-pool.js";
|
|
1
2
|
import { deflateSync } from "node:zlib";
|
|
2
3
|
//#region src/render/png.ts
|
|
3
4
|
var crcTable = new Uint32Array(256);
|
|
@@ -35,14 +36,7 @@ var PNG_SIGNATURE = Buffer.from([
|
|
|
35
36
|
26,
|
|
36
37
|
10
|
|
37
38
|
]);
|
|
38
|
-
|
|
39
|
-
* Encode raw RGBA pixels into a PNG buffer.
|
|
40
|
-
*
|
|
41
|
-
* @param width Image width in pixels.
|
|
42
|
-
* @param height Image height in pixels.
|
|
43
|
-
* @param rgba Raw RGBA pixel data (width × height × 4 bytes, row-major).
|
|
44
|
-
*/
|
|
45
|
-
function encodePng(width, height, rgba) {
|
|
39
|
+
function buildIhdr(width, height) {
|
|
46
40
|
const ihdr = Buffer.alloc(13);
|
|
47
41
|
ihdr.writeUInt32BE(width, 0);
|
|
48
42
|
ihdr.writeUInt32BE(height, 4);
|
|
@@ -51,15 +45,22 @@ function encodePng(width, height, rgba) {
|
|
|
51
45
|
ihdr[10] = 0;
|
|
52
46
|
ihdr[11] = 0;
|
|
53
47
|
ihdr[12] = 0;
|
|
48
|
+
return ihdr;
|
|
49
|
+
}
|
|
50
|
+
function buildFilteredScanlines(width, height, rgba) {
|
|
54
51
|
const rowBytes = width * 4;
|
|
55
|
-
const
|
|
52
|
+
const filteredSize = height * (1 + rowBytes);
|
|
53
|
+
const filtered = getBufferPool().acquire(filteredSize);
|
|
56
54
|
for (let y = 0; y < height; y++) {
|
|
57
55
|
const dstOff = y * (1 + rowBytes);
|
|
58
56
|
filtered[dstOff] = 0;
|
|
59
57
|
const srcOff = y * rowBytes;
|
|
60
|
-
|
|
58
|
+
if (Buffer.isBuffer(rgba)) rgba.copy(filtered, dstOff + 1, srcOff, srcOff + rowBytes);
|
|
59
|
+
else filtered.set(rgba.subarray(srcOff, srcOff + rowBytes), dstOff + 1);
|
|
61
60
|
}
|
|
62
|
-
|
|
61
|
+
return filtered;
|
|
62
|
+
}
|
|
63
|
+
function assemblePng(ihdr, compressed) {
|
|
63
64
|
return Buffer.concat([
|
|
64
65
|
PNG_SIGNATURE,
|
|
65
66
|
pngChunk("IHDR", ihdr),
|
|
@@ -67,5 +68,19 @@ function encodePng(width, height, rgba) {
|
|
|
67
68
|
pngChunk("IEND", Buffer.alloc(0))
|
|
68
69
|
]);
|
|
69
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Encode raw RGBA pixels into a PNG buffer (synchronous).
|
|
73
|
+
*
|
|
74
|
+
* @param width Image width in pixels.
|
|
75
|
+
* @param height Image height in pixels.
|
|
76
|
+
* @param rgba Raw RGBA pixel data (width × height × 4 bytes, row-major).
|
|
77
|
+
*/
|
|
78
|
+
function encodePng(width, height, rgba) {
|
|
79
|
+
const ihdr = buildIhdr(width, height);
|
|
80
|
+
const filtered = buildFilteredScanlines(width, height, rgba);
|
|
81
|
+
const compressed = deflateSync(filtered);
|
|
82
|
+
getBufferPool().release(filtered);
|
|
83
|
+
return assemblePng(ihdr, compressed);
|
|
84
|
+
}
|
|
70
85
|
//#endregion
|
|
71
86
|
export { encodePng };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { FontConfig } from '../types';
|
|
2
|
+
import { VNode } from '../reconciler/vnode';
|
|
3
|
+
export declare class RenderPool {
|
|
4
|
+
private worker;
|
|
5
|
+
private ready;
|
|
6
|
+
private failed;
|
|
7
|
+
private nextId;
|
|
8
|
+
private pending;
|
|
9
|
+
private initPromise;
|
|
10
|
+
private fonts;
|
|
11
|
+
constructor(fonts: FontConfig[]);
|
|
12
|
+
/** Start the worker. Call once during plugin initialization. */
|
|
13
|
+
initialize(): Promise<boolean>;
|
|
14
|
+
private doInitialize;
|
|
15
|
+
/** Whether the worker is available for offloaded rendering. */
|
|
16
|
+
get isAvailable(): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Render VNode children in the worker thread.
|
|
19
|
+
* Returns the raw raster buffer.
|
|
20
|
+
*/
|
|
21
|
+
render(vnodes: VNode[], width: number, height: number, format: string, dpr: number): Promise<Buffer>;
|
|
22
|
+
private handleResponse;
|
|
23
|
+
private handleWorkerDeath;
|
|
24
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
//#region src/render/render-pool.ts
|
|
3
|
+
/** Strip internal fields and function props from a VNode tree for worker transfer. */
|
|
4
|
+
function serializeVNode(node) {
|
|
5
|
+
const cleanProps = {};
|
|
6
|
+
for (const [key, value] of Object.entries(node.props)) if (typeof value !== "function" && typeof value !== "symbol") cleanProps[key] = value;
|
|
7
|
+
return {
|
|
8
|
+
type: node.type,
|
|
9
|
+
props: cleanProps,
|
|
10
|
+
children: node.children.map(serializeVNode),
|
|
11
|
+
text: node.text
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
var RenderPool = class {
|
|
15
|
+
worker = null;
|
|
16
|
+
ready = false;
|
|
17
|
+
failed = false;
|
|
18
|
+
nextId = 0;
|
|
19
|
+
pending = /* @__PURE__ */ new Map();
|
|
20
|
+
initPromise = null;
|
|
21
|
+
fonts;
|
|
22
|
+
constructor(fonts) {
|
|
23
|
+
this.fonts = fonts;
|
|
24
|
+
}
|
|
25
|
+
/** Start the worker. Call once during plugin initialization. */
|
|
26
|
+
async initialize() {
|
|
27
|
+
if (this.ready) return true;
|
|
28
|
+
if (this.failed) return false;
|
|
29
|
+
if (this.initPromise != null) {
|
|
30
|
+
await this.initPromise;
|
|
31
|
+
return this.ready;
|
|
32
|
+
}
|
|
33
|
+
this.initPromise = this.doInitialize();
|
|
34
|
+
await this.initPromise;
|
|
35
|
+
return this.ready;
|
|
36
|
+
}
|
|
37
|
+
async doInitialize() {
|
|
38
|
+
try {
|
|
39
|
+
this.worker = new Worker(new URL("data:video/mp2t;base64,Ly8g4pSA4pSAIFJlbmRlciBXb3JrZXIg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACi8vCi8vIFJ1bnMgaW4gYSBzZXBhcmF0ZSB0aHJlYWQgdmlhIE5vZGUuanMgd29ya2VyX3RocmVhZHMuICBIYW5kbGVzIHRoZQovLyBmdWxsIHJlbmRlciBwaXBlbGluZTogc2VyaWFsaXplZCBWTm9kZSBkYXRhIOKGkiBUYWt1bWkgbm9kZXMg4oaSIHJhc3Rlci4KLy8KLy8gVXNlcyB0aGUgZGlyZWN0IFZOb2RlIOKGkiBUYWt1bWkgbm9kZSBieXBhc3MgKHNhbWUgYXMgbWFpbiB0aHJlYWQncwovLyB2bm9kZVRvVGFrdW1pTm9kZSBpbiBwaXBlbGluZS50cyksIHNraXBwaW5nIHZub2RlVG9FbGVtZW50KCkgYW5kCi8vIGZyb21Kc3goKSBlbnRpcmVseS4KLy8KLy8gVGhpcyB1bmJsb2NrcyB0aGUgbWFpbiB0aHJlYWQgZHVyaW5nIHRoZSBleHBlbnNpdmUgVGFrdW1pCi8vIHJhc3Rlcml6YXRpb24gc3RlcCAofjXigJMzMG1zIHBlciBmcmFtZSkuCi8vCi8vIFdoeSBjb2RlIGlzIGR1cGxpY2F0ZWQgKGlubGluZWQpOgovLyAgIFdvcmtlciB0aHJlYWRzIGNhbid0IGltcG9ydCBmcm9tIHRoZSBtYWluIGJ1bmRsZSDigJQgdGhleSBsb2FkCi8vICAgdGhlIGNvbXBpbGVkIHdvcmtlci5qcyBmaWxlIGluZGVwZW5kZW50bHkuICBTVkcgc2VyaWFsaXphdGlvbgovLyAgIGFuZCBWTm9kZeKGklRha3VtaSBjb252ZXJzaW9uIG11c3QgYmUgc2VsZi1jb250YWluZWQgaGVyZS4KLy8gICBCb3RoIG1pcnJvciB0aGUgbG9naWMgaW4gc3ZnLnRzIGFuZCBwaXBlbGluZS50cyByZXNwZWN0aXZlbHkuCi8vCi8vIFplcm8tY29weSByZXR1cm46Ci8vICAgVGhlIHJlbmRlcmVkIGJ1ZmZlciBpcyB0cmFuc2ZlcnJlZCAobm90IGNvcGllZCkgYmFjayB0byB0aGUgbWFpbgovLyAgIHRocmVhZCB2aWEgcG9zdE1lc3NhZ2UncyB0cmFuc2ZlciBsaXN0LiAgVGhpcyBhdm9pZHMgY29weWluZwovLyAgIHBvdGVudGlhbGx5IGxhcmdlIHJhc3RlciBidWZmZXJzIChlLmcuIDgwMMOXMTAww5c0ID0gMzIwS0IgZm9yCi8vICAgVG91Y2hTdHJpcCkgYWNyb3NzIHRoZSB0aHJlYWQgYm91bmRhcnkuCgppbXBvcnQgeyBwYXJlbnRQb3J0LCB3b3JrZXJEYXRhIH0gZnJvbSAibm9kZTp3b3JrZXJfdGhyZWFkcyI7CgovLyDilIDilIAgVHlwZXMg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACgppbnRlcmZhY2UgU2VyaWFsaXplZFZOb2RlIHsKICB0eXBlOiBzdHJpbmc7CiAgcHJvcHM6IFJlY29yZDxzdHJpbmcsIHVua25vd24+OwogIGNoaWxkcmVuOiBTZXJpYWxpemVkVk5vZGVbXTsKICB0ZXh0Pzogc3RyaW5nOwp9CgovKiogTWF0Y2hlcyBAdGFrdW1pLXJzL2hlbHBlcnMgTm9kZSB1bmlvbi4gUGxhaW4gb2JqZWN0IGFjY2VwdGVkIGJ5IFJlbmRlcmVyLnJlbmRlcigpLiAqLwppbnRlcmZhY2UgVGFrdW1pTm9kZSB7CiAgdHlwZTogc3RyaW5nOwogIFtrZXk6IHN0cmluZ106IHVua25vd247Cn0KCmludGVyZmFjZSBJbml0TWVzc2FnZSB7CiAgdHlwZTogImluaXQiOwogIGZvbnRzOiBBcnJheTx7CiAgICBuYW1lOiBzdHJpbmc7CiAgICBkYXRhOiBBcnJheUJ1ZmZlciB8IEJ1ZmZlcjsKICAgIHdlaWdodDogbnVtYmVyOwogICAgc3R5bGU6IHN0cmluZzsKICB9PjsKfQoKaW50ZXJmYWNlIFJlbmRlck1lc3NhZ2UgewogIHR5cGU6ICJyZW5kZXIiOwogIGlkOiBudW1iZXI7CiAgdm5vZGVzOiBTZXJpYWxpemVkVk5vZGVbXTsKICB3aWR0aDogbnVtYmVyOwogIGhlaWdodDogbnVtYmVyOwogIGZvcm1hdDogc3RyaW5nOwogIGRwcjogbnVtYmVyOwp9CgppbnRlcmZhY2UgU2h1dGRvd25NZXNzYWdlIHsKICB0eXBlOiAic2h1dGRvd24iOwp9Cgp0eXBlIFdvcmtlck1lc3NhZ2UgPSBJbml0TWVzc2FnZSB8IFJlbmRlck1lc3NhZ2UgfCBTaHV0ZG93bk1lc3NhZ2U7CgovLyDilIDilIAgU1ZHIFNlcmlhbGl6YXRpb24gKGlubGluZWQgZm9yIHdvcmtlciBjb250ZXh0KSDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKLy8gTWlycm9ycyB0aGUgc2VyaWFsaXplU3ZnVHJlZSgpIGZyb20gc3ZnLnRzLiBJbmxpbmVkIHRvIGF2b2lkCi8vIGNyb3NzLW1vZHVsZSBpbXBvcnQgaXNzdWVzIGluIHRoZSB3b3JrZXIgdGhyZWFkLgoKY29uc3QgU1ZHX0NBTUVMX0FUVFJTOiBSZWFkb25seVNldDxzdHJpbmc+ID0gbmV3IFNldChbCiAgImFjY2VudEhlaWdodCIsCiAgImFsaWdubWVudEJhc2VsaW5lIiwKICAiYXJhYmljRm9ybSIsCiAgImJhc2VsaW5lU2hpZnQiLAogICJjYXBIZWlnaHQiLAogICJjbGlwUGF0aCIsCiAgImNsaXBQYXRoVW5pdHMiLAogICJjbGlwUnVsZSIsCiAgImNvbG9ySW50ZXJwb2xhdGlvbiIsCiAgImNvbG9ySW50ZXJwb2xhdGlvbkZpbHRlcnMiLAogICJjb2xvclByb2ZpbGUiLAogICJjb2xvclJlbmRlcmluZyIsCiAgImVuYWJsZUJhY2tncm91bmQiLAogICJmaWxsT3BhY2l0eSIsCiAgImZpbGxSdWxlIiwKICAiZmxvb2RDb2xvciIsCiAgImZsb29kT3BhY2l0eSIsCiAgImZvbnRGYW1pbHkiLAogICJmb250U2l6ZSIsCiAgImZvbnRTaXplQWRqdXN0IiwKICAiZm9udFN0cmV0Y2giLAogICJmb250U3R5bGUiLAogICJmb250VmFyaWFudCIsCiAgImZvbnRXZWlnaHQiLAogICJnbHlwaE5hbWUiLAogICJnbHlwaE9yaWVudGF0aW9uSG9yaXpvbnRhbCIsCiAgImdseXBoT3JpZW50YXRpb25WZXJ0aWNhbCIsCiAgImhvcml6QWR2WCIsCiAgImhvcml6T3JpZ2luWCIsCiAgImltYWdlUmVuZGVyaW5nIiwKICAibGV0dGVyU3BhY2luZyIsCiAgImxpZ2h0aW5nQ29sb3IiLAogICJtYXJrZXJFbmQiLAogICJtYXJrZXJNaWQiLAogICJtYXJrZXJTdGFydCIsCiAgIm92ZXJsaW5lUG9zaXRpb24iLAogICJvdmVybGluZVRoaWNrbmVzcyIsCiAgInBhaW50T3JkZXIiLAogICJwb2ludGVyRXZlbnRzIiwKICAicHJlc2VydmVBc3BlY3RSYXRpbyIsCiAgInNoYXBlUmVuZGVyaW5nIiwKICAic3RvcENvbG9yIiwKICAic3RvcE9wYWNpdHkiLAogICJzdHJva2VEYXNoYXJyYXkiLAogICJzdHJva2VEYXNob2Zmc2V0IiwKICAic3Ryb2tlTGluZWNhcCIsCiAgInN0cm9rZUxpbmVqb2luIiwKICAic3Ryb2tlTWl0ZXJsaW1pdCIsCiAgInN0cm9rZU9wYWNpdHkiLAogICJzdHJva2VXaWR0aCIsCiAgInRleHRBbmNob3IiLAogICJ0ZXh0RGVjb3JhdGlvbiIsCiAgInRleHRSZW5kZXJpbmciLAogICJ0cmFuc2Zvcm1PcmlnaW4iLAogICJ1bmRlcmxpbmVQb3NpdGlvbiIsCiAgInVuZGVybGluZVRoaWNrbmVzcyIsCiAgInVuaWNvZGVCaWRpIiwKICAidW5pY29kZVJhbmdlIiwKICAidW5pdHNQZXJFbSIsCiAgInZBbHBoYWJldGljIiwKICAidkhhbmdpbmciLAogICJ2SWRlb2dyYXBoaWMiLAogICJ2TWF0aGVtYXRpY2FsIiwKICAidmVjdG9yRWZmZWN0IiwKICAidmVydEFkdlkiLAogICJ2ZXJ0T3JpZ2luWCIsCiAgInZlcnRPcmlnaW5ZIiwKICAid29yZFNwYWNpbmciLAogICJ3cml0aW5nTW9kZSIsCl0pOwoKY29uc3QgU1ZHX1NLSVBfUFJPUFM6IFJlYWRvbmx5U2V0PHN0cmluZz4gPSBuZXcgU2V0KFsKICAiY2hpbGRyZW4iLAogICJrZXkiLAogICJyZWYiLAogICJfX3NlbGYiLAogICJfX3NvdXJjZSIsCl0pOwoKZnVuY3Rpb24gY2FtZWxUb0tlYmFiKHN0cjogc3RyaW5nKTogc3RyaW5nIHsKICByZXR1cm4gc3RyLnJlcGxhY2UoL1tBLVpdL2csIChjaCkgPT4gYC0ke2NoLnRvTG93ZXJDYXNlKCl9YCk7Cn0KCmZ1bmN0aW9uIGVzY2FwZUF0dHIodmFsdWU6IHN0cmluZyk6IHN0cmluZyB7CiAgcmV0dXJuIHZhbHVlCiAgICAucmVwbGFjZSgvJi9nLCAiJmFtcDsiKQogICAgLnJlcGxhY2UoLyIvZywgIiZxdW90OyIpCiAgICAucmVwbGFjZSgvPC9nLCAiJmx0OyIpCiAgICAucmVwbGFjZSgvPi9nLCAiJmd0OyIpOwp9CgpmdW5jdGlvbiBzZXJpYWxpemVTdmdTdHlsZShzdHlsZTogUmVjb3JkPHN0cmluZywgdW5rbm93bj4pOiBzdHJpbmcgewogIGNvbnN0IHBhcnRzOiBzdHJpbmdbXSA9IFtdOwogIGZvciAoY29uc3Qga2V5IG9mIE9iamVjdC5rZXlzKHN0eWxlKSkgewogICAgY29uc3QgdmFsdWUgPSBzdHlsZVtrZXldOwogICAgaWYgKHZhbHVlID09IG51bGwpIGNvbnRpbnVlOwogICAgcGFydHMucHVzaChgJHtjYW1lbFRvS2ViYWIoa2V5KX06JHtTdHJpbmcodmFsdWUpLnRyaW0oKX1gKTsKICB9CiAgcmV0dXJuIHBhcnRzLmpvaW4oIjsiKTsKfQoKZnVuY3Rpb24gc2VyaWFsaXplU3ZnQXR0cihrZXk6IHN0cmluZywgdmFsdWU6IHVua25vd24pOiBzdHJpbmcgfCBudWxsIHsKICBpZiAoU1ZHX1NLSVBfUFJPUFMuaGFzKGtleSkgfHwgdmFsdWUgPT0gbnVsbCkgcmV0dXJuIG51bGw7CiAgbGV0IGF0dHJOYW1lOiBzdHJpbmc7CiAgaWYgKGtleSA9PT0gImNsYXNzTmFtZSIpIGF0dHJOYW1lID0gImNsYXNzIjsKICBlbHNlIGlmIChTVkdfQ0FNRUxfQVRUUlMuaGFzKGtleSkpIGF0dHJOYW1lID0gY2FtZWxUb0tlYmFiKGtleSk7CiAgZWxzZSBhdHRyTmFtZSA9IGtleTsKICBpZiAoa2V5ID09PSAic3R5bGUiICYmIHR5cGVvZiB2YWx1ZSA9PT0gIm9iamVjdCIpIHsKICAgIGNvbnN0IGNzcyA9IHNlcmlhbGl6ZVN2Z1N0eWxlKHZhbHVlIGFzIFJlY29yZDxzdHJpbmcsIHVua25vd24+KTsKICAgIGlmICghY3NzKSByZXR1cm4gbnVsbDsKICAgIHJldHVybiBgJHthdHRyTmFtZX09IiR7ZXNjYXBlQXR0cihjc3MpfSJgOwogIH0KICBpZiAodHlwZW9mIHZhbHVlID09PSAiYm9vbGVhbiIpIHJldHVybiBgJHthdHRyTmFtZX09IiR7U3RyaW5nKHZhbHVlKX0iYDsKICByZXR1cm4gYCR7YXR0ck5hbWV9PSIke2VzY2FwZUF0dHIoU3RyaW5nKHZhbHVlKSl9ImA7Cn0KCmZ1bmN0aW9uIHNlcmlhbGl6ZVN2Z1ZOb2RlKG5vZGU6IFNlcmlhbGl6ZWRWTm9kZSk6IHN0cmluZyB7CiAgaWYgKG5vZGUudHlwZSA9PT0gIiN0ZXh0IikgcmV0dXJuIG5vZGUudGV4dCA/PyAiIjsKICBjb25zdCBhdHRyczogc3RyaW5nW10gPSBbXTsKICBmb3IgKGNvbnN0IFtrZXksIHZhbHVlXSBvZiBPYmplY3QuZW50cmllcyhub2RlLnByb3BzKSkgewogICAgY29uc3QgYXR0ciA9IHNlcmlhbGl6ZVN2Z0F0dHIoa2V5LCB2YWx1ZSk7CiAgICBpZiAoYXR0ciAhPSBudWxsKSBhdHRycy5wdXNoKGF0dHIpOwogIH0KICBjb25zdCBjaGlsZE1hcmt1cCA9IG5vZGUuY2hpbGRyZW4ubWFwKHNlcmlhbGl6ZVN2Z1ZOb2RlKS5qb2luKCIiKTsKICBjb25zdCBhdHRyU3RyID0gYXR0cnMubGVuZ3RoID4gMCA/IGAgJHthdHRycy5qb2luKCIgIil9YCA6ICIiOwogIHJldHVybiBgPCR7bm9kZS50eXBlfSR7YXR0clN0cn0+JHtjaGlsZE1hcmt1cH08LyR7bm9kZS50eXBlfT5gOwp9CgpmdW5jdGlvbiBzZXJpYWxpemVTdmdUcmVlKHN2Z05vZGU6IFNlcmlhbGl6ZWRWTm9kZSk6IHN0cmluZyB7CiAgaWYgKCEoInhtbG5zIiBpbiBzdmdOb2RlLnByb3BzKSkgewogICAgY29uc3QgYXVnbWVudGVkID0gewogICAgICAuLi5zdmdOb2RlLAogICAgICBwcm9wczogeyAuLi5zdmdOb2RlLnByb3BzLCB4bWxuczogImh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB9LAogICAgfTsKICAgIHJldHVybiBzZXJpYWxpemVTdmdWTm9kZShhdWdtZW50ZWQpOwogIH0KICByZXR1cm4gc2VyaWFsaXplU3ZnVk5vZGUoc3ZnTm9kZSk7Cn0KCi8vIOKUgOKUgCBEaXJlY3QgVk5vZGUg4oaSIFRha3VtaSBOb2RlIENvbnZlcnNpb24g4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACi8vIE1pcnJvcnMgdGhlIG1haW4tdGhyZWFkIHZub2RlVG9UYWt1bWlOb2RlKCkgZnJvbSBwaXBlbGluZS50cy4KLy8gSW5saW5lZCB0byBhdm9pZCBjcm9zcy1tb2R1bGUgaW1wb3J0IGlzc3VlcyBpbiB3b3JrZXIgY29udGV4dC4KCmZ1bmN0aW9uIHZub2RlVG9UYWt1bWlOb2RlKG5vZGU6IFNlcmlhbGl6ZWRWTm9kZSk6IFRha3VtaU5vZGUgewogIC8vIFRleHQgbm9kZXMg4oaSIFRha3VtaSBUZXh0Tm9kZQogIGlmIChub2RlLnR5cGUgPT09ICIjdGV4dCIpIHsKICAgIHJldHVybiB7IHR5cGU6ICJ0ZXh0IiwgdGV4dDogbm9kZS50ZXh0ID8/ICIiIH07CiAgfQoKICBjb25zdCB7IGNoaWxkcmVuOiBfY2hpbGRyZW4sIGNsYXNzTmFtZSwgc3JjLCAuLi5yZXN0UHJvcHMgfSA9IG5vZGUucHJvcHM7CgogIC8vIE1hcCBjbGFzc05hbWUg4oaSIHR3IChzYW1lIGxvZ2ljIGFzIG1haW4gdGhyZWFkKQogIGxldCB0dzogc3RyaW5nIHwgdW5kZWZpbmVkID0gdHlwZW9mIHJlc3RQcm9wcy50dyA9PT0gInN0cmluZyIgPyByZXN0UHJvcHMudHcgOiB1bmRlZmluZWQ7CiAgaWYgKHR5cGVvZiBjbGFzc05hbWUgPT09ICJzdHJpbmciICYmIGNsYXNzTmFtZS5sZW5ndGggPiAwKSB7CiAgICB0dyA9IHR3ID8gdHcgKyAiICIgKyBjbGFzc05hbWUgOiBjbGFzc05hbWU7CiAgfQoKICAvLyBJbWFnZSBub2RlcyDihpIgVGFrdW1pIEltYWdlTm9kZQogIGlmIChub2RlLnR5cGUgPT09ICJpbWciICYmIHR5cGVvZiBzcmMgPT09ICJzdHJpbmciKSB7CiAgICByZXR1cm4gewogICAgICB0eXBlOiAiaW1hZ2UiLAogICAgICBzcmM6IHNyYyBhcyBzdHJpbmcsCiAgICAgIC4uLih0dyA/IHsgdHcgfSA6IHt9KSwKICAgICAgLi4ucmVzdFByb3BzLAogICAgfTsKICB9CgogIC8vIFNWRyBub2RlcyDihpIgVGFrdW1pIEltYWdlTm9kZSAoc2VyaWFsaXplIHN1YnRyZWUgdG8gU1ZHIG1hcmt1cCkKICBpZiAobm9kZS50eXBlID09PSAic3ZnIikgewogICAgY29uc3Qgc3ZnTWFya3VwID0gc2VyaWFsaXplU3ZnVHJlZShub2RlKTsKICAgIGNvbnN0IHdpZHRoID0gdHlwZW9mIG5vZGUucHJvcHMud2lkdGggPT09ICJudW1iZXIiID8gbm9kZS5wcm9wcy53aWR0aCA6IHVuZGVmaW5lZDsKICAgIGNvbnN0IGhlaWdodCA9IHR5cGVvZiBub2RlLnByb3BzLmhlaWdodCA9PT0gIm51bWJlciIgPyBub2RlLnByb3BzLmhlaWdodCA6IHVuZGVmaW5lZDsKICAgIHJldHVybiB7CiAgICAgIHR5cGU6ICJpbWFnZSIsCiAgICAgIHNyYzogc3ZnTWFya3VwLAogICAgICAuLi4od2lkdGggIT0gbnVsbCA/IHsgd2lkdGggfSA6IHt9KSwKICAgICAgLi4uKGhlaWdodCAhPSBudWxsID8geyBoZWlnaHQgfSA6IHt9KSwKICAgICAgLi4uKHR3ID8geyB0dyB9IDoge30pLAogICAgICAuLi4obm9kZS5wcm9wcy5zdHlsZSA/IHsgc3R5bGU6IG5vZGUucHJvcHMuc3R5bGUgfSA6IHt9KSwKICAgICAgdGFnTmFtZTogInN2ZyIsCiAgICB9OwogIH0KCiAgLy8gQWxsIG90aGVyIG5vZGVzIOKGkiBUYWt1bWkgQ29udGFpbmVyTm9kZQogIGNvbnN0IHRha3VtaUNoaWxkcmVuID0KICAgIG5vZGUuY2hpbGRyZW4ubGVuZ3RoID4gMCA/IG5vZGUuY2hpbGRyZW4ubWFwKHZub2RlVG9UYWt1bWlOb2RlKSA6IHVuZGVmaW5lZDsKCiAgcmV0dXJuIHsKICAgIHR5cGU6ICJjb250YWluZXIiLAogICAgLi4uKHR3ID8geyB0dyB9IDoge30pLAogICAgLi4ucmVzdFByb3BzLAogICAgLi4uKHRha3VtaUNoaWxkcmVuID8geyBjaGlsZHJlbjogdGFrdW1pQ2hpbGRyZW4gfSA6IHt9KSwKICB9Owp9CgovLyDilIDilIAgUm9vdCBzdHlsZSBjb25zdGFudCDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIAKCmNvbnN0IFJPT1RfU1RZTEUgPSB7IGRpc3BsYXk6ICJmbGV4Iiwgd2lkdGg6ICIxMDAlIiwgaGVpZ2h0OiAiMTAwJSIgfSBhcyBjb25zdDsKCi8vIOKUgOKUgCBXb3JrZXIgU3RhdGUg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACgpsZXQgcmVuZGVyZXI6IGltcG9ydCgiQHRha3VtaS1ycy9jb3JlIikuUmVuZGVyZXIgfCBudWxsID0gbnVsbDsKCi8vIOKUgOKUgCBNZXNzYWdlIEhhbmRsZXIg4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSACgphc3luYyBmdW5jdGlvbiBoYW5kbGVNZXNzYWdlKG1zZzogV29ya2VyTWVzc2FnZSk6IFByb21pc2U8dm9pZD4gewogIHN3aXRjaCAobXNnLnR5cGUpIHsKICAgIGNhc2UgImluaXQiOiB7CiAgICAgIHRyeSB7CiAgICAgICAgLy8gRHluYW1pYyBpbXBvcnQg4oCUIG1heSBmYWlsIGlmIHRoZSBuYXRpdmUgYWRkb24gY2FuJ3QgbG9hZCBpbiBhIHdvcmtlcgogICAgICAgIGNvbnN0IGNvcmUgPSBhd2FpdCBpbXBvcnQoIkB0YWt1bWktcnMvY29yZSIpOwoKICAgICAgICByZW5kZXJlciA9IG5ldyBjb3JlLlJlbmRlcmVyKHsKICAgICAgICAgIGZvbnRzOiBtc2cuZm9udHMubWFwKChmKSA9PiAoewogICAgICAgICAgICBuYW1lOiBmLm5hbWUsCiAgICAgICAgICAgIGRhdGE6IGYuZGF0YSwKICAgICAgICAgICAgd2VpZ2h0OiBmLndlaWdodCBhcyAxMDAgfCAyMDAgfCAzMDAgfCA0MDAgfCA1MDAgfCA2MDAgfCA3MDAgfCA4MDAgfCA5MDAsCiAgICAgICAgICAgIHN0eWxlOiBmLnN0eWxlIGFzICJub3JtYWwiIHwgIml0YWxpYyIsCiAgICAgICAgICB9KSksCiAgICAgICAgfSk7CgogICAgICAgIHBhcmVudFBvcnQhLnBvc3RNZXNzYWdlKHsgdHlwZTogInJlYWR5IiB9KTsKICAgICAgfSBjYXRjaCAoZXJyKSB7CiAgICAgICAgcGFyZW50UG9ydCEucG9zdE1lc3NhZ2UoewogICAgICAgICAgdHlwZTogImVycm9yIiwKICAgICAgICAgIGlkOiAtMSwKICAgICAgICAgIGVycm9yOiBgV29ya2VyIGluaXQgZmFpbGVkOiAke2VyciBpbnN0YW5jZW9mIEVycm9yID8gZXJyLm1lc3NhZ2UgOiBTdHJpbmcoZXJyKX1gLAogICAgICAgIH0pOwogICAgICB9CiAgICAgIGJyZWFrOwogICAgfQoKICAgIGNhc2UgInJlbmRlciI6IHsKICAgICAgaWYgKHJlbmRlcmVyID09IG51bGwpIHsKICAgICAgICBwYXJlbnRQb3J0IS5wb3N0TWVzc2FnZSh7CiAgICAgICAgICB0eXBlOiAiZXJyb3IiLAogICAgICAgICAgaWQ6IG1zZy5pZCwKICAgICAgICAgIGVycm9yOiAiV29ya2VyIG5vdCBpbml0aWFsaXplZCIsCiAgICAgICAgfSk7CiAgICAgICAgcmV0dXJuOwogICAgICB9CgogICAgICB0cnkgewogICAgICAgIC8vIDEuIENvbnZlcnQgc2VyaWFsaXplZCBWTm9kZSBkYXRhIOKGkiBUYWt1bWkgbm9kZXMgZGlyZWN0bHkgKGJ5cGFzcyBmcm9tSnN4KQogICAgICAgIGNvbnN0IGNoaWxkcmVuID0gbXNnLnZub2Rlcy5tYXAodm5vZGVUb1Rha3VtaU5vZGUpOwogICAgICAgIGNvbnN0IHJvb3ROb2RlOiBUYWt1bWlOb2RlID0gewogICAgICAgICAgdHlwZTogImNvbnRhaW5lciIsCiAgICAgICAgICBzdHlsZTogUk9PVF9TVFlMRSwKICAgICAgICAgIGNoaWxkcmVuLAogICAgICAgIH07CgogICAgICAgIC8vIDIuIFJlbmRlciB0byByYXN0ZXIgaW1hZ2UKICAgICAgICBjb25zdCBidWZmZXIgPSBhd2FpdCByZW5kZXJlci5yZW5kZXIocm9vdE5vZGUsIHsKICAgICAgICAgIHdpZHRoOiBtc2cud2lkdGgsCiAgICAgICAgICBoZWlnaHQ6IG1zZy5oZWlnaHQsCiAgICAgICAgICBmb3JtYXQ6IG1zZy5mb3JtYXQgYXMgaW1wb3J0KCJAdGFrdW1pLXJzL2NvcmUiKS5PdXRwdXRGb3JtYXQsCiAgICAgICAgICBkZXZpY2VQaXhlbFJhdGlvOiBtc2cuZHByLAogICAgICAgIH0pOwoKICAgICAgICAvLyBUcmFuc2ZlciB0aGUgYnVmZmVyICh6ZXJvLWNvcHkpIGJhY2sgdG8gdGhlIG1haW4gdGhyZWFkCiAgICAgICAgY29uc3QgYWIgPQogICAgICAgICAgYnVmZmVyIGluc3RhbmNlb2YgQXJyYXlCdWZmZXIKICAgICAgICAgICAgPyBidWZmZXIKICAgICAgICAgICAgOiBidWZmZXIuYnVmZmVyLnNsaWNlKGJ1ZmZlci5ieXRlT2Zmc2V0LCBidWZmZXIuYnl0ZU9mZnNldCArIGJ1ZmZlci5ieXRlTGVuZ3RoKTsKCiAgICAgICAgcGFyZW50UG9ydCEucG9zdE1lc3NhZ2UoCiAgICAgICAgICB7IHR5cGU6ICJyZXN1bHQiLCBpZDogbXNnLmlkLCBidWZmZXI6IGFiIH0sCiAgICAgICAgICB7IHRyYW5zZmVyOiBbYWIgYXMgQXJyYXlCdWZmZXJdIH0sCiAgICAgICAgKTsKICAgICAgfSBjYXRjaCAoZXJyKSB7CiAgICAgICAgcGFyZW50UG9ydCEucG9zdE1lc3NhZ2UoewogICAgICAgICAgdHlwZTogImVycm9yIiwKICAgICAgICAgIGlkOiBtc2cuaWQsCiAgICAgICAgICBlcnJvcjogZXJyIGluc3RhbmNlb2YgRXJyb3IgPyBlcnIubWVzc2FnZSA6IFN0cmluZyhlcnIpLAogICAgICAgIH0pOwogICAgICB9CiAgICAgIGJyZWFrOwogICAgfQoKICAgIGNhc2UgInNodXRkb3duIjogewogICAgICBwcm9jZXNzLmV4aXQoMCk7CiAgICB9CiAgfQp9CgovLyDilIDilIAgQXV0by1pbml0IGlmIGZvbnRzIHByb3ZpZGVkIHZpYSB3b3JrZXJEYXRhIOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgAoKaWYgKHdvcmtlckRhdGE/LmZvbnRzKSB7CiAgaGFuZGxlTWVzc2FnZSh7IHR5cGU6ICJpbml0IiwgZm9udHM6IHdvcmtlckRhdGEuZm9udHMgfSk7Cn0KCnBhcmVudFBvcnQhLm9uKCJtZXNzYWdlIiwgKG1zZzogV29ya2VyTWVzc2FnZSkgPT4gewogIGhhbmRsZU1lc3NhZ2UobXNnKTsKfSk7Cg==", "" + import.meta.url), { workerData: { fonts: this.fonts.map((f) => ({
|
|
40
|
+
name: f.name,
|
|
41
|
+
data: f.data,
|
|
42
|
+
weight: f.weight,
|
|
43
|
+
style: f.style
|
|
44
|
+
})) } });
|
|
45
|
+
await new Promise((resolve, reject) => {
|
|
46
|
+
const timeout = setTimeout(() => {
|
|
47
|
+
reject(/* @__PURE__ */ new Error("Worker initialization timed out (5s)"));
|
|
48
|
+
}, 5e3);
|
|
49
|
+
const onMessage = (msg) => {
|
|
50
|
+
if (msg.type === "ready") {
|
|
51
|
+
clearTimeout(timeout);
|
|
52
|
+
this.worker.off("message", onMessage);
|
|
53
|
+
this.worker.off("error", onError);
|
|
54
|
+
resolve();
|
|
55
|
+
} else if (msg.type === "error" && msg.id === -1) {
|
|
56
|
+
clearTimeout(timeout);
|
|
57
|
+
this.worker.off("message", onMessage);
|
|
58
|
+
this.worker.off("error", onError);
|
|
59
|
+
reject(new Error(msg.error ?? "Unknown worker init error"));
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
const onError = (err) => {
|
|
63
|
+
clearTimeout(timeout);
|
|
64
|
+
reject(err);
|
|
65
|
+
};
|
|
66
|
+
this.worker.on("message", onMessage);
|
|
67
|
+
this.worker.on("error", onError);
|
|
68
|
+
});
|
|
69
|
+
this.worker.on("message", this.handleResponse.bind(this));
|
|
70
|
+
this.worker.on("exit", (code) => {
|
|
71
|
+
if (code !== 0) console.warn(`[@fcannizzaro/streamdeck-react] Render worker exited with code ${code}`);
|
|
72
|
+
this.handleWorkerDeath();
|
|
73
|
+
});
|
|
74
|
+
this.worker.on("error", (err) => {
|
|
75
|
+
console.error("[@fcannizzaro/streamdeck-react] Render worker error:", err);
|
|
76
|
+
this.handleWorkerDeath();
|
|
77
|
+
});
|
|
78
|
+
this.ready = true;
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.warn("[@fcannizzaro/streamdeck-react] Worker initialization failed, falling back to main-thread rendering:", err instanceof Error ? err.message : err);
|
|
81
|
+
this.failed = true;
|
|
82
|
+
this.worker?.terminate().catch(() => {});
|
|
83
|
+
this.worker = null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/** Whether the worker is available for offloaded rendering. */
|
|
87
|
+
get isAvailable() {
|
|
88
|
+
return this.ready && this.worker != null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Render VNode children in the worker thread.
|
|
92
|
+
* Returns the raw raster buffer.
|
|
93
|
+
*/
|
|
94
|
+
async render(vnodes, width, height, format, dpr) {
|
|
95
|
+
if (!this.isAvailable) throw new Error("Worker not available");
|
|
96
|
+
const id = this.nextId++;
|
|
97
|
+
const serialized = vnodes.map(serializeVNode);
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
this.pending.set(id, {
|
|
100
|
+
resolve,
|
|
101
|
+
reject
|
|
102
|
+
});
|
|
103
|
+
this.worker.postMessage({
|
|
104
|
+
type: "render",
|
|
105
|
+
id,
|
|
106
|
+
vnodes: serialized,
|
|
107
|
+
width,
|
|
108
|
+
height,
|
|
109
|
+
format,
|
|
110
|
+
dpr
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
handleResponse(msg) {
|
|
115
|
+
if (msg.type === "ready") return;
|
|
116
|
+
const req = this.pending.get(msg.id);
|
|
117
|
+
if (req == null) return;
|
|
118
|
+
this.pending.delete(msg.id);
|
|
119
|
+
if (msg.type === "result" && msg.buffer != null) req.resolve(Buffer.from(msg.buffer));
|
|
120
|
+
else if (msg.type === "error") req.reject(new Error(msg.error ?? "Unknown worker render error"));
|
|
121
|
+
}
|
|
122
|
+
handleWorkerDeath() {
|
|
123
|
+
this.ready = false;
|
|
124
|
+
this.worker = null;
|
|
125
|
+
for (const [_, req] of this.pending) req.reject(/* @__PURE__ */ new Error("Worker died unexpectedly"));
|
|
126
|
+
this.pending.clear();
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
//#endregion
|
|
130
|
+
export { RenderPool };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { VNode } from '../reconciler/vnode';
|
|
2
|
+
/**
|
|
3
|
+
* Serialize an `<svg>` VNode (and its entire subtree) to an SVG markup string.
|
|
4
|
+
* Auto-injects `xmlns="http://www.w3.org/2000/svg"` if not present.
|
|
5
|
+
* The returned string can be used as the `src` of a Takumi ImageNode.
|
|
6
|
+
*/
|
|
7
|
+
export declare function serializeSvgTree(svgNode: VNode): string;
|