@vectojs/core 1.34.1 → 1.34.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  }
@@ -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,8 +6300,16 @@ var Scene = (_class15 = class _Scene {
6294
6300
  if (run.font) runElement.style.font = run.font;
6295
6301
  runElement.style.lineHeight = `${lineHeight2}px`;
6296
6302
  if (positioned && run.x !== void 0) {
6297
- 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";
@@ -6305,6 +6319,35 @@ var Scene = (_class15 = class _Scene {
6305
6319
  }
6306
6320
  lineElement.appendChild(runElement);
6307
6321
  }
6322
+ } else if (line.perGraphemeCarriers && line.text.length > 0) {
6323
+ const mctx = _text.getSharedMeasuringContext.call(void 0, );
6324
+ if (mctx) {
6325
+ mctx.font = lineFont;
6326
+ const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
6327
+ const segments = [...segmenter.segment(line.text)];
6328
+ let runningX = 0;
6329
+ for (let segIdx = 0; segIdx < segments.length; segIdx++) {
6330
+ const { segment, index: index2 } = segments[segIdx];
6331
+ const nextIndex = segIdx + 1 < segments.length ? segments[segIdx + 1].index : line.text.length;
6332
+ const canvasX = mctx.measureText(line.text.slice(0, index2)).width;
6333
+ const canvasWidth = mctx.measureText(line.text.slice(0, nextIndex)).width - canvasX;
6334
+ const carrierEl = document.createElement("span");
6335
+ carrierEl.textContent = segment;
6336
+ carrierEl.style.position = "relative";
6337
+ carrierEl.style.display = "inline-block";
6338
+ carrierEl.style.left = `${canvasX - runningX}px`;
6339
+ carrierEl.style.width = `${canvasWidth}px`;
6340
+ carrierEl.style.boxSizing = "border-box";
6341
+ carrierEl.style.lineHeight = `${lineHeight2}px`;
6342
+ carrierEl.style.whiteSpace = "pre";
6343
+ carrierEl.style.verticalAlign = "top";
6344
+ carrierEl.style.unicodeBidi = "isolate";
6345
+ lineElement.appendChild(carrierEl);
6346
+ runningX += canvasWidth;
6347
+ }
6348
+ } else {
6349
+ lineElement.textContent = line.text;
6350
+ }
6308
6351
  } else {
6309
6352
  lineElement.textContent = line.text;
6310
6353
  }
@@ -6838,7 +6881,7 @@ var Scene = (_class15 = class _Scene {
6838
6881
  let walkHadAnimation = false;
6839
6882
  let walkHadInteractive = false;
6840
6883
  const runUpdate = (node) => {
6841
- const overridesUpdate = node.update !== _chunkUEIZDNK7js.Entity.prototype.update;
6884
+ const overridesUpdate = node.update !== _chunkXXU56ZNAjs.Entity.prototype.update;
6842
6885
  let pending = node.hasPendingAnimations();
6843
6886
  if (pending || overridesUpdate) {
6844
6887
  node.update(dt, time);
@@ -6847,7 +6890,7 @@ var Scene = (_class15 = class _Scene {
6847
6890
  if (pending) walkHadAnimation = true;
6848
6891
  if (!walkHadInteractive && node.interactive) walkHadInteractive = true;
6849
6892
  if (this._devActive && this._devFrameCount % 120 === 0) {
6850
- if (overridesUpdate && node.hasPendingAnimations === _chunkUEIZDNK7js.Entity.prototype.hasPendingAnimations) {
6893
+ if (overridesUpdate && node.hasPendingAnimations === _chunkXXU56ZNAjs.Entity.prototype.hasPendingAnimations) {
6851
6894
  this._devWarn(
6852
6895
  `Entity "${node.id}" overrides update() but not hasPendingAnimations(). Custom motion in update() without overriding hasPendingAnimations() causes the idle throttle to drop the animation to ~2fps. Override hasPendingAnimations() to return true while motion is in flight.`
6853
6896
  );
@@ -7273,7 +7316,7 @@ function defaultMeasurer() {
7273
7316
  sharedMeasurer ??= _layout.resolveGlyphMeasurer.call(void 0, "sans-serif");
7274
7317
  return sharedMeasurer;
7275
7318
  }
7276
- var TextEntity = (_class16 = class extends _chunkUEIZDNK7js.Entity {
7319
+ var TextEntity = (_class16 = class extends _chunkXXU56ZNAjs.Entity {
7277
7320
 
7278
7321
 
7279
7322
 
@@ -7287,8 +7330,20 @@ var TextEntity = (_class16 = class extends _chunkUEIZDNK7js.Entity {
7287
7330
  __init181() {this.isHovered = false}
7288
7331
  /** Bumped by {@link applyLayout}; read by `Scene` to skip an unchanged sync. */
7289
7332
  __init182() {this.contentEpoch = 0}
7333
+ /**
7334
+ * Visual lines with real canvas geometry, rebuilt by {@link applyLayout}
7335
+ * exactly when `this.nodes` changes.
7336
+ *
7337
+ * Without this the Scene placed the DOM copy at the entity origin and let the
7338
+ * browser flow it at CSS `normal` line-height, while the canvas lays lines out
7339
+ * at `1.5em` pitch with the baseline at `0.8em` — so every line after the
7340
+ * first drifted further from the painted glyphs (measured ~6 px on line 0 and
7341
+ * ~0.35 em per line for a 24 px font, Firefox). Per-line carriers pin the DOM
7342
+ * baseline to the canvas baseline by construction, same as `ui/Text`.
7343
+ */
7344
+ __init183() {this.projectionLines = []}
7290
7345
  constructor(text, atlas, maxWidth, fontSize = 24) {
7291
- super();_class16.prototype.__init176.call(this);_class16.prototype.__init177.call(this);_class16.prototype.__init178.call(this);_class16.prototype.__init179.call(this);_class16.prototype.__init180.call(this);_class16.prototype.__init181.call(this);_class16.prototype.__init182.call(this);;
7346
+ super();_class16.prototype.__init176.call(this);_class16.prototype.__init177.call(this);_class16.prototype.__init178.call(this);_class16.prototype.__init179.call(this);_class16.prototype.__init180.call(this);_class16.prototype.__init181.call(this);_class16.prototype.__init182.call(this);_class16.prototype.__init183.call(this);;
7292
7347
  this.text = text;
7293
7348
  this.atlas = atlas;
7294
7349
  this.fontSize = fontSize;
@@ -7302,10 +7357,21 @@ var TextEntity = (_class16 = class extends _chunkUEIZDNK7js.Entity {
7302
7357
  /**
7303
7358
  * Mirror the rendered text into the DOM content layer: find-in-page, screen
7304
7359
  * readers, crawlers, and translation see the same string the canvas draws.
7360
+ *
7361
+ * Each visual line is emitted with its own y/baseline/line-height so the DOM
7362
+ * line boxes sit exactly on the drawn glyphs instead of flowing at the
7363
+ * browser's `normal` metrics. Line text is the LOGICAL source slice (not the
7364
+ * per-glyph visual text), so shaped or reordered content still copies and
7365
+ * finds in source order — the same contract `ui/Text` ships.
7305
7366
  */
7306
7367
  getContentProjection() {
7307
7368
  if (!this.text) return null;
7308
- return { text: this.text, font: `${this.fontSize}px sans-serif` };
7369
+ return {
7370
+ text: this.text,
7371
+ font: `${this.fontSize}px sans-serif`,
7372
+ lineHeight: this.fontSize * 1.5,
7373
+ lines: this.projectionLines
7374
+ };
7309
7375
  }
7310
7376
  getContentEpoch() {
7311
7377
  return this.contentEpoch;
@@ -7362,6 +7428,55 @@ var TextEntity = (_class16 = class extends _chunkUEIZDNK7js.Entity {
7362
7428
  this.width = result.totalWidth;
7363
7429
  this.height = result.totalHeight;
7364
7430
  this.a11yOffsetY = 0;
7431
+ const lineQuantum = this.fontSize * 1.5;
7432
+ const nodesByLine = /* @__PURE__ */ new Map();
7433
+ let maxIdx = -1;
7434
+ for (const node of this.nodes) {
7435
+ const idx = Math.round(node.y / lineQuantum);
7436
+ const list = _nullishCoalesce(nodesByLine.get(idx), () => ( []));
7437
+ list.push(node);
7438
+ nodesByLine.set(idx, list);
7439
+ if (idx > maxIdx) maxIdx = idx;
7440
+ }
7441
+ const justify = this.layout.textAlign === "justify";
7442
+ const starts = [];
7443
+ const ends = [];
7444
+ let previousEnd = 0;
7445
+ for (let i = 0; i <= maxIdx; i++) {
7446
+ const nodes = _nullishCoalesce(nodesByLine.get(i), () => ( []));
7447
+ let start = nodes.length > 0 ? Math.min(...nodes.map((node) => _nullishCoalesce(node.sourceIndex, () => ( previousEnd)))) : previousEnd;
7448
+ const end = nodes.length > 0 ? Math.max(
7449
+ ...nodes.map((node) => (_nullishCoalesce(node.sourceIndex, () => ( previousEnd))) + (_nullishCoalesce(node.sourceLength, () => ( 0))))
7450
+ ) : start;
7451
+ if (i === 0) start = 0;
7452
+ starts.push(start);
7453
+ ends.push(end);
7454
+ previousEnd = Math.max(previousEnd, end);
7455
+ }
7456
+ const lines = [];
7457
+ for (let i = 0; i <= maxIdx; i++) {
7458
+ const nodes = _nullishCoalesce(nodesByLine.get(i), () => ( []));
7459
+ const start = starts[i];
7460
+ const end = ends[i];
7461
+ const nextStart = i + 1 <= maxIdx ? starts[i + 1] : this.text.length;
7462
+ const hasRtl = nodes.some((node) => node.isRTL === true);
7463
+ lines.push({
7464
+ text: this.text.slice(start, Math.max(start, end)),
7465
+ separatorAfter: this.text.slice(end, Math.max(end, nextStart)),
7466
+ x: 0,
7467
+ y: i * lineQuantum,
7468
+ baseline: this.fontSize * 0.8,
7469
+ font: `${this.fontSize}px sans-serif`,
7470
+ lineHeight: lineQuantum,
7471
+ // Per-grapheme carriers pin each cluster to its canvas-measured x so
7472
+ // Gecko's grid-fit advance rounding cannot drift the find-in-page
7473
+ // highlight off the drawn glyphs. Bidi lines must keep one text node
7474
+ // (DOM order is logical; per-glyph carriers break caret mapping), and
7475
+ // justify moves glyphs off their natural x, so both fall back to flow.
7476
+ ...hasRtl || justify ? {} : { perGraphemeCarriers: true }
7477
+ });
7478
+ }
7479
+ this.projectionLines = lines;
7365
7480
  }
7366
7481
  isPointInside(globalX, globalY) {
7367
7482
  const local = this.worldToLocal(globalX, globalY);
@@ -7405,17 +7520,17 @@ var TextEntity = (_class16 = class extends _chunkUEIZDNK7js.Entity {
7405
7520
  }, _class16);
7406
7521
 
7407
7522
  // src/components/GridTextEntity.ts
7408
- var GridTextEntity = (_class17 = class extends _chunkUEIZDNK7js.Entity {
7523
+ var GridTextEntity = (_class17 = class extends _chunkXXU56ZNAjs.Entity {
7409
7524
 
7410
- __init183() {this.fillStyle = "#ffffff"}
7411
- __init184() {this.grid = []}
7525
+ __init184() {this.fillStyle = "#ffffff"}
7526
+ __init185() {this.grid = []}
7412
7527
  // Array of rows
7413
- __init185() {this.cols = 0}
7414
- __init186() {this.rows = 0}
7528
+ __init186() {this.cols = 0}
7529
+ __init187() {this.rows = 0}
7415
7530
 
7416
7531
 
7417
7532
  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);;
7533
+ super();_class17.prototype.__init184.call(this);_class17.prototype.__init185.call(this);_class17.prototype.__init186.call(this);_class17.prototype.__init187.call(this);;
7419
7534
  this.fontSize = fontSize;
7420
7535
  this.charWidth = fontSize * 1;
7421
7536
  this.charHeight = fontSize * 1.1;
@@ -7499,7 +7614,7 @@ function distSqToSegment(px, py, x1, y1, x2, y2) {
7499
7614
  const ey = py - cy;
7500
7615
  return ex * ex + ey * ey;
7501
7616
  }
7502
- var SplineEntity = (_class18 = class extends _chunkUEIZDNK7js.Entity {
7617
+ var SplineEntity = (_class18 = class extends _chunkXXU56ZNAjs.Entity {
7503
7618
 
7504
7619
 
7505
7620
 
@@ -7507,11 +7622,11 @@ var SplineEntity = (_class18 = class extends _chunkUEIZDNK7js.Entity {
7507
7622
 
7508
7623
 
7509
7624
 
7510
- __init187() {this.offscreen = null}
7511
- __init188() {this.baked = false}
7625
+ __init188() {this.offscreen = null}
7626
+ __init189() {this.baked = false}
7512
7627
  /** Logical (CSS-pixel) size of the baked bitmap — the blit destination size. */
7513
- __init189() {this.bakedWidth = 0}
7514
- __init190() {this.bakedHeight = 0}
7628
+ __init190() {this.bakedWidth = 0}
7629
+ __init191() {this.bakedHeight = 0}
7515
7630
  /**
7516
7631
  * Gradient strokes can't be baked to a solid-color bitmap; they render
7517
7632
  * per-frame. Derived from `_doc`, so it is NOT readonly — assigning a new
@@ -7519,7 +7634,7 @@ var SplineEntity = (_class18 = class extends _chunkUEIZDNK7js.Entity {
7519
7634
  */
7520
7635
 
7521
7636
  /** Lazily-flattened polylines (one Float32Array of [x,y,...] per segment) for hit-testing. */
7522
- __init191() {this.polylines = null}
7637
+ __init192() {this.polylines = null}
7523
7638
  /**
7524
7639
  * The spline document to render. Assigning a new document invalidates all
7525
7640
  * caches (baked canvas, flattened polylines, bounds).
@@ -7553,9 +7668,9 @@ var SplineEntity = (_class18 = class extends _chunkUEIZDNK7js.Entity {
7553
7668
  * local bounds after painting the curves. Useful for drag feedback and
7554
7669
  * debugging hit areas. Defaults to `false`.
7555
7670
  */
7556
- __init192() {this.showBounds = false}
7671
+ __init193() {this.showBounds = false}
7557
7672
  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);;
7673
+ super();_class18.prototype.__init188.call(this);_class18.prototype.__init189.call(this);_class18.prototype.__init190.call(this);_class18.prototype.__init191.call(this);_class18.prototype.__init192.call(this);_class18.prototype.__init193.call(this);;
7559
7674
  this._doc = doc;
7560
7675
  this._lineWidth = _nullishCoalesce(opts.lineWidth, () => ( 2));
7561
7676
  this.cache = _nullishCoalesce(opts.cache, () => ( true));
@@ -7820,7 +7935,7 @@ async function loadSpline(url) {
7820
7935
  }
7821
7936
 
7822
7937
  // src/components/Rect.ts
7823
- var Rect = class extends _chunkUEIZDNK7js.Entity {
7938
+ var Rect = class extends _chunkXXU56ZNAjs.Entity {
7824
7939
 
7825
7940
 
7826
7941
 
@@ -7876,7 +7991,7 @@ var Rect = class extends _chunkUEIZDNK7js.Entity {
7876
7991
  };
7877
7992
 
7878
7993
  // src/components/Circle.ts
7879
- var Circle = class extends _chunkUEIZDNK7js.Entity {
7994
+ var Circle = class extends _chunkXXU56ZNAjs.Entity {
7880
7995
 
7881
7996
 
7882
7997
 
@@ -7940,7 +8055,7 @@ var Circle = class extends _chunkUEIZDNK7js.Entity {
7940
8055
  };
7941
8056
 
7942
8057
  // src/components/Group.ts
7943
- var Group = class extends _chunkUEIZDNK7js.Entity {
8058
+ var Group = class extends _chunkXXU56ZNAjs.Entity {
7944
8059
  constructor(...children) {
7945
8060
  super();
7946
8061
  if (children.length > 0) this.add(...children);
@@ -7959,21 +8074,21 @@ var _math = require('@vectojs/math'); _createStarExport(_math);
7959
8074
 
7960
8075
 
7961
8076
  // src/tree/DOMPortalEntity.ts
7962
- var DOMPortalEntity = (_class19 = class extends _chunkUEIZDNK7js.Entity {
8077
+ var DOMPortalEntity = (_class19 = class extends _chunkXXU56ZNAjs.Entity {
7963
8078
 
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 = ""}
8079
+ __init194() {this.isDOMPortal = true}
8080
+ __init195() {this.domListeners = []}
8081
+ __init196() {this.resizeObserver = null}
8082
+ __init197() {this.domBound = false}
8083
+ __init198() {this.cachedWidth = 100}
8084
+ __init199() {this.cachedHeight = 100}
8085
+ __init200() {this.lastWidth = ""}
8086
+ __init201() {this.lastHeight = ""}
8087
+ __init202() {this.lastTransform = ""}
8088
+ __init203() {this.lastZIndex = ""}
8089
+ __init204() {this.lastOpacity = ""}
7975
8090
  constructor(domElement, width, height, id) {
7976
- super(id);_class19.prototype.__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);;
8091
+ super(id);_class19.prototype.__init194.call(this);_class19.prototype.__init195.call(this);_class19.prototype.__init196.call(this);_class19.prototype.__init197.call(this);_class19.prototype.__init198.call(this);_class19.prototype.__init199.call(this);_class19.prototype.__init200.call(this);_class19.prototype.__init201.call(this);_class19.prototype.__init202.call(this);_class19.prototype.__init203.call(this);_class19.prototype.__init204.call(this);;
7977
8092
  this.domElement = domElement;
7978
8093
  this.width = _nullishCoalesce(width, () => ( 0));
7979
8094
  this.height = _nullishCoalesce(height, () => ( 0));
@@ -8015,7 +8130,7 @@ var DOMPortalEntity = (_class19 = class extends _chunkUEIZDNK7js.Entity {
8015
8130
  ];
8016
8131
  for (const type of events) {
8017
8132
  const handler = (e) => {
8018
- this.dispatchEvent(new (0, _chunkUEIZDNK7js.VectoJSEvent)(type, this, e));
8133
+ this.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)(type, this, e));
8019
8134
  };
8020
8135
  this.domElement.addEventListener(type, handler);
8021
8136
  this.domListeners.push({ type, handler, capture: false });
@@ -8026,7 +8141,7 @@ var DOMPortalEntity = (_class19 = class extends _chunkUEIZDNK7js.Entity {
8026
8141
  ];
8027
8142
  for (const { native, vecto } of hoverEvents) {
8028
8143
  const handler = (e) => {
8029
- this.dispatchEvent(new (0, _chunkUEIZDNK7js.VectoJSEvent)(vecto, this, e, false));
8144
+ this.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)(vecto, this, e, false));
8030
8145
  };
8031
8146
  this.domElement.addEventListener(native, handler);
8032
8147
  this.domListeners.push({ type: native, handler, capture: false });
@@ -8034,7 +8149,7 @@ var DOMPortalEntity = (_class19 = class extends _chunkUEIZDNK7js.Entity {
8034
8149
  const focusEvents = ["focus", "blur"];
8035
8150
  for (const type of focusEvents) {
8036
8151
  const handler = (e) => {
8037
- this.dispatchEvent(new (0, _chunkUEIZDNK7js.VectoJSEvent)(type, this, e, true));
8152
+ this.dispatchEvent(new (0, _chunkXXU56ZNAjs.VectoJSEvent)(type, this, e, true));
8038
8153
  };
8039
8154
  this.domElement.addEventListener(type, handler, true);
8040
8155
  this.domListeners.push({ type, handler, capture: true });
@@ -8132,4 +8247,4 @@ Scene.registerWebGPUParticleSystemManager(_chunkM73XB4ZKjs.WebGPUParticleSystemM
8132
8247
 
8133
8248
 
8134
8249
 
8135
- exports.CanvasRenderer = _chunkM73XB4ZKjs.CanvasRenderer; exports.Circle = Circle; exports.ComputeParticleEntity = ComputeParticleEntity; exports.DEFAULT_CONTENT_SEMANTIC_BUDGET = DEFAULT_CONTENT_SEMANTIC_BUDGET; exports.DOMPortalEntity = DOMPortalEntity; exports.Entity = _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;
8250
+ exports.CanvasRenderer = _chunkM73XB4ZKjs.CanvasRenderer; exports.Circle = Circle; exports.ComputeParticleEntity = ComputeParticleEntity; exports.DEFAULT_CONTENT_SEMANTIC_BUDGET = DEFAULT_CONTENT_SEMANTIC_BUDGET; exports.DOMPortalEntity = DOMPortalEntity; exports.Entity = _chunkXXU56ZNAjs.Entity; exports.GlyphRasterAtlas = _chunkM73XB4ZKjs.GlyphRasterAtlas; exports.GridTextEntity = GridTextEntity; exports.Group = Group; exports.MSDFTextEntity = _chunkXXU56ZNAjs.MSDFTextEntity; exports.PARTICLE_OFFSET_LIFE = PARTICLE_OFFSET_LIFE; exports.PARTICLE_OFFSET_ORIGIN_X = PARTICLE_OFFSET_ORIGIN_X; exports.PARTICLE_OFFSET_ORIGIN_Y = PARTICLE_OFFSET_ORIGIN_Y; exports.PARTICLE_OFFSET_POSITION_X = PARTICLE_OFFSET_POSITION_X; exports.PARTICLE_OFFSET_POSITION_Y = PARTICLE_OFFSET_POSITION_Y; exports.PARTICLE_OFFSET_SIZE = PARTICLE_OFFSET_SIZE; exports.PARTICLE_OFFSET_VELOCITY_X = PARTICLE_OFFSET_VELOCITY_X; exports.PARTICLE_OFFSET_VELOCITY_Y = PARTICLE_OFFSET_VELOCITY_Y; exports.PARTICLE_STRIDE_FLOATS = PARTICLE_STRIDE_FLOATS; exports.REDUCED_MOTION_FPS = REDUCED_MOTION_FPS; exports.Rect = Rect; exports.SCENE_OPTION_KEYS = SCENE_OPTION_KEYS; exports.SVGEntity = _chunkXXU56ZNAjs.SVGEntity; exports.SVGRenderer = _chunkM73XB4ZKjs.SVGRenderer; exports.Scene = Scene; exports.SplineEntity = SplineEntity; exports.TextEntity = TextEntity; exports.TextRasterCache = _chunkM73XB4ZKjs.TextRasterCache; exports.VECTO_USER_TIMING = VECTO_USER_TIMING; exports.VectoJSEvent = _chunkXXU56ZNAjs.VectoJSEvent; exports.WebGPUParticleSystemManager = _chunkM73XB4ZKjs.WebGPUParticleSystemManager; exports.beginVectoUserTiming = beginVectoUserTiming; exports.contentLineInHint = _chunkXXU56ZNAjs.contentLineInHint; exports.createWebGLPointRenderer = _chunkM73XB4ZKjs.createWebGLPointRenderer; exports.endVectoUserTiming = endVectoUserTiming; exports.installRendererDevTraps = _chunkM73XB4ZKjs.installRendererDevTraps; exports.isRendererDevMode = _chunkM73XB4ZKjs.isRendererDevMode; exports.isSafeUrl = _chunkM73XB4ZKjs.isSafeUrl; exports.loadSpline = loadSpline; exports.measureVectoUserTiming = measureVectoUserTiming; exports.parseColorToRGBA = _chunkM73XB4ZKjs.parseColorToRGBA; exports.polySegmentToBezier = polySegmentToBezier; exports.sanitizeUrl = _chunkM73XB4ZKjs.sanitizeUrl; exports.setRendererDevMode = _chunkM73XB4ZKjs.setRendererDevMode;
package/dist/index.mjs CHANGED
@@ -18,7 +18,7 @@ import {
18
18
  SVGEntity,
19
19
  VectoJSEvent,
20
20
  contentLineInHint
21
- } from "./chunk-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) {
@@ -6286,6 +6290,8 @@ var Scene = class _Scene {
6286
6290
  const separator = line.separatorAfter ?? (index < lines.length - 1 ? "\n" : "");
6287
6291
  if (line.runs && line.runs.length > 0) {
6288
6292
  const positioned = line.runs.some((run) => run.x !== void 0);
6293
+ const flowRelative = positioned && line.runs.every((run) => run.x === void 0 || run.width !== void 0);
6294
+ let runningX = line.x;
6289
6295
  for (let runIndex = 0; runIndex < line.runs.length; runIndex++) {
6290
6296
  const run = line.runs[runIndex];
6291
6297
  const runElement = document.createElement("span");
@@ -6293,8 +6299,16 @@ var Scene = class _Scene {
6293
6299
  if (run.font) runElement.style.font = run.font;
6294
6300
  runElement.style.lineHeight = `${lineHeight2}px`;
6295
6301
  if (positioned && run.x !== void 0) {
6296
- 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";
@@ -6304,6 +6318,35 @@ var Scene = class _Scene {
6304
6318
  }
6305
6319
  lineElement.appendChild(runElement);
6306
6320
  }
6321
+ } else if (line.perGraphemeCarriers && line.text.length > 0) {
6322
+ const mctx = getSharedMeasuringContext();
6323
+ if (mctx) {
6324
+ mctx.font = lineFont;
6325
+ const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
6326
+ const segments = [...segmenter.segment(line.text)];
6327
+ let runningX = 0;
6328
+ for (let segIdx = 0; segIdx < segments.length; segIdx++) {
6329
+ const { segment, index: index2 } = segments[segIdx];
6330
+ const nextIndex = segIdx + 1 < segments.length ? segments[segIdx + 1].index : line.text.length;
6331
+ const canvasX = mctx.measureText(line.text.slice(0, index2)).width;
6332
+ const canvasWidth = mctx.measureText(line.text.slice(0, nextIndex)).width - canvasX;
6333
+ const carrierEl = document.createElement("span");
6334
+ carrierEl.textContent = segment;
6335
+ carrierEl.style.position = "relative";
6336
+ carrierEl.style.display = "inline-block";
6337
+ carrierEl.style.left = `${canvasX - runningX}px`;
6338
+ carrierEl.style.width = `${canvasWidth}px`;
6339
+ carrierEl.style.boxSizing = "border-box";
6340
+ carrierEl.style.lineHeight = `${lineHeight2}px`;
6341
+ carrierEl.style.whiteSpace = "pre";
6342
+ carrierEl.style.verticalAlign = "top";
6343
+ carrierEl.style.unicodeBidi = "isolate";
6344
+ lineElement.appendChild(carrierEl);
6345
+ runningX += canvasWidth;
6346
+ }
6347
+ } else {
6348
+ lineElement.textContent = line.text;
6349
+ }
6307
6350
  } else {
6308
6351
  lineElement.textContent = line.text;
6309
6352
  }
@@ -7286,6 +7329,18 @@ var TextEntity = class extends Entity {
7286
7329
  isHovered = false;
7287
7330
  /** Bumped by {@link applyLayout}; read by `Scene` to skip an unchanged sync. */
7288
7331
  contentEpoch = 0;
7332
+ /**
7333
+ * Visual lines with real canvas geometry, rebuilt by {@link applyLayout}
7334
+ * exactly when `this.nodes` changes.
7335
+ *
7336
+ * Without this the Scene placed the DOM copy at the entity origin and let the
7337
+ * browser flow it at CSS `normal` line-height, while the canvas lays lines out
7338
+ * at `1.5em` pitch with the baseline at `0.8em` — so every line after the
7339
+ * first drifted further from the painted glyphs (measured ~6 px on line 0 and
7340
+ * ~0.35 em per line for a 24 px font, Firefox). Per-line carriers pin the DOM
7341
+ * baseline to the canvas baseline by construction, same as `ui/Text`.
7342
+ */
7343
+ projectionLines = [];
7289
7344
  constructor(text, atlas, maxWidth, fontSize = 24) {
7290
7345
  super();
7291
7346
  this.text = text;
@@ -7301,10 +7356,21 @@ var TextEntity = class extends Entity {
7301
7356
  /**
7302
7357
  * Mirror the rendered text into the DOM content layer: find-in-page, screen
7303
7358
  * readers, crawlers, and translation see the same string the canvas draws.
7359
+ *
7360
+ * Each visual line is emitted with its own y/baseline/line-height so the DOM
7361
+ * line boxes sit exactly on the drawn glyphs instead of flowing at the
7362
+ * browser's `normal` metrics. Line text is the LOGICAL source slice (not the
7363
+ * per-glyph visual text), so shaped or reordered content still copies and
7364
+ * finds in source order — the same contract `ui/Text` ships.
7304
7365
  */
7305
7366
  getContentProjection() {
7306
7367
  if (!this.text) return null;
7307
- return { text: this.text, font: `${this.fontSize}px sans-serif` };
7368
+ return {
7369
+ text: this.text,
7370
+ font: `${this.fontSize}px sans-serif`,
7371
+ lineHeight: this.fontSize * 1.5,
7372
+ lines: this.projectionLines
7373
+ };
7308
7374
  }
7309
7375
  getContentEpoch() {
7310
7376
  return this.contentEpoch;
@@ -7361,6 +7427,55 @@ var TextEntity = class extends Entity {
7361
7427
  this.width = result.totalWidth;
7362
7428
  this.height = result.totalHeight;
7363
7429
  this.a11yOffsetY = 0;
7430
+ const lineQuantum = this.fontSize * 1.5;
7431
+ const nodesByLine = /* @__PURE__ */ new Map();
7432
+ let maxIdx = -1;
7433
+ for (const node of this.nodes) {
7434
+ const idx = Math.round(node.y / lineQuantum);
7435
+ const list = nodesByLine.get(idx) ?? [];
7436
+ list.push(node);
7437
+ nodesByLine.set(idx, list);
7438
+ if (idx > maxIdx) maxIdx = idx;
7439
+ }
7440
+ const justify = this.layout.textAlign === "justify";
7441
+ const starts = [];
7442
+ const ends = [];
7443
+ let previousEnd = 0;
7444
+ for (let i = 0; i <= maxIdx; i++) {
7445
+ const nodes = nodesByLine.get(i) ?? [];
7446
+ let start = nodes.length > 0 ? Math.min(...nodes.map((node) => node.sourceIndex ?? previousEnd)) : previousEnd;
7447
+ const end = nodes.length > 0 ? Math.max(
7448
+ ...nodes.map((node) => (node.sourceIndex ?? previousEnd) + (node.sourceLength ?? 0))
7449
+ ) : start;
7450
+ if (i === 0) start = 0;
7451
+ starts.push(start);
7452
+ ends.push(end);
7453
+ previousEnd = Math.max(previousEnd, end);
7454
+ }
7455
+ const lines = [];
7456
+ for (let i = 0; i <= maxIdx; i++) {
7457
+ const nodes = nodesByLine.get(i) ?? [];
7458
+ const start = starts[i];
7459
+ const end = ends[i];
7460
+ const nextStart = i + 1 <= maxIdx ? starts[i + 1] : this.text.length;
7461
+ const hasRtl = nodes.some((node) => node.isRTL === true);
7462
+ lines.push({
7463
+ text: this.text.slice(start, Math.max(start, end)),
7464
+ separatorAfter: this.text.slice(end, Math.max(end, nextStart)),
7465
+ x: 0,
7466
+ y: i * lineQuantum,
7467
+ baseline: this.fontSize * 0.8,
7468
+ font: `${this.fontSize}px sans-serif`,
7469
+ lineHeight: lineQuantum,
7470
+ // Per-grapheme carriers pin each cluster to its canvas-measured x so
7471
+ // Gecko's grid-fit advance rounding cannot drift the find-in-page
7472
+ // highlight off the drawn glyphs. Bidi lines must keep one text node
7473
+ // (DOM order is logical; per-glyph carriers break caret mapping), and
7474
+ // justify moves glyphs off their natural x, so both fall back to flow.
7475
+ ...hasRtl || justify ? {} : { perGraphemeCarriers: true }
7476
+ });
7477
+ }
7478
+ this.projectionLines = lines;
7364
7479
  }
7365
7480
  isPointInside(globalX, globalY) {
7366
7481
  const local = this.worldToLocal(globalX, globalY);
@@ -41,6 +41,12 @@ export declare class MSDFTextEntity extends Entity {
41
41
  private rgbColorCache;
42
42
  private fontStringCache;
43
43
  private layoutResult;
44
+ /**
45
+ * Visual rows rebuilt from {@link layoutResult} (see
46
+ * {@link rebuildProjectionLines}). Empty until a layout reply lands and the
47
+ * reply's shaped glyphs can be mapped back to the source text 1:1.
48
+ */
49
+ private projectionLines;
44
50
  constructor(text: string, options: MSDFTextEntityOptions);
45
51
  /**
46
52
  * Repaint once the atlas raster decodes.
@@ -91,8 +97,26 @@ export declare class MSDFTextEntity extends Entity {
91
97
  /**
92
98
  * Mirror the rendered text into the DOM content layer: find-in-page, screen
93
99
  * readers, crawlers, and translation see the same string the canvas draws.
100
+ *
101
+ * `baseline` + `lineHeight` are always emitted (they come from the font
102
+ * metrics, no layout reply needed), so the DOM line boxes at least land on
103
+ * the canvas rhythm: the first baseline at `ascender × fontSize` and every
104
+ * row advancing `(ascender − descender) × fontSize`. Once a layout reply is
105
+ * in AND its shaped glyphs map back to the source 1:1 (unshaped LTR text —
106
+ * bidi, shaping, soft hyphens or `\r` all fall back to the coarse branch),
107
+ * per-line carriers pin each row's baseline exactly to the painted glyphs.
94
108
  */
95
109
  getContentProjection(): ContentProjection | null;
110
+ /**
111
+ * Group the worker's positioned glyphs into the same visual rows the canvas
112
+ * draws. Only runs when the reply's glyph sequence equals the source string
113
+ * (one glyph per source char, no bidi reordering, no shaping, no soft
114
+ * hyphens, no `\r`) — only then do glyph offsets line up with source offsets
115
+ * byte-for-byte, which is what keeps find-in-page and the Scene's dev-mode
116
+ * equality check correct. Every other text falls back to the coarse branch's
117
+ * `baseline` + `lineHeight`, which still pins the row rhythm.
118
+ */
119
+ private rebuildProjectionLines;
96
120
  getContentEpoch(): number;
97
121
  isPointInside(globalX: number, globalY: number): boolean;
98
122
  render(renderer: any): void;
package/dist/text.js CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
 
4
4
 
5
- var _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
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectojs/core",
3
- "version": "1.34.1",
3
+ "version": "1.34.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },