@vectojs/core 1.34.0 → 1.34.2
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-VFQMETOR.mjs → chunk-TV4ARAC5.mjs} +98 -1
- package/dist/{chunk-UEIZDNK7.js → chunk-XXU56ZNA.js} +114 -17
- package/dist/components/TextEntity.d.ts +18 -0
- package/dist/index.js +174 -59
- package/dist/index.mjs +120 -5
- package/dist/text/MSDFTextEntity.d.ts +24 -0
- package/dist/text.js +2 -2
- package/dist/text.mjs +1 -1
- package/dist/tree/Entity.d.ts +14 -0
- package/package.json +3 -3
|
@@ -1207,6 +1207,12 @@ var MSDFTextEntity = class extends Entity {
|
|
|
1207
1207
|
rgbColorCache = /* @__PURE__ */ new Map();
|
|
1208
1208
|
fontStringCache = [];
|
|
1209
1209
|
layoutResult = null;
|
|
1210
|
+
/**
|
|
1211
|
+
* Visual rows rebuilt from {@link layoutResult} (see
|
|
1212
|
+
* {@link rebuildProjectionLines}). Empty until a layout reply lands and the
|
|
1213
|
+
* reply's shaped glyphs can be mapped back to the source text 1:1.
|
|
1214
|
+
*/
|
|
1215
|
+
projectionLines = [];
|
|
1210
1216
|
constructor(text, options) {
|
|
1211
1217
|
super();
|
|
1212
1218
|
this.font = options.font;
|
|
@@ -1319,6 +1325,7 @@ var MSDFTextEntity = class extends Entity {
|
|
|
1319
1325
|
}
|
|
1320
1326
|
queueLayout() {
|
|
1321
1327
|
this.contentEpoch++;
|
|
1328
|
+
this.projectionLines = [];
|
|
1322
1329
|
LayoutWorkerManager.getInstance().queueLayout(this.id, this.layoutText, {
|
|
1323
1330
|
fontId: this.font.id,
|
|
1324
1331
|
fontSize: this.fontSize,
|
|
@@ -1332,6 +1339,8 @@ var MSDFTextEntity = class extends Entity {
|
|
|
1332
1339
|
if (res.seqId < this.lastRenderedSeqId) return;
|
|
1333
1340
|
this.lastRenderedSeqId = res.seqId;
|
|
1334
1341
|
this.layoutResult = res;
|
|
1342
|
+
this.contentEpoch++;
|
|
1343
|
+
this.rebuildProjectionLines();
|
|
1335
1344
|
this.scene?.markDirty();
|
|
1336
1345
|
}
|
|
1337
1346
|
});
|
|
@@ -1339,15 +1348,103 @@ var MSDFTextEntity = class extends Entity {
|
|
|
1339
1348
|
/**
|
|
1340
1349
|
* Mirror the rendered text into the DOM content layer: find-in-page, screen
|
|
1341
1350
|
* readers, crawlers, and translation see the same string the canvas draws.
|
|
1351
|
+
*
|
|
1352
|
+
* `baseline` + `lineHeight` are always emitted (they come from the font
|
|
1353
|
+
* metrics, no layout reply needed), so the DOM line boxes at least land on
|
|
1354
|
+
* the canvas rhythm: the first baseline at `ascender × fontSize` and every
|
|
1355
|
+
* row advancing `(ascender − descender) × fontSize`. Once a layout reply is
|
|
1356
|
+
* in AND its shaped glyphs map back to the source 1:1 (unshaped LTR text —
|
|
1357
|
+
* bidi, shaping, soft hyphens or `\r` all fall back to the coarse branch),
|
|
1358
|
+
* per-line carriers pin each row's baseline exactly to the painted glyphs.
|
|
1342
1359
|
*/
|
|
1343
1360
|
getContentProjection() {
|
|
1344
1361
|
if (!this.text) return null;
|
|
1362
|
+
const metrics = this.font.data.metrics;
|
|
1363
|
+
const asc = metrics?.ascender ?? 0.8;
|
|
1364
|
+
const desc = metrics?.descender ?? -0.2;
|
|
1365
|
+
const actualLineHeight = this.lineHeight ?? this.fontSize * (asc - desc);
|
|
1345
1366
|
return {
|
|
1346
1367
|
text: this.text,
|
|
1347
1368
|
font: `${this.fontSize}px ${this.fallbackFont}`,
|
|
1348
|
-
lineHeight:
|
|
1369
|
+
lineHeight: actualLineHeight,
|
|
1370
|
+
baseline: asc * this.fontSize,
|
|
1371
|
+
lines: this.projectionLines.length > 0 ? this.projectionLines : void 0
|
|
1349
1372
|
};
|
|
1350
1373
|
}
|
|
1374
|
+
/**
|
|
1375
|
+
* Group the worker's positioned glyphs into the same visual rows the canvas
|
|
1376
|
+
* draws. Only runs when the reply's glyph sequence equals the source string
|
|
1377
|
+
* (one glyph per source char, no bidi reordering, no shaping, no soft
|
|
1378
|
+
* hyphens, no `\r`) — only then do glyph offsets line up with source offsets
|
|
1379
|
+
* byte-for-byte, which is what keeps find-in-page and the Scene's dev-mode
|
|
1380
|
+
* equality check correct. Every other text falls back to the coarse branch's
|
|
1381
|
+
* `baseline` + `lineHeight`, which still pins the row rhythm.
|
|
1382
|
+
*/
|
|
1383
|
+
rebuildProjectionLines() {
|
|
1384
|
+
const res = this.layoutResult;
|
|
1385
|
+
if (!res) {
|
|
1386
|
+
this.projectionLines = [];
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
const shaped = [];
|
|
1390
|
+
for (let i = 0; i < res.codePoints.length; i++) {
|
|
1391
|
+
shaped.push(String.fromCodePoint(res.codePoints[i]));
|
|
1392
|
+
}
|
|
1393
|
+
const sourceWithoutNewlines = this.text.replace(/\n/g, "");
|
|
1394
|
+
if (this.textAlign !== "left" || shaped.join("") !== sourceWithoutNewlines || res.yCoords.length !== res.codePoints.length) {
|
|
1395
|
+
this.projectionLines = [];
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
const metrics = this.font.data.metrics;
|
|
1399
|
+
const asc = metrics?.ascender ?? 0.8;
|
|
1400
|
+
const desc = metrics?.descender ?? -0.2;
|
|
1401
|
+
const actualLineHeight = this.lineHeight ?? this.fontSize * (asc - desc);
|
|
1402
|
+
const baseline = asc * this.fontSize;
|
|
1403
|
+
const srcIdx = [];
|
|
1404
|
+
for (let i = 0; i < this.text.length; i++) {
|
|
1405
|
+
if (this.text[i] !== "\n") srcIdx.push(i);
|
|
1406
|
+
}
|
|
1407
|
+
const glyphsByLine = /* @__PURE__ */ new Map();
|
|
1408
|
+
let maxIdx = -1;
|
|
1409
|
+
for (let i = 0; i < res.yCoords.length; i++) {
|
|
1410
|
+
const idx = Math.round((res.yCoords[i] - baseline) / actualLineHeight);
|
|
1411
|
+
const list = glyphsByLine.get(idx) ?? [];
|
|
1412
|
+
list.push(i);
|
|
1413
|
+
glyphsByLine.set(idx, list);
|
|
1414
|
+
if (idx > maxIdx) maxIdx = idx;
|
|
1415
|
+
}
|
|
1416
|
+
const starts = [];
|
|
1417
|
+
const ends = [];
|
|
1418
|
+
let previousEnd = 0;
|
|
1419
|
+
for (let i = 0; i <= maxIdx; i++) {
|
|
1420
|
+
const glyphs = glyphsByLine.get(i) ?? [];
|
|
1421
|
+
const start = glyphs.length > 0 ? srcIdx[glyphs[0]] : previousEnd;
|
|
1422
|
+
const end = glyphs.length > 0 ? srcIdx[glyphs[glyphs.length - 1]] + 1 : start;
|
|
1423
|
+
starts.push(start);
|
|
1424
|
+
ends.push(end);
|
|
1425
|
+
previousEnd = Math.max(previousEnd, end);
|
|
1426
|
+
}
|
|
1427
|
+
const lines = [];
|
|
1428
|
+
for (let i = 0; i <= maxIdx; i++) {
|
|
1429
|
+
const start = starts[i];
|
|
1430
|
+
const end = ends[i];
|
|
1431
|
+
const nextStart = i + 1 <= maxIdx ? starts[i + 1] : this.text.length;
|
|
1432
|
+
lines.push({
|
|
1433
|
+
text: this.text.slice(start, Math.max(start, end)),
|
|
1434
|
+
separatorAfter: this.text.slice(end, Math.max(end, nextStart)),
|
|
1435
|
+
// Left-aligned LTR glyphs start at x 0. No perGraphemeCarriers: the
|
|
1436
|
+
// DOM measures the FALLBACK font, not the atlas font, so natural flow
|
|
1437
|
+
// at the fallback's own advances stays self-consistent (pin carriers
|
|
1438
|
+
// to atlas x and the fallback text would misalign instead).
|
|
1439
|
+
x: 0,
|
|
1440
|
+
y: i * actualLineHeight,
|
|
1441
|
+
baseline,
|
|
1442
|
+
font: `${this.fontSize}px ${this.fallbackFont}`,
|
|
1443
|
+
lineHeight: actualLineHeight
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
this.projectionLines = lines;
|
|
1447
|
+
}
|
|
1351
1448
|
getContentEpoch() {
|
|
1352
1449
|
return this.contentEpoch;
|
|
1353
1450
|
}
|
|
@@ -1209,8 +1209,14 @@ var MSDFTextEntity = (_class3 = class extends Entity {
|
|
|
1209
1209
|
__init48() {this.rgbColorCache = /* @__PURE__ */ new Map()}
|
|
1210
1210
|
__init49() {this.fontStringCache = []}
|
|
1211
1211
|
__init50() {this.layoutResult = null}
|
|
1212
|
+
/**
|
|
1213
|
+
* Visual rows rebuilt from {@link layoutResult} (see
|
|
1214
|
+
* {@link rebuildProjectionLines}). Empty until a layout reply lands and the
|
|
1215
|
+
* reply's shaped glyphs can be mapped back to the source text 1:1.
|
|
1216
|
+
*/
|
|
1217
|
+
__init51() {this.projectionLines = []}
|
|
1212
1218
|
constructor(text, options) {
|
|
1213
|
-
super();_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);_class3.prototype.__init48.call(this);_class3.prototype.__init49.call(this);_class3.prototype.__init50.call(this);;
|
|
1219
|
+
super();_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);_class3.prototype.__init48.call(this);_class3.prototype.__init49.call(this);_class3.prototype.__init50.call(this);_class3.prototype.__init51.call(this);;
|
|
1214
1220
|
this.font = options.font;
|
|
1215
1221
|
this.texture = options.texture;
|
|
1216
1222
|
this.fallbackFont = _nullishCoalesce(options.fallbackFont, () => ( "sans-serif"));
|
|
@@ -1321,6 +1327,7 @@ var MSDFTextEntity = (_class3 = class extends Entity {
|
|
|
1321
1327
|
}
|
|
1322
1328
|
queueLayout() {
|
|
1323
1329
|
this.contentEpoch++;
|
|
1330
|
+
this.projectionLines = [];
|
|
1324
1331
|
_layout.LayoutWorkerManager.getInstance().queueLayout(this.id, this.layoutText, {
|
|
1325
1332
|
fontId: this.font.id,
|
|
1326
1333
|
fontSize: this.fontSize,
|
|
@@ -1334,6 +1341,8 @@ var MSDFTextEntity = (_class3 = class extends Entity {
|
|
|
1334
1341
|
if (res.seqId < this.lastRenderedSeqId) return;
|
|
1335
1342
|
this.lastRenderedSeqId = res.seqId;
|
|
1336
1343
|
this.layoutResult = res;
|
|
1344
|
+
this.contentEpoch++;
|
|
1345
|
+
this.rebuildProjectionLines();
|
|
1337
1346
|
_optionalChain([this, 'access', _114 => _114.scene, 'optionalAccess', _115 => _115.markDirty, 'call', _116 => _116()]);
|
|
1338
1347
|
}
|
|
1339
1348
|
});
|
|
@@ -1341,15 +1350,103 @@ var MSDFTextEntity = (_class3 = class extends Entity {
|
|
|
1341
1350
|
/**
|
|
1342
1351
|
* Mirror the rendered text into the DOM content layer: find-in-page, screen
|
|
1343
1352
|
* readers, crawlers, and translation see the same string the canvas draws.
|
|
1353
|
+
*
|
|
1354
|
+
* `baseline` + `lineHeight` are always emitted (they come from the font
|
|
1355
|
+
* metrics, no layout reply needed), so the DOM line boxes at least land on
|
|
1356
|
+
* the canvas rhythm: the first baseline at `ascender × fontSize` and every
|
|
1357
|
+
* row advancing `(ascender − descender) × fontSize`. Once a layout reply is
|
|
1358
|
+
* in AND its shaped glyphs map back to the source 1:1 (unshaped LTR text —
|
|
1359
|
+
* bidi, shaping, soft hyphens or `\r` all fall back to the coarse branch),
|
|
1360
|
+
* per-line carriers pin each row's baseline exactly to the painted glyphs.
|
|
1344
1361
|
*/
|
|
1345
1362
|
getContentProjection() {
|
|
1346
1363
|
if (!this.text) return null;
|
|
1364
|
+
const metrics = this.font.data.metrics;
|
|
1365
|
+
const asc = _nullishCoalesce(_optionalChain([metrics, 'optionalAccess', _117 => _117.ascender]), () => ( 0.8));
|
|
1366
|
+
const desc = _nullishCoalesce(_optionalChain([metrics, 'optionalAccess', _118 => _118.descender]), () => ( -0.2));
|
|
1367
|
+
const actualLineHeight = _nullishCoalesce(this.lineHeight, () => ( this.fontSize * (asc - desc)));
|
|
1347
1368
|
return {
|
|
1348
1369
|
text: this.text,
|
|
1349
1370
|
font: `${this.fontSize}px ${this.fallbackFont}`,
|
|
1350
|
-
lineHeight:
|
|
1371
|
+
lineHeight: actualLineHeight,
|
|
1372
|
+
baseline: asc * this.fontSize,
|
|
1373
|
+
lines: this.projectionLines.length > 0 ? this.projectionLines : void 0
|
|
1351
1374
|
};
|
|
1352
1375
|
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Group the worker's positioned glyphs into the same visual rows the canvas
|
|
1378
|
+
* draws. Only runs when the reply's glyph sequence equals the source string
|
|
1379
|
+
* (one glyph per source char, no bidi reordering, no shaping, no soft
|
|
1380
|
+
* hyphens, no `\r`) — only then do glyph offsets line up with source offsets
|
|
1381
|
+
* byte-for-byte, which is what keeps find-in-page and the Scene's dev-mode
|
|
1382
|
+
* equality check correct. Every other text falls back to the coarse branch's
|
|
1383
|
+
* `baseline` + `lineHeight`, which still pins the row rhythm.
|
|
1384
|
+
*/
|
|
1385
|
+
rebuildProjectionLines() {
|
|
1386
|
+
const res = this.layoutResult;
|
|
1387
|
+
if (!res) {
|
|
1388
|
+
this.projectionLines = [];
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
const shaped = [];
|
|
1392
|
+
for (let i = 0; i < res.codePoints.length; i++) {
|
|
1393
|
+
shaped.push(String.fromCodePoint(res.codePoints[i]));
|
|
1394
|
+
}
|
|
1395
|
+
const sourceWithoutNewlines = this.text.replace(/\n/g, "");
|
|
1396
|
+
if (this.textAlign !== "left" || shaped.join("") !== sourceWithoutNewlines || res.yCoords.length !== res.codePoints.length) {
|
|
1397
|
+
this.projectionLines = [];
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
const metrics = this.font.data.metrics;
|
|
1401
|
+
const asc = _nullishCoalesce(_optionalChain([metrics, 'optionalAccess', _119 => _119.ascender]), () => ( 0.8));
|
|
1402
|
+
const desc = _nullishCoalesce(_optionalChain([metrics, 'optionalAccess', _120 => _120.descender]), () => ( -0.2));
|
|
1403
|
+
const actualLineHeight = _nullishCoalesce(this.lineHeight, () => ( this.fontSize * (asc - desc)));
|
|
1404
|
+
const baseline = asc * this.fontSize;
|
|
1405
|
+
const srcIdx = [];
|
|
1406
|
+
for (let i = 0; i < this.text.length; i++) {
|
|
1407
|
+
if (this.text[i] !== "\n") srcIdx.push(i);
|
|
1408
|
+
}
|
|
1409
|
+
const glyphsByLine = /* @__PURE__ */ new Map();
|
|
1410
|
+
let maxIdx = -1;
|
|
1411
|
+
for (let i = 0; i < res.yCoords.length; i++) {
|
|
1412
|
+
const idx = Math.round((res.yCoords[i] - baseline) / actualLineHeight);
|
|
1413
|
+
const list = _nullishCoalesce(glyphsByLine.get(idx), () => ( []));
|
|
1414
|
+
list.push(i);
|
|
1415
|
+
glyphsByLine.set(idx, list);
|
|
1416
|
+
if (idx > maxIdx) maxIdx = idx;
|
|
1417
|
+
}
|
|
1418
|
+
const starts = [];
|
|
1419
|
+
const ends = [];
|
|
1420
|
+
let previousEnd = 0;
|
|
1421
|
+
for (let i = 0; i <= maxIdx; i++) {
|
|
1422
|
+
const glyphs = _nullishCoalesce(glyphsByLine.get(i), () => ( []));
|
|
1423
|
+
const start = glyphs.length > 0 ? srcIdx[glyphs[0]] : previousEnd;
|
|
1424
|
+
const end = glyphs.length > 0 ? srcIdx[glyphs[glyphs.length - 1]] + 1 : start;
|
|
1425
|
+
starts.push(start);
|
|
1426
|
+
ends.push(end);
|
|
1427
|
+
previousEnd = Math.max(previousEnd, end);
|
|
1428
|
+
}
|
|
1429
|
+
const lines = [];
|
|
1430
|
+
for (let i = 0; i <= maxIdx; i++) {
|
|
1431
|
+
const start = starts[i];
|
|
1432
|
+
const end = ends[i];
|
|
1433
|
+
const nextStart = i + 1 <= maxIdx ? starts[i + 1] : this.text.length;
|
|
1434
|
+
lines.push({
|
|
1435
|
+
text: this.text.slice(start, Math.max(start, end)),
|
|
1436
|
+
separatorAfter: this.text.slice(end, Math.max(end, nextStart)),
|
|
1437
|
+
// Left-aligned LTR glyphs start at x 0. No perGraphemeCarriers: the
|
|
1438
|
+
// DOM measures the FALLBACK font, not the atlas font, so natural flow
|
|
1439
|
+
// at the fallback's own advances stays self-consistent (pin carriers
|
|
1440
|
+
// to atlas x and the fallback text would misalign instead).
|
|
1441
|
+
x: 0,
|
|
1442
|
+
y: i * actualLineHeight,
|
|
1443
|
+
baseline,
|
|
1444
|
+
font: `${this.fontSize}px ${this.fallbackFont}`,
|
|
1445
|
+
lineHeight: actualLineHeight
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
this.projectionLines = lines;
|
|
1449
|
+
}
|
|
1353
1450
|
getContentEpoch() {
|
|
1354
1451
|
return this.contentEpoch;
|
|
1355
1452
|
}
|
|
@@ -1486,23 +1583,23 @@ var SVGEntity = (_class4 = class extends Entity {
|
|
|
1486
1583
|
* rasterized. Set to `'transparent'` to opt out and keep the box empty.
|
|
1487
1584
|
* Default `'rgba(248,113,113,0.9)'`.
|
|
1488
1585
|
*/
|
|
1489
|
-
|
|
1586
|
+
__init52() {this.fallbackStroke = "rgba(248,113,113,0.9)"}
|
|
1490
1587
|
/** Fill behind the fallback marker. Default `'rgba(248,113,113,0.12)'`. */
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1588
|
+
__init53() {this.fallbackFill = "rgba(248,113,113,0.12)"}
|
|
1589
|
+
__init54() {this.svgSource = ""}
|
|
1590
|
+
__init55() {this.imageBitmap = null}
|
|
1591
|
+
__init56() {this.imageElement = null}
|
|
1592
|
+
__init57() {this.blobURL = null}
|
|
1593
|
+
__init58() {this.currentImg = null}
|
|
1594
|
+
__init59() {this.lodTimeout = null}
|
|
1595
|
+
__init60() {this.rasterFailed = false}
|
|
1596
|
+
__init61() {this.cachedDoc = null}
|
|
1597
|
+
__init62() {this.baseWidth = 100}
|
|
1598
|
+
__init63() {this.baseHeight = 100}
|
|
1599
|
+
__init64() {this.lastRasterizedScale = 1}
|
|
1600
|
+
__init65() {this.targetScale = 1}
|
|
1504
1601
|
constructor(svgSource, id) {
|
|
1505
|
-
super(id);_class4.prototype.
|
|
1602
|
+
super(id);_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);_class4.prototype.__init62.call(this);_class4.prototype.__init63.call(this);_class4.prototype.__init64.call(this);_class4.prototype.__init65.call(this);;
|
|
1506
1603
|
this.setSVGSource(svgSource);
|
|
1507
1604
|
}
|
|
1508
1605
|
setSVGSource(svgSource) {
|
|
@@ -14,10 +14,28 @@ export declare class TextEntity extends Entity {
|
|
|
14
14
|
private isHovered;
|
|
15
15
|
/** Bumped by {@link applyLayout}; read by `Scene` to skip an unchanged sync. */
|
|
16
16
|
private contentEpoch;
|
|
17
|
+
/**
|
|
18
|
+
* Visual lines with real canvas geometry, rebuilt by {@link applyLayout}
|
|
19
|
+
* exactly when `this.nodes` changes.
|
|
20
|
+
*
|
|
21
|
+
* Without this the Scene placed the DOM copy at the entity origin and let the
|
|
22
|
+
* browser flow it at CSS `normal` line-height, while the canvas lays lines out
|
|
23
|
+
* at `1.5em` pitch with the baseline at `0.8em` — so every line after the
|
|
24
|
+
* first drifted further from the painted glyphs (measured ~6 px on line 0 and
|
|
25
|
+
* ~0.35 em per line for a 24 px font, Firefox). Per-line carriers pin the DOM
|
|
26
|
+
* baseline to the canvas baseline by construction, same as `ui/Text`.
|
|
27
|
+
*/
|
|
28
|
+
private projectionLines;
|
|
17
29
|
constructor(text: string, atlas: any, maxWidth: number, fontSize?: number);
|
|
18
30
|
/**
|
|
19
31
|
* Mirror the rendered text into the DOM content layer: find-in-page, screen
|
|
20
32
|
* readers, crawlers, and translation see the same string the canvas draws.
|
|
33
|
+
*
|
|
34
|
+
* Each visual line is emitted with its own y/baseline/line-height so the DOM
|
|
35
|
+
* line boxes sit exactly on the drawn glyphs instead of flowing at the
|
|
36
|
+
* browser's `normal` metrics. Line text is the LOGICAL source slice (not the
|
|
37
|
+
* per-glyph visual text), so shaped or reordered content still copies and
|
|
38
|
+
* finds in source order — the same contract `ui/Text` ships.
|
|
21
39
|
*/
|
|
22
40
|
getContentProjection(): ContentProjection | null;
|
|
23
41
|
getContentEpoch(): number;
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ var _chunkM73XB4ZKjs = require('./chunk-M73XB4ZK.js');
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
var
|
|
22
|
+
var _chunkXXU56ZNAjs = require('./chunk-XXU56ZNA.js');
|
|
23
23
|
|
|
24
24
|
// src/tree/ComputeParticleEntity.ts
|
|
25
25
|
var PARTICLE_STRIDE_FLOATS = 8;
|
|
@@ -31,7 +31,7 @@ var PARTICLE_OFFSET_ORIGIN_X = 4;
|
|
|
31
31
|
var PARTICLE_OFFSET_ORIGIN_Y = 5;
|
|
32
32
|
var PARTICLE_OFFSET_SIZE = 6;
|
|
33
33
|
var PARTICLE_OFFSET_LIFE = 7;
|
|
34
|
-
var ComputeParticleEntity = (_class = class extends
|
|
34
|
+
var ComputeParticleEntity = (_class = class extends _chunkXXU56ZNAjs.Entity {
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
|
|
@@ -438,6 +438,10 @@ function measureVectoUserTiming(name, durationMs) {
|
|
|
438
438
|
}
|
|
439
439
|
|
|
440
440
|
// src/tree/Scene.ts
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
|
|
441
445
|
var _text = require('@vectojs/text'); _createStarExport(_text);
|
|
442
446
|
|
|
443
447
|
// src/tree/scene/a11y-dom.ts
|
|
@@ -4732,7 +4736,7 @@ var Scene = (_class15 = class _Scene {
|
|
|
4732
4736
|
this.forcedColorsChangeHandler = () => this.markDirty();
|
|
4733
4737
|
_optionalChain([this, 'access', _118 => _118.forcedColorsQuery, 'access', _119 => _119.addEventListener, 'optionalCall', _120 => _120("change", this.forcedColorsChangeHandler)]);
|
|
4734
4738
|
}
|
|
4735
|
-
this.root = new class RootEntity extends
|
|
4739
|
+
this.root = new class RootEntity extends _chunkXXU56ZNAjs.Entity {
|
|
4736
4740
|
isPointInside() {
|
|
4737
4741
|
return false;
|
|
4738
4742
|
}
|
|
@@ -4742,7 +4746,7 @@ var Scene = (_class15 = class _Scene {
|
|
|
4742
4746
|
}("root");
|
|
4743
4747
|
this.root._scene = this;
|
|
4744
4748
|
this._wasmBackend = new WasmBackendFacade(this.root);
|
|
4745
|
-
this.overlayRoot = new class OverlayRoot extends
|
|
4749
|
+
this.overlayRoot = new class OverlayRoot extends _chunkXXU56ZNAjs.Entity {
|
|
4746
4750
|
isPointInside() {
|
|
4747
4751
|
return false;
|
|
4748
4752
|
}
|
|
@@ -5063,7 +5067,7 @@ var Scene = (_class15 = class _Scene {
|
|
|
5063
5067
|
}
|
|
5064
5068
|
if (this.hoveredA11yElements.has(el)) {
|
|
5065
5069
|
this.hoveredA11yElements.delete(el);
|
|
5066
|
-
node.dispatchEvent(new (0,
|
|
5070
|
+
node.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)("pointerleave", node, void 0, false));
|
|
5067
5071
|
}
|
|
5068
5072
|
this.preserveFocusOnRemoval(el);
|
|
5069
5073
|
el.remove();
|
|
@@ -5654,20 +5658,20 @@ var Scene = (_class15 = class _Scene {
|
|
|
5654
5658
|
el.style.background = "transparent";
|
|
5655
5659
|
}
|
|
5656
5660
|
el.addEventListener("click", (e) => {
|
|
5657
|
-
node.dispatchEvent(new (0,
|
|
5661
|
+
node.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)("click", node, e));
|
|
5658
5662
|
});
|
|
5659
5663
|
el.addEventListener("dblclick", (e) => {
|
|
5660
|
-
node.dispatchEvent(new (0,
|
|
5664
|
+
node.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)("dblclick", node, e));
|
|
5661
5665
|
});
|
|
5662
5666
|
el.addEventListener("mouseenter", (e) => {
|
|
5663
5667
|
if (this.debugA11y) el.style.backgroundColor = "rgba(56, 189, 248, 0.2)";
|
|
5664
5668
|
this.hoveredA11yElements.add(el);
|
|
5665
|
-
node.dispatchEvent(new (0,
|
|
5669
|
+
node.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)("hover", node, e, false));
|
|
5666
5670
|
});
|
|
5667
5671
|
el.addEventListener("mouseleave", (e) => {
|
|
5668
5672
|
if (this.debugA11y) el.style.backgroundColor = "rgba(56, 189, 248, 0.05)";
|
|
5669
5673
|
this.hoveredA11yElements.delete(el);
|
|
5670
|
-
node.dispatchEvent(new (0,
|
|
5674
|
+
node.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)("pointerleave", node, e, false));
|
|
5671
5675
|
});
|
|
5672
5676
|
const capEl = el;
|
|
5673
5677
|
const releasePointer = (event) => {
|
|
@@ -5683,24 +5687,24 @@ var Scene = (_class15 = class _Scene {
|
|
|
5683
5687
|
};
|
|
5684
5688
|
el.addEventListener("pointerdown", (e) => {
|
|
5685
5689
|
if (typeof capEl.setPointerCapture === "function") capEl.setPointerCapture(e.pointerId);
|
|
5686
|
-
node.dispatchEvent(new (0,
|
|
5690
|
+
node.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)("pointerdown", node, e));
|
|
5687
5691
|
});
|
|
5688
5692
|
el.addEventListener("pointerup", (e) => {
|
|
5689
5693
|
releasePointer(e);
|
|
5690
|
-
node.dispatchEvent(new (0,
|
|
5694
|
+
node.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)("pointerup", node, e));
|
|
5691
5695
|
});
|
|
5692
5696
|
el.addEventListener("pointercancel", (e) => {
|
|
5693
5697
|
releasePointer(e);
|
|
5694
|
-
node.dispatchEvent(new (0,
|
|
5698
|
+
node.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)("pointercancel", node, e));
|
|
5695
5699
|
});
|
|
5696
5700
|
el.addEventListener(
|
|
5697
5701
|
"pointermove",
|
|
5698
|
-
(e) => node.dispatchEvent(new (0,
|
|
5702
|
+
(e) => node.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)("pointermove", node, e))
|
|
5699
5703
|
);
|
|
5700
5704
|
el.addEventListener(
|
|
5701
5705
|
"wheel",
|
|
5702
5706
|
(e) => {
|
|
5703
|
-
node.dispatchEvent(new (0,
|
|
5707
|
+
node.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)("wheel", node, e));
|
|
5704
5708
|
},
|
|
5705
5709
|
{ passive: false }
|
|
5706
5710
|
);
|
|
@@ -5725,10 +5729,10 @@ var Scene = (_class15 = class _Scene {
|
|
|
5725
5729
|
);
|
|
5726
5730
|
emitInitialScroll = emitScroll;
|
|
5727
5731
|
el.addEventListener("keydown", (e) => {
|
|
5728
|
-
node.dispatchEvent(new (0,
|
|
5732
|
+
node.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)("keydown", node, e));
|
|
5729
5733
|
});
|
|
5730
5734
|
el.addEventListener("keyup", (e) => {
|
|
5731
|
-
node.dispatchEvent(new (0,
|
|
5735
|
+
node.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)("keyup", node, e));
|
|
5732
5736
|
});
|
|
5733
5737
|
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
5734
5738
|
const input = el;
|
|
@@ -5794,7 +5798,7 @@ var Scene = (_class15 = class _Scene {
|
|
|
5794
5798
|
el.addEventListener("keydown", (e) => {
|
|
5795
5799
|
if (e.key === "Enter" || e.key === " ") {
|
|
5796
5800
|
e.preventDefault();
|
|
5797
|
-
node.dispatchEvent(new (0,
|
|
5801
|
+
node.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)("click", node, e));
|
|
5798
5802
|
}
|
|
5799
5803
|
});
|
|
5800
5804
|
}
|
|
@@ -6231,7 +6235,7 @@ var Scene = (_class15 = class _Scene {
|
|
|
6231
6235
|
el.addEventListener(
|
|
6232
6236
|
"wheel",
|
|
6233
6237
|
(e2) => {
|
|
6234
|
-
node.dispatchEvent(new (0,
|
|
6238
|
+
node.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)("wheel", node, e2));
|
|
6235
6239
|
},
|
|
6236
6240
|
{ passive: false }
|
|
6237
6241
|
);
|
|
@@ -6287,6 +6291,8 @@ var Scene = (_class15 = class _Scene {
|
|
|
6287
6291
|
const separator = _nullishCoalesce(line.separatorAfter, () => ( (index < lines.length - 1 ? "\n" : "")));
|
|
6288
6292
|
if (line.runs && line.runs.length > 0) {
|
|
6289
6293
|
const positioned = line.runs.some((run) => run.x !== void 0);
|
|
6294
|
+
const flowRelative = positioned && line.runs.every((run) => run.x === void 0 || run.width !== void 0);
|
|
6295
|
+
let runningX = line.x;
|
|
6290
6296
|
for (let runIndex = 0; runIndex < line.runs.length; runIndex++) {
|
|
6291
6297
|
const run = line.runs[runIndex];
|
|
6292
6298
|
const runElement = document.createElement("span");
|
|
@@ -6294,8 +6300,16 @@ var Scene = (_class15 = class _Scene {
|
|
|
6294
6300
|
if (run.font) runElement.style.font = run.font;
|
|
6295
6301
|
runElement.style.lineHeight = `${lineHeight2}px`;
|
|
6296
6302
|
if (positioned && run.x !== void 0) {
|
|
6297
|
-
|
|
6298
|
-
|
|
6303
|
+
if (flowRelative) {
|
|
6304
|
+
runElement.style.position = "relative";
|
|
6305
|
+
runElement.style.display = "inline-block";
|
|
6306
|
+
runElement.style.left = `${run.x - runningX}px`;
|
|
6307
|
+
runElement.style.boxSizing = "border-box";
|
|
6308
|
+
runningX += run.width;
|
|
6309
|
+
} else {
|
|
6310
|
+
runElement.style.position = "absolute";
|
|
6311
|
+
runElement.style.left = `${run.x - line.x}px`;
|
|
6312
|
+
}
|
|
6299
6313
|
runElement.style.top = "0";
|
|
6300
6314
|
if (run.width !== void 0) runElement.style.width = `${run.width}px`;
|
|
6301
6315
|
runElement.style.whiteSpace = "pre";
|
|
@@ -6305,6 +6319,35 @@ var Scene = (_class15 = class _Scene {
|
|
|
6305
6319
|
}
|
|
6306
6320
|
lineElement.appendChild(runElement);
|
|
6307
6321
|
}
|
|
6322
|
+
} else if (line.perGraphemeCarriers && line.text.length > 0) {
|
|
6323
|
+
const mctx = _text.getSharedMeasuringContext.call(void 0, );
|
|
6324
|
+
if (mctx) {
|
|
6325
|
+
mctx.font = lineFont;
|
|
6326
|
+
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
6327
|
+
const segments = [...segmenter.segment(line.text)];
|
|
6328
|
+
let runningX = 0;
|
|
6329
|
+
for (let segIdx = 0; segIdx < segments.length; segIdx++) {
|
|
6330
|
+
const { segment, index: index2 } = segments[segIdx];
|
|
6331
|
+
const nextIndex = segIdx + 1 < segments.length ? segments[segIdx + 1].index : line.text.length;
|
|
6332
|
+
const canvasX = mctx.measureText(line.text.slice(0, index2)).width;
|
|
6333
|
+
const canvasWidth = mctx.measureText(line.text.slice(0, nextIndex)).width - canvasX;
|
|
6334
|
+
const carrierEl = document.createElement("span");
|
|
6335
|
+
carrierEl.textContent = segment;
|
|
6336
|
+
carrierEl.style.position = "relative";
|
|
6337
|
+
carrierEl.style.display = "inline-block";
|
|
6338
|
+
carrierEl.style.left = `${canvasX - runningX}px`;
|
|
6339
|
+
carrierEl.style.width = `${canvasWidth}px`;
|
|
6340
|
+
carrierEl.style.boxSizing = "border-box";
|
|
6341
|
+
carrierEl.style.lineHeight = `${lineHeight2}px`;
|
|
6342
|
+
carrierEl.style.whiteSpace = "pre";
|
|
6343
|
+
carrierEl.style.verticalAlign = "top";
|
|
6344
|
+
carrierEl.style.unicodeBidi = "isolate";
|
|
6345
|
+
lineElement.appendChild(carrierEl);
|
|
6346
|
+
runningX += canvasWidth;
|
|
6347
|
+
}
|
|
6348
|
+
} else {
|
|
6349
|
+
lineElement.textContent = line.text;
|
|
6350
|
+
}
|
|
6308
6351
|
} else {
|
|
6309
6352
|
lineElement.textContent = line.text;
|
|
6310
6353
|
}
|
|
@@ -6838,7 +6881,7 @@ var Scene = (_class15 = class _Scene {
|
|
|
6838
6881
|
let walkHadAnimation = false;
|
|
6839
6882
|
let walkHadInteractive = false;
|
|
6840
6883
|
const runUpdate = (node) => {
|
|
6841
|
-
const overridesUpdate = node.update !==
|
|
6884
|
+
const overridesUpdate = node.update !== _chunkXXU56ZNAjs.Entity.prototype.update;
|
|
6842
6885
|
let pending = node.hasPendingAnimations();
|
|
6843
6886
|
if (pending || overridesUpdate) {
|
|
6844
6887
|
node.update(dt, time);
|
|
@@ -6847,7 +6890,7 @@ var Scene = (_class15 = class _Scene {
|
|
|
6847
6890
|
if (pending) walkHadAnimation = true;
|
|
6848
6891
|
if (!walkHadInteractive && node.interactive) walkHadInteractive = true;
|
|
6849
6892
|
if (this._devActive && this._devFrameCount % 120 === 0) {
|
|
6850
|
-
if (overridesUpdate && node.hasPendingAnimations ===
|
|
6893
|
+
if (overridesUpdate && node.hasPendingAnimations === _chunkXXU56ZNAjs.Entity.prototype.hasPendingAnimations) {
|
|
6851
6894
|
this._devWarn(
|
|
6852
6895
|
`Entity "${node.id}" overrides update() but not hasPendingAnimations(). Custom motion in update() without overriding hasPendingAnimations() causes the idle throttle to drop the animation to ~2fps. Override hasPendingAnimations() to return true while motion is in flight.`
|
|
6853
6896
|
);
|
|
@@ -7273,7 +7316,7 @@ function defaultMeasurer() {
|
|
|
7273
7316
|
sharedMeasurer ??= _layout.resolveGlyphMeasurer.call(void 0, "sans-serif");
|
|
7274
7317
|
return sharedMeasurer;
|
|
7275
7318
|
}
|
|
7276
|
-
var TextEntity = (_class16 = class extends
|
|
7319
|
+
var TextEntity = (_class16 = class extends _chunkXXU56ZNAjs.Entity {
|
|
7277
7320
|
|
|
7278
7321
|
|
|
7279
7322
|
|
|
@@ -7287,8 +7330,20 @@ var TextEntity = (_class16 = class extends _chunkUEIZDNK7js.Entity {
|
|
|
7287
7330
|
__init181() {this.isHovered = false}
|
|
7288
7331
|
/** Bumped by {@link applyLayout}; read by `Scene` to skip an unchanged sync. */
|
|
7289
7332
|
__init182() {this.contentEpoch = 0}
|
|
7333
|
+
/**
|
|
7334
|
+
* Visual lines with real canvas geometry, rebuilt by {@link applyLayout}
|
|
7335
|
+
* exactly when `this.nodes` changes.
|
|
7336
|
+
*
|
|
7337
|
+
* Without this the Scene placed the DOM copy at the entity origin and let the
|
|
7338
|
+
* browser flow it at CSS `normal` line-height, while the canvas lays lines out
|
|
7339
|
+
* at `1.5em` pitch with the baseline at `0.8em` — so every line after the
|
|
7340
|
+
* first drifted further from the painted glyphs (measured ~6 px on line 0 and
|
|
7341
|
+
* ~0.35 em per line for a 24 px font, Firefox). Per-line carriers pin the DOM
|
|
7342
|
+
* baseline to the canvas baseline by construction, same as `ui/Text`.
|
|
7343
|
+
*/
|
|
7344
|
+
__init183() {this.projectionLines = []}
|
|
7290
7345
|
constructor(text, atlas, maxWidth, fontSize = 24) {
|
|
7291
|
-
super();_class16.prototype.__init176.call(this);_class16.prototype.__init177.call(this);_class16.prototype.__init178.call(this);_class16.prototype.__init179.call(this);_class16.prototype.__init180.call(this);_class16.prototype.__init181.call(this);_class16.prototype.__init182.call(this);;
|
|
7346
|
+
super();_class16.prototype.__init176.call(this);_class16.prototype.__init177.call(this);_class16.prototype.__init178.call(this);_class16.prototype.__init179.call(this);_class16.prototype.__init180.call(this);_class16.prototype.__init181.call(this);_class16.prototype.__init182.call(this);_class16.prototype.__init183.call(this);;
|
|
7292
7347
|
this.text = text;
|
|
7293
7348
|
this.atlas = atlas;
|
|
7294
7349
|
this.fontSize = fontSize;
|
|
@@ -7302,10 +7357,21 @@ var TextEntity = (_class16 = class extends _chunkUEIZDNK7js.Entity {
|
|
|
7302
7357
|
/**
|
|
7303
7358
|
* Mirror the rendered text into the DOM content layer: find-in-page, screen
|
|
7304
7359
|
* readers, crawlers, and translation see the same string the canvas draws.
|
|
7360
|
+
*
|
|
7361
|
+
* Each visual line is emitted with its own y/baseline/line-height so the DOM
|
|
7362
|
+
* line boxes sit exactly on the drawn glyphs instead of flowing at the
|
|
7363
|
+
* browser's `normal` metrics. Line text is the LOGICAL source slice (not the
|
|
7364
|
+
* per-glyph visual text), so shaped or reordered content still copies and
|
|
7365
|
+
* finds in source order — the same contract `ui/Text` ships.
|
|
7305
7366
|
*/
|
|
7306
7367
|
getContentProjection() {
|
|
7307
7368
|
if (!this.text) return null;
|
|
7308
|
-
return {
|
|
7369
|
+
return {
|
|
7370
|
+
text: this.text,
|
|
7371
|
+
font: `${this.fontSize}px sans-serif`,
|
|
7372
|
+
lineHeight: this.fontSize * 1.5,
|
|
7373
|
+
lines: this.projectionLines
|
|
7374
|
+
};
|
|
7309
7375
|
}
|
|
7310
7376
|
getContentEpoch() {
|
|
7311
7377
|
return this.contentEpoch;
|
|
@@ -7362,6 +7428,55 @@ var TextEntity = (_class16 = class extends _chunkUEIZDNK7js.Entity {
|
|
|
7362
7428
|
this.width = result.totalWidth;
|
|
7363
7429
|
this.height = result.totalHeight;
|
|
7364
7430
|
this.a11yOffsetY = 0;
|
|
7431
|
+
const lineQuantum = this.fontSize * 1.5;
|
|
7432
|
+
const nodesByLine = /* @__PURE__ */ new Map();
|
|
7433
|
+
let maxIdx = -1;
|
|
7434
|
+
for (const node of this.nodes) {
|
|
7435
|
+
const idx = Math.round(node.y / lineQuantum);
|
|
7436
|
+
const list = _nullishCoalesce(nodesByLine.get(idx), () => ( []));
|
|
7437
|
+
list.push(node);
|
|
7438
|
+
nodesByLine.set(idx, list);
|
|
7439
|
+
if (idx > maxIdx) maxIdx = idx;
|
|
7440
|
+
}
|
|
7441
|
+
const justify = this.layout.textAlign === "justify";
|
|
7442
|
+
const starts = [];
|
|
7443
|
+
const ends = [];
|
|
7444
|
+
let previousEnd = 0;
|
|
7445
|
+
for (let i = 0; i <= maxIdx; i++) {
|
|
7446
|
+
const nodes = _nullishCoalesce(nodesByLine.get(i), () => ( []));
|
|
7447
|
+
let start = nodes.length > 0 ? Math.min(...nodes.map((node) => _nullishCoalesce(node.sourceIndex, () => ( previousEnd)))) : previousEnd;
|
|
7448
|
+
const end = nodes.length > 0 ? Math.max(
|
|
7449
|
+
...nodes.map((node) => (_nullishCoalesce(node.sourceIndex, () => ( previousEnd))) + (_nullishCoalesce(node.sourceLength, () => ( 0))))
|
|
7450
|
+
) : start;
|
|
7451
|
+
if (i === 0) start = 0;
|
|
7452
|
+
starts.push(start);
|
|
7453
|
+
ends.push(end);
|
|
7454
|
+
previousEnd = Math.max(previousEnd, end);
|
|
7455
|
+
}
|
|
7456
|
+
const lines = [];
|
|
7457
|
+
for (let i = 0; i <= maxIdx; i++) {
|
|
7458
|
+
const nodes = _nullishCoalesce(nodesByLine.get(i), () => ( []));
|
|
7459
|
+
const start = starts[i];
|
|
7460
|
+
const end = ends[i];
|
|
7461
|
+
const nextStart = i + 1 <= maxIdx ? starts[i + 1] : this.text.length;
|
|
7462
|
+
const hasRtl = nodes.some((node) => node.isRTL === true);
|
|
7463
|
+
lines.push({
|
|
7464
|
+
text: this.text.slice(start, Math.max(start, end)),
|
|
7465
|
+
separatorAfter: this.text.slice(end, Math.max(end, nextStart)),
|
|
7466
|
+
x: 0,
|
|
7467
|
+
y: i * lineQuantum,
|
|
7468
|
+
baseline: this.fontSize * 0.8,
|
|
7469
|
+
font: `${this.fontSize}px sans-serif`,
|
|
7470
|
+
lineHeight: lineQuantum,
|
|
7471
|
+
// Per-grapheme carriers pin each cluster to its canvas-measured x so
|
|
7472
|
+
// Gecko's grid-fit advance rounding cannot drift the find-in-page
|
|
7473
|
+
// highlight off the drawn glyphs. Bidi lines must keep one text node
|
|
7474
|
+
// (DOM order is logical; per-glyph carriers break caret mapping), and
|
|
7475
|
+
// justify moves glyphs off their natural x, so both fall back to flow.
|
|
7476
|
+
...hasRtl || justify ? {} : { perGraphemeCarriers: true }
|
|
7477
|
+
});
|
|
7478
|
+
}
|
|
7479
|
+
this.projectionLines = lines;
|
|
7365
7480
|
}
|
|
7366
7481
|
isPointInside(globalX, globalY) {
|
|
7367
7482
|
const local = this.worldToLocal(globalX, globalY);
|
|
@@ -7405,17 +7520,17 @@ var TextEntity = (_class16 = class extends _chunkUEIZDNK7js.Entity {
|
|
|
7405
7520
|
}, _class16);
|
|
7406
7521
|
|
|
7407
7522
|
// src/components/GridTextEntity.ts
|
|
7408
|
-
var GridTextEntity = (_class17 = class extends
|
|
7523
|
+
var GridTextEntity = (_class17 = class extends _chunkXXU56ZNAjs.Entity {
|
|
7409
7524
|
|
|
7410
|
-
|
|
7411
|
-
|
|
7525
|
+
__init184() {this.fillStyle = "#ffffff"}
|
|
7526
|
+
__init185() {this.grid = []}
|
|
7412
7527
|
// Array of rows
|
|
7413
|
-
|
|
7414
|
-
|
|
7528
|
+
__init186() {this.cols = 0}
|
|
7529
|
+
__init187() {this.rows = 0}
|
|
7415
7530
|
|
|
7416
7531
|
|
|
7417
7532
|
constructor(_atlas, fontSize = 10) {
|
|
7418
|
-
super();_class17.prototype.
|
|
7533
|
+
super();_class17.prototype.__init184.call(this);_class17.prototype.__init185.call(this);_class17.prototype.__init186.call(this);_class17.prototype.__init187.call(this);;
|
|
7419
7534
|
this.fontSize = fontSize;
|
|
7420
7535
|
this.charWidth = fontSize * 1;
|
|
7421
7536
|
this.charHeight = fontSize * 1.1;
|
|
@@ -7499,7 +7614,7 @@ function distSqToSegment(px, py, x1, y1, x2, y2) {
|
|
|
7499
7614
|
const ey = py - cy;
|
|
7500
7615
|
return ex * ex + ey * ey;
|
|
7501
7616
|
}
|
|
7502
|
-
var SplineEntity = (_class18 = class extends
|
|
7617
|
+
var SplineEntity = (_class18 = class extends _chunkXXU56ZNAjs.Entity {
|
|
7503
7618
|
|
|
7504
7619
|
|
|
7505
7620
|
|
|
@@ -7507,11 +7622,11 @@ var SplineEntity = (_class18 = class extends _chunkUEIZDNK7js.Entity {
|
|
|
7507
7622
|
|
|
7508
7623
|
|
|
7509
7624
|
|
|
7510
|
-
|
|
7511
|
-
|
|
7625
|
+
__init188() {this.offscreen = null}
|
|
7626
|
+
__init189() {this.baked = false}
|
|
7512
7627
|
/** Logical (CSS-pixel) size of the baked bitmap — the blit destination size. */
|
|
7513
|
-
|
|
7514
|
-
|
|
7628
|
+
__init190() {this.bakedWidth = 0}
|
|
7629
|
+
__init191() {this.bakedHeight = 0}
|
|
7515
7630
|
/**
|
|
7516
7631
|
* Gradient strokes can't be baked to a solid-color bitmap; they render
|
|
7517
7632
|
* per-frame. Derived from `_doc`, so it is NOT readonly — assigning a new
|
|
@@ -7519,7 +7634,7 @@ var SplineEntity = (_class18 = class extends _chunkUEIZDNK7js.Entity {
|
|
|
7519
7634
|
*/
|
|
7520
7635
|
|
|
7521
7636
|
/** Lazily-flattened polylines (one Float32Array of [x,y,...] per segment) for hit-testing. */
|
|
7522
|
-
|
|
7637
|
+
__init192() {this.polylines = null}
|
|
7523
7638
|
/**
|
|
7524
7639
|
* The spline document to render. Assigning a new document invalidates all
|
|
7525
7640
|
* caches (baked canvas, flattened polylines, bounds).
|
|
@@ -7553,9 +7668,9 @@ var SplineEntity = (_class18 = class extends _chunkUEIZDNK7js.Entity {
|
|
|
7553
7668
|
* local bounds after painting the curves. Useful for drag feedback and
|
|
7554
7669
|
* debugging hit areas. Defaults to `false`.
|
|
7555
7670
|
*/
|
|
7556
|
-
|
|
7671
|
+
__init193() {this.showBounds = false}
|
|
7557
7672
|
constructor(doc, opts = {}) {
|
|
7558
|
-
super();_class18.prototype.
|
|
7673
|
+
super();_class18.prototype.__init188.call(this);_class18.prototype.__init189.call(this);_class18.prototype.__init190.call(this);_class18.prototype.__init191.call(this);_class18.prototype.__init192.call(this);_class18.prototype.__init193.call(this);;
|
|
7559
7674
|
this._doc = doc;
|
|
7560
7675
|
this._lineWidth = _nullishCoalesce(opts.lineWidth, () => ( 2));
|
|
7561
7676
|
this.cache = _nullishCoalesce(opts.cache, () => ( true));
|
|
@@ -7820,7 +7935,7 @@ async function loadSpline(url) {
|
|
|
7820
7935
|
}
|
|
7821
7936
|
|
|
7822
7937
|
// src/components/Rect.ts
|
|
7823
|
-
var Rect = class extends
|
|
7938
|
+
var Rect = class extends _chunkXXU56ZNAjs.Entity {
|
|
7824
7939
|
|
|
7825
7940
|
|
|
7826
7941
|
|
|
@@ -7876,7 +7991,7 @@ var Rect = class extends _chunkUEIZDNK7js.Entity {
|
|
|
7876
7991
|
};
|
|
7877
7992
|
|
|
7878
7993
|
// src/components/Circle.ts
|
|
7879
|
-
var Circle = class extends
|
|
7994
|
+
var Circle = class extends _chunkXXU56ZNAjs.Entity {
|
|
7880
7995
|
|
|
7881
7996
|
|
|
7882
7997
|
|
|
@@ -7940,7 +8055,7 @@ var Circle = class extends _chunkUEIZDNK7js.Entity {
|
|
|
7940
8055
|
};
|
|
7941
8056
|
|
|
7942
8057
|
// src/components/Group.ts
|
|
7943
|
-
var Group = class extends
|
|
8058
|
+
var Group = class extends _chunkXXU56ZNAjs.Entity {
|
|
7944
8059
|
constructor(...children) {
|
|
7945
8060
|
super();
|
|
7946
8061
|
if (children.length > 0) this.add(...children);
|
|
@@ -7959,21 +8074,21 @@ var _math = require('@vectojs/math'); _createStarExport(_math);
|
|
|
7959
8074
|
|
|
7960
8075
|
|
|
7961
8076
|
// src/tree/DOMPortalEntity.ts
|
|
7962
|
-
var DOMPortalEntity = (_class19 = class extends
|
|
8077
|
+
var DOMPortalEntity = (_class19 = class extends _chunkXXU56ZNAjs.Entity {
|
|
7963
8078
|
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
8079
|
+
__init194() {this.isDOMPortal = true}
|
|
8080
|
+
__init195() {this.domListeners = []}
|
|
8081
|
+
__init196() {this.resizeObserver = null}
|
|
8082
|
+
__init197() {this.domBound = false}
|
|
8083
|
+
__init198() {this.cachedWidth = 100}
|
|
8084
|
+
__init199() {this.cachedHeight = 100}
|
|
8085
|
+
__init200() {this.lastWidth = ""}
|
|
8086
|
+
__init201() {this.lastHeight = ""}
|
|
8087
|
+
__init202() {this.lastTransform = ""}
|
|
8088
|
+
__init203() {this.lastZIndex = ""}
|
|
8089
|
+
__init204() {this.lastOpacity = ""}
|
|
7975
8090
|
constructor(domElement, width, height, id) {
|
|
7976
|
-
super(id);_class19.prototype.
|
|
8091
|
+
super(id);_class19.prototype.__init194.call(this);_class19.prototype.__init195.call(this);_class19.prototype.__init196.call(this);_class19.prototype.__init197.call(this);_class19.prototype.__init198.call(this);_class19.prototype.__init199.call(this);_class19.prototype.__init200.call(this);_class19.prototype.__init201.call(this);_class19.prototype.__init202.call(this);_class19.prototype.__init203.call(this);_class19.prototype.__init204.call(this);;
|
|
7977
8092
|
this.domElement = domElement;
|
|
7978
8093
|
this.width = _nullishCoalesce(width, () => ( 0));
|
|
7979
8094
|
this.height = _nullishCoalesce(height, () => ( 0));
|
|
@@ -8015,7 +8130,7 @@ var DOMPortalEntity = (_class19 = class extends _chunkUEIZDNK7js.Entity {
|
|
|
8015
8130
|
];
|
|
8016
8131
|
for (const type of events) {
|
|
8017
8132
|
const handler = (e) => {
|
|
8018
|
-
this.dispatchEvent(new (0,
|
|
8133
|
+
this.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)(type, this, e));
|
|
8019
8134
|
};
|
|
8020
8135
|
this.domElement.addEventListener(type, handler);
|
|
8021
8136
|
this.domListeners.push({ type, handler, capture: false });
|
|
@@ -8026,7 +8141,7 @@ var DOMPortalEntity = (_class19 = class extends _chunkUEIZDNK7js.Entity {
|
|
|
8026
8141
|
];
|
|
8027
8142
|
for (const { native, vecto } of hoverEvents) {
|
|
8028
8143
|
const handler = (e) => {
|
|
8029
|
-
this.dispatchEvent(new (0,
|
|
8144
|
+
this.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)(vecto, this, e, false));
|
|
8030
8145
|
};
|
|
8031
8146
|
this.domElement.addEventListener(native, handler);
|
|
8032
8147
|
this.domListeners.push({ type: native, handler, capture: false });
|
|
@@ -8034,7 +8149,7 @@ var DOMPortalEntity = (_class19 = class extends _chunkUEIZDNK7js.Entity {
|
|
|
8034
8149
|
const focusEvents = ["focus", "blur"];
|
|
8035
8150
|
for (const type of focusEvents) {
|
|
8036
8151
|
const handler = (e) => {
|
|
8037
|
-
this.dispatchEvent(new (0,
|
|
8152
|
+
this.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)(type, this, e, true));
|
|
8038
8153
|
};
|
|
8039
8154
|
this.domElement.addEventListener(type, handler, true);
|
|
8040
8155
|
this.domListeners.push({ type, handler, capture: true });
|
|
@@ -8132,4 +8247,4 @@ Scene.registerWebGPUParticleSystemManager(_chunkM73XB4ZKjs.WebGPUParticleSystemM
|
|
|
8132
8247
|
|
|
8133
8248
|
|
|
8134
8249
|
|
|
8135
|
-
exports.CanvasRenderer = _chunkM73XB4ZKjs.CanvasRenderer; exports.Circle = Circle; exports.ComputeParticleEntity = ComputeParticleEntity; exports.DEFAULT_CONTENT_SEMANTIC_BUDGET = DEFAULT_CONTENT_SEMANTIC_BUDGET; exports.DOMPortalEntity = DOMPortalEntity; exports.Entity =
|
|
8250
|
+
exports.CanvasRenderer = _chunkM73XB4ZKjs.CanvasRenderer; exports.Circle = Circle; exports.ComputeParticleEntity = ComputeParticleEntity; exports.DEFAULT_CONTENT_SEMANTIC_BUDGET = DEFAULT_CONTENT_SEMANTIC_BUDGET; exports.DOMPortalEntity = DOMPortalEntity; exports.Entity = _chunkXXU56ZNAjs.Entity; exports.GlyphRasterAtlas = _chunkM73XB4ZKjs.GlyphRasterAtlas; exports.GridTextEntity = GridTextEntity; exports.Group = Group; exports.MSDFTextEntity = _chunkXXU56ZNAjs.MSDFTextEntity; exports.PARTICLE_OFFSET_LIFE = PARTICLE_OFFSET_LIFE; exports.PARTICLE_OFFSET_ORIGIN_X = PARTICLE_OFFSET_ORIGIN_X; exports.PARTICLE_OFFSET_ORIGIN_Y = PARTICLE_OFFSET_ORIGIN_Y; exports.PARTICLE_OFFSET_POSITION_X = PARTICLE_OFFSET_POSITION_X; exports.PARTICLE_OFFSET_POSITION_Y = PARTICLE_OFFSET_POSITION_Y; exports.PARTICLE_OFFSET_SIZE = PARTICLE_OFFSET_SIZE; exports.PARTICLE_OFFSET_VELOCITY_X = PARTICLE_OFFSET_VELOCITY_X; exports.PARTICLE_OFFSET_VELOCITY_Y = PARTICLE_OFFSET_VELOCITY_Y; exports.PARTICLE_STRIDE_FLOATS = PARTICLE_STRIDE_FLOATS; exports.REDUCED_MOTION_FPS = REDUCED_MOTION_FPS; exports.Rect = Rect; exports.SCENE_OPTION_KEYS = SCENE_OPTION_KEYS; exports.SVGEntity = _chunkXXU56ZNAjs.SVGEntity; exports.SVGRenderer = _chunkM73XB4ZKjs.SVGRenderer; exports.Scene = Scene; exports.SplineEntity = SplineEntity; exports.TextEntity = TextEntity; exports.TextRasterCache = _chunkM73XB4ZKjs.TextRasterCache; exports.VECTO_USER_TIMING = VECTO_USER_TIMING; exports.VectoJSEvent = _chunkXXU56ZNAjs.VectoJSEvent; exports.WebGPUParticleSystemManager = _chunkM73XB4ZKjs.WebGPUParticleSystemManager; exports.beginVectoUserTiming = beginVectoUserTiming; exports.contentLineInHint = _chunkXXU56ZNAjs.contentLineInHint; exports.createWebGLPointRenderer = _chunkM73XB4ZKjs.createWebGLPointRenderer; exports.endVectoUserTiming = endVectoUserTiming; exports.installRendererDevTraps = _chunkM73XB4ZKjs.installRendererDevTraps; exports.isRendererDevMode = _chunkM73XB4ZKjs.isRendererDevMode; exports.isSafeUrl = _chunkM73XB4ZKjs.isSafeUrl; exports.loadSpline = loadSpline; exports.measureVectoUserTiming = measureVectoUserTiming; exports.parseColorToRGBA = _chunkM73XB4ZKjs.parseColorToRGBA; exports.polySegmentToBezier = polySegmentToBezier; exports.sanitizeUrl = _chunkM73XB4ZKjs.sanitizeUrl; exports.setRendererDevMode = _chunkM73XB4ZKjs.setRendererDevMode;
|
package/dist/index.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
SVGEntity,
|
|
19
19
|
VectoJSEvent,
|
|
20
20
|
contentLineInHint
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-TV4ARAC5.mjs";
|
|
22
22
|
|
|
23
23
|
// src/tree/ComputeParticleEntity.ts
|
|
24
24
|
var PARTICLE_STRIDE_FLOATS = 8;
|
|
@@ -437,7 +437,11 @@ function measureVectoUserTiming(name, durationMs) {
|
|
|
437
437
|
}
|
|
438
438
|
|
|
439
439
|
// src/tree/Scene.ts
|
|
440
|
-
import {
|
|
440
|
+
import {
|
|
441
|
+
clearCssLineBoxMetrics,
|
|
442
|
+
cssLineBoxBaseline as cssLineBoxBaseline2,
|
|
443
|
+
getSharedMeasuringContext
|
|
444
|
+
} from "@vectojs/text";
|
|
441
445
|
|
|
442
446
|
// src/tree/scene/a11y-dom.ts
|
|
443
447
|
function isNativelyFocusable(element) {
|
|
@@ -6286,6 +6290,8 @@ var Scene = class _Scene {
|
|
|
6286
6290
|
const separator = line.separatorAfter ?? (index < lines.length - 1 ? "\n" : "");
|
|
6287
6291
|
if (line.runs && line.runs.length > 0) {
|
|
6288
6292
|
const positioned = line.runs.some((run) => run.x !== void 0);
|
|
6293
|
+
const flowRelative = positioned && line.runs.every((run) => run.x === void 0 || run.width !== void 0);
|
|
6294
|
+
let runningX = line.x;
|
|
6289
6295
|
for (let runIndex = 0; runIndex < line.runs.length; runIndex++) {
|
|
6290
6296
|
const run = line.runs[runIndex];
|
|
6291
6297
|
const runElement = document.createElement("span");
|
|
@@ -6293,8 +6299,16 @@ var Scene = class _Scene {
|
|
|
6293
6299
|
if (run.font) runElement.style.font = run.font;
|
|
6294
6300
|
runElement.style.lineHeight = `${lineHeight2}px`;
|
|
6295
6301
|
if (positioned && run.x !== void 0) {
|
|
6296
|
-
|
|
6297
|
-
|
|
6302
|
+
if (flowRelative) {
|
|
6303
|
+
runElement.style.position = "relative";
|
|
6304
|
+
runElement.style.display = "inline-block";
|
|
6305
|
+
runElement.style.left = `${run.x - runningX}px`;
|
|
6306
|
+
runElement.style.boxSizing = "border-box";
|
|
6307
|
+
runningX += run.width;
|
|
6308
|
+
} else {
|
|
6309
|
+
runElement.style.position = "absolute";
|
|
6310
|
+
runElement.style.left = `${run.x - line.x}px`;
|
|
6311
|
+
}
|
|
6298
6312
|
runElement.style.top = "0";
|
|
6299
6313
|
if (run.width !== void 0) runElement.style.width = `${run.width}px`;
|
|
6300
6314
|
runElement.style.whiteSpace = "pre";
|
|
@@ -6304,6 +6318,35 @@ var Scene = class _Scene {
|
|
|
6304
6318
|
}
|
|
6305
6319
|
lineElement.appendChild(runElement);
|
|
6306
6320
|
}
|
|
6321
|
+
} else if (line.perGraphemeCarriers && line.text.length > 0) {
|
|
6322
|
+
const mctx = getSharedMeasuringContext();
|
|
6323
|
+
if (mctx) {
|
|
6324
|
+
mctx.font = lineFont;
|
|
6325
|
+
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
6326
|
+
const segments = [...segmenter.segment(line.text)];
|
|
6327
|
+
let runningX = 0;
|
|
6328
|
+
for (let segIdx = 0; segIdx < segments.length; segIdx++) {
|
|
6329
|
+
const { segment, index: index2 } = segments[segIdx];
|
|
6330
|
+
const nextIndex = segIdx + 1 < segments.length ? segments[segIdx + 1].index : line.text.length;
|
|
6331
|
+
const canvasX = mctx.measureText(line.text.slice(0, index2)).width;
|
|
6332
|
+
const canvasWidth = mctx.measureText(line.text.slice(0, nextIndex)).width - canvasX;
|
|
6333
|
+
const carrierEl = document.createElement("span");
|
|
6334
|
+
carrierEl.textContent = segment;
|
|
6335
|
+
carrierEl.style.position = "relative";
|
|
6336
|
+
carrierEl.style.display = "inline-block";
|
|
6337
|
+
carrierEl.style.left = `${canvasX - runningX}px`;
|
|
6338
|
+
carrierEl.style.width = `${canvasWidth}px`;
|
|
6339
|
+
carrierEl.style.boxSizing = "border-box";
|
|
6340
|
+
carrierEl.style.lineHeight = `${lineHeight2}px`;
|
|
6341
|
+
carrierEl.style.whiteSpace = "pre";
|
|
6342
|
+
carrierEl.style.verticalAlign = "top";
|
|
6343
|
+
carrierEl.style.unicodeBidi = "isolate";
|
|
6344
|
+
lineElement.appendChild(carrierEl);
|
|
6345
|
+
runningX += canvasWidth;
|
|
6346
|
+
}
|
|
6347
|
+
} else {
|
|
6348
|
+
lineElement.textContent = line.text;
|
|
6349
|
+
}
|
|
6307
6350
|
} else {
|
|
6308
6351
|
lineElement.textContent = line.text;
|
|
6309
6352
|
}
|
|
@@ -7286,6 +7329,18 @@ var TextEntity = class extends Entity {
|
|
|
7286
7329
|
isHovered = false;
|
|
7287
7330
|
/** Bumped by {@link applyLayout}; read by `Scene` to skip an unchanged sync. */
|
|
7288
7331
|
contentEpoch = 0;
|
|
7332
|
+
/**
|
|
7333
|
+
* Visual lines with real canvas geometry, rebuilt by {@link applyLayout}
|
|
7334
|
+
* exactly when `this.nodes` changes.
|
|
7335
|
+
*
|
|
7336
|
+
* Without this the Scene placed the DOM copy at the entity origin and let the
|
|
7337
|
+
* browser flow it at CSS `normal` line-height, while the canvas lays lines out
|
|
7338
|
+
* at `1.5em` pitch with the baseline at `0.8em` — so every line after the
|
|
7339
|
+
* first drifted further from the painted glyphs (measured ~6 px on line 0 and
|
|
7340
|
+
* ~0.35 em per line for a 24 px font, Firefox). Per-line carriers pin the DOM
|
|
7341
|
+
* baseline to the canvas baseline by construction, same as `ui/Text`.
|
|
7342
|
+
*/
|
|
7343
|
+
projectionLines = [];
|
|
7289
7344
|
constructor(text, atlas, maxWidth, fontSize = 24) {
|
|
7290
7345
|
super();
|
|
7291
7346
|
this.text = text;
|
|
@@ -7301,10 +7356,21 @@ var TextEntity = class extends Entity {
|
|
|
7301
7356
|
/**
|
|
7302
7357
|
* Mirror the rendered text into the DOM content layer: find-in-page, screen
|
|
7303
7358
|
* readers, crawlers, and translation see the same string the canvas draws.
|
|
7359
|
+
*
|
|
7360
|
+
* Each visual line is emitted with its own y/baseline/line-height so the DOM
|
|
7361
|
+
* line boxes sit exactly on the drawn glyphs instead of flowing at the
|
|
7362
|
+
* browser's `normal` metrics. Line text is the LOGICAL source slice (not the
|
|
7363
|
+
* per-glyph visual text), so shaped or reordered content still copies and
|
|
7364
|
+
* finds in source order — the same contract `ui/Text` ships.
|
|
7304
7365
|
*/
|
|
7305
7366
|
getContentProjection() {
|
|
7306
7367
|
if (!this.text) return null;
|
|
7307
|
-
return {
|
|
7368
|
+
return {
|
|
7369
|
+
text: this.text,
|
|
7370
|
+
font: `${this.fontSize}px sans-serif`,
|
|
7371
|
+
lineHeight: this.fontSize * 1.5,
|
|
7372
|
+
lines: this.projectionLines
|
|
7373
|
+
};
|
|
7308
7374
|
}
|
|
7309
7375
|
getContentEpoch() {
|
|
7310
7376
|
return this.contentEpoch;
|
|
@@ -7361,6 +7427,55 @@ var TextEntity = class extends Entity {
|
|
|
7361
7427
|
this.width = result.totalWidth;
|
|
7362
7428
|
this.height = result.totalHeight;
|
|
7363
7429
|
this.a11yOffsetY = 0;
|
|
7430
|
+
const lineQuantum = this.fontSize * 1.5;
|
|
7431
|
+
const nodesByLine = /* @__PURE__ */ new Map();
|
|
7432
|
+
let maxIdx = -1;
|
|
7433
|
+
for (const node of this.nodes) {
|
|
7434
|
+
const idx = Math.round(node.y / lineQuantum);
|
|
7435
|
+
const list = nodesByLine.get(idx) ?? [];
|
|
7436
|
+
list.push(node);
|
|
7437
|
+
nodesByLine.set(idx, list);
|
|
7438
|
+
if (idx > maxIdx) maxIdx = idx;
|
|
7439
|
+
}
|
|
7440
|
+
const justify = this.layout.textAlign === "justify";
|
|
7441
|
+
const starts = [];
|
|
7442
|
+
const ends = [];
|
|
7443
|
+
let previousEnd = 0;
|
|
7444
|
+
for (let i = 0; i <= maxIdx; i++) {
|
|
7445
|
+
const nodes = nodesByLine.get(i) ?? [];
|
|
7446
|
+
let start = nodes.length > 0 ? Math.min(...nodes.map((node) => node.sourceIndex ?? previousEnd)) : previousEnd;
|
|
7447
|
+
const end = nodes.length > 0 ? Math.max(
|
|
7448
|
+
...nodes.map((node) => (node.sourceIndex ?? previousEnd) + (node.sourceLength ?? 0))
|
|
7449
|
+
) : start;
|
|
7450
|
+
if (i === 0) start = 0;
|
|
7451
|
+
starts.push(start);
|
|
7452
|
+
ends.push(end);
|
|
7453
|
+
previousEnd = Math.max(previousEnd, end);
|
|
7454
|
+
}
|
|
7455
|
+
const lines = [];
|
|
7456
|
+
for (let i = 0; i <= maxIdx; i++) {
|
|
7457
|
+
const nodes = nodesByLine.get(i) ?? [];
|
|
7458
|
+
const start = starts[i];
|
|
7459
|
+
const end = ends[i];
|
|
7460
|
+
const nextStart = i + 1 <= maxIdx ? starts[i + 1] : this.text.length;
|
|
7461
|
+
const hasRtl = nodes.some((node) => node.isRTL === true);
|
|
7462
|
+
lines.push({
|
|
7463
|
+
text: this.text.slice(start, Math.max(start, end)),
|
|
7464
|
+
separatorAfter: this.text.slice(end, Math.max(end, nextStart)),
|
|
7465
|
+
x: 0,
|
|
7466
|
+
y: i * lineQuantum,
|
|
7467
|
+
baseline: this.fontSize * 0.8,
|
|
7468
|
+
font: `${this.fontSize}px sans-serif`,
|
|
7469
|
+
lineHeight: lineQuantum,
|
|
7470
|
+
// Per-grapheme carriers pin each cluster to its canvas-measured x so
|
|
7471
|
+
// Gecko's grid-fit advance rounding cannot drift the find-in-page
|
|
7472
|
+
// highlight off the drawn glyphs. Bidi lines must keep one text node
|
|
7473
|
+
// (DOM order is logical; per-glyph carriers break caret mapping), and
|
|
7474
|
+
// justify moves glyphs off their natural x, so both fall back to flow.
|
|
7475
|
+
...hasRtl || justify ? {} : { perGraphemeCarriers: true }
|
|
7476
|
+
});
|
|
7477
|
+
}
|
|
7478
|
+
this.projectionLines = lines;
|
|
7364
7479
|
}
|
|
7365
7480
|
isPointInside(globalX, globalY) {
|
|
7366
7481
|
const local = this.worldToLocal(globalX, globalY);
|
|
@@ -41,6 +41,12 @@ export declare class MSDFTextEntity extends Entity {
|
|
|
41
41
|
private rgbColorCache;
|
|
42
42
|
private fontStringCache;
|
|
43
43
|
private layoutResult;
|
|
44
|
+
/**
|
|
45
|
+
* Visual rows rebuilt from {@link layoutResult} (see
|
|
46
|
+
* {@link rebuildProjectionLines}). Empty until a layout reply lands and the
|
|
47
|
+
* reply's shaped glyphs can be mapped back to the source text 1:1.
|
|
48
|
+
*/
|
|
49
|
+
private projectionLines;
|
|
44
50
|
constructor(text: string, options: MSDFTextEntityOptions);
|
|
45
51
|
/**
|
|
46
52
|
* Repaint once the atlas raster decodes.
|
|
@@ -91,8 +97,26 @@ export declare class MSDFTextEntity extends Entity {
|
|
|
91
97
|
/**
|
|
92
98
|
* Mirror the rendered text into the DOM content layer: find-in-page, screen
|
|
93
99
|
* readers, crawlers, and translation see the same string the canvas draws.
|
|
100
|
+
*
|
|
101
|
+
* `baseline` + `lineHeight` are always emitted (they come from the font
|
|
102
|
+
* metrics, no layout reply needed), so the DOM line boxes at least land on
|
|
103
|
+
* the canvas rhythm: the first baseline at `ascender × fontSize` and every
|
|
104
|
+
* row advancing `(ascender − descender) × fontSize`. Once a layout reply is
|
|
105
|
+
* in AND its shaped glyphs map back to the source 1:1 (unshaped LTR text —
|
|
106
|
+
* bidi, shaping, soft hyphens or `\r` all fall back to the coarse branch),
|
|
107
|
+
* per-line carriers pin each row's baseline exactly to the painted glyphs.
|
|
94
108
|
*/
|
|
95
109
|
getContentProjection(): ContentProjection | null;
|
|
110
|
+
/**
|
|
111
|
+
* Group the worker's positioned glyphs into the same visual rows the canvas
|
|
112
|
+
* draws. Only runs when the reply's glyph sequence equals the source string
|
|
113
|
+
* (one glyph per source char, no bidi reordering, no shaping, no soft
|
|
114
|
+
* hyphens, no `\r`) — only then do glyph offsets line up with source offsets
|
|
115
|
+
* byte-for-byte, which is what keeps find-in-page and the Scene's dev-mode
|
|
116
|
+
* equality check correct. Every other text falls back to the coarse branch's
|
|
117
|
+
* `baseline` + `lineHeight`, which still pins the row rhythm.
|
|
118
|
+
*/
|
|
119
|
+
private rebuildProjectionLines;
|
|
96
120
|
getContentEpoch(): number;
|
|
97
121
|
isPointInside(globalX: number, globalY: number): boolean;
|
|
98
122
|
render(renderer: any): void;
|
package/dist/text.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
var
|
|
5
|
+
var _chunkXXU56ZNAjs = require('./chunk-XXU56ZNA.js');
|
|
6
6
|
|
|
7
7
|
// src/text/index.ts
|
|
8
8
|
var _text = require('@vectojs/text'); _createStarExport(_text);
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
exports.MSDFTextEntity =
|
|
12
|
+
exports.MSDFTextEntity = _chunkXXU56ZNAjs.MSDFTextEntity; exports.SVGEntity = _chunkXXU56ZNAjs.SVGEntity;
|
package/dist/text.mjs
CHANGED
package/dist/tree/Entity.d.ts
CHANGED
|
@@ -102,6 +102,20 @@ export interface ContentProjectionLine {
|
|
|
102
102
|
lineHeight?: number;
|
|
103
103
|
/** Styled text runs in visual order. */
|
|
104
104
|
runs?: ContentProjectionRun[];
|
|
105
|
+
/**
|
|
106
|
+
* Emit one flow-relative carrier per grapheme cluster on this line instead
|
|
107
|
+
* of a single text node. Used by natural-order (non-bidi, non-justified) text
|
|
108
|
+
* to correct the residual ~0.3% per-character Gecko grid-fit drift that causes
|
|
109
|
+
* selection highlight boxes to lag or lead painted glyphs.
|
|
110
|
+
*
|
|
111
|
+
* Only meaningful when `runs` is absent or empty. Setting it on a line that
|
|
112
|
+
* already has positioned runs is a no-op — those lines use their own
|
|
113
|
+
* flow-relative carriers already.
|
|
114
|
+
*
|
|
115
|
+
* Must NOT be set for bidi/RTL lines: per-glyph carriers break logical caret
|
|
116
|
+
* hit-mapping when DOM order != visual order (PR #146 revert).
|
|
117
|
+
*/
|
|
118
|
+
perGraphemeCarriers?: boolean;
|
|
105
119
|
}
|
|
106
120
|
/**
|
|
107
121
|
* Advice from the {@link Scene} about which part of an entity is worth
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vectojs/core",
|
|
3
|
-
"version": "1.34.
|
|
3
|
+
"version": "1.34.2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -65,9 +65,9 @@
|
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"@vectojs/animation": "^0.1.1",
|
|
68
|
-
"@vectojs/layout": "^0.9.
|
|
68
|
+
"@vectojs/layout": "^0.9.1",
|
|
69
69
|
"@vectojs/math": "^0.1.1",
|
|
70
|
-
"@vectojs/text": "^0.
|
|
70
|
+
"@vectojs/text": "^0.4.0"
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@guidepup/virtual-screen-reader": "0.32.1",
|