@vectojs/core 1.7.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/THIRD_PARTY_NOTICES.md +24 -0
- package/dist/chunk-4AR425AR.js +1121 -0
- package/dist/{chunk-ZBKKRYDH.js → chunk-BA5HUUDF.js} +8 -8
- package/dist/{chunk-DFNK6OV6.js → chunk-FIQAIF55.js} +206 -4
- package/dist/chunk-IESDTEJ4.mjs +1121 -0
- package/dist/{chunk-3CODWSF3.mjs → chunk-RQ2ETTBN.mjs} +204 -2
- package/dist/{chunk-V7GUWG5C.mjs → chunk-X7I465AQ.mjs} +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +829 -130
- package/dist/index.mjs +706 -7
- package/dist/layout.js +3 -3
- package/dist/layout.mjs +2 -2
- package/dist/text/BidiResolver.d.ts +0 -1
- package/dist/text/PreparedContentGrid.d.ts +60 -0
- package/dist/text/index.d.ts +1 -0
- package/dist/text.js +5 -3
- package/dist/text.mjs +6 -4
- package/dist/tree/Entity.d.ts +21 -0
- package/dist/tree/Scene.d.ts +29 -0
- package/package.json +5 -2
- package/dist/chunk-CTZQOM5Z.js +0 -418
- package/dist/chunk-STWPTWO4.mjs +0 -418
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
LayoutResultBuffer,
|
|
4
4
|
computeLineSegments,
|
|
5
5
|
createCanvasMeasurer
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-X7I465AQ.mjs";
|
|
7
7
|
import {
|
|
8
8
|
CanvasRenderer,
|
|
9
9
|
SVGRenderer,
|
|
@@ -25,13 +25,14 @@ import {
|
|
|
25
25
|
VectoJSEvent,
|
|
26
26
|
clearCssLineBoxMetrics,
|
|
27
27
|
cssLineBoxBaseline,
|
|
28
|
-
isTweenConfig
|
|
29
|
-
|
|
28
|
+
isTweenConfig,
|
|
29
|
+
prepareContentGrid
|
|
30
|
+
} from "./chunk-RQ2ETTBN.mjs";
|
|
30
31
|
import {
|
|
31
32
|
ArabicShaper,
|
|
32
33
|
BidiResolver,
|
|
33
34
|
LayoutWorkerManager
|
|
34
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-IESDTEJ4.mjs";
|
|
35
36
|
|
|
36
37
|
// src/tree/ComputeParticleEntity.ts
|
|
37
38
|
var PARTICLE_STRIDE_FLOATS = 8;
|
|
@@ -317,6 +318,297 @@ function parseInlinePx(value) {
|
|
|
317
318
|
const n = parseFloat(value);
|
|
318
319
|
return Number.isFinite(n) && n > 0 ? n : null;
|
|
319
320
|
}
|
|
321
|
+
function collectTextNodes(root) {
|
|
322
|
+
const out = [];
|
|
323
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
324
|
+
for (let n = walker.nextNode(); n; n = walker.nextNode()) out.push(n);
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
var caretGraphemeSegmenter = typeof Intl !== "undefined" && typeof Intl.Segmenter === "function" ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : null;
|
|
328
|
+
var caretWordSegmenter = typeof Intl !== "undefined" && typeof Intl.Segmenter === "function" ? new Intl.Segmenter(void 0, { granularity: "word" }) : null;
|
|
329
|
+
function graphemeBoundaries(text) {
|
|
330
|
+
if (!caretGraphemeSegmenter) {
|
|
331
|
+
const boundaries2 = [0];
|
|
332
|
+
for (let offset = 0; offset < text.length; ) {
|
|
333
|
+
const codePoint = text.codePointAt(offset) ?? 0;
|
|
334
|
+
offset += codePoint > 65535 ? 2 : 1;
|
|
335
|
+
boundaries2.push(offset);
|
|
336
|
+
}
|
|
337
|
+
return boundaries2;
|
|
338
|
+
}
|
|
339
|
+
const boundaries = [0];
|
|
340
|
+
for (const segment of caretGraphemeSegmenter.segment(text)) {
|
|
341
|
+
const end = segment.index + segment.segment.length;
|
|
342
|
+
if (end > boundaries[boundaries.length - 1]) boundaries.push(end);
|
|
343
|
+
}
|
|
344
|
+
return boundaries;
|
|
345
|
+
}
|
|
346
|
+
function distanceToRectSquared(rect, x, y) {
|
|
347
|
+
const dx = x < rect.left ? rect.left - x : x > rect.right ? x - rect.right : 0;
|
|
348
|
+
const dy = y < rect.top ? rect.top - y : y > rect.bottom ? y - rect.bottom : 0;
|
|
349
|
+
return dx * dx + dy * dy;
|
|
350
|
+
}
|
|
351
|
+
function nearestOffsetForPoint(node, x, y) {
|
|
352
|
+
const boundaries = graphemeBoundaries(node.data);
|
|
353
|
+
const range = document.createRange();
|
|
354
|
+
let nearest = { offset: boundaries[0] ?? 0, distance: Infinity };
|
|
355
|
+
for (const offset of boundaries) {
|
|
356
|
+
range.setStart(node, offset);
|
|
357
|
+
range.collapse(true);
|
|
358
|
+
const rect = range.getBoundingClientRect();
|
|
359
|
+
const distance = distanceToRectSquared(rect, x, y);
|
|
360
|
+
if (distance < nearest.distance) nearest = { offset, distance };
|
|
361
|
+
}
|
|
362
|
+
return nearest;
|
|
363
|
+
}
|
|
364
|
+
function gridCellCaret(cell, localX) {
|
|
365
|
+
const node = cell.firstChild;
|
|
366
|
+
if (!(node instanceof Text)) return null;
|
|
367
|
+
const sourceLength = Number(cell.dataset.vectoGridSourceLength ?? node.data.length);
|
|
368
|
+
const level = Number(cell.dataset.vectoGridLevel ?? 0);
|
|
369
|
+
const cellX = Number(cell.dataset.vectoGridX ?? 0);
|
|
370
|
+
const advance = Number(cell.dataset.vectoGridAdvance ?? 0);
|
|
371
|
+
const caretOffsets = (cell.dataset.vectoGridCaretOffsets ?? `0,${sourceLength}`).split(",").map(Number).filter((offset) => Number.isInteger(offset) && offset >= 0 && offset <= sourceLength);
|
|
372
|
+
const visuallyRtl = (level & 1) !== 0;
|
|
373
|
+
const visualFraction = advance > 0 ? Math.max(0, Math.min(1, (localX - cellX) / advance)) : 0;
|
|
374
|
+
const sourceFraction = visuallyRtl ? 1 - visualFraction : visualFraction;
|
|
375
|
+
const caretIndex = Math.round(sourceFraction * Math.max(0, caretOffsets.length - 1));
|
|
376
|
+
return {
|
|
377
|
+
node,
|
|
378
|
+
offset: caretOffsets[caretIndex] ?? 0
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function nearestGridPositionInLine(line, localX) {
|
|
382
|
+
let nearest = null;
|
|
383
|
+
for (const cell of line.querySelectorAll("[data-vecto-grid-cell]")) {
|
|
384
|
+
const x = Number(cell.dataset.vectoGridX ?? 0);
|
|
385
|
+
const advance = Number(cell.dataset.vectoGridAdvance ?? 0);
|
|
386
|
+
if (localX >= x && localX <= x + advance) return gridCellCaret(cell, localX);
|
|
387
|
+
const distance = localX < x ? x - localX : localX - (x + advance);
|
|
388
|
+
if (!nearest || distance < nearest.distance) nearest = { cell, distance };
|
|
389
|
+
}
|
|
390
|
+
if (!nearest) return null;
|
|
391
|
+
return gridCellCaret(nearest.cell, localX);
|
|
392
|
+
}
|
|
393
|
+
function parseCssMatrix(transform) {
|
|
394
|
+
if (!transform || transform === "none") return [1, 0, 0, 1];
|
|
395
|
+
const values = transform.slice(transform.indexOf("(") + 1, transform.lastIndexOf(")")).split(",").map(Number);
|
|
396
|
+
return values.length >= 4 && values.slice(0, 4).every(Number.isFinite) ? [values[0], values[1], values[2], values[3]] : [1, 0, 0, 1];
|
|
397
|
+
}
|
|
398
|
+
function clientToGridLocal(contentEl, canvas, clientX, clientY) {
|
|
399
|
+
const line = contentEl.querySelector("[data-vecto-grid-line]");
|
|
400
|
+
const originMarker = line?.querySelector('[data-vecto-grid-basis="origin"]');
|
|
401
|
+
const xMarker = line?.querySelector('[data-vecto-grid-basis="x"]');
|
|
402
|
+
const yMarker = line?.querySelector('[data-vecto-grid-basis="y"]');
|
|
403
|
+
if (line && originMarker && xMarker && yMarker) {
|
|
404
|
+
const origin = originMarker.getBoundingClientRect();
|
|
405
|
+
const xPoint = xMarker.getBoundingClientRect();
|
|
406
|
+
const yPoint = yMarker.getBoundingClientRect();
|
|
407
|
+
const xx = xPoint.left - origin.left;
|
|
408
|
+
const xy = xPoint.top - origin.top;
|
|
409
|
+
const yx = yPoint.left - origin.left;
|
|
410
|
+
const yy = yPoint.top - origin.top;
|
|
411
|
+
const determinant2 = xx * yy - xy * yx;
|
|
412
|
+
if (Number.isFinite(determinant2) && Math.abs(determinant2) > 1e-9) {
|
|
413
|
+
const dx2 = clientX - origin.left;
|
|
414
|
+
const dy2 = clientY - origin.top;
|
|
415
|
+
return {
|
|
416
|
+
x: (Number.parseFloat(line.style.left) || 0) + (yy * dx2 - yx * dy2) / determinant2,
|
|
417
|
+
y: (Number.parseFloat(line.style.top) || 0) + (-xy * dx2 + xx * dy2) / determinant2
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const [a, b, c, d] = parseCssMatrix(getComputedStyle(contentEl).transform);
|
|
422
|
+
const canvasRect = canvas.getBoundingClientRect();
|
|
423
|
+
const logicalWidth = Number.parseFloat(canvas.style.width) || canvas.clientWidth || canvas.width;
|
|
424
|
+
const logicalHeight = Number.parseFloat(canvas.style.height) || canvas.clientHeight || canvas.height;
|
|
425
|
+
const scaleX = logicalWidth > 0 ? canvasRect.width / logicalWidth : 1;
|
|
426
|
+
const scaleY = logicalHeight > 0 ? canvasRect.height / logicalHeight : 1;
|
|
427
|
+
const worldX = (clientX - canvasRect.left) / scaleX;
|
|
428
|
+
const worldY = (clientY - canvasRect.top) / scaleY;
|
|
429
|
+
const dx = worldX - (Number.parseFloat(contentEl.style.left) || 0);
|
|
430
|
+
const dy = worldY - (Number.parseFloat(contentEl.style.top) || 0);
|
|
431
|
+
const determinant = a * d - b * c;
|
|
432
|
+
if (!Number.isFinite(determinant) || Math.abs(determinant) <= 1e-9) return null;
|
|
433
|
+
return {
|
|
434
|
+
x: (d * dx - c * dy) / determinant,
|
|
435
|
+
y: (-b * dx + a * dy) / determinant
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function nearestGridPosition(contentEl, canvas, clientX, clientY) {
|
|
439
|
+
const lines = [...contentEl.querySelectorAll("[data-vecto-grid-line]")];
|
|
440
|
+
if (lines.length === 0) return null;
|
|
441
|
+
const [a, b, c, d] = parseCssMatrix(getComputedStyle(contentEl).transform);
|
|
442
|
+
if (a > 0 && d > 0 && Math.abs(b) <= 1e-9 && Math.abs(c) <= 1e-9) {
|
|
443
|
+
let nearest2 = null;
|
|
444
|
+
for (const line of lines) {
|
|
445
|
+
const rect = line.getBoundingClientRect();
|
|
446
|
+
const dy = clientY < rect.top ? rect.top - clientY : clientY > rect.bottom ? clientY - rect.bottom : 0;
|
|
447
|
+
const dx = clientX < rect.left ? rect.left - clientX : clientX > rect.right ? clientX - rect.right : 0;
|
|
448
|
+
const distance = dy * 4096 + dx;
|
|
449
|
+
if (!nearest2 || distance < nearest2.distance) nearest2 = { line, distance, rect };
|
|
450
|
+
}
|
|
451
|
+
if (!nearest2) return null;
|
|
452
|
+
const localWidth = Number.parseFloat(nearest2.line.style.width) || 0;
|
|
453
|
+
const scaleX = localWidth > 0 && nearest2.rect.width > 0 ? nearest2.rect.width / localWidth : 1;
|
|
454
|
+
const localX = (clientX - nearest2.rect.left) / scaleX;
|
|
455
|
+
return nearestGridPositionInLine(nearest2.line, localX);
|
|
456
|
+
}
|
|
457
|
+
const local = clientToGridLocal(contentEl, canvas, clientX, clientY);
|
|
458
|
+
if (!local) return null;
|
|
459
|
+
let nearest = null;
|
|
460
|
+
for (const line of lines) {
|
|
461
|
+
const left = Number.parseFloat(line.style.left) || 0;
|
|
462
|
+
const top = Number.parseFloat(line.style.top) || 0;
|
|
463
|
+
const width = Number.parseFloat(line.style.width) || 0;
|
|
464
|
+
const height = Number.parseFloat(line.style.height) || 0;
|
|
465
|
+
const dy = local.y < top ? top - local.y : local.y > top + height ? local.y - top - height : 0;
|
|
466
|
+
const dx = local.x < left ? left - local.x : local.x > left + width ? local.x - left - width : 0;
|
|
467
|
+
const distance = dy * 4096 + dx;
|
|
468
|
+
if (!nearest || distance < nearest.distance) nearest = { line, distance };
|
|
469
|
+
}
|
|
470
|
+
if (!nearest) return null;
|
|
471
|
+
const lineLeft = Number.parseFloat(nearest.line.style.left) || 0;
|
|
472
|
+
return nearestGridPositionInLine(nearest.line, local.x - lineLeft);
|
|
473
|
+
}
|
|
474
|
+
function nearestTextPositionInLine(line, x, y) {
|
|
475
|
+
const texts = collectTextNodes(line);
|
|
476
|
+
if (texts.length === 0) return null;
|
|
477
|
+
let nearest = null;
|
|
478
|
+
for (const node of texts) {
|
|
479
|
+
const candidate = nearestOffsetForPoint(node, x, y);
|
|
480
|
+
if (!nearest || candidate.distance < nearest.distance) {
|
|
481
|
+
let { offset } = candidate;
|
|
482
|
+
while (offset > 0 && node.data[offset - 1] === "\n") offset--;
|
|
483
|
+
nearest = { position: { node, offset }, distance: candidate.distance };
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return nearest?.position ?? null;
|
|
487
|
+
}
|
|
488
|
+
function nearestTextPositionInProjection(contentEl, canvas, x, y, eventTarget) {
|
|
489
|
+
if (contentEl.dataset.vectoContentGrid !== void 0) {
|
|
490
|
+
return nearestGridPosition(contentEl, canvas, x, y);
|
|
491
|
+
}
|
|
492
|
+
let targetLine = eventTarget;
|
|
493
|
+
while (targetLine && targetLine.parentElement !== contentEl) {
|
|
494
|
+
if (!contentEl.contains(targetLine)) {
|
|
495
|
+
targetLine = null;
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
targetLine = targetLine.parentElement;
|
|
499
|
+
}
|
|
500
|
+
if (targetLine?.parentElement === contentEl) {
|
|
501
|
+
return nearestTextPositionInLine(targetLine, x, y);
|
|
502
|
+
}
|
|
503
|
+
let bestLine = null;
|
|
504
|
+
let bestDist = Infinity;
|
|
505
|
+
for (let i = 0; i < contentEl.children.length; i++) {
|
|
506
|
+
const child = contentEl.children[i];
|
|
507
|
+
const rect = child.getBoundingClientRect();
|
|
508
|
+
if (rect.width <= 0 && rect.height <= 0) continue;
|
|
509
|
+
const dy = y < rect.top ? rect.top - y : y > rect.bottom ? y - rect.bottom : 0;
|
|
510
|
+
const dx = x < rect.left ? rect.left - x : x > rect.right ? x - rect.right : 0;
|
|
511
|
+
const dist = dy * 4096 + dx;
|
|
512
|
+
if (dist < bestDist) {
|
|
513
|
+
bestDist = dist;
|
|
514
|
+
bestLine = child;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (bestLine) return nearestTextPositionInLine(bestLine, x, y);
|
|
518
|
+
return nearestTextPositionInLine(contentEl, x, y);
|
|
519
|
+
}
|
|
520
|
+
function projectionAbsoluteOffset(root, caret) {
|
|
521
|
+
let offset = 0;
|
|
522
|
+
for (const node of collectTextNodes(root)) {
|
|
523
|
+
if (node === caret.node) return offset + Math.min(caret.offset, node.data.length);
|
|
524
|
+
offset += node.data.length;
|
|
525
|
+
}
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
function projectionCaretAt(root, absoluteOffset, affinity) {
|
|
529
|
+
const nodes = collectTextNodes(root);
|
|
530
|
+
if (nodes.length === 0) return null;
|
|
531
|
+
let remaining = Math.max(0, absoluteOffset);
|
|
532
|
+
for (let index = 0; index < nodes.length; index++) {
|
|
533
|
+
const node = nodes[index];
|
|
534
|
+
if (remaining < node.data.length || remaining === node.data.length && affinity === "backward") {
|
|
535
|
+
return { node, offset: remaining };
|
|
536
|
+
}
|
|
537
|
+
if (remaining === node.data.length && index === nodes.length - 1) {
|
|
538
|
+
return { node, offset: remaining };
|
|
539
|
+
}
|
|
540
|
+
remaining -= node.data.length;
|
|
541
|
+
}
|
|
542
|
+
const last = nodes[nodes.length - 1];
|
|
543
|
+
return { node: last, offset: last.data.length };
|
|
544
|
+
}
|
|
545
|
+
function selectProjectionUnit(selection, root, caret, unit) {
|
|
546
|
+
const absoluteOffset = projectionAbsoluteOffset(root, caret);
|
|
547
|
+
const text = root.textContent ?? "";
|
|
548
|
+
if (absoluteOffset === null || text.length === 0) return false;
|
|
549
|
+
let start = absoluteOffset;
|
|
550
|
+
let end = absoluteOffset;
|
|
551
|
+
if (unit === "line") {
|
|
552
|
+
for (let index = Math.max(0, absoluteOffset - 1); index >= 0; index--) {
|
|
553
|
+
if (text[index] === "\n" || text[index] === "\r") {
|
|
554
|
+
start = index + 1;
|
|
555
|
+
if (text[index] === "\r" && text[index + 1] === "\n") start++;
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const cr = text.indexOf("\r", absoluteOffset);
|
|
560
|
+
const lf = text.indexOf("\n", absoluteOffset);
|
|
561
|
+
const separator = [cr, lf].filter((index) => index >= 0).sort((a, b) => a - b)[0];
|
|
562
|
+
end = separator === void 0 ? text.length : separator;
|
|
563
|
+
} else if (caretWordSegmenter) {
|
|
564
|
+
const segments = [...caretWordSegmenter.segment(text)];
|
|
565
|
+
const selected = segments.find(
|
|
566
|
+
(segment) => segment.isWordLike && absoluteOffset >= segment.index && absoluteOffset <= segment.index + segment.segment.length
|
|
567
|
+
) ?? segments.find((segment) => segment.isWordLike && segment.index >= absoluteOffset) ?? [...segments].reverse().find((segment) => segment.isWordLike);
|
|
568
|
+
if (selected) {
|
|
569
|
+
start = selected.index;
|
|
570
|
+
end = selected.index + selected.segment.length;
|
|
571
|
+
}
|
|
572
|
+
} else {
|
|
573
|
+
const isWord = (character) => /[\p{L}\p{N}_]/u.test(character);
|
|
574
|
+
while (start > 0 && isWord(text[start - 1])) start--;
|
|
575
|
+
while (end < text.length && isWord(text[end])) end++;
|
|
576
|
+
}
|
|
577
|
+
const anchor = projectionCaretAt(root, start, "forward");
|
|
578
|
+
const focus = projectionCaretAt(root, end, "backward");
|
|
579
|
+
if (!anchor || !focus) return false;
|
|
580
|
+
selection.setBaseAndExtent(anchor.node, anchor.offset, focus.node, focus.offset);
|
|
581
|
+
return true;
|
|
582
|
+
}
|
|
583
|
+
function extendSelection(selection, anchor, focus) {
|
|
584
|
+
try {
|
|
585
|
+
selection.setBaseAndExtent(anchor.node, anchor.offset, focus.node, focus.offset);
|
|
586
|
+
return;
|
|
587
|
+
} catch {
|
|
588
|
+
}
|
|
589
|
+
try {
|
|
590
|
+
selection.collapse(anchor.node, anchor.offset);
|
|
591
|
+
selection.extend(focus.node, focus.offset);
|
|
592
|
+
return;
|
|
593
|
+
} catch {
|
|
594
|
+
}
|
|
595
|
+
const anchorRange = document.createRange();
|
|
596
|
+
anchorRange.setStart(anchor.node, anchor.offset);
|
|
597
|
+
anchorRange.collapse(true);
|
|
598
|
+
const focusRange = document.createRange();
|
|
599
|
+
focusRange.setStart(focus.node, focus.offset);
|
|
600
|
+
focusRange.collapse(true);
|
|
601
|
+
const range = document.createRange();
|
|
602
|
+
if (anchorRange.compareBoundaryPoints(Range.START_TO_START, focusRange) <= 0) {
|
|
603
|
+
range.setStart(anchor.node, anchor.offset);
|
|
604
|
+
range.setEnd(focus.node, focus.offset);
|
|
605
|
+
} else {
|
|
606
|
+
range.setStart(focus.node, focus.offset);
|
|
607
|
+
range.setEnd(anchor.node, anchor.offset);
|
|
608
|
+
}
|
|
609
|
+
selection.removeAllRanges();
|
|
610
|
+
selection.addRange(range);
|
|
611
|
+
}
|
|
320
612
|
var Scene = class _Scene {
|
|
321
613
|
static webglCreator = null;
|
|
322
614
|
static webgpuManagerClass = null;
|
|
@@ -373,7 +665,25 @@ var Scene = class _Scene {
|
|
|
373
665
|
a11yElements = /* @__PURE__ */ new Map();
|
|
374
666
|
/** DOM nodes mirroring static text content, keyed by entity id. */
|
|
375
667
|
contentElements = /* @__PURE__ */ new Map();
|
|
668
|
+
/** Pending cold font-calibration frame per projected grid entity. */
|
|
669
|
+
contentGridCalibrationFrames = /* @__PURE__ */ new Map();
|
|
670
|
+
/** Detached, untransformed font probes used by the cold calibration pass. */
|
|
671
|
+
contentGridCalibrationProbes = /* @__PURE__ */ new Map();
|
|
672
|
+
/** Invalidates grid font calibration after browser font availability changes. */
|
|
673
|
+
contentFontEpoch = 0;
|
|
674
|
+
/** Cached Canvas-to-client scale for the current font/viewport epoch. */
|
|
675
|
+
contentMetricScaleEpoch = -1;
|
|
676
|
+
contentMetricScaleX = 1;
|
|
376
677
|
contentProjectionEnabled = true;
|
|
678
|
+
/**
|
|
679
|
+
* True while a text-selection drag that started on a projection's blank
|
|
680
|
+
* region (no text node under the press) is being driven manually — the
|
|
681
|
+
* browser has no native anchor for it, so mousemove extends the Selection
|
|
682
|
+
* from the position we resolved ourselves.
|
|
683
|
+
*/
|
|
684
|
+
blankRegionSelectionDrag = false;
|
|
685
|
+
contentSelectionAnchor = null;
|
|
686
|
+
contentSelectionEndListener = null;
|
|
377
687
|
// Animation/interactive flags collected during the render walk (tree-walk
|
|
378
688
|
// fusion): the loop reads last frame's answers instead of re-walking the
|
|
379
689
|
// tree up to 4× per tick. Start true so the first tick stays conservative.
|
|
@@ -480,6 +790,90 @@ var Scene = class _Scene {
|
|
|
480
790
|
this.a11yRoot.style.pointerEvents = "none";
|
|
481
791
|
this.a11yRoot.style.overflow = "hidden";
|
|
482
792
|
this.a11yRoot.style.zIndex = "10";
|
|
793
|
+
this.a11yRoot.style.userSelect = "text";
|
|
794
|
+
this.a11yRoot.addEventListener("mousedown", (e) => {
|
|
795
|
+
if (e.button !== 0) return;
|
|
796
|
+
const target = e.target;
|
|
797
|
+
const contentEl = target.closest("[data-vecto-content]");
|
|
798
|
+
if (target === this.a11yRoot || !contentEl) return;
|
|
799
|
+
if (getComputedStyle(contentEl).pointerEvents !== "auto") return;
|
|
800
|
+
const selection = window.getSelection();
|
|
801
|
+
if (!selection) return;
|
|
802
|
+
this.a11yRoot.style.pointerEvents = "auto";
|
|
803
|
+
const resolved = nearestTextPositionInProjection(
|
|
804
|
+
contentEl,
|
|
805
|
+
this.canvas,
|
|
806
|
+
e.clientX,
|
|
807
|
+
e.clientY,
|
|
808
|
+
target
|
|
809
|
+
);
|
|
810
|
+
if (resolved) {
|
|
811
|
+
if (e.detail >= 2) {
|
|
812
|
+
selection.removeAllRanges();
|
|
813
|
+
selectProjectionUnit(selection, contentEl, resolved, e.detail >= 3 ? "line" : "word");
|
|
814
|
+
this.endContentSelectionDrag();
|
|
815
|
+
e.preventDefault();
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const existingAnchor = e.shiftKey && selection.anchorNode instanceof Text ? { node: selection.anchorNode, offset: selection.anchorOffset } : null;
|
|
819
|
+
const anchor = existingAnchor && this.a11yRoot.contains(existingAnchor.node) ? existingAnchor : resolved;
|
|
820
|
+
if (e.shiftKey && existingAnchor) extendSelection(selection, anchor, resolved);
|
|
821
|
+
else selection.collapse(resolved.node, resolved.offset);
|
|
822
|
+
this.contentSelectionAnchor = anchor;
|
|
823
|
+
this.blankRegionSelectionDrag = true;
|
|
824
|
+
e.preventDefault();
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
this.a11yRoot.addEventListener("dblclick", (e) => {
|
|
828
|
+
const target = e.target;
|
|
829
|
+
const contentEl = target.closest("[data-vecto-content]");
|
|
830
|
+
if (!contentEl || getComputedStyle(contentEl).pointerEvents !== "auto") return;
|
|
831
|
+
const selection = window.getSelection();
|
|
832
|
+
const caret = nearestTextPositionInProjection(
|
|
833
|
+
contentEl,
|
|
834
|
+
this.canvas,
|
|
835
|
+
e.clientX,
|
|
836
|
+
e.clientY,
|
|
837
|
+
target
|
|
838
|
+
);
|
|
839
|
+
if (!selection || !caret) return;
|
|
840
|
+
selection.removeAllRanges();
|
|
841
|
+
selectProjectionUnit(selection, contentEl, caret, "word");
|
|
842
|
+
this.endContentSelectionDrag();
|
|
843
|
+
e.preventDefault();
|
|
844
|
+
});
|
|
845
|
+
this.a11yRoot.addEventListener("mousemove", (e) => {
|
|
846
|
+
if (!this.blankRegionSelectionDrag) return;
|
|
847
|
+
const selection = window.getSelection();
|
|
848
|
+
if (!selection || selection.rangeCount === 0) return;
|
|
849
|
+
const target = e.target;
|
|
850
|
+
let contentEl = target.closest("[data-vecto-content]");
|
|
851
|
+
if (!contentEl) {
|
|
852
|
+
let bestDistance = Infinity;
|
|
853
|
+
for (const candidate of this.contentElements.values()) {
|
|
854
|
+
if (getComputedStyle(candidate).pointerEvents !== "auto") continue;
|
|
855
|
+
const rect = candidate.getBoundingClientRect();
|
|
856
|
+
const dx = e.clientX < rect.left ? rect.left - e.clientX : e.clientX > rect.right ? e.clientX - rect.right : 0;
|
|
857
|
+
const dy = e.clientY < rect.top ? rect.top - e.clientY : e.clientY > rect.bottom ? e.clientY - rect.bottom : 0;
|
|
858
|
+
const distance = dx * dx + dy * dy;
|
|
859
|
+
if (distance < bestDistance) {
|
|
860
|
+
bestDistance = distance;
|
|
861
|
+
contentEl = candidate;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const focus = contentEl ? nearestTextPositionInProjection(contentEl, this.canvas, e.clientX, e.clientY, target) : null;
|
|
866
|
+
const anchor = this.contentSelectionAnchor;
|
|
867
|
+
if (focus && anchor) {
|
|
868
|
+
extendSelection(selection, anchor, focus);
|
|
869
|
+
}
|
|
870
|
+
});
|
|
871
|
+
const endDrag = () => this.endContentSelectionDrag();
|
|
872
|
+
this.a11yRoot.addEventListener("mouseup", endDrag);
|
|
873
|
+
this.a11yRoot.addEventListener("mouseleave", endDrag);
|
|
874
|
+
window.addEventListener("mouseup", endDrag);
|
|
875
|
+
window.addEventListener("blur", endDrag);
|
|
876
|
+
this.contentSelectionEndListener = endDrag;
|
|
483
877
|
if (canvas.parentElement) {
|
|
484
878
|
canvas.parentElement.appendChild(this.a11yRoot);
|
|
485
879
|
}
|
|
@@ -522,6 +916,7 @@ var Scene = class _Scene {
|
|
|
522
916
|
if (typeof document !== "undefined" && document.fonts) {
|
|
523
917
|
this.fontLoadHandler = () => {
|
|
524
918
|
clearCssLineBoxMetrics();
|
|
919
|
+
this.contentFontEpoch++;
|
|
525
920
|
this.markDirty();
|
|
526
921
|
};
|
|
527
922
|
document.fonts.ready.then(this.fontLoadHandler);
|
|
@@ -529,6 +924,18 @@ var Scene = class _Scene {
|
|
|
529
924
|
}
|
|
530
925
|
this.setupEvents();
|
|
531
926
|
}
|
|
927
|
+
endContentSelectionDrag() {
|
|
928
|
+
this.blankRegionSelectionDrag = false;
|
|
929
|
+
this.contentSelectionAnchor = null;
|
|
930
|
+
if (this.a11yRoot) this.a11yRoot.style.pointerEvents = "none";
|
|
931
|
+
}
|
|
932
|
+
releaseContentSelectionForRebuild(el) {
|
|
933
|
+
const selection = typeof window !== "undefined" && typeof window.getSelection === "function" ? window.getSelection() : null;
|
|
934
|
+
const ownsSelection = this.contentSelectionAnchor && el.contains(this.contentSelectionAnchor.node) || (selection?.anchorNode ? el.contains(selection.anchorNode) : false) || (selection?.focusNode ? el.contains(selection.focusNode) : false);
|
|
935
|
+
if (!ownsSelection) return;
|
|
936
|
+
this.endContentSelectionDrag();
|
|
937
|
+
selection?.removeAllRanges();
|
|
938
|
+
}
|
|
532
939
|
/**
|
|
533
940
|
* Expose the underlying {@link IRenderer} for advanced direct-draw operations.
|
|
534
941
|
*
|
|
@@ -559,6 +966,24 @@ var Scene = class _Scene {
|
|
|
559
966
|
this.root.add(entity);
|
|
560
967
|
return this;
|
|
561
968
|
}
|
|
969
|
+
clearContentGridState(entityId, el) {
|
|
970
|
+
const calibrationFrame = this.contentGridCalibrationFrames.get(entityId);
|
|
971
|
+
if (calibrationFrame !== void 0 && typeof cancelAnimationFrame === "function") {
|
|
972
|
+
cancelAnimationFrame(calibrationFrame);
|
|
973
|
+
}
|
|
974
|
+
this.contentGridCalibrationFrames.delete(entityId);
|
|
975
|
+
this.contentGridCalibrationProbes.get(entityId)?.remove();
|
|
976
|
+
this.contentGridCalibrationProbes.delete(entityId);
|
|
977
|
+
delete el.dataset.vectoGridCalibrationPending;
|
|
978
|
+
delete el.dataset.vectoGridCalibration;
|
|
979
|
+
delete el.dataset.vectoGridReady;
|
|
980
|
+
delete el.dataset.vectoContentGrid;
|
|
981
|
+
delete el.dataset.vectoGridCarriers;
|
|
982
|
+
delete el.dataset.vectoGridMaterializeMs;
|
|
983
|
+
delete el.dataset.vectoGridCalibrationSamples;
|
|
984
|
+
delete el.dataset.vectoGridCalibrationMs;
|
|
985
|
+
this.releaseContentSelectionForRebuild(el);
|
|
986
|
+
}
|
|
562
987
|
removeA11yRecursively(node) {
|
|
563
988
|
if (node.isDOMPortal) {
|
|
564
989
|
node.domElement.remove();
|
|
@@ -568,6 +993,7 @@ var Scene = class _Scene {
|
|
|
568
993
|
}
|
|
569
994
|
const contentEl = this.contentElements.get(node.id);
|
|
570
995
|
if (contentEl) {
|
|
996
|
+
this.clearContentGridState(node.id, contentEl);
|
|
571
997
|
contentEl.remove();
|
|
572
998
|
this.contentElements.delete(node.id);
|
|
573
999
|
this.a11yNeedsReorder = true;
|
|
@@ -649,6 +1075,11 @@ var Scene = class _Scene {
|
|
|
649
1075
|
if (typeof window !== "undefined" && !this.disableWindowResize) {
|
|
650
1076
|
window.removeEventListener("resize", this.resizeHandler);
|
|
651
1077
|
}
|
|
1078
|
+
if (typeof window !== "undefined" && this.contentSelectionEndListener) {
|
|
1079
|
+
window.removeEventListener("mouseup", this.contentSelectionEndListener);
|
|
1080
|
+
window.removeEventListener("blur", this.contentSelectionEndListener);
|
|
1081
|
+
this.contentSelectionEndListener = null;
|
|
1082
|
+
}
|
|
652
1083
|
if (typeof window !== "undefined" && this.canvas && typeof this.canvas.removeEventListener === "function") {
|
|
653
1084
|
if (this.pointerMoveListener) {
|
|
654
1085
|
this.canvas.removeEventListener("pointermove", this.pointerMoveListener);
|
|
@@ -662,6 +1093,15 @@ var Scene = class _Scene {
|
|
|
662
1093
|
this.a11yElements.clear();
|
|
663
1094
|
for (const el of this.contentElements.values()) el.remove();
|
|
664
1095
|
this.contentElements.clear();
|
|
1096
|
+
if (typeof cancelAnimationFrame === "function") {
|
|
1097
|
+
for (const frame of this.contentGridCalibrationFrames.values()) {
|
|
1098
|
+
cancelAnimationFrame(frame);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
this.contentGridCalibrationFrames.clear();
|
|
1102
|
+
for (const probe of this.contentGridCalibrationProbes.values()) probe.remove();
|
|
1103
|
+
this.contentGridCalibrationProbes.clear();
|
|
1104
|
+
this.endContentSelectionDrag();
|
|
665
1105
|
this.pointRenderer?.destroy();
|
|
666
1106
|
this.renderer.dispose?.();
|
|
667
1107
|
this.glCanvas?.remove();
|
|
@@ -946,6 +1386,10 @@ var Scene = class _Scene {
|
|
|
946
1386
|
if (attrs.label !== void 0 && el.getAttribute("aria-label") !== attrs.label) {
|
|
947
1387
|
el.setAttribute("aria-label", attrs.label);
|
|
948
1388
|
}
|
|
1389
|
+
const semanticPointerEvents = attrs.pointerEvents ?? "auto";
|
|
1390
|
+
if (el.style.pointerEvents !== semanticPointerEvents) {
|
|
1391
|
+
el.style.pointerEvents = semanticPointerEvents;
|
|
1392
|
+
}
|
|
949
1393
|
const implicitTabIndex = !isNativelyFocusable(el) && attrs.role && INTERACTIVE_A11Y_ROLES.has(attrs.role) ? 0 : null;
|
|
950
1394
|
const desiredTabIndex = attrs.tabIndex ?? implicitTabIndex;
|
|
951
1395
|
if (desiredTabIndex === null) {
|
|
@@ -1062,6 +1506,7 @@ var Scene = class _Scene {
|
|
|
1062
1506
|
let el = this.contentElements.get(node.id);
|
|
1063
1507
|
if (!projection || !projection.text) {
|
|
1064
1508
|
if (el) {
|
|
1509
|
+
this.clearContentGridState(node.id, el);
|
|
1065
1510
|
el.remove();
|
|
1066
1511
|
this.contentElements.delete(node.id);
|
|
1067
1512
|
this.a11yNeedsReorder = true;
|
|
@@ -1080,7 +1525,6 @@ var Scene = class _Scene {
|
|
|
1080
1525
|
s.forcedColorAdjust = "none";
|
|
1081
1526
|
s.setProperty("-webkit-text-fill-color", "transparent");
|
|
1082
1527
|
s.whiteSpace = "pre-wrap";
|
|
1083
|
-
s.overflow = "hidden";
|
|
1084
1528
|
s.zIndex = "0";
|
|
1085
1529
|
el.addEventListener(
|
|
1086
1530
|
"wheel",
|
|
@@ -1094,13 +1538,19 @@ var Scene = class _Scene {
|
|
|
1094
1538
|
this.a11yNeedsReorder = true;
|
|
1095
1539
|
}
|
|
1096
1540
|
const lines = projection.lines;
|
|
1097
|
-
if (
|
|
1541
|
+
if (!projection.grid && el.dataset.vectoContentGrid !== void 0) {
|
|
1542
|
+
this.clearContentGridState(node.id, el);
|
|
1543
|
+
}
|
|
1544
|
+
if (projection.grid) {
|
|
1545
|
+
this.syncContentGridProjection(node, el, projection, projection.grid);
|
|
1546
|
+
} else if (lines && lines.length > 0) {
|
|
1098
1547
|
const signature = JSON.stringify({
|
|
1099
1548
|
lines,
|
|
1100
1549
|
fallbackFont: projection.font ?? "",
|
|
1101
1550
|
fallbackLineHeight: projection.lineHeight ?? 16
|
|
1102
1551
|
});
|
|
1103
1552
|
if (el.dataset.vectoProjectionLines !== signature) {
|
|
1553
|
+
this.releaseContentSelectionForRebuild(el);
|
|
1104
1554
|
el.replaceChildren();
|
|
1105
1555
|
for (let index = 0; index < lines.length; index++) {
|
|
1106
1556
|
const line = lines[index];
|
|
@@ -1132,13 +1582,21 @@ var Scene = class _Scene {
|
|
|
1132
1582
|
el.dataset.vectoProjectionLines = signature;
|
|
1133
1583
|
}
|
|
1134
1584
|
} else {
|
|
1135
|
-
if (el.textContent !== projection.text)
|
|
1585
|
+
if (el.textContent !== projection.text) {
|
|
1586
|
+
this.releaseContentSelectionForRebuild(el);
|
|
1587
|
+
el.textContent = projection.text;
|
|
1588
|
+
}
|
|
1136
1589
|
delete el.dataset.vectoProjectionLines;
|
|
1137
1590
|
}
|
|
1138
1591
|
const font = projection.font ?? "";
|
|
1139
1592
|
if (el.style.font !== font) el.style.font = font;
|
|
1140
1593
|
const lineHeight = projection.lineHeight !== void 0 ? `${projection.lineHeight}px` : "";
|
|
1141
1594
|
if (el.style.lineHeight !== lineHeight) el.style.lineHeight = lineHeight;
|
|
1595
|
+
const ligatures = projection.ligatures === "none" ? "none" : "";
|
|
1596
|
+
if (el.style.getPropertyValue("font-variant-ligatures") !== ligatures) {
|
|
1597
|
+
el.style.setProperty("font-variant-ligatures", ligatures);
|
|
1598
|
+
el.style.setProperty("font-kerning", ligatures ? "none" : "");
|
|
1599
|
+
}
|
|
1142
1600
|
const hidden = node.interactive ? "true" : null;
|
|
1143
1601
|
if (el.getAttribute("aria-hidden") !== hidden) {
|
|
1144
1602
|
if (hidden) el.setAttribute("aria-hidden", hidden);
|
|
@@ -1200,6 +1658,245 @@ var Scene = class _Scene {
|
|
|
1200
1658
|
const display = visible ? "" : "none";
|
|
1201
1659
|
if (el.style.display !== display) el.style.display = display;
|
|
1202
1660
|
}
|
|
1661
|
+
/**
|
|
1662
|
+
* Materialize a prepared grid in logical source order while positioning each
|
|
1663
|
+
* carrier from the shared canvas geometry. Browser font measurement happens
|
|
1664
|
+
* later in one cold read/write batch, never inside projection synchronization.
|
|
1665
|
+
*/
|
|
1666
|
+
syncContentGridProjection(node, el, projection, grid) {
|
|
1667
|
+
if (grid.source !== projection.text) {
|
|
1668
|
+
throw new Error("ContentProjection.grid.source must equal ContentProjection.text");
|
|
1669
|
+
}
|
|
1670
|
+
const signature = `${grid.revision}`;
|
|
1671
|
+
if (el.dataset.vectoContentGrid !== signature) {
|
|
1672
|
+
const materializeStart = typeof performance !== "undefined" ? performance.now() : 0;
|
|
1673
|
+
this.clearContentGridState(node.id, el);
|
|
1674
|
+
el.replaceChildren();
|
|
1675
|
+
const projectionLines = projection.lines ?? [];
|
|
1676
|
+
for (let lineIndex = 0; lineIndex < grid.lines.length; lineIndex++) {
|
|
1677
|
+
const gridLine = grid.lines[lineIndex];
|
|
1678
|
+
const projectedLine = projectionLines[lineIndex];
|
|
1679
|
+
const lineHeight = projectedLine?.lineHeight ?? grid.lineHeight;
|
|
1680
|
+
const baseline = projectedLine?.baseline ?? grid.baseline;
|
|
1681
|
+
const lineFont = projectedLine?.font ?? grid.font;
|
|
1682
|
+
const lineElement = document.createElement("span");
|
|
1683
|
+
lineElement.dir = "ltr";
|
|
1684
|
+
lineElement.dataset.vectoGridLine = `${lineIndex}`;
|
|
1685
|
+
lineElement.style.position = "absolute";
|
|
1686
|
+
lineElement.style.left = `${projectedLine?.x ?? 0}px`;
|
|
1687
|
+
lineElement.style.top = `${(projectedLine?.y ?? lineIndex * grid.lineHeight) + baseline - cssLineBoxBaseline(lineFont, lineHeight)}px`;
|
|
1688
|
+
lineElement.style.width = `${gridLine.width}px`;
|
|
1689
|
+
lineElement.style.height = `${lineHeight}px`;
|
|
1690
|
+
lineElement.style.whiteSpace = "pre";
|
|
1691
|
+
lineElement.style.font = lineFont;
|
|
1692
|
+
lineElement.style.lineHeight = `${lineHeight}px`;
|
|
1693
|
+
if (gridLine.cells.length === 0) {
|
|
1694
|
+
lineElement.textContent = grid.source.slice(gridLine.sourceEnd, gridLine.nextSourceStart);
|
|
1695
|
+
} else {
|
|
1696
|
+
let logicalX = 0;
|
|
1697
|
+
for (let cellIndex = 0; cellIndex < gridLine.cells.length; cellIndex++) {
|
|
1698
|
+
const cell = gridLine.cells[cellIndex];
|
|
1699
|
+
const cellElement = document.createElement("span");
|
|
1700
|
+
cellElement.dir = "ltr";
|
|
1701
|
+
const separator = cellIndex === gridLine.cells.length - 1 ? grid.source.slice(gridLine.sourceEnd, gridLine.nextSourceStart) : "";
|
|
1702
|
+
const sourceText = grid.source.slice(cell.sourceStart, cell.sourceEnd);
|
|
1703
|
+
cellElement.textContent = sourceText + separator;
|
|
1704
|
+
cellElement.dataset.vectoGridCell = `${cellIndex}`;
|
|
1705
|
+
cellElement.dataset.vectoGridSourceLength = `${sourceText.length}`;
|
|
1706
|
+
cellElement.dataset.vectoGridSourceStart = `${cell.sourceStart}`;
|
|
1707
|
+
cellElement.dataset.vectoGridSourceEnd = `${cell.sourceEnd}`;
|
|
1708
|
+
cellElement.dataset.vectoGridCaretOffsets = cell.sourceCaretOffsets.join(",");
|
|
1709
|
+
cellElement.dataset.vectoGridLevel = `${cell.level}`;
|
|
1710
|
+
cellElement.dataset.vectoGridAdvance = `${cell.advance}`;
|
|
1711
|
+
cellElement.dataset.vectoGridX = `${cell.x}`;
|
|
1712
|
+
cellElement.style.position = "relative";
|
|
1713
|
+
cellElement.style.display = "inline-block";
|
|
1714
|
+
cellElement.style.left = `${cell.x - logicalX}px`;
|
|
1715
|
+
cellElement.style.top = "0";
|
|
1716
|
+
cellElement.style.width = `${cell.advance}px`;
|
|
1717
|
+
cellElement.style.height = `${lineHeight}px`;
|
|
1718
|
+
cellElement.style.boxSizing = "border-box";
|
|
1719
|
+
cellElement.style.verticalAlign = "top";
|
|
1720
|
+
cellElement.style.whiteSpace = "pre";
|
|
1721
|
+
cellElement.style.font = lineFont;
|
|
1722
|
+
cellElement.style.lineHeight = `${lineHeight}px`;
|
|
1723
|
+
cellElement.style.transformOrigin = "0 50%";
|
|
1724
|
+
lineElement.appendChild(cellElement);
|
|
1725
|
+
logicalX += cell.advance;
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
if (lineIndex === 0) {
|
|
1729
|
+
for (const [basis, left, top] of [
|
|
1730
|
+
["origin", 0, 0],
|
|
1731
|
+
["x", 1, 0],
|
|
1732
|
+
["y", 0, 1]
|
|
1733
|
+
]) {
|
|
1734
|
+
const marker = document.createElement("span");
|
|
1735
|
+
marker.dataset.vectoGridBasis = basis;
|
|
1736
|
+
marker.setAttribute("aria-hidden", "true");
|
|
1737
|
+
marker.style.position = "absolute";
|
|
1738
|
+
marker.style.left = `${left}px`;
|
|
1739
|
+
marker.style.top = `${top}px`;
|
|
1740
|
+
marker.style.width = "0";
|
|
1741
|
+
marker.style.height = "0";
|
|
1742
|
+
marker.style.pointerEvents = "none";
|
|
1743
|
+
marker.style.userSelect = "none";
|
|
1744
|
+
lineElement.appendChild(marker);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
el.appendChild(lineElement);
|
|
1748
|
+
}
|
|
1749
|
+
el.dataset.vectoProjectionLines = signature;
|
|
1750
|
+
el.dataset.vectoContentGrid = signature;
|
|
1751
|
+
el.dataset.vectoGridCarriers = `${el.querySelectorAll("[data-vecto-grid-cell]").length}`;
|
|
1752
|
+
if (typeof performance !== "undefined") {
|
|
1753
|
+
el.dataset.vectoGridMaterializeMs = `${performance.now() - materializeStart}`;
|
|
1754
|
+
}
|
|
1755
|
+
delete el.dataset.vectoGridCalibration;
|
|
1756
|
+
delete el.dataset.vectoGridReady;
|
|
1757
|
+
}
|
|
1758
|
+
const pageScaleX = this.getContentMetricScaleX();
|
|
1759
|
+
const calibrationKey = `${signature}:${this.contentFontEpoch}:${pageScaleX.toFixed(4)}`;
|
|
1760
|
+
if (el.dataset.vectoGridCalibration !== calibrationKey) {
|
|
1761
|
+
this.scheduleContentGridCalibration(node.id, el, calibrationKey, pageScaleX);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
getContentMetricScaleX() {
|
|
1765
|
+
if (this.contentMetricScaleEpoch === this.contentFontEpoch) {
|
|
1766
|
+
return this.contentMetricScaleX;
|
|
1767
|
+
}
|
|
1768
|
+
const rect = this.canvas.getBoundingClientRect();
|
|
1769
|
+
const inlineWidth = parseInlinePx(this.canvas.style.width);
|
|
1770
|
+
const logicalWidth = inlineWidth ?? (this.canvas.clientWidth || this.width);
|
|
1771
|
+
const scale = logicalWidth > 0 ? rect.width / logicalWidth : 1;
|
|
1772
|
+
this.contentMetricScaleX = Number.isFinite(scale) && scale > 0 ? scale : 1;
|
|
1773
|
+
this.contentMetricScaleEpoch = this.contentFontEpoch;
|
|
1774
|
+
return this.contentMetricScaleX;
|
|
1775
|
+
}
|
|
1776
|
+
scheduleContentGridCalibration(entityId, el, calibrationKey, pageScaleX) {
|
|
1777
|
+
if (typeof requestAnimationFrame !== "function") return;
|
|
1778
|
+
if (el.dataset.vectoGridCalibrationPending === calibrationKey) return;
|
|
1779
|
+
const previous = this.contentGridCalibrationFrames.get(entityId);
|
|
1780
|
+
if (previous !== void 0 && typeof cancelAnimationFrame === "function") {
|
|
1781
|
+
cancelAnimationFrame(previous);
|
|
1782
|
+
}
|
|
1783
|
+
this.contentGridCalibrationProbes.get(entityId)?.remove();
|
|
1784
|
+
this.contentGridCalibrationProbes.delete(entityId);
|
|
1785
|
+
const calibrationStart = typeof performance !== "undefined" ? performance.now() : 0;
|
|
1786
|
+
const probe = document.createElement("div");
|
|
1787
|
+
probe.setAttribute("aria-hidden", "true");
|
|
1788
|
+
probe.dataset.vectoGridProbe = entityId;
|
|
1789
|
+
probe.style.position = "absolute";
|
|
1790
|
+
probe.style.left = "-100000px";
|
|
1791
|
+
probe.style.top = "0";
|
|
1792
|
+
probe.style.width = "100000px";
|
|
1793
|
+
probe.style.height = "1px";
|
|
1794
|
+
probe.style.visibility = "hidden";
|
|
1795
|
+
probe.style.pointerEvents = "none";
|
|
1796
|
+
probe.style.whiteSpace = "pre";
|
|
1797
|
+
probe.style.contain = "layout style paint";
|
|
1798
|
+
const probeOrigin = document.createElement("span");
|
|
1799
|
+
probeOrigin.style.position = "absolute";
|
|
1800
|
+
probeOrigin.style.left = "0";
|
|
1801
|
+
probeOrigin.style.top = "0";
|
|
1802
|
+
const probeX = document.createElement("span");
|
|
1803
|
+
probeX.style.position = "absolute";
|
|
1804
|
+
probeX.style.left = "1px";
|
|
1805
|
+
probeX.style.top = "0";
|
|
1806
|
+
probe.append(probeOrigin, probeX);
|
|
1807
|
+
const measurements = [];
|
|
1808
|
+
const measurementsByKey = /* @__PURE__ */ new Map();
|
|
1809
|
+
for (const target of el.querySelectorAll("[data-vecto-grid-cell]")) {
|
|
1810
|
+
const sourceLength = Number(target.dataset.vectoGridSourceLength ?? 0);
|
|
1811
|
+
const targetWidth = Number(target.dataset.vectoGridAdvance ?? 0);
|
|
1812
|
+
if (sourceLength <= 0 || targetWidth <= 0) continue;
|
|
1813
|
+
const sourceText = target.textContent?.slice(0, sourceLength) ?? "";
|
|
1814
|
+
if (!sourceText) continue;
|
|
1815
|
+
const measurementKey = JSON.stringify([
|
|
1816
|
+
target.style.font,
|
|
1817
|
+
target.style.lineHeight,
|
|
1818
|
+
targetWidth,
|
|
1819
|
+
sourceText
|
|
1820
|
+
]);
|
|
1821
|
+
const shared = measurementsByKey.get(measurementKey);
|
|
1822
|
+
if (shared) {
|
|
1823
|
+
shared.targets.push(target);
|
|
1824
|
+
continue;
|
|
1825
|
+
}
|
|
1826
|
+
const carrier = document.createElement("span");
|
|
1827
|
+
carrier.dir = "ltr";
|
|
1828
|
+
carrier.style.position = "absolute";
|
|
1829
|
+
carrier.style.left = "0";
|
|
1830
|
+
carrier.style.top = "0";
|
|
1831
|
+
carrier.style.whiteSpace = "pre";
|
|
1832
|
+
carrier.style.font = target.style.font;
|
|
1833
|
+
carrier.style.lineHeight = target.style.lineHeight;
|
|
1834
|
+
carrier.style.fontVariantLigatures = "none";
|
|
1835
|
+
carrier.style.fontKerning = "none";
|
|
1836
|
+
const source = document.createTextNode(sourceText);
|
|
1837
|
+
carrier.appendChild(source);
|
|
1838
|
+
probe.appendChild(carrier);
|
|
1839
|
+
const measurement = { targets: [target], targetWidth, sourceLength, source };
|
|
1840
|
+
measurements.push(measurement);
|
|
1841
|
+
measurementsByKey.set(measurementKey, measurement);
|
|
1842
|
+
}
|
|
1843
|
+
(this.a11yRoot ?? document.body ?? document.documentElement).appendChild(probe);
|
|
1844
|
+
el.dataset.vectoGridCalibrationSamples = `${measurements.length}`;
|
|
1845
|
+
this.contentGridCalibrationProbes.set(entityId, probe);
|
|
1846
|
+
el.dataset.vectoGridCalibrationPending = calibrationKey;
|
|
1847
|
+
delete el.dataset.vectoGridReady;
|
|
1848
|
+
const readFrame = requestAnimationFrame(() => {
|
|
1849
|
+
if (!el.isConnected || el.dataset.vectoGridCalibrationPending !== calibrationKey) {
|
|
1850
|
+
probe.remove();
|
|
1851
|
+
this.contentGridCalibrationProbes.delete(entityId);
|
|
1852
|
+
this.contentGridCalibrationFrames.delete(entityId);
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
const updates = [];
|
|
1856
|
+
const probeOriginRect = probeOrigin.getBoundingClientRect();
|
|
1857
|
+
const probeXRect = probeX.getBoundingClientRect();
|
|
1858
|
+
const basisScale = Math.abs(probeXRect.left - probeOriginRect.left);
|
|
1859
|
+
const projectionPageScaleX = Number.isFinite(basisScale) && basisScale > 0 ? basisScale : pageScaleX;
|
|
1860
|
+
let valid = true;
|
|
1861
|
+
for (const measurement of measurements) {
|
|
1862
|
+
const range = document.createRange();
|
|
1863
|
+
range.setStart(measurement.source, 0);
|
|
1864
|
+
range.setEnd(measurement.source, measurement.sourceLength);
|
|
1865
|
+
const natural = range.getBoundingClientRect().width;
|
|
1866
|
+
if (!Number.isFinite(natural) || natural <= 0) {
|
|
1867
|
+
valid = false;
|
|
1868
|
+
break;
|
|
1869
|
+
}
|
|
1870
|
+
const scale = measurement.targetWidth * projectionPageScaleX / natural;
|
|
1871
|
+
for (const element of measurement.targets) updates.push({ element, scale });
|
|
1872
|
+
}
|
|
1873
|
+
probe.remove();
|
|
1874
|
+
this.contentGridCalibrationProbes.delete(entityId);
|
|
1875
|
+
if (!valid) {
|
|
1876
|
+
delete el.dataset.vectoGridCalibrationPending;
|
|
1877
|
+
this.contentGridCalibrationFrames.delete(entityId);
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
const writeFrame = requestAnimationFrame(() => {
|
|
1881
|
+
if (!el.isConnected || el.dataset.vectoGridCalibrationPending !== calibrationKey) {
|
|
1882
|
+
this.contentGridCalibrationFrames.delete(entityId);
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
for (const { element, scale } of updates) {
|
|
1886
|
+
element.style.transform = Math.abs(scale - 1) <= 1e-3 ? "" : `scaleX(${scale})`;
|
|
1887
|
+
}
|
|
1888
|
+
el.dataset.vectoGridCalibration = calibrationKey;
|
|
1889
|
+
el.dataset.vectoGridReady = "true";
|
|
1890
|
+
if (typeof performance !== "undefined") {
|
|
1891
|
+
el.dataset.vectoGridCalibrationMs = `${performance.now() - calibrationStart}`;
|
|
1892
|
+
}
|
|
1893
|
+
delete el.dataset.vectoGridCalibrationPending;
|
|
1894
|
+
this.contentGridCalibrationFrames.delete(entityId);
|
|
1895
|
+
});
|
|
1896
|
+
this.contentGridCalibrationFrames.set(entityId, writeFrame);
|
|
1897
|
+
});
|
|
1898
|
+
this.contentGridCalibrationFrames.set(entityId, readFrame);
|
|
1899
|
+
}
|
|
1203
1900
|
enforceA11yDomOrder() {
|
|
1204
1901
|
if (!this.a11yRoot) return;
|
|
1205
1902
|
this.fullViewportElements.length = 0;
|
|
@@ -1727,6 +2424,7 @@ var Scene = class _Scene {
|
|
|
1727
2424
|
resize(width, height) {
|
|
1728
2425
|
this.width = width;
|
|
1729
2426
|
this.height = height;
|
|
2427
|
+
this.contentFontEpoch++;
|
|
1730
2428
|
if (typeof this.renderer.resize === "function") {
|
|
1731
2429
|
this.renderer.resize(width, height);
|
|
1732
2430
|
}
|
|
@@ -2654,5 +3352,6 @@ export {
|
|
|
2654
3352
|
loadSpline,
|
|
2655
3353
|
parseColorToRGBA,
|
|
2656
3354
|
polySegmentToBezier,
|
|
3355
|
+
prepareContentGrid,
|
|
2657
3356
|
sanitizeUrl
|
|
2658
3357
|
};
|