Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/tests/quads-rendered.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { type INode } from '@lightningjs/renderer';
import logo from '../assets/lightning.png';
import type { ExampleSettings } from '../common/ExampleSettings.js';
import { waitUntilIdle } from '../common/utils.js';

export async function automation(settings: ExampleSettings) {
const destroy = await test(settings);
// The displayed quad count is driven by the async `renderUpdate` event and
// only settles once every image texture / SDF layout has loaded (and the
// count text, itself made of quads, has converged). Wait for the renderer to
// go idle so the snapshot captures that stable final state instead of a
// mid-load frame at a fixed timeout.
await waitUntilIdle(settings.renderer);
await settings.snapshot();
destroy(120);
await waitUntilIdle(settings.renderer);
await settings.snapshot();
}

Expand Down
145 changes: 145 additions & 0 deletions src/core/CoreTextNode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { CoreTextNode, type CoreTextNodeProps } from './CoreTextNode.js';
import { Stage } from './Stage.js';
import { CoreRenderer } from './renderers/CoreRenderer.js';
import { createBound } from './lib/utils.js';
import type {
FontHandler,
TextRenderer,
TextRenderInfo,
} from './text-rendering/TextRenderer.js';
import type { Texture } from './textures/Texture.js';

const defaultProps = (
overrides?: Partial<CoreTextNodeProps>,
): CoreTextNodeProps => ({
// CoreNodeProps
alpha: 1,
autosize: false,
boundsMargin: null,
clipping: false,
color: 0xffffffff,
colorBl: 0xffffffff,
colorBottom: 0xffffffff,
colorBr: 0xffffffff,
colorLeft: 0xffffffff,
colorRight: 0xffffffff,
colorTl: 0xffffffff,
colorTop: 0xffffffff,
colorTr: 0xffffffff,
h: 0,
mount: 0,
mountX: 0,
mountY: 0,
parent: null,
pivot: 0,
pivotX: 0,
pivotY: 0,
rotation: 0,
rtt: false,
scale: 1,
scaleX: 1,
scaleY: 1,
shader: null,
src: '',
texture: null,
textureOptions: {} as never,
w: 0,
x: 0,
y: 0,
zIndex: 0,
preventDestroy: false,
// TrProps
text: '',
textAlign: 'left',
fontFamily: 'Ubuntu',
fontStyle: 'normal',
fontSize: 100,
maxWidth: 0,
maxHeight: 0,
offsetY: 0,
letterSpacing: 0,
lineHeight: 0,
maxLines: 0,
verticalAlign: 'top',
overflowSuffix: '...',
wordBreak: 'break-word',
contain: 'none',
// CoreTextNodeProps
textRendererOverride: null,
forceLoad: false,
...overrides,
});

const makeStage = (texture: Texture) =>
mock<Stage>({
strictBound: createBound(0, 0, 200, 200),
preloadBound: createBound(0, 0, 200, 200),
defaultTexture: { state: 'loaded' } as never,
defShaderNode: null as never,
renderer: mock<CoreRenderer>() as CoreRenderer,
txManager: { createTexture: vi.fn(() => texture) } as never,
});

const makeCanvasRenderer = (): TextRenderer => {
const font = mock<FontHandler>({ type: 'canvas' });
return {
type: 'canvas',
font,
renderText: vi.fn(),
addQuads: vi.fn(),
renderQuads: vi.fn(),
init: vi.fn(),
} as unknown as TextRenderer;
};

const makeLoadedTexture = () =>
({
state: 'loaded',
dimensions: { w: 100, h: 50 },
retryCount: 0,
maxRetryCount: 0,
on: vi.fn(),
off: vi.fn(),
once: vi.fn(),
setRenderableOwner: vi.fn(),
} as unknown as Texture);

describe('CoreTextNode (canvas) clearing text', () => {
it('clears the stale texture when text becomes empty', () => {
const renderer = makeCanvasRenderer();
const texture = makeLoadedTexture();
const stage = makeStage(texture);

const node = new CoreTextNode(
stage,
defaultProps({ text: 'Hello' }),
renderer,
);

// Simulate "Hello" having been rendered: canvas returns ImageData and a
// texture is created/assigned.
const helloResult: TextRenderInfo = {
imageData: {} as ImageData,
width: 100,
height: 50,
};
(
node as unknown as { handleRenderResult(r: TextRenderInfo): void }
).handleRenderResult(helloResult);

expect(node.texture).toBe(texture);

// Now set text to empty. The canvas renderer returns no imageData.
const emptyResult: TextRenderInfo = { width: 0, height: 0 };
(
node as unknown as { handleRenderResult(r: TextRenderInfo): void }
).handleRenderResult(emptyResult);

// The previously-rendered texture must be cleared so the old text does not
// linger and get re-marked renderable by CoreNode.updateIsRenderable.
expect(node.texture).toBe(null);
expect(node.isRenderable).toBe(false);
});
});
4 changes: 3 additions & 1 deletion src/core/CoreTextNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,8 +245,10 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
// Handle Canvas renderer (uses ImageData)
if (textRendererType === 'canvas') {
if (result.imageData === undefined) {
// Empty text returns no imageData — mark not renderable and continue
// Empty text returns no imageData — clear the stale texture so the
// previous text doesn't linger, then mark not renderable and continue
// to update dimensions (w=0, h=0) rather than emitting a failure.
this.texture = null;
this.setRenderable(false);
} else {
this.texture = this.stage.txManager.createTexture('ImageTexture', {
Expand Down
Loading