@solidtv/renderer 1.1.1 → 1.1.3
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/README.md +1 -4
- package/dist/src/core/CoreNode.d.ts +27 -0
- package/dist/src/core/CoreNode.js +164 -43
- package/dist/src/core/CoreNode.js.map +1 -1
- package/dist/src/core/CoreTextureManager.d.ts +0 -4
- package/dist/src/core/CoreTextureManager.js +70 -17
- package/dist/src/core/CoreTextureManager.js.map +1 -1
- package/dist/src/core/Stage.js +5 -4
- package/dist/src/core/Stage.js.map +1 -1
- package/dist/src/core/lib/Matrix3d.d.ts +9 -0
- package/dist/src/core/lib/Matrix3d.js +29 -3
- package/dist/src/core/lib/Matrix3d.js.map +1 -1
- package/dist/src/core/lib/WebGlContextWrapper.js +0 -4
- package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
- package/dist/src/core/lib/collectionUtils.js +1 -1
- package/dist/src/core/lib/collectionUtils.js.map +1 -1
- package/dist/src/core/renderers/canvas/CanvasRenderer.d.ts +3 -3
- package/dist/src/core/renderers/canvas/CanvasRenderer.js +3 -3
- package/dist/src/core/renderers/canvas/CanvasRenderer.js.map +1 -1
- package/dist/src/core/text-rendering/CanvasFontHandler.d.ts +1 -1
- package/dist/src/core/text-rendering/CanvasFontHandler.js +3 -3
- package/dist/src/core/text-rendering/CanvasFontHandler.js.map +1 -1
- package/dist/src/core/text-rendering/CanvasTextRenderer.d.ts +2 -3
- package/dist/src/core/text-rendering/CanvasTextRenderer.js +2 -1
- package/dist/src/core/text-rendering/CanvasTextRenderer.js.map +1 -1
- package/dist/src/core/text-rendering/SdfFontHandler.js +3 -3
- package/dist/src/core/text-rendering/SdfFontHandler.js.map +1 -1
- package/dist/src/core/text-rendering/Utils.js.map +1 -1
- package/dist/src/core/textures/ImageTexture.js +1 -1
- package/dist/src/core/textures/ImageTexture.js.map +1 -1
- package/dist/src/core/textures/SubTexture.js.map +1 -1
- package/dist/src/core/textures/Texture.d.ts +6 -0
- package/dist/src/core/textures/Texture.js +6 -0
- package/dist/src/core/textures/Texture.js.map +1 -1
- package/dist/tsconfig.dist.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/src/core/CoreNode.test.ts +438 -0
- package/src/core/CoreNode.ts +176 -61
- package/src/core/CoreTextureManager.ts +77 -20
- package/src/core/Stage.ts +10 -7
- package/src/core/lib/Matrix3d.test.ts +72 -0
- package/src/core/lib/Matrix3d.ts +30 -3
- package/src/core/lib/WebGlContextWrapper.ts +0 -2
- package/src/core/lib/collectionUtils.ts +1 -1
- package/src/core/renderers/canvas/CanvasRenderer.ts +3 -3
- package/src/core/text-rendering/CanvasFontHandler.ts +4 -4
- package/src/core/text-rendering/CanvasTextRenderer.ts +7 -2
- package/src/core/text-rendering/SdfFontHandler.ts +4 -4
- package/src/core/text-rendering/Utils.ts +0 -2
- package/src/core/textures/ImageTexture.ts +1 -4
- package/src/core/textures/SubTexture.ts +0 -2
- package/src/core/textures/Texture.ts +7 -0
package/src/core/CoreNode.ts
CHANGED
|
@@ -761,10 +761,15 @@ export class CoreNode extends EventEmitter {
|
|
|
761
761
|
protected _id: number = getNewId();
|
|
762
762
|
readonly props: CoreNodeProps;
|
|
763
763
|
public readonly isCoreNode = true as const;
|
|
764
|
+
/**
|
|
765
|
+
* Lazily allocated on first `animateProp` call. Animations are rare across
|
|
766
|
+
* the scene graph, so deferring the object literal avoids per-node GC
|
|
767
|
+
* pressure during construction.
|
|
768
|
+
*/
|
|
764
769
|
private _animations: Record<
|
|
765
770
|
string,
|
|
766
771
|
{ controller: AnimationConfig; settings: Partial<AnimationSettings> }
|
|
767
|
-
> =
|
|
772
|
+
> | null = null;
|
|
768
773
|
|
|
769
774
|
// WebGL Render Op State
|
|
770
775
|
public renderOpBufferIdx: number = 0;
|
|
@@ -811,6 +816,28 @@ export class CoreNode extends EventEmitter {
|
|
|
811
816
|
public isRenderable = false;
|
|
812
817
|
public renderState: CoreNodeRenderState = CoreNodeRenderState.Init;
|
|
813
818
|
public isSimple = true;
|
|
819
|
+
/**
|
|
820
|
+
* `true` when `localTransform` is in identity-shape (ta=1, tb=0, tc=0, td=1)
|
|
821
|
+
* — i.e. a pure translation. Lets the simple-path `updateLocalTransform`
|
|
822
|
+
* skip redundant ta/tb/tc/td writes between frames. Defaults to `true`
|
|
823
|
+
* because the matrix is eagerly allocated as an identity in the constructor.
|
|
824
|
+
*/
|
|
825
|
+
public _localIsTranslate = true;
|
|
826
|
+
/**
|
|
827
|
+
* Cached result of the texture `contain` resizeMode check used by
|
|
828
|
+
* `updateLocalTransform` and `updateIsSimple`. Updated whenever the
|
|
829
|
+
* texture or textureOptions change (via `updateIsSimple`), so the hot
|
|
830
|
+
* paths can avoid the optional-chain + string compare on every frame.
|
|
831
|
+
*/
|
|
832
|
+
public _hasContainResize = false;
|
|
833
|
+
/**
|
|
834
|
+
* `true` when `globalTransform` is in identity-shape (ta=1, tb=0, tc=0, td=1).
|
|
835
|
+
* Propagates from parent: a node's global is translate-only iff the parent's
|
|
836
|
+
* global is translate-only AND the node itself is `isSimple`. Default `true`
|
|
837
|
+
* because freshly-constructed nodes have no transform applied yet, and the
|
|
838
|
+
* Stage root is configured with an identity-shape global.
|
|
839
|
+
*/
|
|
840
|
+
public _globalIsTranslate = true;
|
|
814
841
|
|
|
815
842
|
public worldAlpha = 1;
|
|
816
843
|
public premultipliedColorTl = 0;
|
|
@@ -835,6 +862,13 @@ export class CoreNode extends EventEmitter {
|
|
|
835
862
|
constructor(readonly stage: Stage, props: CoreNodeProps) {
|
|
836
863
|
super();
|
|
837
864
|
|
|
865
|
+
// Eagerly allocate the local/global transform matrices as identity.
|
|
866
|
+
// This keeps the field's hidden class monomorphic (Matrix3d, not
|
|
867
|
+
// Matrix3d|undefined) from construction onward and lets the simple
|
|
868
|
+
// / translate-only fast paths take effect on the very first update.
|
|
869
|
+
this.localTransform = Matrix3d.identity();
|
|
870
|
+
this.globalTransform = Matrix3d.identity();
|
|
871
|
+
|
|
838
872
|
//inital update type
|
|
839
873
|
let initialUpdateType =
|
|
840
874
|
UpdateType.Local | UpdateType.RenderBounds | UpdateType.RenderState;
|
|
@@ -1089,7 +1123,18 @@ export class CoreNode extends EventEmitter {
|
|
|
1089
1123
|
const { x, y } = p;
|
|
1090
1124
|
|
|
1091
1125
|
if (this.isSimple) {
|
|
1126
|
+
// Fast path: when localTransform is already in identity-shape
|
|
1127
|
+
// (ta=1, tb=0, tc=0, td=1), only tx/ty change between frames, so we
|
|
1128
|
+
// skip the 4 redundant field writes Matrix3d.translate would do.
|
|
1129
|
+
// _localIsTranslate becomes stale when a node was non-simple (had
|
|
1130
|
+
// rotation/scale/mount) on a previous frame — in that case do a
|
|
1131
|
+
// full reset to identity-translate.
|
|
1132
|
+
if (this._localIsTranslate === true) {
|
|
1133
|
+
this.localTransform!.setTranslate(x, y);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1092
1136
|
this.localTransform = Matrix3d.translate(x, y, this.localTransform);
|
|
1137
|
+
this._localIsTranslate = true;
|
|
1093
1138
|
return;
|
|
1094
1139
|
}
|
|
1095
1140
|
|
|
@@ -1097,10 +1142,15 @@ export class CoreNode extends EventEmitter {
|
|
|
1097
1142
|
const mountTranslateX = p.mountX * w;
|
|
1098
1143
|
const mountTranslateY = p.mountY * h;
|
|
1099
1144
|
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1145
|
+
const rotation = p.rotation;
|
|
1146
|
+
const scaleX = p.scaleX;
|
|
1147
|
+
const scaleY = p.scaleY;
|
|
1148
|
+
|
|
1149
|
+
if (rotation !== 0) {
|
|
1150
|
+
// Full rotation (+ optional scale + pivot)
|
|
1151
|
+
const scaleRotate = Matrix3d.rotate(rotation, Matrix3d.temp).scale(
|
|
1152
|
+
scaleX,
|
|
1153
|
+
scaleY,
|
|
1104
1154
|
);
|
|
1105
1155
|
const pivotTranslateX = p.pivotX * w;
|
|
1106
1156
|
const pivotTranslateY = p.pivotY * h;
|
|
@@ -1112,7 +1162,21 @@ export class CoreNode extends EventEmitter {
|
|
|
1112
1162
|
)
|
|
1113
1163
|
.multiply(scaleRotate)
|
|
1114
1164
|
.translate(-pivotTranslateX, -pivotTranslateY);
|
|
1165
|
+
} else if (scaleX !== 1 || scaleY !== 1) {
|
|
1166
|
+
// Scale (+ optional pivot) without rotation — skip the rotate matrix
|
|
1167
|
+
// and the 8-mul multiply; `.scale()` is a 4-mul in-place op.
|
|
1168
|
+
const pivotTranslateX = p.pivotX * w;
|
|
1169
|
+
const pivotTranslateY = p.pivotY * h;
|
|
1170
|
+
|
|
1171
|
+
this.localTransform = Matrix3d.translate(
|
|
1172
|
+
x - mountTranslateX + pivotTranslateX,
|
|
1173
|
+
y - mountTranslateY + pivotTranslateY,
|
|
1174
|
+
this.localTransform,
|
|
1175
|
+
)
|
|
1176
|
+
.scale(scaleX, scaleY)
|
|
1177
|
+
.translate(-pivotTranslateX, -pivotTranslateY);
|
|
1115
1178
|
} else {
|
|
1179
|
+
// Mount (or texture-contain) only — pure translation.
|
|
1116
1180
|
this.localTransform = Matrix3d.translate(
|
|
1117
1181
|
x - mountTranslateX,
|
|
1118
1182
|
y - mountTranslateY,
|
|
@@ -1120,12 +1184,13 @@ export class CoreNode extends EventEmitter {
|
|
|
1120
1184
|
);
|
|
1121
1185
|
}
|
|
1122
1186
|
|
|
1123
|
-
// Handle 'contain' resize mode
|
|
1187
|
+
// Handle 'contain' resize mode (cached check; dimensions still need
|
|
1188
|
+
// a runtime null-check because they're populated asynchronously).
|
|
1124
1189
|
const texture = p.texture;
|
|
1125
1190
|
if (
|
|
1126
|
-
|
|
1127
|
-
texture
|
|
1128
|
-
|
|
1191
|
+
this._hasContainResize === true &&
|
|
1192
|
+
texture !== null &&
|
|
1193
|
+
texture.dimensions !== null
|
|
1129
1194
|
) {
|
|
1130
1195
|
let resizeModeScaleX = 1;
|
|
1131
1196
|
let resizeModeScaleY = 1;
|
|
@@ -1157,17 +1222,23 @@ export class CoreNode extends EventEmitter {
|
|
|
1157
1222
|
.translate(extraX, extraY)
|
|
1158
1223
|
.scale(resizeModeScaleX, resizeModeScaleY);
|
|
1159
1224
|
}
|
|
1225
|
+
|
|
1226
|
+
this._localIsTranslate = false;
|
|
1160
1227
|
}
|
|
1161
1228
|
|
|
1162
1229
|
updateIsSimple() {
|
|
1163
1230
|
const p = this.props;
|
|
1231
|
+
// Cache the texture-contain check so updateLocalTransform doesn't have to
|
|
1232
|
+
// run the optional-chain + string compare on every Local update.
|
|
1233
|
+
this._hasContainResize =
|
|
1234
|
+
p.texture !== null && p.textureOptions?.resizeMode?.type === 'contain';
|
|
1164
1235
|
this.isSimple =
|
|
1165
1236
|
p.rotation === 0 &&
|
|
1166
1237
|
p.scaleX === 1 &&
|
|
1167
1238
|
p.scaleY === 1 &&
|
|
1168
1239
|
p.mountX === 0 &&
|
|
1169
1240
|
p.mountY === 0 &&
|
|
1170
|
-
|
|
1241
|
+
this._hasContainResize === false;
|
|
1171
1242
|
}
|
|
1172
1243
|
|
|
1173
1244
|
/**
|
|
@@ -1209,6 +1280,10 @@ export class CoreNode extends EventEmitter {
|
|
|
1209
1280
|
}
|
|
1210
1281
|
|
|
1211
1282
|
if (updateType & UpdateType.Global) {
|
|
1283
|
+
const lt = this.localTransform!;
|
|
1284
|
+
const gt = this.globalTransform!;
|
|
1285
|
+
let fastPathApplied = false;
|
|
1286
|
+
|
|
1212
1287
|
if (
|
|
1213
1288
|
USE_RTT &&
|
|
1214
1289
|
this.parentHasRenderTexture === true &&
|
|
@@ -1216,7 +1291,7 @@ export class CoreNode extends EventEmitter {
|
|
|
1216
1291
|
) {
|
|
1217
1292
|
// we are at the start of the RTT chain, so we need to reset the globalTransform
|
|
1218
1293
|
// for correct RTT rendering
|
|
1219
|
-
|
|
1294
|
+
Matrix3d.identity(gt);
|
|
1220
1295
|
|
|
1221
1296
|
// Maintain a full scene global transform for bounds detection
|
|
1222
1297
|
const parentTransform =
|
|
@@ -1225,7 +1300,10 @@ export class CoreNode extends EventEmitter {
|
|
|
1225
1300
|
this.sceneGlobalTransform = Matrix3d.copy(
|
|
1226
1301
|
parentTransform,
|
|
1227
1302
|
this.sceneGlobalTransform,
|
|
1228
|
-
).translateOrMultiply(
|
|
1303
|
+
).translateOrMultiply(lt);
|
|
1304
|
+
|
|
1305
|
+
// identity * local => translate-only iff this node is simple
|
|
1306
|
+
this._globalIsTranslate = this.isSimple;
|
|
1229
1307
|
} else if (
|
|
1230
1308
|
USE_RTT &&
|
|
1231
1309
|
this.parentHasRenderTexture === true &&
|
|
@@ -1234,32 +1312,48 @@ export class CoreNode extends EventEmitter {
|
|
|
1234
1312
|
// we're part of an RTT chain but our parent is not the main RTT node
|
|
1235
1313
|
// so we need to propogate the sceneGlobalTransform of the parent
|
|
1236
1314
|
// to maintain a full scene global transform for bounds detection
|
|
1237
|
-
const parentSceneTransform =
|
|
1238
|
-
parent.sceneGlobalTransform || this.localTransform!;
|
|
1315
|
+
const parentSceneTransform = parent.sceneGlobalTransform || lt;
|
|
1239
1316
|
|
|
1240
1317
|
this.sceneGlobalTransform = Matrix3d.copy(
|
|
1241
1318
|
parentSceneTransform,
|
|
1242
1319
|
this.sceneGlobalTransform,
|
|
1243
|
-
).translateOrMultiply(
|
|
1320
|
+
).translateOrMultiply(lt);
|
|
1244
1321
|
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1322
|
+
Matrix3d.copy(parent.globalTransform!, gt);
|
|
1323
|
+
|
|
1324
|
+
// Conservative: RTT chains rarely hit the translate fast path
|
|
1325
|
+
this._globalIsTranslate = false;
|
|
1249
1326
|
} else {
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1327
|
+
// Common non-RTT path
|
|
1328
|
+
const parentGT = parent.globalTransform!;
|
|
1329
|
+
if (this.isSimple === true && parent._globalIsTranslate === true) {
|
|
1330
|
+
// Translate-only fast path: parent global and local are both pure
|
|
1331
|
+
// translations, so the resulting global is also a pure translation
|
|
1332
|
+
// and collapses to 2 adds on tx/ty.
|
|
1333
|
+
if (this._globalIsTranslate === false) {
|
|
1334
|
+
// Transitioning back into translate-only — reset ta/tb/tc/td
|
|
1335
|
+
// that may have been left non-identity by a prior frame.
|
|
1336
|
+
gt.ta = 1;
|
|
1337
|
+
gt.tb = 0;
|
|
1338
|
+
gt.tc = 0;
|
|
1339
|
+
gt.td = 1;
|
|
1340
|
+
}
|
|
1341
|
+
gt.setTranslate(parentGT.tx + lt.tx, parentGT.ty + lt.ty);
|
|
1342
|
+
this._globalIsTranslate = true;
|
|
1343
|
+
fastPathApplied = true;
|
|
1344
|
+
} else {
|
|
1345
|
+
Matrix3d.copy(parentGT, gt);
|
|
1346
|
+
this._globalIsTranslate =
|
|
1347
|
+
this.isSimple === true && parent._globalIsTranslate === true;
|
|
1348
|
+
}
|
|
1254
1349
|
}
|
|
1255
1350
|
|
|
1256
|
-
if (
|
|
1257
|
-
this.
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
this.globalTransform.translateOrMultiply(this.localTransform!);
|
|
1351
|
+
if (fastPathApplied === false) {
|
|
1352
|
+
if (this.isSimple) {
|
|
1353
|
+
gt.translate(lt.tx, lt.ty);
|
|
1354
|
+
} else {
|
|
1355
|
+
gt.translateOrMultiply(lt);
|
|
1356
|
+
}
|
|
1263
1357
|
}
|
|
1264
1358
|
this.calculateRenderCoords();
|
|
1265
1359
|
this.updateBoundingRect();
|
|
@@ -1380,18 +1474,30 @@ export class CoreNode extends EventEmitter {
|
|
|
1380
1474
|
childClippingRect = NO_CLIPPING_RECT;
|
|
1381
1475
|
}
|
|
1382
1476
|
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1477
|
+
const children = this.children;
|
|
1478
|
+
const length = children.length;
|
|
1479
|
+
if (childUpdateType !== 0) {
|
|
1480
|
+
// Specialized loop: OR-in the inherited update bits for every child,
|
|
1481
|
+
// then update if non-zero. Avoids the per-iter `childUpdateType !== 0`
|
|
1482
|
+
// compare.
|
|
1483
|
+
for (let i = 0; i < length; i++) {
|
|
1484
|
+
const child = children[i] as CoreNode;
|
|
1387
1485
|
child.updateType |= childUpdateType;
|
|
1486
|
+
if (child.updateType === 0) {
|
|
1487
|
+
continue;
|
|
1488
|
+
}
|
|
1489
|
+
child.update(delta, childClippingRect);
|
|
1388
1490
|
}
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1491
|
+
} else {
|
|
1492
|
+
// Specialized loop: nothing to inherit, so only walk children that
|
|
1493
|
+
// already have pending work of their own.
|
|
1494
|
+
for (let i = 0; i < length; i++) {
|
|
1495
|
+
const child = children[i] as CoreNode;
|
|
1496
|
+
if (child.updateType === 0) {
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
child.update(delta, childClippingRect);
|
|
1392
1500
|
}
|
|
1393
|
-
|
|
1394
|
-
child.update(delta, childClippingRect);
|
|
1395
1501
|
}
|
|
1396
1502
|
}
|
|
1397
1503
|
|
|
@@ -1503,7 +1609,7 @@ export class CoreNode extends EventEmitter {
|
|
|
1503
1609
|
const renderCoords = (this.sceneRenderCoords ||
|
|
1504
1610
|
this.renderCoords) as RenderCoords;
|
|
1505
1611
|
|
|
1506
|
-
if (transform.tb === 0
|
|
1612
|
+
if (transform.tb === 0 && transform.tc === 0) {
|
|
1507
1613
|
this.renderBound = createBound(
|
|
1508
1614
|
renderCoords.x1,
|
|
1509
1615
|
renderCoords.y1,
|
|
@@ -2835,27 +2941,32 @@ export class CoreNode extends EventEmitter {
|
|
|
2835
2941
|
value: number,
|
|
2836
2942
|
settings: Partial<AnimationSettings>,
|
|
2837
2943
|
): IAnimationController {
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
values
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2944
|
+
let animations = this._animations;
|
|
2945
|
+
if (animations !== null) {
|
|
2946
|
+
const existing = animations[name];
|
|
2947
|
+
|
|
2948
|
+
if (existing && existing.settings === settings) {
|
|
2949
|
+
const controller = existing.controller;
|
|
2950
|
+
const values = controller.props ? controller.props[name] : null;
|
|
2951
|
+
|
|
2952
|
+
if (values) {
|
|
2953
|
+
values.start = (this as any)[name] ?? 0;
|
|
2954
|
+
values.target = value;
|
|
2955
|
+
controller.progress = 0;
|
|
2956
|
+
|
|
2957
|
+
if (settings.adaptiveDuration === true) {
|
|
2958
|
+
const now = performance.now();
|
|
2959
|
+
const elapsed = now - controller.lastRunTime;
|
|
2960
|
+
controller.lastRunTime = now;
|
|
2961
|
+
const duration = settings.duration ?? controller.duration;
|
|
2962
|
+
controller.duration = elapsed < duration ? elapsed : duration;
|
|
2963
|
+
}
|
|
2856
2964
|
|
|
2857
|
-
|
|
2965
|
+
return controller.start();
|
|
2966
|
+
}
|
|
2858
2967
|
}
|
|
2968
|
+
} else {
|
|
2969
|
+
animations = this._animations = {};
|
|
2859
2970
|
}
|
|
2860
2971
|
|
|
2861
2972
|
const animationProps: Partial<CoreNodeAnimateProps> = { [name]: value };
|
|
@@ -2865,12 +2976,16 @@ export class CoreNode extends EventEmitter {
|
|
|
2865
2976
|
animationProps,
|
|
2866
2977
|
settings,
|
|
2867
2978
|
);
|
|
2868
|
-
|
|
2979
|
+
animations[name] = { controller, settings };
|
|
2869
2980
|
return controller.start();
|
|
2870
2981
|
}
|
|
2871
2982
|
|
|
2872
2983
|
animateToTarget(prop: string): number | undefined {
|
|
2873
|
-
const
|
|
2984
|
+
const animations = this._animations;
|
|
2985
|
+
if (animations === null) {
|
|
2986
|
+
return undefined;
|
|
2987
|
+
}
|
|
2988
|
+
const animation = animations[prop];
|
|
2874
2989
|
if (!animation) {
|
|
2875
2990
|
return undefined;
|
|
2876
2991
|
}
|
|
@@ -159,16 +159,78 @@ export interface TextureOptions {
|
|
|
159
159
|
resizeMode?: ResizeModeOptions;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Insertion-ordered, dedup'd FIFO queue used by the texture upload
|
|
164
|
+
* pipeline.
|
|
165
|
+
*
|
|
166
|
+
* Backed by an array (for ordered traversal without per-call iterator
|
|
167
|
+
* allocation) plus a Set (for O(1) membership and cancel-by-reference).
|
|
168
|
+
* Removed entries are tombstoned in the array — they're skipped on the
|
|
169
|
+
* next `shift()`, and the array is compacted once the consumed prefix
|
|
170
|
+
* grows large enough to be worth reclaiming.
|
|
171
|
+
*/
|
|
172
|
+
class TextureUploadQueue {
|
|
173
|
+
private list: Texture[] = [];
|
|
174
|
+
private membership: Set<Texture> = new Set();
|
|
175
|
+
private head = 0;
|
|
176
|
+
|
|
177
|
+
get size(): number {
|
|
178
|
+
return this.membership.size;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
has(texture: Texture): boolean {
|
|
182
|
+
return this.membership.has(texture);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
add(texture: Texture): void {
|
|
186
|
+
if (this.membership.has(texture)) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
this.membership.add(texture);
|
|
190
|
+
this.list.push(texture);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
delete(texture: Texture): boolean {
|
|
194
|
+
return this.membership.delete(texture);
|
|
195
|
+
}
|
|
196
|
+
|
|
163
197
|
/**
|
|
164
|
-
*
|
|
198
|
+
* Remove and return the oldest live entry, or `undefined` if empty.
|
|
165
199
|
*/
|
|
166
|
-
|
|
200
|
+
shift(): Texture | undefined {
|
|
201
|
+
const list = this.list;
|
|
202
|
+
const membership = this.membership;
|
|
203
|
+
while (this.head < list.length) {
|
|
204
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
205
|
+
const texture = list[this.head++]!;
|
|
206
|
+
if (membership.has(texture)) {
|
|
207
|
+
membership.delete(texture);
|
|
208
|
+
this.compactIfNeeded();
|
|
209
|
+
return texture;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// Fully drained — reset for the next batch.
|
|
213
|
+
this.head = 0;
|
|
214
|
+
list.length = 0;
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private compactIfNeeded(): void {
|
|
219
|
+
// Drop the consumed prefix once it's substantial in absolute terms
|
|
220
|
+
// and at least half the array. Avoids growing the array unboundedly
|
|
221
|
+
// when shift() runs faster than the queue refills.
|
|
222
|
+
if (this.head >= 64 && this.head >= this.list.length >> 1) {
|
|
223
|
+
this.list = this.list.slice(this.head);
|
|
224
|
+
this.head = 0;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
167
228
|
|
|
229
|
+
export class CoreTextureManager extends EventEmitter {
|
|
168
230
|
/**
|
|
169
|
-
* Map of
|
|
231
|
+
* Map of textures by cache key
|
|
170
232
|
*/
|
|
171
|
-
|
|
233
|
+
keyCache: Map<string, Texture> = new Map();
|
|
172
234
|
|
|
173
235
|
/**
|
|
174
236
|
* Map of texture constructors by their type name
|
|
@@ -176,7 +238,7 @@ export class CoreTextureManager extends EventEmitter {
|
|
|
176
238
|
txConstructors: Partial<TextureMap> = {};
|
|
177
239
|
|
|
178
240
|
public maxRetryCount: number;
|
|
179
|
-
private uploadTextureQueue:
|
|
241
|
+
private uploadTextureQueue: TextureUploadQueue = new TextureUploadQueue();
|
|
180
242
|
private initialized = false;
|
|
181
243
|
private stage: Stage;
|
|
182
244
|
private numImageWorkers: number;
|
|
@@ -494,13 +556,9 @@ export class CoreTextureManager extends EventEmitter {
|
|
|
494
556
|
texture.state === 'failed' || texture.state === 'freed';
|
|
495
557
|
|
|
496
558
|
const fillPrefetch = () => {
|
|
497
|
-
while (
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
) {
|
|
501
|
-
const [texture] = this.uploadTextureQueue;
|
|
502
|
-
if (!texture) break;
|
|
503
|
-
this.uploadTextureQueue.delete(texture);
|
|
559
|
+
while (pending.length < prefetchLimit) {
|
|
560
|
+
const texture = this.uploadTextureQueue.shift();
|
|
561
|
+
if (texture === undefined) break;
|
|
504
562
|
|
|
505
563
|
if (isDead(texture)) {
|
|
506
564
|
continue;
|
|
@@ -570,9 +628,8 @@ export class CoreTextureManager extends EventEmitter {
|
|
|
570
628
|
* @param cacheKey Cache key for the texture
|
|
571
629
|
*/
|
|
572
630
|
initTextureToCache(texture: Texture, cacheKey: string) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
inverseKeyCache.set(texture, cacheKey);
|
|
631
|
+
this.keyCache.set(cacheKey, texture);
|
|
632
|
+
texture.cacheKey = cacheKey;
|
|
576
633
|
}
|
|
577
634
|
|
|
578
635
|
/**
|
|
@@ -593,10 +650,10 @@ export class CoreTextureManager extends EventEmitter {
|
|
|
593
650
|
* @param texture
|
|
594
651
|
*/
|
|
595
652
|
removeTextureFromCache(texture: Texture) {
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
653
|
+
const cacheKey = texture.cacheKey;
|
|
654
|
+
if (cacheKey !== null) {
|
|
655
|
+
this.keyCache.delete(cacheKey);
|
|
656
|
+
texture.cacheKey = null;
|
|
600
657
|
}
|
|
601
658
|
}
|
|
602
659
|
|
package/src/core/Stage.ts
CHANGED
|
@@ -359,9 +359,10 @@ export class Stage {
|
|
|
359
359
|
|
|
360
360
|
this.root = rootNode;
|
|
361
361
|
|
|
362
|
-
// Initialize root node properties
|
|
362
|
+
// Initialize root node properties. Copy in place into the eagerly-allocated
|
|
363
|
+
// matrices so the hidden class for these fields stays stable.
|
|
363
364
|
rootNode.updateLocalTransform();
|
|
364
|
-
|
|
365
|
+
Matrix3d.copy(rootNode.localTransform!, rootNode.globalTransform);
|
|
365
366
|
rootNode.sceneGlobalTransform = Matrix3d.copy(rootNode.localTransform!);
|
|
366
367
|
rootNode.calculateRenderCoords();
|
|
367
368
|
rootNode.updateBoundingRect();
|
|
@@ -1066,7 +1067,7 @@ export class Stage {
|
|
|
1066
1067
|
* @param options - Font loading options specific to the renderer type
|
|
1067
1068
|
* @returns Promise that resolves when the font is loaded
|
|
1068
1069
|
*/
|
|
1069
|
-
|
|
1070
|
+
loadFont(
|
|
1070
1071
|
rendererType: TextRenderers,
|
|
1071
1072
|
options: FontLoadOptions,
|
|
1072
1073
|
): Promise<void> {
|
|
@@ -1074,10 +1075,12 @@ export class Stage {
|
|
|
1074
1075
|
const fontHandler = this.fontHandlers[rendererTypeKey];
|
|
1075
1076
|
|
|
1076
1077
|
if (!fontHandler) {
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1078
|
+
return Promise.reject(
|
|
1079
|
+
new Error(
|
|
1080
|
+
`Font handler for renderer type '${rendererTypeKey}' not found. Available types: ${Object.keys(
|
|
1081
|
+
this.fontHandlers,
|
|
1082
|
+
).join(', ')}`,
|
|
1083
|
+
),
|
|
1081
1084
|
);
|
|
1082
1085
|
}
|
|
1083
1086
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Matrix3d } from './Matrix3d.js';
|
|
3
|
+
|
|
4
|
+
describe('Matrix3d.setTranslate', () => {
|
|
5
|
+
it('writes tx/ty without touching ta/tb/tc/td', () => {
|
|
6
|
+
const m = Matrix3d.identity();
|
|
7
|
+
m.setTranslate(10, 20);
|
|
8
|
+
expect(m.tx).toBe(10);
|
|
9
|
+
expect(m.ty).toBe(20);
|
|
10
|
+
expect(m.ta).toBe(1);
|
|
11
|
+
expect(m.tb).toBe(0);
|
|
12
|
+
expect(m.tc).toBe(0);
|
|
13
|
+
expect(m.td).toBe(1);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('updates the float array on subsequent getFloatArr() calls', () => {
|
|
17
|
+
const m = Matrix3d.identity();
|
|
18
|
+
m.setTranslate(5, 7);
|
|
19
|
+
// First read populates the array
|
|
20
|
+
const arr = m.getFloatArr();
|
|
21
|
+
expect(arr[6]).toBe(5);
|
|
22
|
+
expect(arr[7]).toBe(7);
|
|
23
|
+
|
|
24
|
+
// Mutate, then expect getFloatArr() to pick up the change.
|
|
25
|
+
m.setTranslate(9, 11);
|
|
26
|
+
const arr2 = m.getFloatArr();
|
|
27
|
+
expect(arr2[6]).toBe(9);
|
|
28
|
+
expect(arr2[7]).toBe(11);
|
|
29
|
+
// Same array reference reused (no GC pressure).
|
|
30
|
+
expect(arr2).toBe(arr);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('overwrites prior tx/ty values', () => {
|
|
34
|
+
const m = Matrix3d.translate(100, 200);
|
|
35
|
+
m.setTranslate(0, 0);
|
|
36
|
+
expect(m.tx).toBe(0);
|
|
37
|
+
expect(m.ty).toBe(0);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('Matrix3d.rotate(0) fast path', () => {
|
|
42
|
+
it('produces an identity matrix for angle=0', () => {
|
|
43
|
+
const m = Matrix3d.rotate(0);
|
|
44
|
+
expect(m.ta).toBe(1);
|
|
45
|
+
expect(m.tb).toBe(0);
|
|
46
|
+
expect(m.tc).toBe(0);
|
|
47
|
+
expect(m.td).toBe(1);
|
|
48
|
+
expect(m.tx).toBe(0);
|
|
49
|
+
expect(m.ty).toBe(0);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('resets a pre-populated out matrix to identity on angle=0', () => {
|
|
53
|
+
const m = Matrix3d.rotate(Math.PI / 4);
|
|
54
|
+
// Now reuse `m` with angle=0; should overwrite to identity.
|
|
55
|
+
Matrix3d.rotate(0, m);
|
|
56
|
+
expect(m.ta).toBe(1);
|
|
57
|
+
expect(m.tb).toBe(0);
|
|
58
|
+
expect(m.tc).toBe(0);
|
|
59
|
+
expect(m.td).toBe(1);
|
|
60
|
+
expect(m.tx).toBe(0);
|
|
61
|
+
expect(m.ty).toBe(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('still produces a real rotation when angle != 0', () => {
|
|
65
|
+
const m = Matrix3d.rotate(Math.PI / 2);
|
|
66
|
+
// cos(pi/2) ~ 0, sin(pi/2) = 1
|
|
67
|
+
expect(Math.abs(m.ta)).toBeLessThan(1e-10);
|
|
68
|
+
expect(m.tb).toBeCloseTo(-1, 10);
|
|
69
|
+
expect(m.tc).toBeCloseTo(1, 10);
|
|
70
|
+
expect(Math.abs(m.td)).toBeLessThan(1e-10);
|
|
71
|
+
});
|
|
72
|
+
});
|
package/src/core/lib/Matrix3d.ts
CHANGED
|
@@ -124,11 +124,24 @@ export class Matrix3d {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
public static rotate(angle: number, out?: Matrix3d): Matrix3d {
|
|
127
|
-
|
|
128
|
-
const sin = Math.sin(angle);
|
|
129
|
-
if (!out) {
|
|
127
|
+
if (out === undefined) {
|
|
130
128
|
out = new Matrix3d();
|
|
131
129
|
}
|
|
130
|
+
if (angle === 0) {
|
|
131
|
+
// Skip Math.cos(0) / Math.sin(0) — neither V8 nor JSC constant-folds
|
|
132
|
+
// these calls. Identity rotation is the common case when callers
|
|
133
|
+
// combine rotate().scale() and only the scale is non-default.
|
|
134
|
+
out.ta = 1;
|
|
135
|
+
out.tb = 0;
|
|
136
|
+
out.tx = 0;
|
|
137
|
+
out.tc = 0;
|
|
138
|
+
out.td = 1;
|
|
139
|
+
out.ty = 0;
|
|
140
|
+
out.mutation = true;
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
const cos = Math.cos(angle);
|
|
144
|
+
const sin = Math.sin(angle);
|
|
132
145
|
out.ta = cos;
|
|
133
146
|
out.tb = -sin;
|
|
134
147
|
out.tx = 0;
|
|
@@ -160,6 +173,20 @@ export class Matrix3d {
|
|
|
160
173
|
return this;
|
|
161
174
|
}
|
|
162
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Writes `tx`/`ty` directly and marks the matrix mutated.
|
|
178
|
+
*
|
|
179
|
+
* Caller is responsible for ensuring `ta`/`tb`/`tc`/`td` are already the
|
|
180
|
+
* desired values — typically the identity rotation/scale (1, 0, 0, 1).
|
|
181
|
+
* Used by hot paths that know the matrix is in identity-shape so the
|
|
182
|
+
* 4 redundant field writes performed by `Matrix3d.translate` can be skipped.
|
|
183
|
+
*/
|
|
184
|
+
public setTranslate(x: number, y: number): void {
|
|
185
|
+
this.tx = x;
|
|
186
|
+
this.ty = y;
|
|
187
|
+
this.mutation = true;
|
|
188
|
+
}
|
|
189
|
+
|
|
163
190
|
public scale(sx: number, sy: number): Matrix3d {
|
|
164
191
|
this.ta = this.ta * sx;
|
|
165
192
|
this.tb = this.tb * sy;
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
|
2
2
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
3
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
|
4
|
-
|
|
5
|
-
import { assertTruthy, isProductionEnvironment } from '../../utils.js';
|
|
6
4
|
import type {
|
|
7
5
|
Vec2,
|
|
8
6
|
Vec3,
|