@vectojs/core 1.34.1 → 1.35.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-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 +181 -60
- package/dist/index.mjs +127 -6
- 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 +26 -0
- package/package.json +1 -1
|
@@ -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
|
}
|
|
@@ -6208,7 +6212,7 @@ var Scene = (_class15 = class _Scene {
|
|
|
6208
6212
|
return;
|
|
6209
6213
|
}
|
|
6210
6214
|
const projection = node.getContentProjection(
|
|
6211
|
-
lineBand ? { minY: lineBand.minY, maxY: lineBand.maxY } : void 0
|
|
6215
|
+
lineBand ? { minY: lineBand.minY, maxY: lineBand.maxY, textOnly: tier === "coarse" } : tier === "coarse" ? { textOnly: true } : void 0
|
|
6212
6216
|
);
|
|
6213
6217
|
if (!projection || !projection.text) {
|
|
6214
6218
|
releaseProjectionEl();
|
|
@@ -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,17 +6300,60 @@ 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";
|
|
6302
6316
|
runElement.style.verticalAlign = "top";
|
|
6303
6317
|
runElement.style.unicodeBidi = "isolate";
|
|
6304
6318
|
runElement.dir = "ltr";
|
|
6319
|
+
} else if (run.width !== void 0) {
|
|
6320
|
+
runElement.style.display = "inline-block";
|
|
6321
|
+
runElement.style.width = `${run.width}px`;
|
|
6322
|
+
runElement.style.boxSizing = "border-box";
|
|
6323
|
+
runElement.style.whiteSpace = "pre";
|
|
6324
|
+
runElement.style.verticalAlign = "top";
|
|
6305
6325
|
}
|
|
6306
6326
|
lineElement.appendChild(runElement);
|
|
6307
6327
|
}
|
|
6328
|
+
} else if (line.perGraphemeCarriers && line.text.length > 0) {
|
|
6329
|
+
const mctx = _text.getSharedMeasuringContext.call(void 0, );
|
|
6330
|
+
if (mctx) {
|
|
6331
|
+
mctx.font = lineFont;
|
|
6332
|
+
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
6333
|
+
const segments = [...segmenter.segment(line.text)];
|
|
6334
|
+
let runningX = 0;
|
|
6335
|
+
for (let segIdx = 0; segIdx < segments.length; segIdx++) {
|
|
6336
|
+
const { segment, index: index2 } = segments[segIdx];
|
|
6337
|
+
const nextIndex = segIdx + 1 < segments.length ? segments[segIdx + 1].index : line.text.length;
|
|
6338
|
+
const canvasX = mctx.measureText(line.text.slice(0, index2)).width;
|
|
6339
|
+
const canvasWidth = mctx.measureText(line.text.slice(0, nextIndex)).width - canvasX;
|
|
6340
|
+
const carrierEl = document.createElement("span");
|
|
6341
|
+
carrierEl.textContent = segment;
|
|
6342
|
+
carrierEl.style.position = "relative";
|
|
6343
|
+
carrierEl.style.display = "inline-block";
|
|
6344
|
+
carrierEl.style.left = `${canvasX - runningX}px`;
|
|
6345
|
+
carrierEl.style.width = `${canvasWidth}px`;
|
|
6346
|
+
carrierEl.style.boxSizing = "border-box";
|
|
6347
|
+
carrierEl.style.lineHeight = `${lineHeight2}px`;
|
|
6348
|
+
carrierEl.style.whiteSpace = "pre";
|
|
6349
|
+
carrierEl.style.verticalAlign = "top";
|
|
6350
|
+
carrierEl.style.unicodeBidi = "isolate";
|
|
6351
|
+
lineElement.appendChild(carrierEl);
|
|
6352
|
+
runningX += canvasWidth;
|
|
6353
|
+
}
|
|
6354
|
+
} else {
|
|
6355
|
+
lineElement.textContent = line.text;
|
|
6356
|
+
}
|
|
6308
6357
|
} else {
|
|
6309
6358
|
lineElement.textContent = line.text;
|
|
6310
6359
|
}
|
|
@@ -6838,7 +6887,7 @@ var Scene = (_class15 = class _Scene {
|
|
|
6838
6887
|
let walkHadAnimation = false;
|
|
6839
6888
|
let walkHadInteractive = false;
|
|
6840
6889
|
const runUpdate = (node) => {
|
|
6841
|
-
const overridesUpdate = node.update !==
|
|
6890
|
+
const overridesUpdate = node.update !== _chunkXXU56ZNAjs.Entity.prototype.update;
|
|
6842
6891
|
let pending = node.hasPendingAnimations();
|
|
6843
6892
|
if (pending || overridesUpdate) {
|
|
6844
6893
|
node.update(dt, time);
|
|
@@ -6847,7 +6896,7 @@ var Scene = (_class15 = class _Scene {
|
|
|
6847
6896
|
if (pending) walkHadAnimation = true;
|
|
6848
6897
|
if (!walkHadInteractive && node.interactive) walkHadInteractive = true;
|
|
6849
6898
|
if (this._devActive && this._devFrameCount % 120 === 0) {
|
|
6850
|
-
if (overridesUpdate && node.hasPendingAnimations ===
|
|
6899
|
+
if (overridesUpdate && node.hasPendingAnimations === _chunkXXU56ZNAjs.Entity.prototype.hasPendingAnimations) {
|
|
6851
6900
|
this._devWarn(
|
|
6852
6901
|
`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
6902
|
);
|
|
@@ -7273,7 +7322,7 @@ function defaultMeasurer() {
|
|
|
7273
7322
|
sharedMeasurer ??= _layout.resolveGlyphMeasurer.call(void 0, "sans-serif");
|
|
7274
7323
|
return sharedMeasurer;
|
|
7275
7324
|
}
|
|
7276
|
-
var TextEntity = (_class16 = class extends
|
|
7325
|
+
var TextEntity = (_class16 = class extends _chunkXXU56ZNAjs.Entity {
|
|
7277
7326
|
|
|
7278
7327
|
|
|
7279
7328
|
|
|
@@ -7287,8 +7336,20 @@ var TextEntity = (_class16 = class extends _chunkUEIZDNK7js.Entity {
|
|
|
7287
7336
|
__init181() {this.isHovered = false}
|
|
7288
7337
|
/** Bumped by {@link applyLayout}; read by `Scene` to skip an unchanged sync. */
|
|
7289
7338
|
__init182() {this.contentEpoch = 0}
|
|
7339
|
+
/**
|
|
7340
|
+
* Visual lines with real canvas geometry, rebuilt by {@link applyLayout}
|
|
7341
|
+
* exactly when `this.nodes` changes.
|
|
7342
|
+
*
|
|
7343
|
+
* Without this the Scene placed the DOM copy at the entity origin and let the
|
|
7344
|
+
* browser flow it at CSS `normal` line-height, while the canvas lays lines out
|
|
7345
|
+
* at `1.5em` pitch with the baseline at `0.8em` — so every line after the
|
|
7346
|
+
* first drifted further from the painted glyphs (measured ~6 px on line 0 and
|
|
7347
|
+
* ~0.35 em per line for a 24 px font, Firefox). Per-line carriers pin the DOM
|
|
7348
|
+
* baseline to the canvas baseline by construction, same as `ui/Text`.
|
|
7349
|
+
*/
|
|
7350
|
+
__init183() {this.projectionLines = []}
|
|
7290
7351
|
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);;
|
|
7352
|
+
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
7353
|
this.text = text;
|
|
7293
7354
|
this.atlas = atlas;
|
|
7294
7355
|
this.fontSize = fontSize;
|
|
@@ -7302,10 +7363,21 @@ var TextEntity = (_class16 = class extends _chunkUEIZDNK7js.Entity {
|
|
|
7302
7363
|
/**
|
|
7303
7364
|
* Mirror the rendered text into the DOM content layer: find-in-page, screen
|
|
7304
7365
|
* readers, crawlers, and translation see the same string the canvas draws.
|
|
7366
|
+
*
|
|
7367
|
+
* Each visual line is emitted with its own y/baseline/line-height so the DOM
|
|
7368
|
+
* line boxes sit exactly on the drawn glyphs instead of flowing at the
|
|
7369
|
+
* browser's `normal` metrics. Line text is the LOGICAL source slice (not the
|
|
7370
|
+
* per-glyph visual text), so shaped or reordered content still copies and
|
|
7371
|
+
* finds in source order — the same contract `ui/Text` ships.
|
|
7305
7372
|
*/
|
|
7306
7373
|
getContentProjection() {
|
|
7307
7374
|
if (!this.text) return null;
|
|
7308
|
-
return {
|
|
7375
|
+
return {
|
|
7376
|
+
text: this.text,
|
|
7377
|
+
font: `${this.fontSize}px sans-serif`,
|
|
7378
|
+
lineHeight: this.fontSize * 1.5,
|
|
7379
|
+
lines: this.projectionLines
|
|
7380
|
+
};
|
|
7309
7381
|
}
|
|
7310
7382
|
getContentEpoch() {
|
|
7311
7383
|
return this.contentEpoch;
|
|
@@ -7362,6 +7434,55 @@ var TextEntity = (_class16 = class extends _chunkUEIZDNK7js.Entity {
|
|
|
7362
7434
|
this.width = result.totalWidth;
|
|
7363
7435
|
this.height = result.totalHeight;
|
|
7364
7436
|
this.a11yOffsetY = 0;
|
|
7437
|
+
const lineQuantum = this.fontSize * 1.5;
|
|
7438
|
+
const nodesByLine = /* @__PURE__ */ new Map();
|
|
7439
|
+
let maxIdx = -1;
|
|
7440
|
+
for (const node of this.nodes) {
|
|
7441
|
+
const idx = Math.round(node.y / lineQuantum);
|
|
7442
|
+
const list = _nullishCoalesce(nodesByLine.get(idx), () => ( []));
|
|
7443
|
+
list.push(node);
|
|
7444
|
+
nodesByLine.set(idx, list);
|
|
7445
|
+
if (idx > maxIdx) maxIdx = idx;
|
|
7446
|
+
}
|
|
7447
|
+
const justify = this.layout.textAlign === "justify";
|
|
7448
|
+
const starts = [];
|
|
7449
|
+
const ends = [];
|
|
7450
|
+
let previousEnd = 0;
|
|
7451
|
+
for (let i = 0; i <= maxIdx; i++) {
|
|
7452
|
+
const nodes = _nullishCoalesce(nodesByLine.get(i), () => ( []));
|
|
7453
|
+
let start = nodes.length > 0 ? Math.min(...nodes.map((node) => _nullishCoalesce(node.sourceIndex, () => ( previousEnd)))) : previousEnd;
|
|
7454
|
+
const end = nodes.length > 0 ? Math.max(
|
|
7455
|
+
...nodes.map((node) => (_nullishCoalesce(node.sourceIndex, () => ( previousEnd))) + (_nullishCoalesce(node.sourceLength, () => ( 0))))
|
|
7456
|
+
) : start;
|
|
7457
|
+
if (i === 0) start = 0;
|
|
7458
|
+
starts.push(start);
|
|
7459
|
+
ends.push(end);
|
|
7460
|
+
previousEnd = Math.max(previousEnd, end);
|
|
7461
|
+
}
|
|
7462
|
+
const lines = [];
|
|
7463
|
+
for (let i = 0; i <= maxIdx; i++) {
|
|
7464
|
+
const nodes = _nullishCoalesce(nodesByLine.get(i), () => ( []));
|
|
7465
|
+
const start = starts[i];
|
|
7466
|
+
const end = ends[i];
|
|
7467
|
+
const nextStart = i + 1 <= maxIdx ? starts[i + 1] : this.text.length;
|
|
7468
|
+
const hasRtl = nodes.some((node) => node.isRTL === true);
|
|
7469
|
+
lines.push({
|
|
7470
|
+
text: this.text.slice(start, Math.max(start, end)),
|
|
7471
|
+
separatorAfter: this.text.slice(end, Math.max(end, nextStart)),
|
|
7472
|
+
x: 0,
|
|
7473
|
+
y: i * lineQuantum,
|
|
7474
|
+
baseline: this.fontSize * 0.8,
|
|
7475
|
+
font: `${this.fontSize}px sans-serif`,
|
|
7476
|
+
lineHeight: lineQuantum,
|
|
7477
|
+
// Per-grapheme carriers pin each cluster to its canvas-measured x so
|
|
7478
|
+
// Gecko's grid-fit advance rounding cannot drift the find-in-page
|
|
7479
|
+
// highlight off the drawn glyphs. Bidi lines must keep one text node
|
|
7480
|
+
// (DOM order is logical; per-glyph carriers break caret mapping), and
|
|
7481
|
+
// justify moves glyphs off their natural x, so both fall back to flow.
|
|
7482
|
+
...hasRtl || justify ? {} : { perGraphemeCarriers: true }
|
|
7483
|
+
});
|
|
7484
|
+
}
|
|
7485
|
+
this.projectionLines = lines;
|
|
7365
7486
|
}
|
|
7366
7487
|
isPointInside(globalX, globalY) {
|
|
7367
7488
|
const local = this.worldToLocal(globalX, globalY);
|
|
@@ -7405,17 +7526,17 @@ var TextEntity = (_class16 = class extends _chunkUEIZDNK7js.Entity {
|
|
|
7405
7526
|
}, _class16);
|
|
7406
7527
|
|
|
7407
7528
|
// src/components/GridTextEntity.ts
|
|
7408
|
-
var GridTextEntity = (_class17 = class extends
|
|
7529
|
+
var GridTextEntity = (_class17 = class extends _chunkXXU56ZNAjs.Entity {
|
|
7409
7530
|
|
|
7410
|
-
|
|
7411
|
-
|
|
7531
|
+
__init184() {this.fillStyle = "#ffffff"}
|
|
7532
|
+
__init185() {this.grid = []}
|
|
7412
7533
|
// Array of rows
|
|
7413
|
-
|
|
7414
|
-
|
|
7534
|
+
__init186() {this.cols = 0}
|
|
7535
|
+
__init187() {this.rows = 0}
|
|
7415
7536
|
|
|
7416
7537
|
|
|
7417
7538
|
constructor(_atlas, fontSize = 10) {
|
|
7418
|
-
super();_class17.prototype.
|
|
7539
|
+
super();_class17.prototype.__init184.call(this);_class17.prototype.__init185.call(this);_class17.prototype.__init186.call(this);_class17.prototype.__init187.call(this);;
|
|
7419
7540
|
this.fontSize = fontSize;
|
|
7420
7541
|
this.charWidth = fontSize * 1;
|
|
7421
7542
|
this.charHeight = fontSize * 1.1;
|
|
@@ -7499,7 +7620,7 @@ function distSqToSegment(px, py, x1, y1, x2, y2) {
|
|
|
7499
7620
|
const ey = py - cy;
|
|
7500
7621
|
return ex * ex + ey * ey;
|
|
7501
7622
|
}
|
|
7502
|
-
var SplineEntity = (_class18 = class extends
|
|
7623
|
+
var SplineEntity = (_class18 = class extends _chunkXXU56ZNAjs.Entity {
|
|
7503
7624
|
|
|
7504
7625
|
|
|
7505
7626
|
|
|
@@ -7507,11 +7628,11 @@ var SplineEntity = (_class18 = class extends _chunkUEIZDNK7js.Entity {
|
|
|
7507
7628
|
|
|
7508
7629
|
|
|
7509
7630
|
|
|
7510
|
-
|
|
7511
|
-
|
|
7631
|
+
__init188() {this.offscreen = null}
|
|
7632
|
+
__init189() {this.baked = false}
|
|
7512
7633
|
/** Logical (CSS-pixel) size of the baked bitmap — the blit destination size. */
|
|
7513
|
-
|
|
7514
|
-
|
|
7634
|
+
__init190() {this.bakedWidth = 0}
|
|
7635
|
+
__init191() {this.bakedHeight = 0}
|
|
7515
7636
|
/**
|
|
7516
7637
|
* Gradient strokes can't be baked to a solid-color bitmap; they render
|
|
7517
7638
|
* per-frame. Derived from `_doc`, so it is NOT readonly — assigning a new
|
|
@@ -7519,7 +7640,7 @@ var SplineEntity = (_class18 = class extends _chunkUEIZDNK7js.Entity {
|
|
|
7519
7640
|
*/
|
|
7520
7641
|
|
|
7521
7642
|
/** Lazily-flattened polylines (one Float32Array of [x,y,...] per segment) for hit-testing. */
|
|
7522
|
-
|
|
7643
|
+
__init192() {this.polylines = null}
|
|
7523
7644
|
/**
|
|
7524
7645
|
* The spline document to render. Assigning a new document invalidates all
|
|
7525
7646
|
* caches (baked canvas, flattened polylines, bounds).
|
|
@@ -7553,9 +7674,9 @@ var SplineEntity = (_class18 = class extends _chunkUEIZDNK7js.Entity {
|
|
|
7553
7674
|
* local bounds after painting the curves. Useful for drag feedback and
|
|
7554
7675
|
* debugging hit areas. Defaults to `false`.
|
|
7555
7676
|
*/
|
|
7556
|
-
|
|
7677
|
+
__init193() {this.showBounds = false}
|
|
7557
7678
|
constructor(doc, opts = {}) {
|
|
7558
|
-
super();_class18.prototype.
|
|
7679
|
+
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
7680
|
this._doc = doc;
|
|
7560
7681
|
this._lineWidth = _nullishCoalesce(opts.lineWidth, () => ( 2));
|
|
7561
7682
|
this.cache = _nullishCoalesce(opts.cache, () => ( true));
|
|
@@ -7820,7 +7941,7 @@ async function loadSpline(url) {
|
|
|
7820
7941
|
}
|
|
7821
7942
|
|
|
7822
7943
|
// src/components/Rect.ts
|
|
7823
|
-
var Rect = class extends
|
|
7944
|
+
var Rect = class extends _chunkXXU56ZNAjs.Entity {
|
|
7824
7945
|
|
|
7825
7946
|
|
|
7826
7947
|
|
|
@@ -7876,7 +7997,7 @@ var Rect = class extends _chunkUEIZDNK7js.Entity {
|
|
|
7876
7997
|
};
|
|
7877
7998
|
|
|
7878
7999
|
// src/components/Circle.ts
|
|
7879
|
-
var Circle = class extends
|
|
8000
|
+
var Circle = class extends _chunkXXU56ZNAjs.Entity {
|
|
7880
8001
|
|
|
7881
8002
|
|
|
7882
8003
|
|
|
@@ -7940,7 +8061,7 @@ var Circle = class extends _chunkUEIZDNK7js.Entity {
|
|
|
7940
8061
|
};
|
|
7941
8062
|
|
|
7942
8063
|
// src/components/Group.ts
|
|
7943
|
-
var Group = class extends
|
|
8064
|
+
var Group = class extends _chunkXXU56ZNAjs.Entity {
|
|
7944
8065
|
constructor(...children) {
|
|
7945
8066
|
super();
|
|
7946
8067
|
if (children.length > 0) this.add(...children);
|
|
@@ -7959,21 +8080,21 @@ var _math = require('@vectojs/math'); _createStarExport(_math);
|
|
|
7959
8080
|
|
|
7960
8081
|
|
|
7961
8082
|
// src/tree/DOMPortalEntity.ts
|
|
7962
|
-
var DOMPortalEntity = (_class19 = class extends
|
|
8083
|
+
var DOMPortalEntity = (_class19 = class extends _chunkXXU56ZNAjs.Entity {
|
|
7963
8084
|
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
8085
|
+
__init194() {this.isDOMPortal = true}
|
|
8086
|
+
__init195() {this.domListeners = []}
|
|
8087
|
+
__init196() {this.resizeObserver = null}
|
|
8088
|
+
__init197() {this.domBound = false}
|
|
8089
|
+
__init198() {this.cachedWidth = 100}
|
|
8090
|
+
__init199() {this.cachedHeight = 100}
|
|
8091
|
+
__init200() {this.lastWidth = ""}
|
|
8092
|
+
__init201() {this.lastHeight = ""}
|
|
8093
|
+
__init202() {this.lastTransform = ""}
|
|
8094
|
+
__init203() {this.lastZIndex = ""}
|
|
8095
|
+
__init204() {this.lastOpacity = ""}
|
|
7975
8096
|
constructor(domElement, width, height, id) {
|
|
7976
|
-
super(id);_class19.prototype.
|
|
8097
|
+
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
8098
|
this.domElement = domElement;
|
|
7978
8099
|
this.width = _nullishCoalesce(width, () => ( 0));
|
|
7979
8100
|
this.height = _nullishCoalesce(height, () => ( 0));
|
|
@@ -8015,7 +8136,7 @@ var DOMPortalEntity = (_class19 = class extends _chunkUEIZDNK7js.Entity {
|
|
|
8015
8136
|
];
|
|
8016
8137
|
for (const type of events) {
|
|
8017
8138
|
const handler = (e) => {
|
|
8018
|
-
this.dispatchEvent(new (0,
|
|
8139
|
+
this.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)(type, this, e));
|
|
8019
8140
|
};
|
|
8020
8141
|
this.domElement.addEventListener(type, handler);
|
|
8021
8142
|
this.domListeners.push({ type, handler, capture: false });
|
|
@@ -8026,7 +8147,7 @@ var DOMPortalEntity = (_class19 = class extends _chunkUEIZDNK7js.Entity {
|
|
|
8026
8147
|
];
|
|
8027
8148
|
for (const { native, vecto } of hoverEvents) {
|
|
8028
8149
|
const handler = (e) => {
|
|
8029
|
-
this.dispatchEvent(new (0,
|
|
8150
|
+
this.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)(vecto, this, e, false));
|
|
8030
8151
|
};
|
|
8031
8152
|
this.domElement.addEventListener(native, handler);
|
|
8032
8153
|
this.domListeners.push({ type: native, handler, capture: false });
|
|
@@ -8034,7 +8155,7 @@ var DOMPortalEntity = (_class19 = class extends _chunkUEIZDNK7js.Entity {
|
|
|
8034
8155
|
const focusEvents = ["focus", "blur"];
|
|
8035
8156
|
for (const type of focusEvents) {
|
|
8036
8157
|
const handler = (e) => {
|
|
8037
|
-
this.dispatchEvent(new (0,
|
|
8158
|
+
this.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)(type, this, e, true));
|
|
8038
8159
|
};
|
|
8039
8160
|
this.domElement.addEventListener(type, handler, true);
|
|
8040
8161
|
this.domListeners.push({ type, handler, capture: true });
|
|
@@ -8132,4 +8253,4 @@ Scene.registerWebGPUParticleSystemManager(_chunkM73XB4ZKjs.WebGPUParticleSystemM
|
|
|
8132
8253
|
|
|
8133
8254
|
|
|
8134
8255
|
|
|
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 =
|
|
8256
|
+
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) {
|
|
@@ -6207,7 +6211,7 @@ var Scene = class _Scene {
|
|
|
6207
6211
|
return;
|
|
6208
6212
|
}
|
|
6209
6213
|
const projection = node.getContentProjection(
|
|
6210
|
-
lineBand ? { minY: lineBand.minY, maxY: lineBand.maxY } : void 0
|
|
6214
|
+
lineBand ? { minY: lineBand.minY, maxY: lineBand.maxY, textOnly: tier === "coarse" } : tier === "coarse" ? { textOnly: true } : void 0
|
|
6211
6215
|
);
|
|
6212
6216
|
if (!projection || !projection.text) {
|
|
6213
6217
|
releaseProjectionEl();
|
|
@@ -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,17 +6299,60 @@ 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";
|
|
6301
6315
|
runElement.style.verticalAlign = "top";
|
|
6302
6316
|
runElement.style.unicodeBidi = "isolate";
|
|
6303
6317
|
runElement.dir = "ltr";
|
|
6318
|
+
} else if (run.width !== void 0) {
|
|
6319
|
+
runElement.style.display = "inline-block";
|
|
6320
|
+
runElement.style.width = `${run.width}px`;
|
|
6321
|
+
runElement.style.boxSizing = "border-box";
|
|
6322
|
+
runElement.style.whiteSpace = "pre";
|
|
6323
|
+
runElement.style.verticalAlign = "top";
|
|
6304
6324
|
}
|
|
6305
6325
|
lineElement.appendChild(runElement);
|
|
6306
6326
|
}
|
|
6327
|
+
} else if (line.perGraphemeCarriers && line.text.length > 0) {
|
|
6328
|
+
const mctx = getSharedMeasuringContext();
|
|
6329
|
+
if (mctx) {
|
|
6330
|
+
mctx.font = lineFont;
|
|
6331
|
+
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
|
|
6332
|
+
const segments = [...segmenter.segment(line.text)];
|
|
6333
|
+
let runningX = 0;
|
|
6334
|
+
for (let segIdx = 0; segIdx < segments.length; segIdx++) {
|
|
6335
|
+
const { segment, index: index2 } = segments[segIdx];
|
|
6336
|
+
const nextIndex = segIdx + 1 < segments.length ? segments[segIdx + 1].index : line.text.length;
|
|
6337
|
+
const canvasX = mctx.measureText(line.text.slice(0, index2)).width;
|
|
6338
|
+
const canvasWidth = mctx.measureText(line.text.slice(0, nextIndex)).width - canvasX;
|
|
6339
|
+
const carrierEl = document.createElement("span");
|
|
6340
|
+
carrierEl.textContent = segment;
|
|
6341
|
+
carrierEl.style.position = "relative";
|
|
6342
|
+
carrierEl.style.display = "inline-block";
|
|
6343
|
+
carrierEl.style.left = `${canvasX - runningX}px`;
|
|
6344
|
+
carrierEl.style.width = `${canvasWidth}px`;
|
|
6345
|
+
carrierEl.style.boxSizing = "border-box";
|
|
6346
|
+
carrierEl.style.lineHeight = `${lineHeight2}px`;
|
|
6347
|
+
carrierEl.style.whiteSpace = "pre";
|
|
6348
|
+
carrierEl.style.verticalAlign = "top";
|
|
6349
|
+
carrierEl.style.unicodeBidi = "isolate";
|
|
6350
|
+
lineElement.appendChild(carrierEl);
|
|
6351
|
+
runningX += canvasWidth;
|
|
6352
|
+
}
|
|
6353
|
+
} else {
|
|
6354
|
+
lineElement.textContent = line.text;
|
|
6355
|
+
}
|
|
6307
6356
|
} else {
|
|
6308
6357
|
lineElement.textContent = line.text;
|
|
6309
6358
|
}
|
|
@@ -7286,6 +7335,18 @@ var TextEntity = class extends Entity {
|
|
|
7286
7335
|
isHovered = false;
|
|
7287
7336
|
/** Bumped by {@link applyLayout}; read by `Scene` to skip an unchanged sync. */
|
|
7288
7337
|
contentEpoch = 0;
|
|
7338
|
+
/**
|
|
7339
|
+
* Visual lines with real canvas geometry, rebuilt by {@link applyLayout}
|
|
7340
|
+
* exactly when `this.nodes` changes.
|
|
7341
|
+
*
|
|
7342
|
+
* Without this the Scene placed the DOM copy at the entity origin and let the
|
|
7343
|
+
* browser flow it at CSS `normal` line-height, while the canvas lays lines out
|
|
7344
|
+
* at `1.5em` pitch with the baseline at `0.8em` — so every line after the
|
|
7345
|
+
* first drifted further from the painted glyphs (measured ~6 px on line 0 and
|
|
7346
|
+
* ~0.35 em per line for a 24 px font, Firefox). Per-line carriers pin the DOM
|
|
7347
|
+
* baseline to the canvas baseline by construction, same as `ui/Text`.
|
|
7348
|
+
*/
|
|
7349
|
+
projectionLines = [];
|
|
7289
7350
|
constructor(text, atlas, maxWidth, fontSize = 24) {
|
|
7290
7351
|
super();
|
|
7291
7352
|
this.text = text;
|
|
@@ -7301,10 +7362,21 @@ var TextEntity = class extends Entity {
|
|
|
7301
7362
|
/**
|
|
7302
7363
|
* Mirror the rendered text into the DOM content layer: find-in-page, screen
|
|
7303
7364
|
* readers, crawlers, and translation see the same string the canvas draws.
|
|
7365
|
+
*
|
|
7366
|
+
* Each visual line is emitted with its own y/baseline/line-height so the DOM
|
|
7367
|
+
* line boxes sit exactly on the drawn glyphs instead of flowing at the
|
|
7368
|
+
* browser's `normal` metrics. Line text is the LOGICAL source slice (not the
|
|
7369
|
+
* per-glyph visual text), so shaped or reordered content still copies and
|
|
7370
|
+
* finds in source order — the same contract `ui/Text` ships.
|
|
7304
7371
|
*/
|
|
7305
7372
|
getContentProjection() {
|
|
7306
7373
|
if (!this.text) return null;
|
|
7307
|
-
return {
|
|
7374
|
+
return {
|
|
7375
|
+
text: this.text,
|
|
7376
|
+
font: `${this.fontSize}px sans-serif`,
|
|
7377
|
+
lineHeight: this.fontSize * 1.5,
|
|
7378
|
+
lines: this.projectionLines
|
|
7379
|
+
};
|
|
7308
7380
|
}
|
|
7309
7381
|
getContentEpoch() {
|
|
7310
7382
|
return this.contentEpoch;
|
|
@@ -7361,6 +7433,55 @@ var TextEntity = class extends Entity {
|
|
|
7361
7433
|
this.width = result.totalWidth;
|
|
7362
7434
|
this.height = result.totalHeight;
|
|
7363
7435
|
this.a11yOffsetY = 0;
|
|
7436
|
+
const lineQuantum = this.fontSize * 1.5;
|
|
7437
|
+
const nodesByLine = /* @__PURE__ */ new Map();
|
|
7438
|
+
let maxIdx = -1;
|
|
7439
|
+
for (const node of this.nodes) {
|
|
7440
|
+
const idx = Math.round(node.y / lineQuantum);
|
|
7441
|
+
const list = nodesByLine.get(idx) ?? [];
|
|
7442
|
+
list.push(node);
|
|
7443
|
+
nodesByLine.set(idx, list);
|
|
7444
|
+
if (idx > maxIdx) maxIdx = idx;
|
|
7445
|
+
}
|
|
7446
|
+
const justify = this.layout.textAlign === "justify";
|
|
7447
|
+
const starts = [];
|
|
7448
|
+
const ends = [];
|
|
7449
|
+
let previousEnd = 0;
|
|
7450
|
+
for (let i = 0; i <= maxIdx; i++) {
|
|
7451
|
+
const nodes = nodesByLine.get(i) ?? [];
|
|
7452
|
+
let start = nodes.length > 0 ? Math.min(...nodes.map((node) => node.sourceIndex ?? previousEnd)) : previousEnd;
|
|
7453
|
+
const end = nodes.length > 0 ? Math.max(
|
|
7454
|
+
...nodes.map((node) => (node.sourceIndex ?? previousEnd) + (node.sourceLength ?? 0))
|
|
7455
|
+
) : start;
|
|
7456
|
+
if (i === 0) start = 0;
|
|
7457
|
+
starts.push(start);
|
|
7458
|
+
ends.push(end);
|
|
7459
|
+
previousEnd = Math.max(previousEnd, end);
|
|
7460
|
+
}
|
|
7461
|
+
const lines = [];
|
|
7462
|
+
for (let i = 0; i <= maxIdx; i++) {
|
|
7463
|
+
const nodes = nodesByLine.get(i) ?? [];
|
|
7464
|
+
const start = starts[i];
|
|
7465
|
+
const end = ends[i];
|
|
7466
|
+
const nextStart = i + 1 <= maxIdx ? starts[i + 1] : this.text.length;
|
|
7467
|
+
const hasRtl = nodes.some((node) => node.isRTL === true);
|
|
7468
|
+
lines.push({
|
|
7469
|
+
text: this.text.slice(start, Math.max(start, end)),
|
|
7470
|
+
separatorAfter: this.text.slice(end, Math.max(end, nextStart)),
|
|
7471
|
+
x: 0,
|
|
7472
|
+
y: i * lineQuantum,
|
|
7473
|
+
baseline: this.fontSize * 0.8,
|
|
7474
|
+
font: `${this.fontSize}px sans-serif`,
|
|
7475
|
+
lineHeight: lineQuantum,
|
|
7476
|
+
// Per-grapheme carriers pin each cluster to its canvas-measured x so
|
|
7477
|
+
// Gecko's grid-fit advance rounding cannot drift the find-in-page
|
|
7478
|
+
// highlight off the drawn glyphs. Bidi lines must keep one text node
|
|
7479
|
+
// (DOM order is logical; per-glyph carriers break caret mapping), and
|
|
7480
|
+
// justify moves glyphs off their natural x, so both fall back to flow.
|
|
7481
|
+
...hasRtl || justify ? {} : { perGraphemeCarriers: true }
|
|
7482
|
+
});
|
|
7483
|
+
}
|
|
7484
|
+
this.projectionLines = lines;
|
|
7364
7485
|
}
|
|
7365
7486
|
isPointInside(globalX, globalY) {
|
|
7366
7487
|
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
|
|
@@ -130,6 +144,18 @@ export interface ContentProjectionHint {
|
|
|
130
144
|
*/
|
|
131
145
|
minY?: number;
|
|
132
146
|
maxY?: number;
|
|
147
|
+
/**
|
|
148
|
+
* When `true`, the caller only needs {@link ContentProjection.text} — no
|
|
149
|
+
* `lines`, no `grid`. Entities receiving this should return the full source
|
|
150
|
+
* text without building per-line or per-glyph structures, which avoids the
|
|
151
|
+
* O(glyphs) layout walk that the coarse resident tier would discard anyway.
|
|
152
|
+
*
|
|
153
|
+
* Entities may ignore this hint and still return `lines`; the caller will
|
|
154
|
+
* simply not use them. Returning fewer than all lines is **not** safe under
|
|
155
|
+
* this hint: if `lines` is non-empty, Scene interprets it as the line
|
|
156
|
+
* window, which must cover the whole text for correctness.
|
|
157
|
+
*/
|
|
158
|
+
textOnly?: boolean;
|
|
133
159
|
}
|
|
134
160
|
/**
|
|
135
161
|
* Whether a line at `y` of height `height` is worth projecting under `hint`.
|