@vectojs/core 1.23.0 → 1.25.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/{chunk-UPULSLKA.mjs → chunk-I4WQY7CJ.mjs} +178 -94
- package/dist/{chunk-LHONR3GO.js → chunk-KHGHP2J3.js} +178 -94
- package/dist/{chunk-FRMLD4PP.mjs → chunk-MVFPN4Y5.mjs} +114 -1
- package/dist/{chunk-AGP4VLF4.js → chunk-Z3HFY75R.js} +131 -18
- package/dist/index.d.ts +1 -0
- package/dist/index.js +481 -268
- package/dist/index.mjs +234 -21
- package/dist/performance/UserTiming.d.ts +44 -0
- package/dist/renderer.js +2 -2
- package/dist/renderer.mjs +1 -1
- package/dist/text/MSDFTextEntity.d.ts +24 -0
- package/dist/text/SVGEntity.d.ts +24 -0
- package/dist/text.js +2 -2
- package/dist/text.mjs +1 -1
- package/dist/tree/Scene.d.ts +49 -1
- package/package.json +7 -6
|
@@ -924,10 +924,11 @@ var FLOATS_PER_POINT = 7;
|
|
|
924
924
|
var POINT_STRIDE = FLOATS_PER_POINT * 4;
|
|
925
925
|
var VERTS_PER_QUAD = 4;
|
|
926
926
|
var INDICES_PER_QUAD = 6;
|
|
927
|
-
var
|
|
928
|
-
var
|
|
929
|
-
var
|
|
930
|
-
var
|
|
927
|
+
var PACKED_QUAD_VERT_STRIDE = 16;
|
|
928
|
+
var PACKED_RECT_VERT_STRIDE = 12;
|
|
929
|
+
var PACKED_UV_OFFSET = 8;
|
|
930
|
+
var PACKED_TINT_OFFSET = 12;
|
|
931
|
+
var PACKED_RECT_TINT_OFFSET = 8;
|
|
931
932
|
function buildQuadIndices(quads) {
|
|
932
933
|
const out = new Uint32Array(quads * INDICES_PER_QUAD);
|
|
933
934
|
for (let i = 0; i < quads; i++) {
|
|
@@ -942,6 +943,35 @@ function buildQuadIndices(quads) {
|
|
|
942
943
|
}
|
|
943
944
|
return out;
|
|
944
945
|
}
|
|
946
|
+
function encodeUnorm(v, max) {
|
|
947
|
+
return v <= 0 ? 0 : v >= 1 ? max : v * max + 0.5 | 0;
|
|
948
|
+
}
|
|
949
|
+
var LITTLE_ENDIAN = new Uint8Array(new Uint32Array([1]).buffer)[0] === 1;
|
|
950
|
+
function packTint(r, g, b, a) {
|
|
951
|
+
const ri = encodeUnorm(r, 255);
|
|
952
|
+
const gi = encodeUnorm(g, 255);
|
|
953
|
+
const bi = encodeUnorm(b, 255);
|
|
954
|
+
const ai = encodeUnorm(a, 255);
|
|
955
|
+
return LITTLE_ENDIAN ? ai << 24 | bi << 16 | gi << 8 | ri : ri << 24 | gi << 16 | bi << 8 | ai;
|
|
956
|
+
}
|
|
957
|
+
function createPackedBuffer(byteLength) {
|
|
958
|
+
const bytes = new ArrayBuffer(byteLength);
|
|
959
|
+
return {
|
|
960
|
+
bytes,
|
|
961
|
+
f32: new Float32Array(bytes),
|
|
962
|
+
u16: new Uint16Array(bytes),
|
|
963
|
+
u32: new Uint32Array(bytes),
|
|
964
|
+
u8: new Uint8Array(bytes)
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
function growPacked(buf, neededBytes) {
|
|
968
|
+
if (neededBytes <= buf.bytes.byteLength) return buf;
|
|
969
|
+
let cap = buf.bytes.byteLength;
|
|
970
|
+
while (cap < neededBytes) cap *= 2;
|
|
971
|
+
const grown = createPackedBuffer(cap);
|
|
972
|
+
grown.u8.set(buf.u8);
|
|
973
|
+
return grown;
|
|
974
|
+
}
|
|
945
975
|
var POINT_VERT = `#version 300 es
|
|
946
976
|
in vec2 a_pos;
|
|
947
977
|
in float a_radius;
|
|
@@ -1069,7 +1099,7 @@ function link(gl, vsSrc, fsSrc) {
|
|
|
1069
1099
|
}
|
|
1070
1100
|
return program;
|
|
1071
1101
|
}
|
|
1072
|
-
function writeQuad(out,
|
|
1102
|
+
function writeQuad(out, byteOffset, x, y, width, height, u0, v0, u1, v1, r, g, b, al, rotation) {
|
|
1073
1103
|
let x1;
|
|
1074
1104
|
let y1;
|
|
1075
1105
|
let x2;
|
|
@@ -1097,40 +1127,39 @@ function writeQuad(out, o, x, y, width, height, u0, v0, u1, v1, r, g, b, al, rot
|
|
|
1097
1127
|
x3 = x - hs;
|
|
1098
1128
|
y3 = y + hc;
|
|
1099
1129
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
out[o + 31] = al;
|
|
1130
|
+
const iu0 = encodeUnorm(u0, 65535);
|
|
1131
|
+
const iv0 = encodeUnorm(v0, 65535);
|
|
1132
|
+
const iu1 = encodeUnorm(u1, 65535);
|
|
1133
|
+
const iv1 = encodeUnorm(v1, 65535);
|
|
1134
|
+
const tint = packTint(r, g, b, al);
|
|
1135
|
+
const f32 = out.f32;
|
|
1136
|
+
const u16 = out.u16;
|
|
1137
|
+
const u32 = out.u32;
|
|
1138
|
+
const fi = byteOffset >> 2;
|
|
1139
|
+
const si = byteOffset + PACKED_UV_OFFSET >> 1;
|
|
1140
|
+
const ti = byteOffset + PACKED_TINT_OFFSET >> 2;
|
|
1141
|
+
f32[fi] = x;
|
|
1142
|
+
f32[fi + 1] = y;
|
|
1143
|
+
u16[si] = iu0;
|
|
1144
|
+
u16[si + 1] = iv0;
|
|
1145
|
+
u32[ti] = tint;
|
|
1146
|
+
f32[fi + 4] = x1;
|
|
1147
|
+
f32[fi + 5] = y1;
|
|
1148
|
+
u16[si + 8] = iu1;
|
|
1149
|
+
u16[si + 9] = iv0;
|
|
1150
|
+
u32[ti + 4] = tint;
|
|
1151
|
+
f32[fi + 8] = x2;
|
|
1152
|
+
f32[fi + 9] = y2;
|
|
1153
|
+
u16[si + 16] = iu1;
|
|
1154
|
+
u16[si + 17] = iv1;
|
|
1155
|
+
u32[ti + 8] = tint;
|
|
1156
|
+
f32[fi + 12] = x3;
|
|
1157
|
+
f32[fi + 13] = y3;
|
|
1158
|
+
u16[si + 24] = iu0;
|
|
1159
|
+
u16[si + 25] = iv1;
|
|
1160
|
+
u32[ti + 12] = tint;
|
|
1132
1161
|
}
|
|
1133
|
-
function writeRectQuad(out,
|
|
1162
|
+
function writeRectQuad(out, byteOffset, x, y, width, height, r, g, b, al, rotation) {
|
|
1134
1163
|
let x1;
|
|
1135
1164
|
let y1;
|
|
1136
1165
|
let x2;
|
|
@@ -1158,30 +1187,22 @@ function writeRectQuad(out, o, x, y, width, height, r, g, b, al, rotation) {
|
|
|
1158
1187
|
x3 = x - hs;
|
|
1159
1188
|
y3 = y + hc;
|
|
1160
1189
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
out[o + 16] = b;
|
|
1178
|
-
out[o + 17] = al;
|
|
1179
|
-
out[o + 18] = x3;
|
|
1180
|
-
out[o + 19] = y3;
|
|
1181
|
-
out[o + 20] = r;
|
|
1182
|
-
out[o + 21] = g;
|
|
1183
|
-
out[o + 22] = b;
|
|
1184
|
-
out[o + 23] = al;
|
|
1190
|
+
const tint = packTint(r, g, b, al);
|
|
1191
|
+
const f32 = out.f32;
|
|
1192
|
+
const u32 = out.u32;
|
|
1193
|
+
const fi = byteOffset >> 2;
|
|
1194
|
+
f32[fi] = x;
|
|
1195
|
+
f32[fi + 1] = y;
|
|
1196
|
+
u32[fi + 2] = tint;
|
|
1197
|
+
f32[fi + 3] = x1;
|
|
1198
|
+
f32[fi + 4] = y1;
|
|
1199
|
+
u32[fi + 5] = tint;
|
|
1200
|
+
f32[fi + 6] = x2;
|
|
1201
|
+
f32[fi + 7] = y2;
|
|
1202
|
+
u32[fi + 8] = tint;
|
|
1203
|
+
f32[fi + 9] = x3;
|
|
1204
|
+
f32[fi + 10] = y3;
|
|
1205
|
+
u32[fi + 11] = tint;
|
|
1185
1206
|
}
|
|
1186
1207
|
function grow(data, needed) {
|
|
1187
1208
|
if (needed <= data.length) return data;
|
|
@@ -1191,6 +1212,16 @@ function grow(data, needed) {
|
|
|
1191
1212
|
grown.set(data);
|
|
1192
1213
|
return grown;
|
|
1193
1214
|
}
|
|
1215
|
+
function isSourceReady(source) {
|
|
1216
|
+
const candidate = source;
|
|
1217
|
+
if (typeof candidate.complete === "boolean") {
|
|
1218
|
+
if (!candidate.complete) return false;
|
|
1219
|
+
if (typeof candidate.naturalWidth === "number" && candidate.naturalWidth === 0) return false;
|
|
1220
|
+
return true;
|
|
1221
|
+
}
|
|
1222
|
+
if (typeof candidate.readyState === "number") return candidate.readyState >= 2;
|
|
1223
|
+
return true;
|
|
1224
|
+
}
|
|
1194
1225
|
function createWebGLPointRenderer(canvas) {
|
|
1195
1226
|
const gl = canvas.getContext("webgl2", {
|
|
1196
1227
|
premultipliedAlpha: false
|
|
@@ -1228,9 +1259,16 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1228
1259
|
gl.bindVertexArray(rectVAO);
|
|
1229
1260
|
gl.bindBuffer(gl.ARRAY_BUFFER, rectBuffer);
|
|
1230
1261
|
gl.enableVertexAttribArray(rAPos);
|
|
1231
|
-
gl.vertexAttribPointer(rAPos, 2, gl.FLOAT, false,
|
|
1262
|
+
gl.vertexAttribPointer(rAPos, 2, gl.FLOAT, false, PACKED_RECT_VERT_STRIDE, 0);
|
|
1232
1263
|
gl.enableVertexAttribArray(rAColor);
|
|
1233
|
-
gl.vertexAttribPointer(
|
|
1264
|
+
gl.vertexAttribPointer(
|
|
1265
|
+
rAColor,
|
|
1266
|
+
4,
|
|
1267
|
+
gl.UNSIGNED_BYTE,
|
|
1268
|
+
true,
|
|
1269
|
+
PACKED_RECT_VERT_STRIDE,
|
|
1270
|
+
PACKED_RECT_TINT_OFFSET
|
|
1271
|
+
);
|
|
1234
1272
|
const sAPos = gl.getAttribLocation(spriteProgram, "a_pos");
|
|
1235
1273
|
const sAUv = gl.getAttribLocation(spriteProgram, "a_uv");
|
|
1236
1274
|
const sATint = gl.getAttribLocation(spriteProgram, "a_tint");
|
|
@@ -1241,11 +1279,25 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1241
1279
|
gl.bindVertexArray(spriteVAO);
|
|
1242
1280
|
gl.bindBuffer(gl.ARRAY_BUFFER, spriteBuffer);
|
|
1243
1281
|
gl.enableVertexAttribArray(sAPos);
|
|
1244
|
-
gl.vertexAttribPointer(sAPos, 2, gl.FLOAT, false,
|
|
1282
|
+
gl.vertexAttribPointer(sAPos, 2, gl.FLOAT, false, PACKED_QUAD_VERT_STRIDE, 0);
|
|
1245
1283
|
gl.enableVertexAttribArray(sAUv);
|
|
1246
|
-
gl.vertexAttribPointer(
|
|
1284
|
+
gl.vertexAttribPointer(
|
|
1285
|
+
sAUv,
|
|
1286
|
+
2,
|
|
1287
|
+
gl.UNSIGNED_SHORT,
|
|
1288
|
+
true,
|
|
1289
|
+
PACKED_QUAD_VERT_STRIDE,
|
|
1290
|
+
PACKED_UV_OFFSET
|
|
1291
|
+
);
|
|
1247
1292
|
gl.enableVertexAttribArray(sATint);
|
|
1248
|
-
gl.vertexAttribPointer(
|
|
1293
|
+
gl.vertexAttribPointer(
|
|
1294
|
+
sATint,
|
|
1295
|
+
4,
|
|
1296
|
+
gl.UNSIGNED_BYTE,
|
|
1297
|
+
true,
|
|
1298
|
+
PACKED_QUAD_VERT_STRIDE,
|
|
1299
|
+
PACKED_TINT_OFFSET
|
|
1300
|
+
);
|
|
1249
1301
|
const gAPos = gl.getAttribLocation(msdfProgram, "a_pos");
|
|
1250
1302
|
const gAUv = gl.getAttribLocation(msdfProgram, "a_uv");
|
|
1251
1303
|
const gATint = gl.getAttribLocation(msdfProgram, "a_tint");
|
|
@@ -1257,11 +1309,25 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1257
1309
|
gl.bindVertexArray(glyphVAO);
|
|
1258
1310
|
gl.bindBuffer(gl.ARRAY_BUFFER, glyphBuffer);
|
|
1259
1311
|
gl.enableVertexAttribArray(gAPos);
|
|
1260
|
-
gl.vertexAttribPointer(gAPos, 2, gl.FLOAT, false,
|
|
1312
|
+
gl.vertexAttribPointer(gAPos, 2, gl.FLOAT, false, PACKED_QUAD_VERT_STRIDE, 0);
|
|
1261
1313
|
gl.enableVertexAttribArray(gAUv);
|
|
1262
|
-
gl.vertexAttribPointer(
|
|
1314
|
+
gl.vertexAttribPointer(
|
|
1315
|
+
gAUv,
|
|
1316
|
+
2,
|
|
1317
|
+
gl.UNSIGNED_SHORT,
|
|
1318
|
+
true,
|
|
1319
|
+
PACKED_QUAD_VERT_STRIDE,
|
|
1320
|
+
PACKED_UV_OFFSET
|
|
1321
|
+
);
|
|
1263
1322
|
gl.enableVertexAttribArray(gATint);
|
|
1264
|
-
gl.vertexAttribPointer(
|
|
1323
|
+
gl.vertexAttribPointer(
|
|
1324
|
+
gATint,
|
|
1325
|
+
4,
|
|
1326
|
+
gl.UNSIGNED_BYTE,
|
|
1327
|
+
true,
|
|
1328
|
+
PACKED_QUAD_VERT_STRIDE,
|
|
1329
|
+
PACKED_TINT_OFFSET
|
|
1330
|
+
);
|
|
1265
1331
|
const cAPos = gl.getAttribLocation(circleQuadProgram, "a_pos");
|
|
1266
1332
|
const cAUv = gl.getAttribLocation(circleQuadProgram, "a_uv");
|
|
1267
1333
|
const cATint = gl.getAttribLocation(circleQuadProgram, "a_tint");
|
|
@@ -1271,11 +1337,25 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1271
1337
|
gl.bindVertexArray(circleQuadVAO);
|
|
1272
1338
|
gl.bindBuffer(gl.ARRAY_BUFFER, circleQuadBuffer);
|
|
1273
1339
|
gl.enableVertexAttribArray(cAPos);
|
|
1274
|
-
gl.vertexAttribPointer(cAPos, 2, gl.FLOAT, false,
|
|
1340
|
+
gl.vertexAttribPointer(cAPos, 2, gl.FLOAT, false, PACKED_QUAD_VERT_STRIDE, 0);
|
|
1275
1341
|
gl.enableVertexAttribArray(cAUv);
|
|
1276
|
-
gl.vertexAttribPointer(
|
|
1342
|
+
gl.vertexAttribPointer(
|
|
1343
|
+
cAUv,
|
|
1344
|
+
2,
|
|
1345
|
+
gl.UNSIGNED_SHORT,
|
|
1346
|
+
true,
|
|
1347
|
+
PACKED_QUAD_VERT_STRIDE,
|
|
1348
|
+
PACKED_UV_OFFSET
|
|
1349
|
+
);
|
|
1277
1350
|
gl.enableVertexAttribArray(cATint);
|
|
1278
|
-
gl.vertexAttribPointer(
|
|
1351
|
+
gl.vertexAttribPointer(
|
|
1352
|
+
cATint,
|
|
1353
|
+
4,
|
|
1354
|
+
gl.UNSIGNED_BYTE,
|
|
1355
|
+
true,
|
|
1356
|
+
PACKED_QUAD_VERT_STRIDE,
|
|
1357
|
+
PACKED_TINT_OFFSET
|
|
1358
|
+
);
|
|
1279
1359
|
gl.bindVertexArray(null);
|
|
1280
1360
|
const quadIndexBuffer = gl.createBuffer();
|
|
1281
1361
|
let quadIndexCapacity = 0;
|
|
@@ -1285,7 +1365,6 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1285
1365
|
while (cap < quads) cap *= 2;
|
|
1286
1366
|
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, quadIndexBuffer);
|
|
1287
1367
|
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, buildQuadIndices(cap), gl.STATIC_DRAW);
|
|
1288
|
-
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
|
|
1289
1368
|
quadIndexCapacity = cap;
|
|
1290
1369
|
};
|
|
1291
1370
|
for (const vao of [rectVAO, spriteVAO, glyphVAO, circleQuadVAO]) {
|
|
@@ -1300,11 +1379,11 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1300
1379
|
let distanceRange = 4;
|
|
1301
1380
|
let pointData = new Float32Array(FLOATS_PER_POINT * 1024);
|
|
1302
1381
|
let pointCount = 0;
|
|
1303
|
-
let rectData =
|
|
1382
|
+
let rectData = createPackedBuffer(PACKED_RECT_VERT_STRIDE * VERTS_PER_QUAD * 256);
|
|
1304
1383
|
let rectCount = 0;
|
|
1305
|
-
let spriteData =
|
|
1384
|
+
let spriteData = createPackedBuffer(PACKED_QUAD_VERT_STRIDE * VERTS_PER_QUAD * 256);
|
|
1306
1385
|
let spriteCount = 0;
|
|
1307
|
-
let glyphData =
|
|
1386
|
+
let glyphData = createPackedBuffer(PACKED_QUAD_VERT_STRIDE * VERTS_PER_QUAD * 1024);
|
|
1308
1387
|
let glyphCount = 0;
|
|
1309
1388
|
let frameDrawCalls = 0;
|
|
1310
1389
|
let lastFrameDrawCalls = 0;
|
|
@@ -1312,7 +1391,7 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1312
1391
|
let atlasSwitches = 0;
|
|
1313
1392
|
let circleQuadFallbacks = 0;
|
|
1314
1393
|
let circlePoints = 0;
|
|
1315
|
-
let circleQuadData =
|
|
1394
|
+
let circleQuadData = createPackedBuffer(PACKED_QUAD_VERT_STRIDE * VERTS_PER_QUAD * 64);
|
|
1316
1395
|
let circleQuadCount = 0;
|
|
1317
1396
|
let logicalW = 0;
|
|
1318
1397
|
let logicalH = 0;
|
|
@@ -1322,11 +1401,11 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1322
1401
|
const maxPointSize = pointSizeRange ? pointSizeRange[1] : Infinity;
|
|
1323
1402
|
const drawGlyphs = () => {
|
|
1324
1403
|
if (glyphCount === 0 || !msdfTexture) return;
|
|
1325
|
-
const
|
|
1404
|
+
const bytes = glyphCount * VERTS_PER_QUAD * PACKED_QUAD_VERT_STRIDE;
|
|
1326
1405
|
gl.useProgram(msdfProgram);
|
|
1327
1406
|
gl.bindVertexArray(glyphVAO);
|
|
1328
1407
|
gl.bindBuffer(gl.ARRAY_BUFFER, glyphBuffer);
|
|
1329
|
-
gl.bufferData(gl.ARRAY_BUFFER, glyphData.subarray(0,
|
|
1408
|
+
gl.bufferData(gl.ARRAY_BUFFER, glyphData.u8.subarray(0, bytes), gl.DYNAMIC_DRAW);
|
|
1330
1409
|
gl.activeTexture(gl.TEXTURE0);
|
|
1331
1410
|
gl.bindTexture(gl.TEXTURE_2D, msdfTexture);
|
|
1332
1411
|
gl.uniform1i(gUTex, 0);
|
|
@@ -1378,6 +1457,7 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1378
1457
|
},
|
|
1379
1458
|
setTexture(source) {
|
|
1380
1459
|
if (source === textureSource && texture) return;
|
|
1460
|
+
if (!isSourceReady(source)) return;
|
|
1381
1461
|
if (!texture) {
|
|
1382
1462
|
texture = gl.createTexture();
|
|
1383
1463
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
@@ -1393,8 +1473,8 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1393
1473
|
},
|
|
1394
1474
|
addSprite(x, y, width, height, u0, v0, u1, v1, color = "#ffffff", alpha = 1, rotation = 0) {
|
|
1395
1475
|
if (!texture) return;
|
|
1396
|
-
const stride =
|
|
1397
|
-
spriteData =
|
|
1476
|
+
const stride = PACKED_QUAD_VERT_STRIDE * VERTS_PER_QUAD;
|
|
1477
|
+
spriteData = growPacked(spriteData, (spriteCount + 1) * stride);
|
|
1398
1478
|
const rgba = parseColorToRGBA(color);
|
|
1399
1479
|
writeQuad(
|
|
1400
1480
|
spriteData,
|
|
@@ -1420,6 +1500,10 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1420
1500
|
distanceRange = range;
|
|
1421
1501
|
return;
|
|
1422
1502
|
}
|
|
1503
|
+
if (!isSourceReady(source)) {
|
|
1504
|
+
distanceRange = range;
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1423
1507
|
atlasSwitches++;
|
|
1424
1508
|
drawGlyphs();
|
|
1425
1509
|
distanceRange = range;
|
|
@@ -1438,8 +1522,8 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1438
1522
|
},
|
|
1439
1523
|
addGlyph(x, y, width, height, u0, v0, u1, v1, color = "#ffffff", alpha = 1, rotation = 0) {
|
|
1440
1524
|
if (!msdfTexture) return;
|
|
1441
|
-
const stride =
|
|
1442
|
-
glyphData =
|
|
1525
|
+
const stride = PACKED_QUAD_VERT_STRIDE * VERTS_PER_QUAD;
|
|
1526
|
+
glyphData = growPacked(glyphData, (glyphCount + 1) * stride);
|
|
1443
1527
|
const rgba = parseColorToRGBA(color);
|
|
1444
1528
|
writeQuad(
|
|
1445
1529
|
glyphData,
|
|
@@ -1464,8 +1548,8 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1464
1548
|
const needsQuad = logicalW > 0 && (x < radius || y < radius || x > logicalW - radius || y > logicalH - radius) || radius * 2 * dpr > maxPointSize;
|
|
1465
1549
|
if (needsQuad) {
|
|
1466
1550
|
circleQuadFallbacks++;
|
|
1467
|
-
const stride =
|
|
1468
|
-
circleQuadData =
|
|
1551
|
+
const stride = PACKED_QUAD_VERT_STRIDE * VERTS_PER_QUAD;
|
|
1552
|
+
circleQuadData = growPacked(circleQuadData, (circleQuadCount + 1) * stride);
|
|
1469
1553
|
const rgba = parseColorToRGBA(color);
|
|
1470
1554
|
const d = radius * 2;
|
|
1471
1555
|
writeQuad(
|
|
@@ -1502,8 +1586,8 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1502
1586
|
pointCount++;
|
|
1503
1587
|
},
|
|
1504
1588
|
addRect(x, y, width, height, color, alpha = 1, rotation = 0) {
|
|
1505
|
-
const stride =
|
|
1506
|
-
rectData =
|
|
1589
|
+
const stride = PACKED_RECT_VERT_STRIDE * VERTS_PER_QUAD;
|
|
1590
|
+
rectData = growPacked(rectData, (rectCount + 1) * stride);
|
|
1507
1591
|
const rgba = parseColorToRGBA(color);
|
|
1508
1592
|
writeRectQuad(
|
|
1509
1593
|
rectData,
|
|
@@ -1522,11 +1606,11 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1522
1606
|
},
|
|
1523
1607
|
flush() {
|
|
1524
1608
|
if (rectCount > 0) {
|
|
1525
|
-
const
|
|
1609
|
+
const bytes = rectCount * VERTS_PER_QUAD * PACKED_RECT_VERT_STRIDE;
|
|
1526
1610
|
gl.useProgram(rectProgram);
|
|
1527
1611
|
gl.bindVertexArray(rectVAO);
|
|
1528
1612
|
gl.bindBuffer(gl.ARRAY_BUFFER, rectBuffer);
|
|
1529
|
-
gl.bufferData(gl.ARRAY_BUFFER, rectData.subarray(0,
|
|
1613
|
+
gl.bufferData(gl.ARRAY_BUFFER, rectData.u8.subarray(0, bytes), gl.DYNAMIC_DRAW);
|
|
1530
1614
|
gl.uniform2f(rURes, logicalW, logicalH);
|
|
1531
1615
|
ensureQuadIndices(rectCount);
|
|
1532
1616
|
gl.drawElements(gl.TRIANGLES, rectCount * INDICES_PER_QUAD, gl.UNSIGNED_INT, 0);
|
|
@@ -1547,22 +1631,22 @@ function createWebGLPointRenderer(canvas) {
|
|
|
1547
1631
|
frameDrawCalls++;
|
|
1548
1632
|
}
|
|
1549
1633
|
if (circleQuadCount > 0) {
|
|
1550
|
-
const
|
|
1634
|
+
const bytes = circleQuadCount * VERTS_PER_QUAD * PACKED_QUAD_VERT_STRIDE;
|
|
1551
1635
|
gl.useProgram(circleQuadProgram);
|
|
1552
1636
|
gl.bindVertexArray(circleQuadVAO);
|
|
1553
1637
|
gl.bindBuffer(gl.ARRAY_BUFFER, circleQuadBuffer);
|
|
1554
|
-
gl.bufferData(gl.ARRAY_BUFFER, circleQuadData.subarray(0,
|
|
1638
|
+
gl.bufferData(gl.ARRAY_BUFFER, circleQuadData.u8.subarray(0, bytes), gl.DYNAMIC_DRAW);
|
|
1555
1639
|
gl.uniform2f(cURes, logicalW, logicalH);
|
|
1556
1640
|
ensureQuadIndices(circleQuadCount);
|
|
1557
1641
|
gl.drawElements(gl.TRIANGLES, circleQuadCount * INDICES_PER_QUAD, gl.UNSIGNED_INT, 0);
|
|
1558
1642
|
frameDrawCalls++;
|
|
1559
1643
|
}
|
|
1560
1644
|
if (spriteCount > 0 && texture) {
|
|
1561
|
-
const
|
|
1645
|
+
const bytes = spriteCount * VERTS_PER_QUAD * PACKED_QUAD_VERT_STRIDE;
|
|
1562
1646
|
gl.useProgram(spriteProgram);
|
|
1563
1647
|
gl.bindVertexArray(spriteVAO);
|
|
1564
1648
|
gl.bindBuffer(gl.ARRAY_BUFFER, spriteBuffer);
|
|
1565
|
-
gl.bufferData(gl.ARRAY_BUFFER, spriteData.subarray(0,
|
|
1649
|
+
gl.bufferData(gl.ARRAY_BUFFER, spriteData.u8.subarray(0, bytes), gl.DYNAMIC_DRAW);
|
|
1566
1650
|
gl.activeTexture(gl.TEXTURE0);
|
|
1567
1651
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
1568
1652
|
gl.uniform1i(sUTex, 0);
|
|
@@ -1093,6 +1093,11 @@ var MSDFTextEntity = class extends Entity {
|
|
|
1093
1093
|
layoutText = "";
|
|
1094
1094
|
text = "";
|
|
1095
1095
|
lastRenderedSeqId = 0;
|
|
1096
|
+
// Atlas-decode subscription (see watchAtlasDecode). Held so `destroy()` can
|
|
1097
|
+
// release it: the handler closes over `this`, so leaving it attached to a
|
|
1098
|
+
// long-lived shared atlas image would retain the whole entity.
|
|
1099
|
+
atlasDecodeTarget = null;
|
|
1100
|
+
atlasDecodeHandler = null;
|
|
1096
1101
|
rgbColorCache = /* @__PURE__ */ new Map();
|
|
1097
1102
|
fontStringCache = [];
|
|
1098
1103
|
layoutResult = null;
|
|
@@ -1108,8 +1113,53 @@ var MSDFTextEntity = class extends Entity {
|
|
|
1108
1113
|
this.maxWidth = options.maxWidth ?? 1e3;
|
|
1109
1114
|
this.maxHeight = options.maxHeight ?? 1e3;
|
|
1110
1115
|
this.textAlign = options.textAlign ?? "left";
|
|
1116
|
+
this.watchAtlasDecode();
|
|
1111
1117
|
this.setText(text);
|
|
1112
1118
|
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Repaint once the atlas raster decodes.
|
|
1121
|
+
*
|
|
1122
|
+
* The WebGL backend refuses to upload a not-yet-decoded atlas (it would pin an
|
|
1123
|
+
* empty texture in its identity cache forever), so the upload has to happen on
|
|
1124
|
+
* a LATER frame — and nothing else schedules one. Layout marks the scene dirty
|
|
1125
|
+
* when the worker replies, which for a network-served atlas is long before the
|
|
1126
|
+
* image lands, so the scene is already idle by then.
|
|
1127
|
+
*
|
|
1128
|
+
* Measured on Chromium and Firefox (2026-07-31) with a 600 ms atlas: the
|
|
1129
|
+
* scene's own rAF loop never uploaded a decoded atlas in EITHER render mode.
|
|
1130
|
+
* `onDemand` skips idle frames outright; `always` throttles to 2 FPS when
|
|
1131
|
+
* idle, so whether it recovers is down to whether a throttled tick happens to
|
|
1132
|
+
* land after the decode — Chromium got one, Firefox did not. Neither is a
|
|
1133
|
+
* mechanism, which is why this listener exists rather than relying on the
|
|
1134
|
+
* frame loop to come back around.
|
|
1135
|
+
*
|
|
1136
|
+
* Only `HTMLImageElement`-shaped sources have a decode to wait for; a canvas,
|
|
1137
|
+
* `ImageBitmap`, or `VideoFrame` atlas is ready on arrival.
|
|
1138
|
+
*/
|
|
1139
|
+
watchAtlasDecode() {
|
|
1140
|
+
const source = this.texture;
|
|
1141
|
+
if (typeof source.complete !== "boolean" || typeof source.addEventListener !== "function" || typeof source.removeEventListener !== "function") {
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
if (source.complete && typeof source.naturalWidth === "number" && source.naturalWidth > 0) {
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
const target = this.texture;
|
|
1148
|
+
this.atlasDecodeHandler = () => {
|
|
1149
|
+
this.detachAtlasDecodeListener();
|
|
1150
|
+
this.scene?.markDirty();
|
|
1151
|
+
};
|
|
1152
|
+
target.addEventListener("load", this.atlasDecodeHandler);
|
|
1153
|
+
target.addEventListener("error", this.atlasDecodeHandler);
|
|
1154
|
+
this.atlasDecodeTarget = target;
|
|
1155
|
+
}
|
|
1156
|
+
detachAtlasDecodeListener() {
|
|
1157
|
+
if (!this.atlasDecodeTarget || !this.atlasDecodeHandler) return;
|
|
1158
|
+
this.atlasDecodeTarget.removeEventListener("load", this.atlasDecodeHandler);
|
|
1159
|
+
this.atlasDecodeTarget.removeEventListener("error", this.atlasDecodeHandler);
|
|
1160
|
+
this.atlasDecodeTarget = null;
|
|
1161
|
+
this.atlasDecodeHandler = null;
|
|
1162
|
+
}
|
|
1113
1163
|
/** Change the wrap boundary and re-run layout for the current text. */
|
|
1114
1164
|
setMaxWidth(maxWidth) {
|
|
1115
1165
|
if (this.maxWidth === maxWidth) return;
|
|
@@ -1281,11 +1331,13 @@ var MSDFTextEntity = class extends Entity {
|
|
|
1281
1331
|
}
|
|
1282
1332
|
destroy() {
|
|
1283
1333
|
LayoutWorkerManager.cancelLayoutForEntity(this.id);
|
|
1334
|
+
this.detachAtlasDecodeListener();
|
|
1284
1335
|
super.destroy();
|
|
1285
1336
|
}
|
|
1286
1337
|
};
|
|
1287
1338
|
|
|
1288
1339
|
// src/text/SVGEntity.ts
|
|
1340
|
+
var SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
1289
1341
|
function isSvgWhitespace(ch) {
|
|
1290
1342
|
return ch === " " || ch === " " || ch === "\n" || ch === "\r";
|
|
1291
1343
|
}
|
|
@@ -1317,12 +1369,21 @@ function readSvgAttribute(source, name) {
|
|
|
1317
1369
|
return null;
|
|
1318
1370
|
}
|
|
1319
1371
|
var SVGEntity = class extends Entity {
|
|
1372
|
+
/**
|
|
1373
|
+
* Stroke colour of the fallback marker drawn when the source cannot be
|
|
1374
|
+
* rasterized. Set to `'transparent'` to opt out and keep the box empty.
|
|
1375
|
+
* Default `'rgba(248,113,113,0.9)'`.
|
|
1376
|
+
*/
|
|
1377
|
+
fallbackStroke = "rgba(248,113,113,0.9)";
|
|
1378
|
+
/** Fill behind the fallback marker. Default `'rgba(248,113,113,0.12)'`. */
|
|
1379
|
+
fallbackFill = "rgba(248,113,113,0.12)";
|
|
1320
1380
|
svgSource = "";
|
|
1321
1381
|
imageBitmap = null;
|
|
1322
1382
|
imageElement = null;
|
|
1323
1383
|
blobURL = null;
|
|
1324
1384
|
currentImg = null;
|
|
1325
1385
|
lodTimeout = null;
|
|
1386
|
+
rasterFailed = false;
|
|
1326
1387
|
cachedDoc = null;
|
|
1327
1388
|
baseWidth = 100;
|
|
1328
1389
|
baseHeight = 100;
|
|
@@ -1349,6 +1410,7 @@ var SVGEntity = class extends Entity {
|
|
|
1349
1410
|
const parserError = doc.querySelector("parsererror");
|
|
1350
1411
|
if (parserError) {
|
|
1351
1412
|
console.error("SVG Parsing error:", parserError.textContent);
|
|
1413
|
+
this.rasterFailed = true;
|
|
1352
1414
|
} else {
|
|
1353
1415
|
this.cachedDoc = doc;
|
|
1354
1416
|
const svgEl = doc.documentElement;
|
|
@@ -1368,6 +1430,7 @@ var SVGEntity = class extends Entity {
|
|
|
1368
1430
|
}
|
|
1369
1431
|
} catch (e) {
|
|
1370
1432
|
console.error("Failed parsing SVG via DOMParser, falling back to attribute scan:", e);
|
|
1433
|
+
this.rasterFailed = true;
|
|
1371
1434
|
}
|
|
1372
1435
|
} else {
|
|
1373
1436
|
const wAttr = readSvgAttribute(this.svgSource, "width");
|
|
@@ -1391,6 +1454,7 @@ var SVGEntity = class extends Entity {
|
|
|
1391
1454
|
}
|
|
1392
1455
|
triggerRasterization(scale) {
|
|
1393
1456
|
if (typeof window === "undefined" || typeof Blob === "undefined") return;
|
|
1457
|
+
this.rasterFailed = false;
|
|
1394
1458
|
if (this.currentImg) {
|
|
1395
1459
|
this.currentImg.onload = null;
|
|
1396
1460
|
this.currentImg.onerror = null;
|
|
@@ -1414,6 +1478,7 @@ var SVGEntity = class extends Entity {
|
|
|
1414
1478
|
"SVG Parsing validation error in triggerRasterization:",
|
|
1415
1479
|
parserError.textContent
|
|
1416
1480
|
);
|
|
1481
|
+
this.rasterFailed = true;
|
|
1417
1482
|
} else {
|
|
1418
1483
|
const clonedDoc = doc.cloneNode(true);
|
|
1419
1484
|
const svgEl = clonedDoc.documentElement;
|
|
@@ -1427,10 +1492,14 @@ var SVGEntity = class extends Entity {
|
|
|
1427
1492
|
}
|
|
1428
1493
|
const serializer = new XMLSerializer();
|
|
1429
1494
|
processedSource = serializer.serializeToString(clonedDoc);
|
|
1495
|
+
if (svgEl.namespaceURI === null && !/\sxmlns\s*=/.test(processedSource)) {
|
|
1496
|
+
processedSource = processedSource.replace(/<svg/i, `<svg xmlns="${SVG_NAMESPACE}"`);
|
|
1497
|
+
}
|
|
1430
1498
|
}
|
|
1431
1499
|
}
|
|
1432
1500
|
} catch (e) {
|
|
1433
1501
|
console.error("Failed to apply LOD scaling to SVG XML:", e);
|
|
1502
|
+
this.rasterFailed = true;
|
|
1434
1503
|
}
|
|
1435
1504
|
const blob = new Blob([processedSource], { type: "image/svg+xml;charset=utf-8" });
|
|
1436
1505
|
this.blobURL = URL.createObjectURL(blob);
|
|
@@ -1459,15 +1528,36 @@ var SVGEntity = class extends Entity {
|
|
|
1459
1528
|
}).catch((e) => {
|
|
1460
1529
|
console.error("Failed to create ImageBitmap from SVG:", e);
|
|
1461
1530
|
this.currentImg = null;
|
|
1531
|
+
if (!this.imageElement) this.rasterFailed = true;
|
|
1532
|
+
if (this.scene) this.scene.markDirty();
|
|
1462
1533
|
});
|
|
1463
1534
|
};
|
|
1464
1535
|
img.onerror = (e) => {
|
|
1465
1536
|
if (this.currentImg !== img) return;
|
|
1466
1537
|
console.error("Failed to load SVG Image element:", e);
|
|
1467
1538
|
this.currentImg = null;
|
|
1539
|
+
this.rasterFailed = true;
|
|
1540
|
+
if (this.scene) this.scene.markDirty();
|
|
1468
1541
|
};
|
|
1469
1542
|
img.src = this.blobURL;
|
|
1470
1543
|
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Whether the source genuinely rasterized to a bitmap.
|
|
1546
|
+
*
|
|
1547
|
+
* Distinguishes "drew the real artwork" from "drew the fallback marker",
|
|
1548
|
+
* which pixel counts alone cannot tell apart — both are non-blank.
|
|
1549
|
+
*/
|
|
1550
|
+
hasRasterBitmap() {
|
|
1551
|
+
return this.imageBitmap !== null;
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Whether rasterization failed, so {@link render} draws the fallback marker.
|
|
1555
|
+
*
|
|
1556
|
+
* `false` while a raster is still in flight; only a settled failure sets it.
|
|
1557
|
+
*/
|
|
1558
|
+
hasRasterFailed() {
|
|
1559
|
+
return this.rasterFailed;
|
|
1560
|
+
}
|
|
1471
1561
|
isPointInside(globalX, globalY) {
|
|
1472
1562
|
const local = this.worldToLocal(globalX, globalY);
|
|
1473
1563
|
if (!local) return false;
|
|
@@ -1492,9 +1582,32 @@ var SVGEntity = class extends Entity {
|
|
|
1492
1582
|
}
|
|
1493
1583
|
if (this.imageBitmap) {
|
|
1494
1584
|
r.drawImage(this.imageBitmap, 0, 0, this.width, this.height);
|
|
1495
|
-
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
if (this.imageElement) {
|
|
1496
1588
|
r.drawImage(this.imageElement, 0, 0, this.width, this.height);
|
|
1589
|
+
return;
|
|
1497
1590
|
}
|
|
1591
|
+
if (this.rasterFailed) this.drawFallback(r);
|
|
1592
|
+
}
|
|
1593
|
+
/** Box outline plus a diagonal cross — the conventional "broken image" mark. */
|
|
1594
|
+
drawFallback(r) {
|
|
1595
|
+
const w = this.width;
|
|
1596
|
+
const h = this.height;
|
|
1597
|
+
if (w <= 0 || h <= 0) return;
|
|
1598
|
+
r.beginPath();
|
|
1599
|
+
r.roundRect(0, 0, w, h, 0);
|
|
1600
|
+
r.fill(this.fallbackFill);
|
|
1601
|
+
r.beginPath();
|
|
1602
|
+
r.roundRect(0, 0, w, h, 0);
|
|
1603
|
+
r.stroke(this.fallbackStroke, 1);
|
|
1604
|
+
const inset = Math.min(w, h) * 0.2;
|
|
1605
|
+
r.beginPath();
|
|
1606
|
+
r.moveTo(inset, inset);
|
|
1607
|
+
r.lineTo(w - inset, h - inset);
|
|
1608
|
+
r.moveTo(w - inset, inset);
|
|
1609
|
+
r.lineTo(inset, h - inset);
|
|
1610
|
+
r.stroke(this.fallbackStroke, 1);
|
|
1498
1611
|
}
|
|
1499
1612
|
destroy() {
|
|
1500
1613
|
if (this.lodTimeout) {
|