@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
|
@@ -1095,11 +1095,16 @@ var MSDFTextEntity = (_class3 = class extends Entity {
|
|
|
1095
1095
|
__init40() {this.layoutText = ""}
|
|
1096
1096
|
__init41() {this.text = ""}
|
|
1097
1097
|
__init42() {this.lastRenderedSeqId = 0}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1098
|
+
// Atlas-decode subscription (see watchAtlasDecode). Held so `destroy()` can
|
|
1099
|
+
// release it: the handler closes over `this`, so leaving it attached to a
|
|
1100
|
+
// long-lived shared atlas image would retain the whole entity.
|
|
1101
|
+
__init43() {this.atlasDecodeTarget = null}
|
|
1102
|
+
__init44() {this.atlasDecodeHandler = null}
|
|
1103
|
+
__init45() {this.rgbColorCache = /* @__PURE__ */ new Map()}
|
|
1104
|
+
__init46() {this.fontStringCache = []}
|
|
1105
|
+
__init47() {this.layoutResult = null}
|
|
1101
1106
|
constructor(text, options) {
|
|
1102
|
-
super();_class3.prototype.__init39.call(this);_class3.prototype.__init40.call(this);_class3.prototype.__init41.call(this);_class3.prototype.__init42.call(this);_class3.prototype.__init43.call(this);_class3.prototype.__init44.call(this);_class3.prototype.__init45.call(this);;
|
|
1107
|
+
super();_class3.prototype.__init39.call(this);_class3.prototype.__init40.call(this);_class3.prototype.__init41.call(this);_class3.prototype.__init42.call(this);_class3.prototype.__init43.call(this);_class3.prototype.__init44.call(this);_class3.prototype.__init45.call(this);_class3.prototype.__init46.call(this);_class3.prototype.__init47.call(this);;
|
|
1103
1108
|
this.font = options.font;
|
|
1104
1109
|
this.texture = options.texture;
|
|
1105
1110
|
this.fallbackFont = _nullishCoalesce(options.fallbackFont, () => ( "sans-serif"));
|
|
@@ -1110,8 +1115,53 @@ var MSDFTextEntity = (_class3 = class extends Entity {
|
|
|
1110
1115
|
this.maxWidth = _nullishCoalesce(options.maxWidth, () => ( 1e3));
|
|
1111
1116
|
this.maxHeight = _nullishCoalesce(options.maxHeight, () => ( 1e3));
|
|
1112
1117
|
this.textAlign = _nullishCoalesce(options.textAlign, () => ( "left"));
|
|
1118
|
+
this.watchAtlasDecode();
|
|
1113
1119
|
this.setText(text);
|
|
1114
1120
|
}
|
|
1121
|
+
/**
|
|
1122
|
+
* Repaint once the atlas raster decodes.
|
|
1123
|
+
*
|
|
1124
|
+
* The WebGL backend refuses to upload a not-yet-decoded atlas (it would pin an
|
|
1125
|
+
* empty texture in its identity cache forever), so the upload has to happen on
|
|
1126
|
+
* a LATER frame — and nothing else schedules one. Layout marks the scene dirty
|
|
1127
|
+
* when the worker replies, which for a network-served atlas is long before the
|
|
1128
|
+
* image lands, so the scene is already idle by then.
|
|
1129
|
+
*
|
|
1130
|
+
* Measured on Chromium and Firefox (2026-07-31) with a 600 ms atlas: the
|
|
1131
|
+
* scene's own rAF loop never uploaded a decoded atlas in EITHER render mode.
|
|
1132
|
+
* `onDemand` skips idle frames outright; `always` throttles to 2 FPS when
|
|
1133
|
+
* idle, so whether it recovers is down to whether a throttled tick happens to
|
|
1134
|
+
* land after the decode — Chromium got one, Firefox did not. Neither is a
|
|
1135
|
+
* mechanism, which is why this listener exists rather than relying on the
|
|
1136
|
+
* frame loop to come back around.
|
|
1137
|
+
*
|
|
1138
|
+
* Only `HTMLImageElement`-shaped sources have a decode to wait for; a canvas,
|
|
1139
|
+
* `ImageBitmap`, or `VideoFrame` atlas is ready on arrival.
|
|
1140
|
+
*/
|
|
1141
|
+
watchAtlasDecode() {
|
|
1142
|
+
const source = this.texture;
|
|
1143
|
+
if (typeof source.complete !== "boolean" || typeof source.addEventListener !== "function" || typeof source.removeEventListener !== "function") {
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
if (source.complete && typeof source.naturalWidth === "number" && source.naturalWidth > 0) {
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
const target = this.texture;
|
|
1150
|
+
this.atlasDecodeHandler = () => {
|
|
1151
|
+
this.detachAtlasDecodeListener();
|
|
1152
|
+
_optionalChain([this, 'access', _108 => _108.scene, 'optionalAccess', _109 => _109.markDirty, 'call', _110 => _110()]);
|
|
1153
|
+
};
|
|
1154
|
+
target.addEventListener("load", this.atlasDecodeHandler);
|
|
1155
|
+
target.addEventListener("error", this.atlasDecodeHandler);
|
|
1156
|
+
this.atlasDecodeTarget = target;
|
|
1157
|
+
}
|
|
1158
|
+
detachAtlasDecodeListener() {
|
|
1159
|
+
if (!this.atlasDecodeTarget || !this.atlasDecodeHandler) return;
|
|
1160
|
+
this.atlasDecodeTarget.removeEventListener("load", this.atlasDecodeHandler);
|
|
1161
|
+
this.atlasDecodeTarget.removeEventListener("error", this.atlasDecodeHandler);
|
|
1162
|
+
this.atlasDecodeTarget = null;
|
|
1163
|
+
this.atlasDecodeHandler = null;
|
|
1164
|
+
}
|
|
1115
1165
|
/** Change the wrap boundary and re-run layout for the current text. */
|
|
1116
1166
|
setMaxWidth(maxWidth) {
|
|
1117
1167
|
if (this.maxWidth === maxWidth) return;
|
|
@@ -1177,7 +1227,7 @@ var MSDFTextEntity = (_class3 = class extends Entity {
|
|
|
1177
1227
|
if (res.seqId < this.lastRenderedSeqId) return;
|
|
1178
1228
|
this.lastRenderedSeqId = res.seqId;
|
|
1179
1229
|
this.layoutResult = res;
|
|
1180
|
-
_optionalChain([this, 'access',
|
|
1230
|
+
_optionalChain([this, 'access', _111 => _111.scene, 'optionalAccess', _112 => _112.markDirty, 'call', _113 => _113()]);
|
|
1181
1231
|
}
|
|
1182
1232
|
});
|
|
1183
1233
|
}
|
|
@@ -1283,11 +1333,13 @@ var MSDFTextEntity = (_class3 = class extends Entity {
|
|
|
1283
1333
|
}
|
|
1284
1334
|
destroy() {
|
|
1285
1335
|
_layout.LayoutWorkerManager.cancelLayoutForEntity(this.id);
|
|
1336
|
+
this.detachAtlasDecodeListener();
|
|
1286
1337
|
super.destroy();
|
|
1287
1338
|
}
|
|
1288
1339
|
}, _class3);
|
|
1289
1340
|
|
|
1290
1341
|
// src/text/SVGEntity.ts
|
|
1342
|
+
var SVG_NAMESPACE = "http://www.w3.org/2000/svg";
|
|
1291
1343
|
function isSvgWhitespace(ch) {
|
|
1292
1344
|
return ch === " " || ch === " " || ch === "\n" || ch === "\r";
|
|
1293
1345
|
}
|
|
@@ -1319,19 +1371,28 @@ function readSvgAttribute(source, name) {
|
|
|
1319
1371
|
return null;
|
|
1320
1372
|
}
|
|
1321
1373
|
var SVGEntity = (_class4 = class extends Entity {
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1374
|
+
/**
|
|
1375
|
+
* Stroke colour of the fallback marker drawn when the source cannot be
|
|
1376
|
+
* rasterized. Set to `'transparent'` to opt out and keep the box empty.
|
|
1377
|
+
* Default `'rgba(248,113,113,0.9)'`.
|
|
1378
|
+
*/
|
|
1379
|
+
__init48() {this.fallbackStroke = "rgba(248,113,113,0.9)"}
|
|
1380
|
+
/** Fill behind the fallback marker. Default `'rgba(248,113,113,0.12)'`. */
|
|
1381
|
+
__init49() {this.fallbackFill = "rgba(248,113,113,0.12)"}
|
|
1382
|
+
__init50() {this.svgSource = ""}
|
|
1383
|
+
__init51() {this.imageBitmap = null}
|
|
1384
|
+
__init52() {this.imageElement = null}
|
|
1385
|
+
__init53() {this.blobURL = null}
|
|
1386
|
+
__init54() {this.currentImg = null}
|
|
1387
|
+
__init55() {this.lodTimeout = null}
|
|
1388
|
+
__init56() {this.rasterFailed = false}
|
|
1389
|
+
__init57() {this.cachedDoc = null}
|
|
1390
|
+
__init58() {this.baseWidth = 100}
|
|
1391
|
+
__init59() {this.baseHeight = 100}
|
|
1392
|
+
__init60() {this.lastRasterizedScale = 1}
|
|
1393
|
+
__init61() {this.targetScale = 1}
|
|
1333
1394
|
constructor(svgSource, id) {
|
|
1334
|
-
super(id);_class4.prototype.
|
|
1395
|
+
super(id);_class4.prototype.__init48.call(this);_class4.prototype.__init49.call(this);_class4.prototype.__init50.call(this);_class4.prototype.__init51.call(this);_class4.prototype.__init52.call(this);_class4.prototype.__init53.call(this);_class4.prototype.__init54.call(this);_class4.prototype.__init55.call(this);_class4.prototype.__init56.call(this);_class4.prototype.__init57.call(this);_class4.prototype.__init58.call(this);_class4.prototype.__init59.call(this);_class4.prototype.__init60.call(this);_class4.prototype.__init61.call(this);;
|
|
1335
1396
|
this.setSVGSource(svgSource);
|
|
1336
1397
|
}
|
|
1337
1398
|
setSVGSource(svgSource) {
|
|
@@ -1351,6 +1412,7 @@ var SVGEntity = (_class4 = class extends Entity {
|
|
|
1351
1412
|
const parserError = doc.querySelector("parsererror");
|
|
1352
1413
|
if (parserError) {
|
|
1353
1414
|
console.error("SVG Parsing error:", parserError.textContent);
|
|
1415
|
+
this.rasterFailed = true;
|
|
1354
1416
|
} else {
|
|
1355
1417
|
this.cachedDoc = doc;
|
|
1356
1418
|
const svgEl = doc.documentElement;
|
|
@@ -1370,6 +1432,7 @@ var SVGEntity = (_class4 = class extends Entity {
|
|
|
1370
1432
|
}
|
|
1371
1433
|
} catch (e) {
|
|
1372
1434
|
console.error("Failed parsing SVG via DOMParser, falling back to attribute scan:", e);
|
|
1435
|
+
this.rasterFailed = true;
|
|
1373
1436
|
}
|
|
1374
1437
|
} else {
|
|
1375
1438
|
const wAttr = readSvgAttribute(this.svgSource, "width");
|
|
@@ -1393,6 +1456,7 @@ var SVGEntity = (_class4 = class extends Entity {
|
|
|
1393
1456
|
}
|
|
1394
1457
|
triggerRasterization(scale) {
|
|
1395
1458
|
if (typeof window === "undefined" || typeof Blob === "undefined") return;
|
|
1459
|
+
this.rasterFailed = false;
|
|
1396
1460
|
if (this.currentImg) {
|
|
1397
1461
|
this.currentImg.onload = null;
|
|
1398
1462
|
this.currentImg.onerror = null;
|
|
@@ -1416,6 +1480,7 @@ var SVGEntity = (_class4 = class extends Entity {
|
|
|
1416
1480
|
"SVG Parsing validation error in triggerRasterization:",
|
|
1417
1481
|
parserError.textContent
|
|
1418
1482
|
);
|
|
1483
|
+
this.rasterFailed = true;
|
|
1419
1484
|
} else {
|
|
1420
1485
|
const clonedDoc = doc.cloneNode(true);
|
|
1421
1486
|
const svgEl = clonedDoc.documentElement;
|
|
@@ -1429,10 +1494,14 @@ var SVGEntity = (_class4 = class extends Entity {
|
|
|
1429
1494
|
}
|
|
1430
1495
|
const serializer = new XMLSerializer();
|
|
1431
1496
|
processedSource = serializer.serializeToString(clonedDoc);
|
|
1497
|
+
if (svgEl.namespaceURI === null && !/\sxmlns\s*=/.test(processedSource)) {
|
|
1498
|
+
processedSource = processedSource.replace(/<svg/i, `<svg xmlns="${SVG_NAMESPACE}"`);
|
|
1499
|
+
}
|
|
1432
1500
|
}
|
|
1433
1501
|
}
|
|
1434
1502
|
} catch (e) {
|
|
1435
1503
|
console.error("Failed to apply LOD scaling to SVG XML:", e);
|
|
1504
|
+
this.rasterFailed = true;
|
|
1436
1505
|
}
|
|
1437
1506
|
const blob = new Blob([processedSource], { type: "image/svg+xml;charset=utf-8" });
|
|
1438
1507
|
this.blobURL = URL.createObjectURL(blob);
|
|
@@ -1461,15 +1530,36 @@ var SVGEntity = (_class4 = class extends Entity {
|
|
|
1461
1530
|
}).catch((e) => {
|
|
1462
1531
|
console.error("Failed to create ImageBitmap from SVG:", e);
|
|
1463
1532
|
this.currentImg = null;
|
|
1533
|
+
if (!this.imageElement) this.rasterFailed = true;
|
|
1534
|
+
if (this.scene) this.scene.markDirty();
|
|
1464
1535
|
});
|
|
1465
1536
|
};
|
|
1466
1537
|
img.onerror = (e) => {
|
|
1467
1538
|
if (this.currentImg !== img) return;
|
|
1468
1539
|
console.error("Failed to load SVG Image element:", e);
|
|
1469
1540
|
this.currentImg = null;
|
|
1541
|
+
this.rasterFailed = true;
|
|
1542
|
+
if (this.scene) this.scene.markDirty();
|
|
1470
1543
|
};
|
|
1471
1544
|
img.src = this.blobURL;
|
|
1472
1545
|
}
|
|
1546
|
+
/**
|
|
1547
|
+
* Whether the source genuinely rasterized to a bitmap.
|
|
1548
|
+
*
|
|
1549
|
+
* Distinguishes "drew the real artwork" from "drew the fallback marker",
|
|
1550
|
+
* which pixel counts alone cannot tell apart — both are non-blank.
|
|
1551
|
+
*/
|
|
1552
|
+
hasRasterBitmap() {
|
|
1553
|
+
return this.imageBitmap !== null;
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Whether rasterization failed, so {@link render} draws the fallback marker.
|
|
1557
|
+
*
|
|
1558
|
+
* `false` while a raster is still in flight; only a settled failure sets it.
|
|
1559
|
+
*/
|
|
1560
|
+
hasRasterFailed() {
|
|
1561
|
+
return this.rasterFailed;
|
|
1562
|
+
}
|
|
1473
1563
|
isPointInside(globalX, globalY) {
|
|
1474
1564
|
const local = this.worldToLocal(globalX, globalY);
|
|
1475
1565
|
if (!local) return false;
|
|
@@ -1494,9 +1584,32 @@ var SVGEntity = (_class4 = class extends Entity {
|
|
|
1494
1584
|
}
|
|
1495
1585
|
if (this.imageBitmap) {
|
|
1496
1586
|
r.drawImage(this.imageBitmap, 0, 0, this.width, this.height);
|
|
1497
|
-
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
if (this.imageElement) {
|
|
1498
1590
|
r.drawImage(this.imageElement, 0, 0, this.width, this.height);
|
|
1591
|
+
return;
|
|
1499
1592
|
}
|
|
1593
|
+
if (this.rasterFailed) this.drawFallback(r);
|
|
1594
|
+
}
|
|
1595
|
+
/** Box outline plus a diagonal cross — the conventional "broken image" mark. */
|
|
1596
|
+
drawFallback(r) {
|
|
1597
|
+
const w = this.width;
|
|
1598
|
+
const h = this.height;
|
|
1599
|
+
if (w <= 0 || h <= 0) return;
|
|
1600
|
+
r.beginPath();
|
|
1601
|
+
r.roundRect(0, 0, w, h, 0);
|
|
1602
|
+
r.fill(this.fallbackFill);
|
|
1603
|
+
r.beginPath();
|
|
1604
|
+
r.roundRect(0, 0, w, h, 0);
|
|
1605
|
+
r.stroke(this.fallbackStroke, 1);
|
|
1606
|
+
const inset = Math.min(w, h) * 0.2;
|
|
1607
|
+
r.beginPath();
|
|
1608
|
+
r.moveTo(inset, inset);
|
|
1609
|
+
r.lineTo(w - inset, h - inset);
|
|
1610
|
+
r.moveTo(w - inset, inset);
|
|
1611
|
+
r.lineTo(inset, h - inset);
|
|
1612
|
+
r.stroke(this.fallbackStroke, 1);
|
|
1500
1613
|
}
|
|
1501
1614
|
destroy() {
|
|
1502
1615
|
if (this.lodTimeout) {
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from './renderer/colorParse';
|
|
|
7
7
|
export * from './renderer/url';
|
|
8
8
|
export * from './renderer/GlyphRasterAtlas';
|
|
9
9
|
export * from './renderer/TextRasterCache';
|
|
10
|
+
export * from './performance/UserTiming';
|
|
10
11
|
export * from './tree/Entity';
|
|
11
12
|
export * from './tree/Scene';
|
|
12
13
|
export * from './components/TextEntity';
|