@lightningjs/renderer 2.9.0-beta3 → 2.9.0-beta4

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.
@@ -17,55 +17,73 @@
17
17
  * limitations under the License.
18
18
  */
19
19
 
20
- import { describe, expect, it } from 'vitest';
20
+ import { describe, expect, it, vi } from 'vitest';
21
21
  import { CoreNode, type CoreNodeProps, UpdateType } from './CoreNode.js';
22
22
  import { Stage } from './Stage.js';
23
23
  import { mock } from 'vitest-mock-extended';
24
24
  import { type TextureOptions } from './CoreTextureManager.js';
25
25
  import { type BaseShaderController } from '../main-api/ShaderController';
26
+ import { createBound } from './lib/utils.js';
27
+ import { ImageTexture } from './textures/ImageTexture.js';
26
28
 
27
- describe('set color()', () => {
28
- const defaultProps: CoreNodeProps = {
29
- alpha: 0,
30
- autosize: false,
31
- clipping: false,
32
- color: 0,
33
- colorBl: 0,
34
- colorBottom: 0,
35
- colorBr: 0,
36
- colorLeft: 0,
37
- colorRight: 0,
38
- colorTl: 0,
39
- colorTop: 0,
40
- colorTr: 0,
41
- height: 0,
42
- mount: 0,
43
- mountX: 0,
44
- mountY: 0,
45
- parent: null,
46
- pivot: 0,
47
- pivotX: 0,
48
- pivotY: 0,
49
- rotation: 0,
50
- rtt: false,
51
- scale: 0,
52
- scaleX: 0,
53
- scaleY: 0,
54
- shader: mock<BaseShaderController>(),
55
- src: '',
56
- texture: null,
57
- textureOptions: {} as TextureOptions,
58
- width: 0,
59
- x: 0,
60
- y: 0,
61
- zIndex: 0,
62
- zIndexLocked: 0,
63
- preventCleanup: false,
64
- strictBounds: false,
65
- };
29
+ const defaultProps: CoreNodeProps = {
30
+ alpha: 0,
31
+ autosize: false,
32
+ clipping: false,
33
+ color: 0,
34
+ colorBl: 0,
35
+ colorBottom: 0,
36
+ colorBr: 0,
37
+ colorLeft: 0,
38
+ colorRight: 0,
39
+ colorTl: 0,
40
+ colorTop: 0,
41
+ colorTr: 0,
42
+ height: 0,
43
+ mount: 0,
44
+ mountX: 0,
45
+ mountY: 0,
46
+ parent: null,
47
+ pivot: 0,
48
+ pivotX: 0,
49
+ pivotY: 0,
50
+ rotation: 0,
51
+ rtt: false,
52
+ scale: 0,
53
+ scaleX: 0,
54
+ scaleY: 0,
55
+ shader: mock<BaseShaderController>(),
56
+ src: '',
57
+ texture: null,
58
+ textureOptions: {} as TextureOptions,
59
+ width: 0,
60
+ x: 0,
61
+ y: 0,
62
+ zIndex: 0,
63
+ zIndexLocked: 0,
64
+ preventCleanup: false,
65
+ strictBounds: false,
66
+ };
67
+
68
+ const clippingRect = {
69
+ x: 0,
70
+ y: 0,
71
+ width: 200,
72
+ height: 200,
73
+ valid: false,
74
+ };
75
+
76
+ const stage = mock<Stage>({
77
+ strictBound: createBound(0, 0, 200, 200),
78
+ preloadBound: createBound(0, 0, 200, 200),
79
+ defaultTexture: {
80
+ state: 'loaded',
81
+ },
82
+ });
66
83
 
84
+ describe('set color()', () => {
67
85
  it('should set all color subcomponents.', () => {
68
- const node = new CoreNode(mock<Stage>(), defaultProps);
86
+ const node = new CoreNode(stage, defaultProps);
69
87
  node.colorBl = 0x99aabbff;
70
88
  node.colorBr = 0xaabbccff;
71
89
  node.colorTl = 0xbbcceeff;
@@ -85,7 +103,7 @@ describe('set color()', () => {
85
103
  });
86
104
 
87
105
  it('should set update type.', () => {
88
- const node = new CoreNode(mock<Stage>(), defaultProps);
106
+ const node = new CoreNode(stage, defaultProps);
89
107
  node.updateType = 0;
90
108
 
91
109
  node.color = 0xffffffff;
@@ -93,3 +111,89 @@ describe('set color()', () => {
93
111
  expect(node.updateType).toBe(UpdateType.PremultipliedColors);
94
112
  });
95
113
  });
114
+
115
+ describe('isRenderable checks', () => {
116
+ it('should return false if node is not renderable', () => {
117
+ const node = new CoreNode(stage, defaultProps);
118
+ expect(node.isRenderable).toBe(false);
119
+ });
120
+
121
+ it('visible node that is a color texture', () => {
122
+ const node = new CoreNode(stage, defaultProps);
123
+ node.alpha = 1;
124
+ node.x = 0;
125
+ node.y = 0;
126
+ node.width = 100;
127
+ node.height = 100;
128
+ node.color = 0xffffffff;
129
+
130
+ node.update(0, clippingRect);
131
+ expect(node.isRenderable).toBe(true);
132
+ });
133
+
134
+ it('visible node that is a texture', () => {
135
+ const node = new CoreNode(stage, defaultProps);
136
+ node.alpha = 1;
137
+ node.x = 0;
138
+ node.y = 0;
139
+ node.width = 100;
140
+ node.height = 100;
141
+ node.texture = mock<ImageTexture>({
142
+ state: 'initial',
143
+ });
144
+
145
+ node.update(0, clippingRect);
146
+ expect(node.isRenderable).toBe(false);
147
+
148
+ node.texture.state = 'loaded';
149
+ node.setUpdateType(UpdateType.IsRenderable);
150
+ node.update(1, clippingRect);
151
+
152
+ expect(node.isRenderable).toBe(true);
153
+ });
154
+
155
+ it('a node with a texture with alpha 0 should not be renderable', () => {
156
+ const node = new CoreNode(stage, defaultProps);
157
+ node.alpha = 0;
158
+ node.x = 0;
159
+ node.y = 0;
160
+ node.width = 100;
161
+ node.height = 100;
162
+ node.texture = mock<ImageTexture>({
163
+ state: 'loaded',
164
+ });
165
+
166
+ node.update(0, clippingRect);
167
+ expect(node.isRenderable).toBe(false);
168
+ });
169
+
170
+ it('a node with a texture that is OutOfBounds should not be renderable', () => {
171
+ const node = new CoreNode(stage, defaultProps);
172
+ node.alpha = 1;
173
+ node.x = 300;
174
+ node.y = 300;
175
+ node.width = 100;
176
+ node.height = 100;
177
+ node.texture = mock<ImageTexture>({
178
+ state: 'loaded',
179
+ });
180
+
181
+ node.update(0, clippingRect);
182
+ expect(node.isRenderable).toBe(false);
183
+ });
184
+
185
+ it('a node with a freed texture should not be renderable', () => {
186
+ const node = new CoreNode(stage, defaultProps);
187
+ node.alpha = 1;
188
+ node.x = 0;
189
+ node.y = 0;
190
+ node.width = 100;
191
+ node.height = 100;
192
+ node.texture = mock<ImageTexture>({
193
+ state: 'freed',
194
+ });
195
+
196
+ node.update(0, clippingRect);
197
+ expect(node.isRenderable).toBe(false);
198
+ });
199
+ });
@@ -768,7 +768,16 @@ export class CoreNode extends EventEmitter {
768
768
  UpdateType.RenderState,
769
769
  );
770
770
 
771
- this.createDefaultTexture();
771
+ // if the default texture isn't loaded yet, wait for it to load
772
+ // this only happens when the node is created before the stage is ready
773
+ if (
774
+ this.stage.defaultTexture &&
775
+ this.stage.defaultTexture.state !== 'loaded'
776
+ ) {
777
+ this.stage.defaultTexture.once('loaded', () => {
778
+ this.setUpdateType(UpdateType.IsRenderable);
779
+ });
780
+ }
772
781
  }
773
782
 
774
783
  //#region Textures
@@ -808,18 +817,6 @@ export class CoreNode extends EventEmitter {
808
817
  });
809
818
  }
810
819
 
811
- createDefaultTexture(): void {
812
- // load default texture if no texture is set
813
- if (
814
- this.stage.defaultTexture !== null &&
815
- this.props.src === null &&
816
- this.props.texture === null &&
817
- this.props.rtt === false
818
- ) {
819
- this.texture = this.stage.defaultTexture;
820
- }
821
- }
822
-
823
820
  unloadTexture(): void {
824
821
  if (this.texture !== null) {
825
822
  this.texture.off('loaded', this.onTextureLoaded);
@@ -1017,7 +1014,7 @@ export class CoreNode extends EventEmitter {
1017
1014
  if (this.updateType & UpdateType.RenderTexture && this.rtt) {
1018
1015
  // Only the RTT node itself triggers `renderToTexture`
1019
1016
  this.hasRTTupdates = true;
1020
- this.stage.renderer?.renderToTexture(this);
1017
+ this.loadRenderTexture();
1021
1018
  }
1022
1019
 
1023
1020
  if (this.updateType & UpdateType.Global) {
@@ -1222,50 +1219,6 @@ export class CoreNode extends EventEmitter {
1222
1219
  }
1223
1220
  }
1224
1221
 
1225
- //check if CoreNode is renderable based on props
1226
- hasRenderableProperties(): boolean {
1227
- if (this.texture !== null) {
1228
- if (this.texture.state === 'loaded') {
1229
- return true;
1230
- }
1231
-
1232
- return false;
1233
- }
1234
-
1235
- if (!this.props.width || !this.props.height) {
1236
- return false;
1237
- }
1238
-
1239
- if (this.props.shader !== this.stage.defShaderCtr) {
1240
- return true;
1241
- }
1242
-
1243
- if (this.props.clipping === true) {
1244
- return true;
1245
- }
1246
-
1247
- if (this.props.color !== 0) {
1248
- return true;
1249
- }
1250
-
1251
- // Consider removing these checks and just using the color property check above.
1252
- // Maybe add a forceRender prop for nodes that should always render.
1253
- if (
1254
- this.props.colorTop !== 0 ||
1255
- this.props.colorBottom !== 0 ||
1256
- this.props.colorLeft !== 0 ||
1257
- this.props.colorRight !== 0 ||
1258
- this.props.colorTl !== 0 ||
1259
- this.props.colorTr !== 0 ||
1260
- this.props.colorBl !== 0 ||
1261
- this.props.colorBr !== 0
1262
- ) {
1263
- return true;
1264
- }
1265
-
1266
- return false;
1267
- }
1268
-
1269
1222
  checkRenderBounds(): CoreNodeRenderState {
1270
1223
  assertTruthy(this.renderBound);
1271
1224
  assertTruthy(this.strictBound);
@@ -1395,38 +1348,102 @@ export class CoreNode extends EventEmitter {
1395
1348
  }
1396
1349
 
1397
1350
  /**
1398
- * This function updates the `isRenderable` property based on certain conditions.
1399
- *
1400
- * @returns
1351
+ * Updates the `isRenderable` property based on various conditions.
1401
1352
  */
1402
1353
  updateIsRenderable() {
1403
- let newIsRenderable: boolean;
1404
- if (this.worldAlpha === 0 || !this.hasRenderableProperties()) {
1405
- newIsRenderable = false;
1406
- } else {
1407
- newIsRenderable = this.renderState > CoreNodeRenderState.OutOfBounds;
1354
+ let newIsRenderable = false;
1355
+ let needsTextureOwnership = false;
1356
+
1357
+ // If the node is out of bounds or has an alpha of 0, it is not renderable
1358
+ if (this.checkBasicRenderability() === false) {
1359
+ this.updateTextureOwnership(false);
1360
+ this.setRenderable(false);
1361
+ return;
1408
1362
  }
1409
1363
 
1410
- // If the texture is not loaded and the node is renderable, load the texture
1411
- // this only needs to happen once or until the texture is no longer loaded
1412
- if (
1413
- this.texture !== null &&
1414
- this.texture.state === 'freed' &&
1415
- this.renderState > CoreNodeRenderState.OutOfBounds
1364
+ if (this.texture !== null) {
1365
+ needsTextureOwnership = true;
1366
+
1367
+ // we're only renderable if the texture state is loaded
1368
+ newIsRenderable = this.texture.state === 'loaded';
1369
+ } else if (
1370
+ (this.hasShader() || this.hasColorProperties() === true) &&
1371
+ this.hasDimensions() === true
1416
1372
  ) {
1417
- this.stage.txManager.loadTexture(this.texture);
1373
+ // This mean we have dimensions and a color set, so we can render a ColorTexture
1374
+ if (
1375
+ this.stage.defaultTexture &&
1376
+ this.stage.defaultTexture.state === 'loaded'
1377
+ ) {
1378
+ newIsRenderable = true;
1379
+ }
1418
1380
  }
1419
1381
 
1420
- if (this.isRenderable !== newIsRenderable) {
1421
- this.isRenderable = newIsRenderable;
1422
- this.onChangeIsRenderable(newIsRenderable);
1382
+ this.updateTextureOwnership(needsTextureOwnership);
1383
+ this.setRenderable(newIsRenderable);
1384
+ }
1385
+
1386
+ /**
1387
+ * Checks if the node is renderable based on world alpha, dimensions and out of bounds status.
1388
+ */
1389
+ checkBasicRenderability(): boolean {
1390
+ if (this.worldAlpha === 0 || this.isOutOfBounds() === true) {
1391
+ return false;
1392
+ } else {
1393
+ return true;
1423
1394
  }
1424
1395
  }
1425
1396
 
1426
- onChangeIsRenderable(isRenderable: boolean) {
1397
+ /**
1398
+ * Sets the renderable state and triggers changes if necessary.
1399
+ * @param isRenderable - The new renderable state
1400
+ */
1401
+ setRenderable(isRenderable: boolean) {
1402
+ this.isRenderable = isRenderable;
1403
+ }
1404
+
1405
+ /**
1406
+ * Changes the renderable state of the node.
1407
+ */
1408
+ updateTextureOwnership(isRenderable: boolean) {
1427
1409
  this.texture?.setRenderableOwner(this, isRenderable);
1428
1410
  }
1429
1411
 
1412
+ /**
1413
+ * Checks if the node is out of the viewport bounds.
1414
+ */
1415
+ isOutOfBounds(): boolean {
1416
+ return this.renderState <= CoreNodeRenderState.OutOfBounds;
1417
+ }
1418
+
1419
+ /**
1420
+ * Checks if the node has dimensions (width/height)
1421
+ */
1422
+ hasDimensions(): boolean {
1423
+ return this.props.width !== 0 && this.props.height !== 0;
1424
+ }
1425
+
1426
+ /**
1427
+ * Checks if the node has any color properties set.
1428
+ */
1429
+ hasColorProperties(): boolean {
1430
+ return (
1431
+ this.props.color !== 0 ||
1432
+ this.props.colorTop !== 0 ||
1433
+ this.props.colorBottom !== 0 ||
1434
+ this.props.colorLeft !== 0 ||
1435
+ this.props.colorRight !== 0 ||
1436
+ this.props.colorTl !== 0 ||
1437
+ this.props.colorTr !== 0 ||
1438
+ this.props.colorBl !== 0 ||
1439
+ this.props.colorBr !== 0
1440
+ );
1441
+ }
1442
+
1443
+ hasShader(): boolean {
1444
+ return this.props.shader !== null;
1445
+ }
1446
+
1430
1447
  calculateRenderCoords() {
1431
1448
  const { width, height, globalTransform: transform } = this;
1432
1449
  assertTruthy(transform);
@@ -1567,7 +1584,6 @@ export class CoreNode extends EventEmitter {
1567
1584
 
1568
1585
  assertTruthy(this.globalTransform);
1569
1586
  assertTruthy(this.renderCoords);
1570
- assertTruthy(this.texture);
1571
1587
 
1572
1588
  // add to list of renderables to be sorted before rendering
1573
1589
  renderer.addQuad({
@@ -1577,7 +1593,9 @@ export class CoreNode extends EventEmitter {
1577
1593
  colorTr: this.premultipliedColorTr,
1578
1594
  colorBl: this.premultipliedColorBl,
1579
1595
  colorBr: this.premultipliedColorBr,
1580
- texture: this.texture,
1596
+ // if we do not have a texture, use the default texture
1597
+ // this assumes any renderable node is either a distinct texture or a ColorTexture
1598
+ texture: this.texture || this.stage.defaultTexture,
1581
1599
  textureOptions: this.textureOptions,
1582
1600
  zIndex: this.zIndex,
1583
1601
  shader: this.shader.shader,
@@ -2045,10 +2063,26 @@ export class CoreNode extends EventEmitter {
2045
2063
  height: this.height,
2046
2064
  });
2047
2065
 
2066
+ this.loadRenderTexture();
2067
+ }
2068
+
2069
+ private loadRenderTexture() {
2070
+ if (this.texture === null) {
2071
+ return;
2072
+ }
2073
+
2074
+ // If the texture is already loaded, render to it immediately
2075
+ if (this.texture.state === 'loaded') {
2076
+ this.stage.renderer?.renderToTexture(this);
2077
+ return;
2078
+ }
2079
+
2048
2080
  // call load immediately to ensure the texture is created
2049
2081
  this.stage.txManager.loadTexture(this.texture, true);
2050
-
2051
- this.stage.renderer?.renderToTexture(this); // Only this RTT node
2082
+ this.texture.once('loaded', () => {
2083
+ this.stage.renderer?.renderToTexture(this); // Only this RTT node
2084
+ this.setUpdateType(UpdateType.IsRenderable);
2085
+ });
2052
2086
  }
2053
2087
 
2054
2088
  private cleanupRenderTexture() {
@@ -2228,11 +2262,8 @@ export class CoreNode extends EventEmitter {
2228
2262
 
2229
2263
  this.props.texture = value;
2230
2264
  if (value !== null) {
2231
- value.setRenderableOwner(this, this.isRenderable);
2265
+ value.setRenderableOwner(this, this.isRenderable); // WVB TODO: check if this is correct
2232
2266
  this.loadTexture();
2233
- } else {
2234
- // If the texture is null, create a default texture
2235
- this.createDefaultTexture();
2236
2267
  }
2237
2268
 
2238
2269
  this.setUpdateType(UpdateType.IsRenderable);
@@ -368,15 +368,20 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
368
368
  this.textRenderer.set.y(this.trState, this.globalTransform.ty);
369
369
  }
370
370
 
371
- override hasRenderableProperties(): boolean {
371
+ override checkBasicRenderability() {
372
+ if (this.worldAlpha === 0 || this.isOutOfBounds() === true) {
373
+ return false;
374
+ }
375
+
372
376
  if (this.trState && this.trState.props.text !== '') {
373
377
  return true;
374
378
  }
375
- return super.hasRenderableProperties();
379
+
380
+ return false;
376
381
  }
377
382
 
378
- override onChangeIsRenderable(isRenderable: boolean) {
379
- super.onChangeIsRenderable(isRenderable);
383
+ override setRenderable(isRenderable: boolean) {
384
+ super.setRenderable(isRenderable);
380
385
  this.textRenderer.setIsRenderable(this.trState, isRenderable);
381
386
  }
382
387
 
package/src/core/Stage.ts CHANGED
@@ -454,6 +454,7 @@ export class Stage {
454
454
  addQuads(node: CoreNode) {
455
455
  assertTruthy(this.renderer);
456
456
 
457
+ // If the node is renderable and has a loaded texture, render it
457
458
  if (node.isRenderable === true) {
458
459
  node.renderQuads(this.renderer);
459
460
  }