@solidtv/renderer 1.0.8 → 1.1.1
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/dist/src/core/CoreNode.js +38 -11
- package/dist/src/core/CoreNode.js.map +1 -1
- package/dist/src/core/CoreTextureManager.js +82 -24
- package/dist/src/core/CoreTextureManager.js.map +1 -1
- package/dist/src/core/Stage.d.ts +23 -2
- package/dist/src/core/Stage.js +44 -8
- package/dist/src/core/Stage.js.map +1 -1
- package/dist/src/core/TextureError.d.ts +2 -1
- package/dist/src/core/TextureError.js +2 -0
- package/dist/src/core/TextureError.js.map +1 -1
- package/dist/src/core/lib/ImageWorker.d.ts +1 -1
- package/dist/src/core/lib/ImageWorker.js +46 -26
- package/dist/src/core/lib/ImageWorker.js.map +1 -1
- package/dist/src/core/text-rendering/SdfTextRenderer.js +2 -2
- package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
- package/dist/src/core/textures/ColorTexture.js +3 -1
- package/dist/src/core/textures/ColorTexture.js.map +1 -1
- package/dist/src/core/textures/ImageTexture.js +24 -16
- package/dist/src/core/textures/ImageTexture.js.map +1 -1
- package/dist/src/main-api/Renderer.d.ts +20 -3
- package/dist/src/main-api/Renderer.js +24 -4
- package/dist/src/main-api/Renderer.js.map +1 -1
- package/dist/tsconfig.dist.tsbuildinfo +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/core/CoreNode.ts +36 -11
- package/src/core/CoreTextureManager.ts +101 -22
- package/src/core/Stage.ts +57 -17
- package/src/core/TextureError.ts +2 -0
- package/src/core/lib/ImageWorker.ts +50 -27
- package/src/core/text-rendering/SdfTextRenderer.ts +3 -2
- package/src/core/textures/ColorTexture.ts +3 -1
- package/src/core/textures/ImageTexture.ts +29 -20
- package/src/main-api/Renderer.ts +32 -3
|
@@ -281,12 +281,21 @@ export class CoreTextureManager extends EventEmitter {
|
|
|
281
281
|
);
|
|
282
282
|
} else {
|
|
283
283
|
console.warn(
|
|
284
|
-
'[Lightning]
|
|
284
|
+
'[Lightning] Image worker count is 0 or workers are not supported on this browser. Image loading will be slower.',
|
|
285
285
|
);
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
this.initialized = true;
|
|
289
289
|
this.emit('initialized');
|
|
290
|
+
|
|
291
|
+
// Anything that arrived before initialization completed is now safe to
|
|
292
|
+
// process. Without this, queued textures would sit until the next frame
|
|
293
|
+
// tick happens to call processSome().
|
|
294
|
+
if (this.uploadTextureQueue.size > 0) {
|
|
295
|
+
this.processSome(Infinity).catch((err) => {
|
|
296
|
+
console.error('Failed to drain pre-init texture queue:', err);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
290
299
|
}
|
|
291
300
|
|
|
292
301
|
/**
|
|
@@ -295,6 +304,9 @@ export class CoreTextureManager extends EventEmitter {
|
|
|
295
304
|
* @param texture - The texture to upload
|
|
296
305
|
*/
|
|
297
306
|
enqueueUploadTexture(texture: Texture): void {
|
|
307
|
+
if (texture.state === 'failed' || texture.state === 'freed') {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
298
310
|
this.uploadTextureQueue.add(texture);
|
|
299
311
|
}
|
|
300
312
|
|
|
@@ -308,7 +320,6 @@ export class CoreTextureManager extends EventEmitter {
|
|
|
308
320
|
textureType: Type,
|
|
309
321
|
props: ExtractProps<TextureMap[Type]>,
|
|
310
322
|
): InstanceType<TextureMap[Type]> {
|
|
311
|
-
let texture: Texture | undefined;
|
|
312
323
|
const TextureClass = this.txConstructors[textureType];
|
|
313
324
|
if (!TextureClass) {
|
|
314
325
|
throw new TextureError(
|
|
@@ -316,20 +327,26 @@ export class CoreTextureManager extends EventEmitter {
|
|
|
316
327
|
`Texture type "${textureType}" is not registered`,
|
|
317
328
|
);
|
|
318
329
|
}
|
|
319
|
-
const resolvedProps = TextureClass.resolveDefaults(props as any);
|
|
320
|
-
const cacheKey = TextureClass.makeCacheKey(resolvedProps as any);
|
|
321
|
-
if (cacheKey && this.keyCache.has(cacheKey)) {
|
|
322
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
323
|
-
texture = this.keyCache.get(cacheKey)!;
|
|
324
|
-
} else {
|
|
325
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
|
|
326
|
-
texture = new TextureClass(this, resolvedProps as any);
|
|
327
330
|
|
|
328
|
-
|
|
329
|
-
|
|
331
|
+
// Cache key is computed from raw props (each Texture's makeCacheKey
|
|
332
|
+
// inlines its own defaults) so we can skip the resolveDefaults
|
|
333
|
+
// allocation on a cache hit.
|
|
334
|
+
const cacheKey = TextureClass.makeCacheKey(props as any);
|
|
335
|
+
if (cacheKey) {
|
|
336
|
+
const cached = this.keyCache.get(cacheKey);
|
|
337
|
+
if (cached) {
|
|
338
|
+
return cached as InstanceType<TextureMap[Type]>;
|
|
330
339
|
}
|
|
331
340
|
}
|
|
332
341
|
|
|
342
|
+
const resolvedProps = TextureClass.resolveDefaults(props as any);
|
|
343
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
|
|
344
|
+
const texture = new TextureClass(this, resolvedProps as any);
|
|
345
|
+
|
|
346
|
+
if (cacheKey) {
|
|
347
|
+
this.initTextureToCache(texture, cacheKey);
|
|
348
|
+
}
|
|
349
|
+
|
|
333
350
|
return texture as InstanceType<TextureMap[Type]>;
|
|
334
351
|
}
|
|
335
352
|
|
|
@@ -381,7 +398,10 @@ export class CoreTextureManager extends EventEmitter {
|
|
|
381
398
|
console.error(`Failed to upload texture:`, err);
|
|
382
399
|
texture.setState(
|
|
383
400
|
'failed',
|
|
384
|
-
new TextureError(
|
|
401
|
+
new TextureError(
|
|
402
|
+
TextureErrorCode.TEXTURE_UPLOAD_FAILED,
|
|
403
|
+
err instanceof Error ? err.message : undefined,
|
|
404
|
+
),
|
|
385
405
|
);
|
|
386
406
|
});
|
|
387
407
|
return;
|
|
@@ -432,7 +452,7 @@ export class CoreTextureManager extends EventEmitter {
|
|
|
432
452
|
}
|
|
433
453
|
|
|
434
454
|
const coreContext = texture.loadCtxTexture();
|
|
435
|
-
if (coreContext
|
|
455
|
+
if (coreContext.state === 'loaded') {
|
|
436
456
|
texture.setState('loaded');
|
|
437
457
|
return;
|
|
438
458
|
}
|
|
@@ -460,24 +480,83 @@ export class CoreTextureManager extends EventEmitter {
|
|
|
460
480
|
const platform = this.platform;
|
|
461
481
|
const startTime = platform.getTimeStamp();
|
|
462
482
|
|
|
463
|
-
//
|
|
483
|
+
// Decode / fetch ("getTextureData") is IO-bound and parallelisable across
|
|
484
|
+
// image workers, while GPU upload is effectively serial. Keep a small
|
|
485
|
+
// sliding window of in-flight data fetches so the next decode runs while
|
|
486
|
+
// we're uploading the current one.
|
|
487
|
+
const prefetchLimit = Math.max(1, this.numImageWorkers);
|
|
488
|
+
const pending: Array<{ texture: Texture; data: Promise<unknown> }> = [];
|
|
489
|
+
|
|
490
|
+
// Helper avoids TS narrowing `texture.state` permanently after the first
|
|
491
|
+
// discriminated check — the property is mutable and can transition across
|
|
492
|
+
// awaits, so we need to re-read it freshly each time.
|
|
493
|
+
const isDead = (texture: Texture): boolean =>
|
|
494
|
+
texture.state === 'failed' || texture.state === 'freed';
|
|
495
|
+
|
|
496
|
+
const fillPrefetch = () => {
|
|
497
|
+
while (
|
|
498
|
+
pending.length < prefetchLimit &&
|
|
499
|
+
this.uploadTextureQueue.size > 0
|
|
500
|
+
) {
|
|
501
|
+
const [texture] = this.uploadTextureQueue;
|
|
502
|
+
if (!texture) break;
|
|
503
|
+
this.uploadTextureQueue.delete(texture);
|
|
504
|
+
|
|
505
|
+
if (isDead(texture)) {
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Swallow the rejection here so an early failure doesn't surface as
|
|
510
|
+
// an unhandled promise rejection while it sits in the prefetch
|
|
511
|
+
// window; we re-check state after awaiting.
|
|
512
|
+
const data =
|
|
513
|
+
texture.textureData === null
|
|
514
|
+
? texture.getTextureData().catch((err) => {
|
|
515
|
+
console.error('Failed to fetch texture data:', err);
|
|
516
|
+
return null;
|
|
517
|
+
})
|
|
518
|
+
: Promise.resolve(texture.textureData);
|
|
519
|
+
|
|
520
|
+
pending.push({ texture, data });
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
fillPrefetch();
|
|
525
|
+
|
|
464
526
|
while (
|
|
465
|
-
|
|
527
|
+
pending.length > 0 &&
|
|
466
528
|
platform.getTimeStamp() - startTime < maxProcessingTime
|
|
467
529
|
) {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
530
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
531
|
+
const next = pending.shift()!;
|
|
532
|
+
// Top up the prefetch window before awaiting — the next decode starts
|
|
533
|
+
// now and overlaps with this upload.
|
|
534
|
+
fillPrefetch();
|
|
535
|
+
|
|
536
|
+
if (isDead(next.texture)) {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
|
|
471
540
|
try {
|
|
472
|
-
|
|
473
|
-
|
|
541
|
+
await next.data;
|
|
542
|
+
if (isDead(next.texture)) {
|
|
543
|
+
continue;
|
|
474
544
|
}
|
|
475
|
-
await this.uploadTexture(texture);
|
|
545
|
+
await this.uploadTexture(next.texture);
|
|
476
546
|
} catch (error) {
|
|
477
547
|
console.error('Failed to upload texture:', error);
|
|
478
548
|
// Continue with next texture instead of stopping entire queue
|
|
479
549
|
}
|
|
480
550
|
}
|
|
551
|
+
|
|
552
|
+
// Time ran out before we got to these. Put them back so we don't lose
|
|
553
|
+
// them — their getTextureData() is already in flight and will populate
|
|
554
|
+
// `textureData` for the next tick.
|
|
555
|
+
for (const { texture } of pending) {
|
|
556
|
+
if (!isDead(texture)) {
|
|
557
|
+
this.uploadTextureQueue.add(texture);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
481
560
|
}
|
|
482
561
|
|
|
483
562
|
public hasUpdates(): boolean {
|
package/src/core/Stage.ts
CHANGED
|
@@ -750,15 +750,67 @@ export class Stage {
|
|
|
750
750
|
return null;
|
|
751
751
|
}
|
|
752
752
|
|
|
753
|
-
createNode(props: Partial<CoreNodeProps
|
|
754
|
-
|
|
753
|
+
createNode(props: Partial<CoreNodeProps>, resolved = false) {
|
|
754
|
+
// When `resolved` is true, the caller (typically a framework integration
|
|
755
|
+
// like solid-tv) built `props` via `createNodeProps` and filled it in
|
|
756
|
+
// over time. CoreNode adopts that bag directly — no second resolution
|
|
757
|
+
// pass and no second allocation.
|
|
758
|
+
const resolvedProps = resolved
|
|
759
|
+
? (props as CoreNodeProps)
|
|
760
|
+
: this.resolveNodeDefaults(props);
|
|
755
761
|
return new CoreNode(this, resolvedProps);
|
|
756
762
|
}
|
|
757
763
|
|
|
758
|
-
createTextNode(props: Partial<CoreTextNodeProps
|
|
764
|
+
createTextNode(props: Partial<CoreTextNodeProps>, resolved = false) {
|
|
765
|
+
const resolvedProps = resolved
|
|
766
|
+
? (props as CoreTextNodeProps)
|
|
767
|
+
: this.resolveTextNodeDefaults(props);
|
|
768
|
+
|
|
769
|
+
const resolvedTextRenderer = this.resolveTextRenderer(
|
|
770
|
+
resolvedProps,
|
|
771
|
+
resolvedProps.textRendererOverride as keyof TextRenderers | null,
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
if (!resolvedTextRenderer) {
|
|
775
|
+
throw new Error(
|
|
776
|
+
`No compatible text renderer found for ${resolvedProps.fontFamily}`,
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
return new CoreTextNode(this, resolvedProps, resolvedTextRenderer);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Allocate a fully-resolved CoreNodeProps bag — the same shape and
|
|
785
|
+
* defaults the renderer would otherwise build inside `createNode`.
|
|
786
|
+
*
|
|
787
|
+
* Frameworks (e.g. solid-tv) call this once per node at construction
|
|
788
|
+
* time, fill it in as user props flow in, then pass it back via
|
|
789
|
+
* `createNode(props, true)`. The renderer adopts the bag directly:
|
|
790
|
+
* one allocation instead of two, and a stable hidden class for the
|
|
791
|
+
* node's lifetime.
|
|
792
|
+
*/
|
|
793
|
+
createNodeProps(initial?: Partial<CoreNodeProps>): CoreNodeProps {
|
|
794
|
+
return this.resolveNodeDefaults(initial ?? {});
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Allocate a fully-resolved CoreTextNodeProps bag. See
|
|
799
|
+
* {@link createNodeProps}.
|
|
800
|
+
*/
|
|
801
|
+
createTextNodeProps(initial?: Partial<CoreTextNodeProps>): CoreTextNodeProps {
|
|
802
|
+
return this.resolveTextNodeDefaults(initial ?? {});
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Apply text-specific defaults on top of a base CoreNodeProps build.
|
|
807
|
+
* Shared by `createTextNode` and `createTextNodeProps`.
|
|
808
|
+
*/
|
|
809
|
+
protected resolveTextNodeDefaults(
|
|
810
|
+
props: Partial<CoreTextNodeProps>,
|
|
811
|
+
): CoreTextNodeProps {
|
|
759
812
|
const fontSize = props.fontSize || 16;
|
|
760
813
|
const resolvedProps = this.resolveNodeDefaults(props) as CoreTextNodeProps;
|
|
761
|
-
|
|
762
814
|
resolvedProps.text = props.text ?? '';
|
|
763
815
|
resolvedProps.textRendererOverride = props.textRendererOverride ?? null;
|
|
764
816
|
resolvedProps.fontSize = fontSize;
|
|
@@ -776,19 +828,7 @@ export class Stage {
|
|
|
776
828
|
resolvedProps.maxWidth = props.maxWidth || 0;
|
|
777
829
|
resolvedProps.maxHeight = props.maxHeight || 0;
|
|
778
830
|
resolvedProps.forceLoad = props.forceLoad || false;
|
|
779
|
-
|
|
780
|
-
const resolvedTextRenderer = this.resolveTextRenderer(
|
|
781
|
-
resolvedProps,
|
|
782
|
-
resolvedProps.textRendererOverride as keyof TextRenderers | null,
|
|
783
|
-
);
|
|
784
|
-
|
|
785
|
-
if (!resolvedTextRenderer) {
|
|
786
|
-
throw new Error(
|
|
787
|
-
`No compatible text renderer found for ${resolvedProps.fontFamily}`,
|
|
788
|
-
);
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
return new CoreTextNode(this, resolvedProps, resolvedTextRenderer);
|
|
831
|
+
return resolvedProps;
|
|
792
832
|
}
|
|
793
833
|
|
|
794
834
|
setBoundsMargin(value: number | [number, number, number, number]) {
|
package/src/core/TextureError.ts
CHANGED
|
@@ -2,6 +2,7 @@ export enum TextureErrorCode {
|
|
|
2
2
|
MEMORY_THRESHOLD_EXCEEDED = 'MEMORY_THRESHOLD_EXCEEDED',
|
|
3
3
|
TEXTURE_DATA_NULL = 'TEXTURE_DATA_NULL',
|
|
4
4
|
TEXTURE_TYPE_NOT_REGISTERED = 'TEXTURE_TYPE_NOT_REGISTERED',
|
|
5
|
+
TEXTURE_UPLOAD_FAILED = 'TEXTURE_UPLOAD_FAILED',
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
const defaultMessages: Record<TextureErrorCode, string> = {
|
|
@@ -9,6 +10,7 @@ const defaultMessages: Record<TextureErrorCode, string> = {
|
|
|
9
10
|
[TextureErrorCode.TEXTURE_DATA_NULL]: 'Texture data is null',
|
|
10
11
|
[TextureErrorCode.TEXTURE_TYPE_NOT_REGISTERED]:
|
|
11
12
|
'Texture type is not registered',
|
|
13
|
+
[TextureErrorCode.TEXTURE_UPLOAD_FAILED]: 'Texture upload failed',
|
|
12
14
|
};
|
|
13
15
|
|
|
14
16
|
export class TextureError extends Error {
|
|
@@ -67,7 +67,7 @@ function createImageWorker() {
|
|
|
67
67
|
|
|
68
68
|
var blob = xhr.response;
|
|
69
69
|
var withAlphaChannel =
|
|
70
|
-
premultiplyAlpha !== undefined
|
|
70
|
+
premultiplyAlpha !== undefined && premultiplyAlpha !== null
|
|
71
71
|
? premultiplyAlpha
|
|
72
72
|
: hasAlphaChannel(blob.type);
|
|
73
73
|
|
|
@@ -83,7 +83,7 @@ function createImageWorker() {
|
|
|
83
83
|
imageOrientation: 'none',
|
|
84
84
|
})
|
|
85
85
|
.then(function (data) {
|
|
86
|
-
resolve({ data, premultiplyAlpha:
|
|
86
|
+
resolve({ data: data, premultiplyAlpha: withAlphaChannel });
|
|
87
87
|
})
|
|
88
88
|
.catch(function (error) {
|
|
89
89
|
reject(error);
|
|
@@ -91,13 +91,13 @@ function createImageWorker() {
|
|
|
91
91
|
return;
|
|
92
92
|
} else if (
|
|
93
93
|
supportsOptionsCreateImageBitmap === false &&
|
|
94
|
-
|
|
94
|
+
supportsFullCreateImageBitmap === false
|
|
95
95
|
) {
|
|
96
96
|
// Fallback for browsers that do not support createImageBitmap with options
|
|
97
97
|
// this is supported for Chrome v50 to v52/54 that doesn't support options
|
|
98
98
|
createImageBitmap(blob)
|
|
99
99
|
.then(function (data) {
|
|
100
|
-
resolve({ data, premultiplyAlpha:
|
|
100
|
+
resolve({ data: data, premultiplyAlpha: withAlphaChannel });
|
|
101
101
|
})
|
|
102
102
|
.catch(function (error) {
|
|
103
103
|
reject(error);
|
|
@@ -109,7 +109,7 @@ function createImageWorker() {
|
|
|
109
109
|
imageOrientation: 'none',
|
|
110
110
|
})
|
|
111
111
|
.then(function (data) {
|
|
112
|
-
resolve({ data, premultiplyAlpha:
|
|
112
|
+
resolve({ data: data, premultiplyAlpha: withAlphaChannel });
|
|
113
113
|
})
|
|
114
114
|
.catch(function (error) {
|
|
115
115
|
reject(error);
|
|
@@ -156,7 +156,6 @@ function createImageWorker() {
|
|
|
156
156
|
/* eslint-enable */
|
|
157
157
|
|
|
158
158
|
export class ImageWorkerManager {
|
|
159
|
-
imageWorkersEnabled = true;
|
|
160
159
|
messageManager: Record<number, MessageCallback> = {};
|
|
161
160
|
workers: Worker[] = [];
|
|
162
161
|
workerLoad: number[] = [];
|
|
@@ -172,6 +171,8 @@ export class ImageWorkerManager {
|
|
|
172
171
|
);
|
|
173
172
|
this.workers.forEach((worker, index) => {
|
|
174
173
|
worker.onmessage = (event) => this.handleMessage(event, index);
|
|
174
|
+
worker.onerror = (event) => this.handleWorkerError(event, index);
|
|
175
|
+
worker.onmessageerror = (event) => this.handleWorkerError(event, index);
|
|
175
176
|
});
|
|
176
177
|
}
|
|
177
178
|
|
|
@@ -194,6 +195,25 @@ export class ImageWorkerManager {
|
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
|
|
198
|
+
private handleWorkerError(event: Event | ErrorEvent, workerIndex: number) {
|
|
199
|
+
const message =
|
|
200
|
+
event instanceof ErrorEvent && event.message
|
|
201
|
+
? event.message
|
|
202
|
+
: 'Image worker encountered an unrecoverable error';
|
|
203
|
+
|
|
204
|
+
// Reject all pending requests; we cannot map a worker-level crash to a
|
|
205
|
+
// specific message id, so fail everything outstanding to avoid hangs.
|
|
206
|
+
for (const id in this.messageManager) {
|
|
207
|
+
const msg = this.messageManager[id];
|
|
208
|
+
if (msg) {
|
|
209
|
+
const [, reject] = msg;
|
|
210
|
+
delete this.messageManager[id];
|
|
211
|
+
reject(new Error(message));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
this.workerLoad[workerIndex] = 0;
|
|
215
|
+
}
|
|
216
|
+
|
|
197
217
|
private createWorkers(
|
|
198
218
|
numWorkers = 1,
|
|
199
219
|
createImageBitmapSupport: CreateImageBitmapSupport,
|
|
@@ -224,19 +244,22 @@ export class ImageWorkerManager {
|
|
|
224
244
|
const blob: Blob = new Blob([workerCode], {
|
|
225
245
|
type: 'application/javascript',
|
|
226
246
|
});
|
|
227
|
-
const
|
|
247
|
+
const urlFactory = self.URL ? URL : webkitURL;
|
|
248
|
+
const blobURL: string = urlFactory.createObjectURL(blob);
|
|
228
249
|
const workers: Worker[] = [];
|
|
229
250
|
for (let i = 0; i < numWorkers; i++) {
|
|
230
251
|
workers.push(new Worker(blobURL));
|
|
231
252
|
this.workerLoad.push(0);
|
|
232
253
|
}
|
|
254
|
+
// Workers retain the script; the URL itself is no longer needed.
|
|
255
|
+
urlFactory.revokeObjectURL(blobURL);
|
|
233
256
|
return workers;
|
|
234
257
|
}
|
|
235
258
|
|
|
236
259
|
private getNextWorkerIndex(): number {
|
|
237
260
|
if (this.workers.length === 0) return -1;
|
|
238
261
|
|
|
239
|
-
let minLoad =
|
|
262
|
+
let minLoad = Infinity;
|
|
240
263
|
let workerIndex = 0;
|
|
241
264
|
|
|
242
265
|
for (let i = 0; i < this.workers.length; i++) {
|
|
@@ -264,26 +287,26 @@ export class ImageWorkerManager {
|
|
|
264
287
|
): Promise<TextureData> {
|
|
265
288
|
return new Promise((resolve, reject) => {
|
|
266
289
|
try {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (nextWorkerIndex !== -1) {
|
|
273
|
-
const worker = this.workers[nextWorkerIndex];
|
|
274
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
275
|
-
this.workerLoad[nextWorkerIndex]!++;
|
|
276
|
-
worker!.postMessage({
|
|
277
|
-
id,
|
|
278
|
-
src: src,
|
|
279
|
-
premultiplyAlpha,
|
|
280
|
-
sx,
|
|
281
|
-
sy,
|
|
282
|
-
sw,
|
|
283
|
-
sh,
|
|
284
|
-
});
|
|
285
|
-
}
|
|
290
|
+
const nextWorkerIndex = this.getNextWorkerIndex();
|
|
291
|
+
if (nextWorkerIndex === -1) {
|
|
292
|
+
reject(new Error('No image workers available'));
|
|
293
|
+
return;
|
|
286
294
|
}
|
|
295
|
+
|
|
296
|
+
const id = this.nextId++;
|
|
297
|
+
this.messageManager[id] = [resolve, reject];
|
|
298
|
+
const worker = this.workers[nextWorkerIndex];
|
|
299
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
300
|
+
this.workerLoad[nextWorkerIndex]!++;
|
|
301
|
+
worker!.postMessage({
|
|
302
|
+
id,
|
|
303
|
+
src: src,
|
|
304
|
+
premultiplyAlpha,
|
|
305
|
+
sx,
|
|
306
|
+
sy,
|
|
307
|
+
sw,
|
|
308
|
+
sh,
|
|
309
|
+
});
|
|
287
310
|
} catch (error) {
|
|
288
311
|
reject(error);
|
|
289
312
|
}
|
|
@@ -216,13 +216,14 @@ const generateTextLayout = (
|
|
|
216
216
|
): TextLayout => {
|
|
217
217
|
const fontSize = props.fontSize;
|
|
218
218
|
const fontFamily = props.fontFamily;
|
|
219
|
-
const lineHeight = props.lineHeight;
|
|
220
219
|
const metrics = SdfFontHandler.getFontMetrics(fontFamily, fontSize);
|
|
221
|
-
const verticalAlign = props.verticalAlign;
|
|
222
220
|
|
|
223
221
|
const fontData = fontCache.data;
|
|
224
222
|
const commonFontData = fontData.common;
|
|
225
223
|
const designFontSize = fontData.info.size;
|
|
224
|
+
const designLineHeight = commonFontData.lineHeight;
|
|
225
|
+
const lineHeight =
|
|
226
|
+
props.lineHeight || (designLineHeight * fontSize) / designFontSize;
|
|
226
227
|
|
|
227
228
|
const atlasWidth = commonFontData.scaleW;
|
|
228
229
|
const atlasHeight = commonFontData.scaleH;
|
|
@@ -70,7 +70,9 @@ export class ColorTexture extends Texture {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
static override makeCacheKey(props: ColorTextureProps): string {
|
|
73
|
-
|
|
73
|
+
// Mirror the default from resolveDefaults so the key is stable whether
|
|
74
|
+
// or not the caller has run defaults first.
|
|
75
|
+
return `ColorTexture,${props.color || 0xffffffff}`;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
static override resolveDefaults(
|
|
@@ -147,11 +147,22 @@ export class ImageTexture extends Texture {
|
|
|
147
147
|
data: HTMLImageElement | null;
|
|
148
148
|
premultiplyAlpha: boolean;
|
|
149
149
|
}>((resolve, reject) => {
|
|
150
|
+
let objectUrl: string | null = null;
|
|
151
|
+
|
|
152
|
+
const cleanup = () => {
|
|
153
|
+
if (objectUrl !== null) {
|
|
154
|
+
URL.revokeObjectURL(objectUrl);
|
|
155
|
+
objectUrl = null;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
150
159
|
img.onload = () => {
|
|
160
|
+
cleanup();
|
|
151
161
|
resolve({ data: img, premultiplyAlpha: hasAlpha });
|
|
152
162
|
};
|
|
153
163
|
|
|
154
164
|
img.onerror = (err) => {
|
|
165
|
+
cleanup();
|
|
155
166
|
const errorMessage =
|
|
156
167
|
err instanceof Error
|
|
157
168
|
? err.message
|
|
@@ -162,7 +173,8 @@ export class ImageTexture extends Texture {
|
|
|
162
173
|
};
|
|
163
174
|
|
|
164
175
|
if (src instanceof Blob) {
|
|
165
|
-
|
|
176
|
+
objectUrl = URL.createObjectURL(src);
|
|
177
|
+
img.src = objectUrl;
|
|
166
178
|
} else {
|
|
167
179
|
img.src = src;
|
|
168
180
|
}
|
|
@@ -218,10 +230,11 @@ export class ImageTexture extends Texture {
|
|
|
218
230
|
|
|
219
231
|
async loadImage(src: string) {
|
|
220
232
|
const { premultiplyAlpha, sx, sy, sw, sh } = this.props;
|
|
233
|
+
const isBase64 = isBase64Image(src);
|
|
221
234
|
|
|
222
235
|
if (this.txManager.hasCreateImageBitmap === true) {
|
|
223
236
|
if (
|
|
224
|
-
|
|
237
|
+
isBase64 === false &&
|
|
225
238
|
this.txManager.hasWorker === true &&
|
|
226
239
|
this.txManager.imageWorkerManager !== null
|
|
227
240
|
) {
|
|
@@ -235,15 +248,9 @@ export class ImageTexture extends Texture {
|
|
|
235
248
|
);
|
|
236
249
|
}
|
|
237
250
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
blob = dataURIToBlob(src);
|
|
242
|
-
} else {
|
|
243
|
-
blob = await fetchJson(src, 'blob').then(
|
|
244
|
-
(response) => response as Blob,
|
|
245
|
-
);
|
|
246
|
-
}
|
|
251
|
+
const blob = isBase64
|
|
252
|
+
? dataURIToBlob(src)
|
|
253
|
+
: ((await fetchJson(src, 'blob')) as Blob);
|
|
247
254
|
|
|
248
255
|
return this.createImageBitmap(blob, premultiplyAlpha, sx, sy, sw, sh);
|
|
249
256
|
}
|
|
@@ -357,16 +364,18 @@ export class ImageTexture extends Texture {
|
|
|
357
364
|
return false;
|
|
358
365
|
}
|
|
359
366
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
367
|
+
// Inline default values so the key is stable whether or not the caller
|
|
368
|
+
// has run them through resolveDefaults first. Must mirror the defaults
|
|
369
|
+
// in resolveDefaults below.
|
|
370
|
+
const premultiplyAlpha = props.premultiplyAlpha ?? true;
|
|
371
|
+
const maxRetryCount = props.maxRetryCount ?? 5;
|
|
372
|
+
|
|
373
|
+
let cacheKey = `ImageTexture,${key},${premultiplyAlpha},${maxRetryCount}`;
|
|
363
374
|
|
|
364
|
-
if (props.sh
|
|
365
|
-
cacheKey += '
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
cacheKey += props.sw || '';
|
|
369
|
-
cacheKey += props.sh || '';
|
|
375
|
+
if (props.sh != null && props.sw != null) {
|
|
376
|
+
cacheKey += `,${props.sx ?? ''},${props.sy ?? ''},${props.sw || ''},${
|
|
377
|
+
props.sh || ''
|
|
378
|
+
}`;
|
|
370
379
|
}
|
|
371
380
|
|
|
372
381
|
return cacheKey;
|
package/src/main-api/Renderer.ts
CHANGED
|
@@ -680,8 +680,12 @@ export class RendererMain extends EventEmitter {
|
|
|
680
680
|
*/
|
|
681
681
|
createNode<ShNode extends CoreShaderNode<any>>(
|
|
682
682
|
props: Partial<INodeProps<ShNode>>,
|
|
683
|
+
resolved = false,
|
|
683
684
|
): INode<ShNode> {
|
|
684
|
-
const node = this.stage.createNode(
|
|
685
|
+
const node = this.stage.createNode(
|
|
686
|
+
props as Partial<CoreNodeProps>,
|
|
687
|
+
resolved,
|
|
688
|
+
);
|
|
685
689
|
|
|
686
690
|
if (ENABLE_INSPECTOR && this.inspector) {
|
|
687
691
|
return this.inspector.createNode(node) as unknown as INode<ShNode>;
|
|
@@ -690,6 +694,20 @@ export class RendererMain extends EventEmitter {
|
|
|
690
694
|
return node as unknown as INode<ShNode>;
|
|
691
695
|
}
|
|
692
696
|
|
|
697
|
+
/**
|
|
698
|
+
* Allocate a fully-resolved CoreNodeProps bag — the same shape and
|
|
699
|
+
* defaults the renderer would otherwise build inside `createNode`.
|
|
700
|
+
*
|
|
701
|
+
* Frameworks (e.g. solid-tv) call this once per node at construction
|
|
702
|
+
* time, fill it in as user props flow in, then pass it back via
|
|
703
|
+
* `createNode(props, true)`. The renderer adopts the bag directly:
|
|
704
|
+
* one allocation instead of two, and a stable hidden class for the
|
|
705
|
+
* node's lifetime.
|
|
706
|
+
*/
|
|
707
|
+
createNodeProps(initial?: Partial<CoreNodeProps>): CoreNodeProps {
|
|
708
|
+
return this.stage.createNodeProps(initial);
|
|
709
|
+
}
|
|
710
|
+
|
|
693
711
|
/**
|
|
694
712
|
* Create a new scene graph text node
|
|
695
713
|
*
|
|
@@ -704,8 +722,11 @@ export class RendererMain extends EventEmitter {
|
|
|
704
722
|
* @param props
|
|
705
723
|
* @returns
|
|
706
724
|
*/
|
|
707
|
-
createTextNode(props: Partial<ITextNodeProps
|
|
708
|
-
const textNode = this.stage.createTextNode(
|
|
725
|
+
createTextNode(props: Partial<ITextNodeProps>, resolved = false): ITextNode {
|
|
726
|
+
const textNode = this.stage.createTextNode(
|
|
727
|
+
props as CoreTextNodeProps,
|
|
728
|
+
resolved,
|
|
729
|
+
);
|
|
709
730
|
|
|
710
731
|
if (ENABLE_INSPECTOR && this.inspector) {
|
|
711
732
|
return this.inspector.createTextNode(textNode) as unknown as ITextNode;
|
|
@@ -714,6 +735,14 @@ export class RendererMain extends EventEmitter {
|
|
|
714
735
|
return textNode as unknown as ITextNode;
|
|
715
736
|
}
|
|
716
737
|
|
|
738
|
+
/**
|
|
739
|
+
* Allocate a fully-resolved CoreTextNodeProps bag. See
|
|
740
|
+
* {@link createNodeProps}.
|
|
741
|
+
*/
|
|
742
|
+
createTextNodeProps(initial?: Partial<CoreTextNodeProps>): CoreTextNodeProps {
|
|
743
|
+
return this.stage.createTextNodeProps(initial);
|
|
744
|
+
}
|
|
745
|
+
|
|
717
746
|
/**
|
|
718
747
|
* Destroy a node
|
|
719
748
|
*
|