@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.
@@ -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: this.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: this.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
- __init51() {this.fallbackStroke = "rgba(248,113,113,0.9)"}
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
- __init52() {this.fallbackFill = "rgba(248,113,113,0.12)"}
1492
- __init53() {this.svgSource = ""}
1493
- __init54() {this.imageBitmap = null}
1494
- __init55() {this.imageElement = null}
1495
- __init56() {this.blobURL = null}
1496
- __init57() {this.currentImg = null}
1497
- __init58() {this.lodTimeout = null}
1498
- __init59() {this.rasterFailed = false}
1499
- __init60() {this.cachedDoc = null}
1500
- __init61() {this.baseWidth = 100}
1501
- __init62() {this.baseHeight = 100}
1502
- __init63() {this.lastRasterizedScale = 1}
1503
- __init64() {this.targetScale = 1}
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.__init51.call(this);_class4.prototype.__init52.call(this);_class4.prototype.__init53.call(this);_class4.prototype.__init54.call(this);_class4.prototype.__init55.call(this);_class4.prototype.__init56.call(this);_class4.prototype.__init57.call(this);_class4.prototype.__init58.call(this);_class4.prototype.__init59.call(this);_class4.prototype.__init60.call(this);_class4.prototype.__init61.call(this);_class4.prototype.__init62.call(this);_class4.prototype.__init63.call(this);_class4.prototype.__init64.call(this);;
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 _chunkUEIZDNK7js = require('./chunk-UEIZDNK7.js');
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 _chunkUEIZDNK7js.Entity {
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 _chunkUEIZDNK7js.Entity {
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 _chunkUEIZDNK7js.Entity {
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, _chunkUEIZDNK7js.VectoJSEvent)("pointerleave", node, void 0, false));
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, _chunkUEIZDNK7js.VectoJSEvent)("click", node, e));
5661
+ node.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)("click", node, e));
5658
5662
  });
5659
5663
  el.addEventListener("dblclick", (e) => {
5660
- node.dispatchEvent(new (0, _chunkUEIZDNK7js.VectoJSEvent)("dblclick", node, e));
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, _chunkUEIZDNK7js.VectoJSEvent)("hover", node, e, false));
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, _chunkUEIZDNK7js.VectoJSEvent)("pointerleave", node, e, false));
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, _chunkUEIZDNK7js.VectoJSEvent)("pointerdown", node, e));
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, _chunkUEIZDNK7js.VectoJSEvent)("pointerup", node, e));
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, _chunkUEIZDNK7js.VectoJSEvent)("pointercancel", node, e));
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, _chunkUEIZDNK7js.VectoJSEvent)("pointermove", node, e))
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, _chunkUEIZDNK7js.VectoJSEvent)("wheel", node, e));
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, _chunkUEIZDNK7js.VectoJSEvent)("keydown", node, e));
5732
+ node.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)("keydown", node, e));
5729
5733
  });
5730
5734
  el.addEventListener("keyup", (e) => {
5731
- node.dispatchEvent(new (0, _chunkUEIZDNK7js.VectoJSEvent)("keyup", node, e));
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, _chunkUEIZDNK7js.VectoJSEvent)("click", node, e));
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, _chunkUEIZDNK7js.VectoJSEvent)("wheel", node, e2));
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
- runElement.style.position = "absolute";
6298
- runElement.style.left = `${run.x - line.x}px`;
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 !== _chunkUEIZDNK7js.Entity.prototype.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 === _chunkUEIZDNK7js.Entity.prototype.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 _chunkUEIZDNK7js.Entity {
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 { text: this.text, font: `${this.fontSize}px sans-serif` };
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 _chunkUEIZDNK7js.Entity {
7529
+ var GridTextEntity = (_class17 = class extends _chunkXXU56ZNAjs.Entity {
7409
7530
 
7410
- __init183() {this.fillStyle = "#ffffff"}
7411
- __init184() {this.grid = []}
7531
+ __init184() {this.fillStyle = "#ffffff"}
7532
+ __init185() {this.grid = []}
7412
7533
  // Array of rows
7413
- __init185() {this.cols = 0}
7414
- __init186() {this.rows = 0}
7534
+ __init186() {this.cols = 0}
7535
+ __init187() {this.rows = 0}
7415
7536
 
7416
7537
 
7417
7538
  constructor(_atlas, fontSize = 10) {
7418
- super();_class17.prototype.__init183.call(this);_class17.prototype.__init184.call(this);_class17.prototype.__init185.call(this);_class17.prototype.__init186.call(this);;
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 _chunkUEIZDNK7js.Entity {
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
- __init187() {this.offscreen = null}
7511
- __init188() {this.baked = false}
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
- __init189() {this.bakedWidth = 0}
7514
- __init190() {this.bakedHeight = 0}
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
- __init191() {this.polylines = null}
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
- __init192() {this.showBounds = false}
7677
+ __init193() {this.showBounds = false}
7557
7678
  constructor(doc, opts = {}) {
7558
- super();_class18.prototype.__init187.call(this);_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);;
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 _chunkUEIZDNK7js.Entity {
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 _chunkUEIZDNK7js.Entity {
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 _chunkUEIZDNK7js.Entity {
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 _chunkUEIZDNK7js.Entity {
8083
+ var DOMPortalEntity = (_class19 = class extends _chunkXXU56ZNAjs.Entity {
7963
8084
 
7964
- __init193() {this.isDOMPortal = true}
7965
- __init194() {this.domListeners = []}
7966
- __init195() {this.resizeObserver = null}
7967
- __init196() {this.domBound = false}
7968
- __init197() {this.cachedWidth = 100}
7969
- __init198() {this.cachedHeight = 100}
7970
- __init199() {this.lastWidth = ""}
7971
- __init200() {this.lastHeight = ""}
7972
- __init201() {this.lastTransform = ""}
7973
- __init202() {this.lastZIndex = ""}
7974
- __init203() {this.lastOpacity = ""}
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.__init193.call(this);_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);;
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, _chunkUEIZDNK7js.VectoJSEvent)(type, this, e));
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, _chunkUEIZDNK7js.VectoJSEvent)(vecto, this, e, false));
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, _chunkUEIZDNK7js.VectoJSEvent)(type, this, e, true));
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 = _chunkUEIZDNK7js.Entity; exports.GlyphRasterAtlas = _chunkM73XB4ZKjs.GlyphRasterAtlas; exports.GridTextEntity = GridTextEntity; exports.Group = Group; exports.MSDFTextEntity = _chunkUEIZDNK7js.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 = _chunkUEIZDNK7js.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 = _chunkUEIZDNK7js.VectoJSEvent; exports.WebGPUParticleSystemManager = _chunkM73XB4ZKjs.WebGPUParticleSystemManager; exports.beginVectoUserTiming = beginVectoUserTiming; exports.contentLineInHint = _chunkUEIZDNK7js.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;
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-VFQMETOR.mjs";
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 { clearCssLineBoxMetrics, cssLineBoxBaseline as cssLineBoxBaseline2 } from "@vectojs/text";
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
- runElement.style.position = "absolute";
6297
- runElement.style.left = `${run.x - line.x}px`;
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 { text: this.text, font: `${this.fontSize}px sans-serif` };
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 _chunkUEIZDNK7js = require('./chunk-UEIZDNK7.js');
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 = _chunkUEIZDNK7js.MSDFTextEntity; exports.SVGEntity = _chunkUEIZDNK7js.SVGEntity;
12
+ exports.MSDFTextEntity = _chunkXXU56ZNAjs.MSDFTextEntity; exports.SVGEntity = _chunkXXU56ZNAjs.SVGEntity;
package/dist/text.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  MSDFTextEntity,
3
3
  SVGEntity
4
- } from "./chunk-VFQMETOR.mjs";
4
+ } from "./chunk-TV4ARAC5.mjs";
5
5
 
6
6
  // src/text/index.ts
7
7
  export * from "@vectojs/text";
@@ -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`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectojs/core",
3
- "version": "1.34.1",
3
+ "version": "1.35.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },