@solidtv/renderer 1.0.7 → 1.1.0
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 +51 -13
- package/dist/src/core/CoreNode.js.map +1 -1
- package/dist/src/core/Stage.d.ts +23 -2
- package/dist/src/core/Stage.js +50 -8
- package/dist/src/core/Stage.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlRenderer.d.ts +10 -0
- package/dist/src/core/renderers/webgl/WebGlRenderer.js +25 -5
- package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
- package/dist/src/core/text-rendering/SdfTextRenderer.js +3 -3
- package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
- package/dist/src/core/text-rendering/TextLayoutEngine.js +43 -12
- package/dist/src/core/text-rendering/TextLayoutEngine.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/package.json +1 -1
- package/src/core/CoreNode.ts +49 -13
- package/src/core/Stage.ts +63 -17
- package/src/core/renderers/webgl/WebGlRenderer.ts +25 -5
- package/src/core/text-rendering/SdfTextRenderer.ts +4 -3
- package/src/core/text-rendering/TextLayoutEngine.ts +61 -28
- package/src/core/text-rendering/tests/TextLayoutEngine.test.ts +20 -0
- package/src/main-api/Renderer.ts +32 -3
|
@@ -227,7 +227,9 @@ export const wrapText = (
|
|
|
227
227
|
: [[['', 0, false, 0, 0]], remainingLines, i < lines.length - 1];
|
|
228
228
|
|
|
229
229
|
remainingLines--;
|
|
230
|
-
|
|
230
|
+
for (let j = 0; j < wrappedLine.length; j++) {
|
|
231
|
+
wrappedLines.push(wrappedLine[j]!);
|
|
232
|
+
}
|
|
231
233
|
|
|
232
234
|
if (hasMaxLines === true && remainingLines <= 0) {
|
|
233
235
|
const lastLine = wrappedLines[wrappedLines.length - 1]!;
|
|
@@ -278,44 +280,75 @@ export const wrapLine = (
|
|
|
278
280
|
let hasRemainingText = true;
|
|
279
281
|
|
|
280
282
|
const wrapFn = getWrapStrategy(wordBreak);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
283
|
+
let wordIdx = 0;
|
|
284
|
+
let spaceIdx = 0;
|
|
285
|
+
let pendingWord = '';
|
|
286
|
+
|
|
287
|
+
while (
|
|
288
|
+
(pendingWord.length > 0 || wordIdx < words.length) &&
|
|
289
|
+
remainingLines > 0
|
|
290
|
+
) {
|
|
291
|
+
let word: string;
|
|
292
|
+
let wordWidth: number;
|
|
284
293
|
let remainingWord = '';
|
|
285
294
|
|
|
295
|
+
if (pendingWord.length > 0) {
|
|
296
|
+
word = pendingWord;
|
|
297
|
+
pendingWord = '';
|
|
298
|
+
} else {
|
|
299
|
+
word = words[wordIdx++]!;
|
|
300
|
+
}
|
|
301
|
+
wordWidth = measureText(word, fontFamily, letterSpacing);
|
|
302
|
+
|
|
286
303
|
//handle first word of new line separately to avoid empty line issues
|
|
287
304
|
if (currentLineWidth === 0) {
|
|
288
305
|
// Word doesn't fit on current line
|
|
289
306
|
//if first word doesn't fit on empty line
|
|
290
307
|
if (wordWidth > maxWidth) {
|
|
291
308
|
remainingLines--;
|
|
309
|
+
const isLastLine = remainingLines === 0;
|
|
310
|
+
let lineTruncated = isLastLine;
|
|
292
311
|
//truncate word to fit
|
|
293
|
-
[word, remainingWord, wordWidth] =
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
);
|
|
312
|
+
[word, remainingWord, wordWidth] = isLastLine
|
|
313
|
+
? truncateWord(
|
|
314
|
+
measureText,
|
|
315
|
+
word,
|
|
316
|
+
wordWidth,
|
|
317
|
+
maxWidth,
|
|
318
|
+
fontFamily,
|
|
319
|
+
letterSpacing,
|
|
320
|
+
overflowSuffix,
|
|
321
|
+
overflowWidth,
|
|
322
|
+
)
|
|
323
|
+
: splitWord(
|
|
324
|
+
measureText,
|
|
325
|
+
word,
|
|
326
|
+
wordWidth,
|
|
327
|
+
maxWidth,
|
|
328
|
+
fontFamily,
|
|
329
|
+
letterSpacing,
|
|
330
|
+
);
|
|
313
331
|
|
|
314
332
|
if (remainingWord.length > 0) {
|
|
315
|
-
|
|
333
|
+
if (word.length === 0) {
|
|
334
|
+
if (overflowSuffix.length > 0) {
|
|
335
|
+
word = overflowSuffix;
|
|
336
|
+
wordWidth = overflowWidth;
|
|
337
|
+
} else {
|
|
338
|
+
word = remainingWord.charAt(0);
|
|
339
|
+
if (word.length === 0) {
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
wordWidth = measureText(word, fontFamily, letterSpacing);
|
|
343
|
+
}
|
|
344
|
+
remainingWord = '';
|
|
345
|
+
remainingLines = 0;
|
|
346
|
+
lineTruncated = true;
|
|
347
|
+
}
|
|
348
|
+
pendingWord = remainingWord;
|
|
316
349
|
}
|
|
317
350
|
// first word doesn't fit on an empty line
|
|
318
|
-
wrappedLines.push([word, wordWidth,
|
|
351
|
+
wrappedLines.push([word, wordWidth, lineTruncated, 0, 0]);
|
|
319
352
|
} else if (wordWidth + spaceWidth >= maxWidth) {
|
|
320
353
|
remainingLines--;
|
|
321
354
|
// word with space doesn't fit, but word itself fits - put on new line
|
|
@@ -326,7 +359,7 @@ export const wrapLine = (
|
|
|
326
359
|
}
|
|
327
360
|
continue;
|
|
328
361
|
}
|
|
329
|
-
const space = spaces
|
|
362
|
+
const space = spaces[spaceIdx++] || '';
|
|
330
363
|
// For width calculation, treat ZWSP as having 0 width but regular space functionality
|
|
331
364
|
const effectiveSpaceWidth = space === '\u200B' ? 0 : spaceWidth;
|
|
332
365
|
const totalWidth = currentLineWidth + effectiveSpaceWidth + wordWidth;
|
|
@@ -367,7 +400,7 @@ export const wrapLine = (
|
|
|
367
400
|
);
|
|
368
401
|
|
|
369
402
|
if (remainingWord.length > 0) {
|
|
370
|
-
|
|
403
|
+
pendingWord = remainingWord;
|
|
371
404
|
}
|
|
372
405
|
}
|
|
373
406
|
|
|
@@ -228,6 +228,26 @@ describe('SDF Text Utils', () => {
|
|
|
228
228
|
expect(result[0].length).toBeGreaterThan(1);
|
|
229
229
|
expect(result[0][0]?.[0]).toBe('hello world test');
|
|
230
230
|
});
|
|
231
|
+
|
|
232
|
+
it('should return non-empty output when maxWidth is smaller than glyph and suffix width', () => {
|
|
233
|
+
const result = wrapLine(
|
|
234
|
+
testMeasureText,
|
|
235
|
+
'hello',
|
|
236
|
+
'Arial',
|
|
237
|
+
5,
|
|
238
|
+
0,
|
|
239
|
+
10,
|
|
240
|
+
'...',
|
|
241
|
+
30,
|
|
242
|
+
'break-word',
|
|
243
|
+
1,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
expect(result[0]).toHaveLength(1);
|
|
247
|
+
expect(result[0][0]?.[0]).toBe('...');
|
|
248
|
+
expect(result[0][0]?.[2]).toBe(true);
|
|
249
|
+
expect(result[1]).toBe(0);
|
|
250
|
+
});
|
|
231
251
|
});
|
|
232
252
|
|
|
233
253
|
describe('wrapText', () => {
|
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
|
*
|