@vectojs/core 1.7.1 → 1.9.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-GKLTTGBR.js} +234 -7
- package/dist/chunk-IESDTEJ4.mjs +1121 -0
- package/dist/{chunk-3CODWSF3.mjs → chunk-J2P6QZJV.mjs} +232 -5
- package/dist/{chunk-V7GUWG5C.mjs → chunk-X7I465AQ.mjs} +1 -1
- package/dist/components/Circle.d.ts +48 -0
- package/dist/components/Group.d.ts +24 -0
- package/dist/components/Rect.d.ts +48 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +947 -134
- package/dist/index.mjs +824 -11
- 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 +40 -2
- 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-J2P6QZJV.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.
|
|
@@ -482,16 +792,88 @@ var Scene = class _Scene {
|
|
|
482
792
|
this.a11yRoot.style.zIndex = "10";
|
|
483
793
|
this.a11yRoot.style.userSelect = "text";
|
|
484
794
|
this.a11yRoot.addEventListener("mousedown", (e) => {
|
|
795
|
+
if (e.button !== 0) return;
|
|
485
796
|
const target = e.target;
|
|
486
|
-
|
|
487
|
-
|
|
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();
|
|
488
825
|
}
|
|
489
826
|
});
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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();
|
|
493
872
|
this.a11yRoot.addEventListener("mouseup", endDrag);
|
|
494
873
|
this.a11yRoot.addEventListener("mouseleave", endDrag);
|
|
874
|
+
window.addEventListener("mouseup", endDrag);
|
|
875
|
+
window.addEventListener("blur", endDrag);
|
|
876
|
+
this.contentSelectionEndListener = endDrag;
|
|
495
877
|
if (canvas.parentElement) {
|
|
496
878
|
canvas.parentElement.appendChild(this.a11yRoot);
|
|
497
879
|
}
|
|
@@ -534,6 +916,7 @@ var Scene = class _Scene {
|
|
|
534
916
|
if (typeof document !== "undefined" && document.fonts) {
|
|
535
917
|
this.fontLoadHandler = () => {
|
|
536
918
|
clearCssLineBoxMetrics();
|
|
919
|
+
this.contentFontEpoch++;
|
|
537
920
|
this.markDirty();
|
|
538
921
|
};
|
|
539
922
|
document.fonts.ready.then(this.fontLoadHandler);
|
|
@@ -541,6 +924,18 @@ var Scene = class _Scene {
|
|
|
541
924
|
}
|
|
542
925
|
this.setupEvents();
|
|
543
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
|
+
}
|
|
544
939
|
/**
|
|
545
940
|
* Expose the underlying {@link IRenderer} for advanced direct-draw operations.
|
|
546
941
|
*
|
|
@@ -571,6 +966,24 @@ var Scene = class _Scene {
|
|
|
571
966
|
this.root.add(entity);
|
|
572
967
|
return this;
|
|
573
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
|
+
}
|
|
574
987
|
removeA11yRecursively(node) {
|
|
575
988
|
if (node.isDOMPortal) {
|
|
576
989
|
node.domElement.remove();
|
|
@@ -580,6 +993,7 @@ var Scene = class _Scene {
|
|
|
580
993
|
}
|
|
581
994
|
const contentEl = this.contentElements.get(node.id);
|
|
582
995
|
if (contentEl) {
|
|
996
|
+
this.clearContentGridState(node.id, contentEl);
|
|
583
997
|
contentEl.remove();
|
|
584
998
|
this.contentElements.delete(node.id);
|
|
585
999
|
this.a11yNeedsReorder = true;
|
|
@@ -661,6 +1075,11 @@ var Scene = class _Scene {
|
|
|
661
1075
|
if (typeof window !== "undefined" && !this.disableWindowResize) {
|
|
662
1076
|
window.removeEventListener("resize", this.resizeHandler);
|
|
663
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
|
+
}
|
|
664
1083
|
if (typeof window !== "undefined" && this.canvas && typeof this.canvas.removeEventListener === "function") {
|
|
665
1084
|
if (this.pointerMoveListener) {
|
|
666
1085
|
this.canvas.removeEventListener("pointermove", this.pointerMoveListener);
|
|
@@ -674,6 +1093,15 @@ var Scene = class _Scene {
|
|
|
674
1093
|
this.a11yElements.clear();
|
|
675
1094
|
for (const el of this.contentElements.values()) el.remove();
|
|
676
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();
|
|
677
1105
|
this.pointRenderer?.destroy();
|
|
678
1106
|
this.renderer.dispose?.();
|
|
679
1107
|
this.glCanvas?.remove();
|
|
@@ -958,6 +1386,10 @@ var Scene = class _Scene {
|
|
|
958
1386
|
if (attrs.label !== void 0 && el.getAttribute("aria-label") !== attrs.label) {
|
|
959
1387
|
el.setAttribute("aria-label", attrs.label);
|
|
960
1388
|
}
|
|
1389
|
+
const semanticPointerEvents = attrs.pointerEvents ?? "auto";
|
|
1390
|
+
if (el.style.pointerEvents !== semanticPointerEvents) {
|
|
1391
|
+
el.style.pointerEvents = semanticPointerEvents;
|
|
1392
|
+
}
|
|
961
1393
|
const implicitTabIndex = !isNativelyFocusable(el) && attrs.role && INTERACTIVE_A11Y_ROLES.has(attrs.role) ? 0 : null;
|
|
962
1394
|
const desiredTabIndex = attrs.tabIndex ?? implicitTabIndex;
|
|
963
1395
|
if (desiredTabIndex === null) {
|
|
@@ -1074,6 +1506,7 @@ var Scene = class _Scene {
|
|
|
1074
1506
|
let el = this.contentElements.get(node.id);
|
|
1075
1507
|
if (!projection || !projection.text) {
|
|
1076
1508
|
if (el) {
|
|
1509
|
+
this.clearContentGridState(node.id, el);
|
|
1077
1510
|
el.remove();
|
|
1078
1511
|
this.contentElements.delete(node.id);
|
|
1079
1512
|
this.a11yNeedsReorder = true;
|
|
@@ -1105,13 +1538,19 @@ var Scene = class _Scene {
|
|
|
1105
1538
|
this.a11yNeedsReorder = true;
|
|
1106
1539
|
}
|
|
1107
1540
|
const lines = projection.lines;
|
|
1108
|
-
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) {
|
|
1109
1547
|
const signature = JSON.stringify({
|
|
1110
1548
|
lines,
|
|
1111
1549
|
fallbackFont: projection.font ?? "",
|
|
1112
1550
|
fallbackLineHeight: projection.lineHeight ?? 16
|
|
1113
1551
|
});
|
|
1114
1552
|
if (el.dataset.vectoProjectionLines !== signature) {
|
|
1553
|
+
this.releaseContentSelectionForRebuild(el);
|
|
1115
1554
|
el.replaceChildren();
|
|
1116
1555
|
for (let index = 0; index < lines.length; index++) {
|
|
1117
1556
|
const line = lines[index];
|
|
@@ -1143,13 +1582,21 @@ var Scene = class _Scene {
|
|
|
1143
1582
|
el.dataset.vectoProjectionLines = signature;
|
|
1144
1583
|
}
|
|
1145
1584
|
} else {
|
|
1146
|
-
if (el.textContent !== projection.text)
|
|
1585
|
+
if (el.textContent !== projection.text) {
|
|
1586
|
+
this.releaseContentSelectionForRebuild(el);
|
|
1587
|
+
el.textContent = projection.text;
|
|
1588
|
+
}
|
|
1147
1589
|
delete el.dataset.vectoProjectionLines;
|
|
1148
1590
|
}
|
|
1149
1591
|
const font = projection.font ?? "";
|
|
1150
1592
|
if (el.style.font !== font) el.style.font = font;
|
|
1151
1593
|
const lineHeight = projection.lineHeight !== void 0 ? `${projection.lineHeight}px` : "";
|
|
1152
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
|
+
}
|
|
1153
1600
|
const hidden = node.interactive ? "true" : null;
|
|
1154
1601
|
if (el.getAttribute("aria-hidden") !== hidden) {
|
|
1155
1602
|
if (hidden) el.setAttribute("aria-hidden", hidden);
|
|
@@ -1211,6 +1658,245 @@ var Scene = class _Scene {
|
|
|
1211
1658
|
const display = visible ? "" : "none";
|
|
1212
1659
|
if (el.style.display !== display) el.style.display = display;
|
|
1213
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
|
+
}
|
|
1214
1900
|
enforceA11yDomOrder() {
|
|
1215
1901
|
if (!this.a11yRoot) return;
|
|
1216
1902
|
this.fullViewportElements.length = 0;
|
|
@@ -1738,6 +2424,7 @@ var Scene = class _Scene {
|
|
|
1738
2424
|
resize(width, height) {
|
|
1739
2425
|
this.width = width;
|
|
1740
2426
|
this.height = height;
|
|
2427
|
+
this.contentFontEpoch++;
|
|
1741
2428
|
if (typeof this.renderer.resize === "function") {
|
|
1742
2429
|
this.renderer.resize(width, height);
|
|
1743
2430
|
}
|
|
@@ -2423,6 +3110,128 @@ async function loadSpline(url) {
|
|
|
2423
3110
|
return await res.json();
|
|
2424
3111
|
}
|
|
2425
3112
|
|
|
3113
|
+
// src/components/Rect.ts
|
|
3114
|
+
var Rect = class extends Entity {
|
|
3115
|
+
fill;
|
|
3116
|
+
stroke;
|
|
3117
|
+
strokeWidth;
|
|
3118
|
+
radius;
|
|
3119
|
+
constructor(opts = {}) {
|
|
3120
|
+
super();
|
|
3121
|
+
this.width = opts.width ?? 0;
|
|
3122
|
+
this.height = opts.height ?? 0;
|
|
3123
|
+
this.fill = opts.fill === void 0 ? "#38bdf8" : opts.fill;
|
|
3124
|
+
this.stroke = opts.stroke ?? null;
|
|
3125
|
+
this.strokeWidth = opts.strokeWidth ?? 1;
|
|
3126
|
+
this.radius = opts.radius ?? 0;
|
|
3127
|
+
}
|
|
3128
|
+
getBounds() {
|
|
3129
|
+
return { x: 0, y: 0, width: this.width, height: this.height };
|
|
3130
|
+
}
|
|
3131
|
+
/**
|
|
3132
|
+
* Solid-fill, square-cornered, unstroked rectangles opt into the GPU
|
|
3133
|
+
* instanced-rect batch (WebGL `pointBackend` only). Any stroke or corner
|
|
3134
|
+
* radius needs the exact Canvas path, so return `null` to fall back to
|
|
3135
|
+
* {@link render}. Read each frame, so an animated `fill` still batches.
|
|
3136
|
+
*/
|
|
3137
|
+
getBatchRect() {
|
|
3138
|
+
if (!this.fill || this.stroke || this.radius > 0) return null;
|
|
3139
|
+
return { width: this.width, height: this.height, color: this.fill };
|
|
3140
|
+
}
|
|
3141
|
+
isPointInside(globalX, globalY) {
|
|
3142
|
+
const local = this.worldToLocal(globalX, globalY);
|
|
3143
|
+
if (!local) return false;
|
|
3144
|
+
return local.x >= 0 && local.x <= this.width && local.y >= 0 && local.y <= this.height;
|
|
3145
|
+
}
|
|
3146
|
+
render(renderer) {
|
|
3147
|
+
renderer.beginPath();
|
|
3148
|
+
if (this.radius > 0) {
|
|
3149
|
+
renderer.roundRect(0, 0, this.width, this.height, this.radius);
|
|
3150
|
+
} else {
|
|
3151
|
+
renderer.moveTo(0, 0);
|
|
3152
|
+
renderer.lineTo(this.width, 0);
|
|
3153
|
+
renderer.lineTo(this.width, this.height);
|
|
3154
|
+
renderer.lineTo(0, this.height);
|
|
3155
|
+
renderer.closePath();
|
|
3156
|
+
}
|
|
3157
|
+
if (this.fill) renderer.fill(this.fill);
|
|
3158
|
+
if (this.stroke) renderer.stroke(this.stroke, this.strokeWidth);
|
|
3159
|
+
}
|
|
3160
|
+
};
|
|
3161
|
+
|
|
3162
|
+
// src/components/Circle.ts
|
|
3163
|
+
var Circle = class extends Entity {
|
|
3164
|
+
fill;
|
|
3165
|
+
stroke;
|
|
3166
|
+
strokeWidth;
|
|
3167
|
+
_radius;
|
|
3168
|
+
constructor(opts = {}) {
|
|
3169
|
+
super();
|
|
3170
|
+
this._radius = opts.radius ?? 0;
|
|
3171
|
+
this.fill = opts.fill === void 0 ? "#38bdf8" : opts.fill;
|
|
3172
|
+
this.stroke = opts.stroke ?? null;
|
|
3173
|
+
this.strokeWidth = opts.strokeWidth ?? 1;
|
|
3174
|
+
this.syncBox();
|
|
3175
|
+
}
|
|
3176
|
+
get radius() {
|
|
3177
|
+
return this._radius;
|
|
3178
|
+
}
|
|
3179
|
+
set radius(v) {
|
|
3180
|
+
this._radius = v;
|
|
3181
|
+
this.syncBox();
|
|
3182
|
+
}
|
|
3183
|
+
/** Keep the a11y box (a square around the centered disc) in sync with radius. */
|
|
3184
|
+
syncBox() {
|
|
3185
|
+
this.width = this._radius * 2;
|
|
3186
|
+
this.height = this._radius * 2;
|
|
3187
|
+
this.a11yOffsetX = -this._radius;
|
|
3188
|
+
this.a11yOffsetY = -this._radius;
|
|
3189
|
+
}
|
|
3190
|
+
getBounds() {
|
|
3191
|
+
return {
|
|
3192
|
+
x: -this._radius,
|
|
3193
|
+
y: -this._radius,
|
|
3194
|
+
width: this._radius * 2,
|
|
3195
|
+
height: this._radius * 2
|
|
3196
|
+
};
|
|
3197
|
+
}
|
|
3198
|
+
/**
|
|
3199
|
+
* A solid-fill, unstroked circle opts into the renderer's circle batch
|
|
3200
|
+
* (center = entity origin, radius scaled by world scale). A stroke needs the
|
|
3201
|
+
* exact Canvas path, so return `null` there. Read each frame, so an animated
|
|
3202
|
+
* `fill`/`radius` still batches.
|
|
3203
|
+
*/
|
|
3204
|
+
getBatchCircle() {
|
|
3205
|
+
if (!this.fill || this.stroke) return null;
|
|
3206
|
+
return { radius: this._radius, color: this.fill };
|
|
3207
|
+
}
|
|
3208
|
+
isPointInside(globalX, globalY) {
|
|
3209
|
+
const local = this.worldToLocal(globalX, globalY);
|
|
3210
|
+
if (!local) return false;
|
|
3211
|
+
return local.x * local.x + local.y * local.y <= this._radius * this._radius;
|
|
3212
|
+
}
|
|
3213
|
+
render(renderer) {
|
|
3214
|
+
renderer.beginPath();
|
|
3215
|
+
renderer.arc(0, 0, this._radius, 0, Math.PI * 2);
|
|
3216
|
+
renderer.closePath();
|
|
3217
|
+
if (this.fill) renderer.fill(this.fill);
|
|
3218
|
+
if (this.stroke) renderer.stroke(this.stroke, this.strokeWidth);
|
|
3219
|
+
}
|
|
3220
|
+
};
|
|
3221
|
+
|
|
3222
|
+
// src/components/Group.ts
|
|
3223
|
+
var Group = class extends Entity {
|
|
3224
|
+
constructor(...children) {
|
|
3225
|
+
super();
|
|
3226
|
+
if (children.length > 0) this.add(...children);
|
|
3227
|
+
}
|
|
3228
|
+
isPointInside() {
|
|
3229
|
+
return false;
|
|
3230
|
+
}
|
|
3231
|
+
render(_renderer) {
|
|
3232
|
+
}
|
|
3233
|
+
};
|
|
3234
|
+
|
|
2426
3235
|
// src/math/SpatialHashGrid.ts
|
|
2427
3236
|
var SpatialHashGrid = class {
|
|
2428
3237
|
cellSize;
|
|
@@ -2624,11 +3433,13 @@ export {
|
|
|
2624
3433
|
ArabicShaper,
|
|
2625
3434
|
BidiResolver,
|
|
2626
3435
|
CanvasRenderer,
|
|
3436
|
+
Circle,
|
|
2627
3437
|
ComputeParticleEntity,
|
|
2628
3438
|
DOMPortalEntity,
|
|
2629
3439
|
Easing,
|
|
2630
3440
|
Entity,
|
|
2631
3441
|
GridTextEntity,
|
|
3442
|
+
Group,
|
|
2632
3443
|
LayoutEngine,
|
|
2633
3444
|
LayoutResultBuffer,
|
|
2634
3445
|
LayoutWorkerManager,
|
|
@@ -2644,6 +3455,7 @@ export {
|
|
|
2644
3455
|
PARTICLE_OFFSET_VELOCITY_Y,
|
|
2645
3456
|
PARTICLE_STRIDE_FLOATS,
|
|
2646
3457
|
REDUCED_MOTION_FPS,
|
|
3458
|
+
Rect,
|
|
2647
3459
|
SVGEntity,
|
|
2648
3460
|
SVGRenderer,
|
|
2649
3461
|
Scene,
|
|
@@ -2665,5 +3477,6 @@ export {
|
|
|
2665
3477
|
loadSpline,
|
|
2666
3478
|
parseColorToRGBA,
|
|
2667
3479
|
polySegmentToBezier,
|
|
3480
|
+
prepareContentGrid,
|
|
2668
3481
|
sanitizeUrl
|
|
2669
3482
|
};
|