@vectojs/core 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -720,73 +720,76 @@ var Scene = (_class2 = class _Scene {
720
720
  __init29() {this.contentMetricScaleEpoch = -1}
721
721
  __init30() {this.contentMetricScaleX = 1}
722
722
  __init31() {this.contentProjectionEnabled = true}
723
+ // Virtualization margin (px) for content projection; `undefined` → one
724
+ // viewport height, resolved at sync time. `Infinity` = materialize everything.
725
+ __init32() {this.contentProjectionMargin = void 0}
723
726
  /**
724
727
  * True while a text-selection drag that started on a projection's blank
725
728
  * region (no text node under the press) is being driven manually — the
726
729
  * browser has no native anchor for it, so mousemove extends the Selection
727
730
  * from the position we resolved ourselves.
728
731
  */
729
- __init32() {this.blankRegionSelectionDrag = false}
730
- __init33() {this.contentSelectionAnchor = null}
731
- __init34() {this.contentSelectionEndListener = null}
732
+ __init33() {this.blankRegionSelectionDrag = false}
733
+ __init34() {this.contentSelectionAnchor = null}
734
+ __init35() {this.contentSelectionEndListener = null}
732
735
  // Animation/interactive flags collected during the render walk (tree-walk
733
736
  // fusion): the loop reads last frame's answers instead of re-walking the
734
737
  // tree up to 4× per tick. Start true so the first tick stays conservative.
735
- __init35() {this.frameHadAnimation = true}
736
- __init36() {this.frameHadInteractive = true}
738
+ __init36() {this.frameHadAnimation = true}
739
+ __init37() {this.frameHadInteractive = true}
737
740
 
738
- __init37() {this.focusedA11yElement = null}
739
- __init38() {this.caretBlinkTimer = null}
740
- __init39() {this.a11yNeedsReorder = true}
741
- __init40() {this.portalRoot = null}
742
- __init41() {this.fullViewportElements = []}
743
- __init42() {this.normalElements = []}
744
- __init43() {this.activeIds = /* @__PURE__ */ new Set()}
745
- __init44() {this.activePortalsThisFrame = /* @__PURE__ */ new Set()}
746
- __init45() {this.activePortalsPrevFrame = /* @__PURE__ */ new Set()}
747
- __init46() {this.portalEntities = /* @__PURE__ */ new Map()}
748
- __init47() {this.renderOrderCounter = 0}
741
+ __init38() {this.focusedA11yElement = null}
742
+ __init39() {this.caretBlinkTimer = null}
743
+ __init40() {this.a11yNeedsReorder = true}
744
+ __init41() {this.portalRoot = null}
745
+ __init42() {this.fullViewportElements = []}
746
+ __init43() {this.normalElements = []}
747
+ __init44() {this.activeIds = /* @__PURE__ */ new Set()}
748
+ __init45() {this.activePortalsThisFrame = /* @__PURE__ */ new Set()}
749
+ __init46() {this.activePortalsPrevFrame = /* @__PURE__ */ new Set()}
750
+ __init47() {this.portalEntities = /* @__PURE__ */ new Map()}
751
+ __init48() {this.renderOrderCounter = 0}
749
752
  /**
750
753
  * Authoritative paint order for semantic nodes discovered during the main
751
754
  * render. A node may not have a DOM projection until the following a11y
752
755
  * sync, so retaining the order prevents a newly opened overlay from spending
753
756
  * its first frame below previously projected controls.
754
757
  */
755
- __init48() {this.a11yRenderOrders = /* @__PURE__ */ new Map()}
758
+ __init49() {this.a11yRenderOrders = /* @__PURE__ */ new Map()}
756
759
  // Optional WebGL point-cloud layer (see SceneOptions.pointBackend).
757
- __init49() {this.pointRenderer = null}
758
- __init50() {this.glCanvas = null}
760
+ __init50() {this.pointRenderer = null}
761
+ __init51() {this.glCanvas = null}
759
762
 
760
763
 
761
764
 
762
- __init51() {this.disableWindowResize = false}
765
+ __init52() {this.disableWindowResize = false}
763
766
  /** See {@link SceneOptions.maxDPR}. `undefined` = uncapped (real DPR). */
764
767
 
765
768
  // WebGPU properties
766
- __init52() {this.destroyed = false}
767
- __init53() {this.device = null}
768
- __init54() {this.deviceLost = false}
769
- __init55() {this.particleBackend = "auto"}
770
- __init56() {this._webgpuDisabled = false}
769
+ __init53() {this.destroyed = false}
770
+ __init54() {this.device = null}
771
+ __init55() {this.deviceLost = false}
772
+ __init56() {this.particleBackend = "auto"}
773
+ __init57() {this._webgpuDisabled = false}
771
774
  get webgpuDisabled() {
772
775
  return this._webgpuDisabled || this.particleBackend === "cpu";
773
776
  }
774
777
  set webgpuDisabled(value) {
775
778
  this._webgpuDisabled = value;
776
779
  }
777
- __init57() {this.recoveryTimerId = null}
778
- __init58() {this.manager = null}
779
- __init59() {this.initializingWebGPU = false}
780
- __init60() {this.gpuCanvas = null}
781
- __init61() {this.gpuContext = null}
780
+ __init58() {this.recoveryTimerId = null}
781
+ __init59() {this.manager = null}
782
+ __init60() {this.initializingWebGPU = false}
783
+ __init61() {this.gpuCanvas = null}
784
+ __init62() {this.gpuContext = null}
782
785
  /** True while the GPU canvas holds a presented particle frame (needs clearing when they leave). */
783
- __init62() {this.gpuHasContent = false}
784
- __init63() {this.mouseX = -9999}
785
- __init64() {this.mouseY = -9999}
786
- __init65() {this.pointerMoveListener = null}
787
- __init66() {this.pointerLeaveListener = null}
788
- __init67() {this.hasWarnedZeroSize = false}
789
- __init68() {this.fontLoadHandler = null}
786
+ __init63() {this.gpuHasContent = false}
787
+ __init64() {this.mouseX = -9999}
788
+ __init65() {this.mouseY = -9999}
789
+ __init66() {this.pointerMoveListener = null}
790
+ __init67() {this.pointerLeaveListener = null}
791
+ __init68() {this.hasWarnedZeroSize = false}
792
+ __init69() {this.fontLoadHandler = null}
790
793
  // ── Dev-mode warning infrastructure ──────────────────────────────
791
794
  //
792
795
  // Enable with `Scene.devMode = true` or by setting `globalThis.__DEV__`.
@@ -804,7 +807,7 @@ var Scene = (_class2 = class _Scene {
804
807
  return false;
805
808
  }
806
809
 
807
- __init69() {this._devFrameCount = 0}
810
+ __init70() {this._devFrameCount = 0}
808
811
  _devWarn(message) {
809
812
  if (!this._devActive) return;
810
813
  console.warn(`[vectojs/dev] ${message}`);
@@ -848,7 +851,7 @@ var Scene = (_class2 = class _Scene {
848
851
  };
849
852
  walkProjections(this.root);
850
853
  }
851
- constructor(canvas, options = {}) {;_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);_class2.prototype.__init9.call(this);_class2.prototype.__init10.call(this);_class2.prototype.__init11.call(this);_class2.prototype.__init12.call(this);_class2.prototype.__init13.call(this);_class2.prototype.__init14.call(this);_class2.prototype.__init15.call(this);_class2.prototype.__init16.call(this);_class2.prototype.__init17.call(this);_class2.prototype.__init18.call(this);_class2.prototype.__init19.call(this);_class2.prototype.__init20.call(this);_class2.prototype.__init21.call(this);_class2.prototype.__init22.call(this);_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);_class2.prototype.__init25.call(this);_class2.prototype.__init26.call(this);_class2.prototype.__init27.call(this);_class2.prototype.__init28.call(this);_class2.prototype.__init29.call(this);_class2.prototype.__init30.call(this);_class2.prototype.__init31.call(this);_class2.prototype.__init32.call(this);_class2.prototype.__init33.call(this);_class2.prototype.__init34.call(this);_class2.prototype.__init35.call(this);_class2.prototype.__init36.call(this);_class2.prototype.__init37.call(this);_class2.prototype.__init38.call(this);_class2.prototype.__init39.call(this);_class2.prototype.__init40.call(this);_class2.prototype.__init41.call(this);_class2.prototype.__init42.call(this);_class2.prototype.__init43.call(this);_class2.prototype.__init44.call(this);_class2.prototype.__init45.call(this);_class2.prototype.__init46.call(this);_class2.prototype.__init47.call(this);_class2.prototype.__init48.call(this);_class2.prototype.__init49.call(this);_class2.prototype.__init50.call(this);_class2.prototype.__init51.call(this);_class2.prototype.__init52.call(this);_class2.prototype.__init53.call(this);_class2.prototype.__init54.call(this);_class2.prototype.__init55.call(this);_class2.prototype.__init56.call(this);_class2.prototype.__init57.call(this);_class2.prototype.__init58.call(this);_class2.prototype.__init59.call(this);_class2.prototype.__init60.call(this);_class2.prototype.__init61.call(this);_class2.prototype.__init62.call(this);_class2.prototype.__init63.call(this);_class2.prototype.__init64.call(this);_class2.prototype.__init65.call(this);_class2.prototype.__init66.call(this);_class2.prototype.__init67.call(this);_class2.prototype.__init68.call(this);_class2.prototype.__init69.call(this);
854
+ constructor(canvas, options = {}) {;_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);_class2.prototype.__init9.call(this);_class2.prototype.__init10.call(this);_class2.prototype.__init11.call(this);_class2.prototype.__init12.call(this);_class2.prototype.__init13.call(this);_class2.prototype.__init14.call(this);_class2.prototype.__init15.call(this);_class2.prototype.__init16.call(this);_class2.prototype.__init17.call(this);_class2.prototype.__init18.call(this);_class2.prototype.__init19.call(this);_class2.prototype.__init20.call(this);_class2.prototype.__init21.call(this);_class2.prototype.__init22.call(this);_class2.prototype.__init23.call(this);_class2.prototype.__init24.call(this);_class2.prototype.__init25.call(this);_class2.prototype.__init26.call(this);_class2.prototype.__init27.call(this);_class2.prototype.__init28.call(this);_class2.prototype.__init29.call(this);_class2.prototype.__init30.call(this);_class2.prototype.__init31.call(this);_class2.prototype.__init32.call(this);_class2.prototype.__init33.call(this);_class2.prototype.__init34.call(this);_class2.prototype.__init35.call(this);_class2.prototype.__init36.call(this);_class2.prototype.__init37.call(this);_class2.prototype.__init38.call(this);_class2.prototype.__init39.call(this);_class2.prototype.__init40.call(this);_class2.prototype.__init41.call(this);_class2.prototype.__init42.call(this);_class2.prototype.__init43.call(this);_class2.prototype.__init44.call(this);_class2.prototype.__init45.call(this);_class2.prototype.__init46.call(this);_class2.prototype.__init47.call(this);_class2.prototype.__init48.call(this);_class2.prototype.__init49.call(this);_class2.prototype.__init50.call(this);_class2.prototype.__init51.call(this);_class2.prototype.__init52.call(this);_class2.prototype.__init53.call(this);_class2.prototype.__init54.call(this);_class2.prototype.__init55.call(this);_class2.prototype.__init56.call(this);_class2.prototype.__init57.call(this);_class2.prototype.__init58.call(this);_class2.prototype.__init59.call(this);_class2.prototype.__init60.call(this);_class2.prototype.__init61.call(this);_class2.prototype.__init62.call(this);_class2.prototype.__init63.call(this);_class2.prototype.__init64.call(this);_class2.prototype.__init65.call(this);_class2.prototype.__init66.call(this);_class2.prototype.__init67.call(this);_class2.prototype.__init68.call(this);_class2.prototype.__init69.call(this);_class2.prototype.__init70.call(this);
852
855
  this.canvas = canvas;
853
856
  this.debugA11y = _nullishCoalesce(options.debugA11y, () => ( false));
854
857
  this.disableWindowResize = _nullishCoalesce(options.disableWindowResize, () => ( false));
@@ -870,6 +873,7 @@ var Scene = (_class2 = class _Scene {
870
873
  this.particleBackend = _nullishCoalesce(options.particleBackend, () => ( "auto"));
871
874
  this.a11ySyncInterval = _nullishCoalesce(options.a11ySyncInterval, () => ( 0));
872
875
  this.contentProjectionEnabled = _nullishCoalesce(options.contentProjection, () => ( true));
876
+ this.contentProjectionMargin = options.contentProjectionMargin;
873
877
  this._devActive = _Scene._devModeDetected();
874
878
  this.reducedMotionQuery = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
875
879
  this.root = new class RootEntity extends _chunk2Z23LTH3js.Entity {
@@ -1661,6 +1665,56 @@ var Scene = (_class2 = class _Scene {
1661
1665
  * hidden (`display: none`) so text-heavy scenes only materialize what is
1662
1666
  * visible to the browser's text machinery anyway.
1663
1667
  */
1668
+ /**
1669
+ * Whether `node`'s world-space box, expanded by `margin` px on every side,
1670
+ * overlaps the scene viewport AND every `clipChildren` ancestor's box. Used
1671
+ * both to virtualize content projection (materialize only near-viewport text,
1672
+ * at `margin = contentProjectionMargin`) and for the exact `display:none`
1673
+ * visibility test (`margin = 0`). Boundless nodes (width/height 0) opt out of
1674
+ * culling and always count as visible, matching the legacy behavior.
1675
+ */
1676
+ projectionBoxVisible(node, tf, margin) {
1677
+ if (!(node.width > 0 && node.height > 0)) return true;
1678
+ const { a, b, c, d, e, f } = tf;
1679
+ const worldCorners = [];
1680
+ let minX = Infinity;
1681
+ let minY = Infinity;
1682
+ let maxX = -Infinity;
1683
+ let maxY = -Infinity;
1684
+ for (let i = 0; i < 4; i++) {
1685
+ const lx = i & 1 ? node.width : 0;
1686
+ const ly = i & 2 ? node.height : 0;
1687
+ const wx = a * lx + c * ly + e;
1688
+ const wy = b * lx + d * ly + f;
1689
+ worldCorners.push({ x: wx, y: wy });
1690
+ if (wx < minX) minX = wx;
1691
+ if (wx > maxX) maxX = wx;
1692
+ if (wy < minY) minY = wy;
1693
+ if (wy > maxY) maxY = wy;
1694
+ }
1695
+ if (!(maxX >= -margin && minX <= this.width + margin && maxY >= -margin && minY <= this.height + margin)) {
1696
+ return false;
1697
+ }
1698
+ for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
1699
+ if (!ancestor.clipChildren || ancestor.width <= 0 || ancestor.height <= 0) continue;
1700
+ let localMinX = Infinity;
1701
+ let localMinY = Infinity;
1702
+ let localMaxX = -Infinity;
1703
+ let localMaxY = -Infinity;
1704
+ for (const corner of worldCorners) {
1705
+ const local = ancestor.worldToLocal(corner.x, corner.y);
1706
+ if (!local) continue;
1707
+ localMinX = Math.min(localMinX, local.x);
1708
+ localMinY = Math.min(localMinY, local.y);
1709
+ localMaxX = Math.max(localMaxX, local.x);
1710
+ localMaxY = Math.max(localMaxY, local.y);
1711
+ }
1712
+ if (!(localMaxX >= -margin && localMinX <= ancestor.width + margin && localMaxY >= -margin && localMinY <= ancestor.height + margin)) {
1713
+ return false;
1714
+ }
1715
+ }
1716
+ return true;
1717
+ }
1664
1718
  syncContentProjection(node) {
1665
1719
  if (!this.contentProjectionEnabled || !this.a11yRoot) return;
1666
1720
  const projection = node.getContentProjection();
@@ -1674,6 +1728,17 @@ var Scene = (_class2 = class _Scene {
1674
1728
  }
1675
1729
  return;
1676
1730
  }
1731
+ const worldTf = node.getWorldTransform();
1732
+ const margin = _nullishCoalesce(this.contentProjectionMargin, () => ( this.height));
1733
+ if (Number.isFinite(margin) && !this.projectionBoxVisible(node, worldTf, margin)) {
1734
+ if (el) {
1735
+ this.clearContentGridState(node.id, el);
1736
+ el.remove();
1737
+ this.contentElements.delete(node.id);
1738
+ this.a11yNeedsReorder = true;
1739
+ }
1740
+ return;
1741
+ }
1677
1742
  if (!el) {
1678
1743
  el = document.createElement("div");
1679
1744
  el.setAttribute("data-vecto-content", node.id);
@@ -1770,7 +1835,7 @@ var Scene = (_class2 = class _Scene {
1770
1835
  el.style.userSelect = selectable ? "text" : "none";
1771
1836
  el.style.cursor = selectable ? "text" : "";
1772
1837
  }
1773
- const { a, b, c, d, e, f } = node.getWorldTransform();
1838
+ const { a, b, c, d, e, f } = worldTf;
1774
1839
  const contentX = _nullishCoalesce(projection.contentX, () => ( 0));
1775
1840
  const contentY = _nullishCoalesce(projection.contentY, () => ( 0));
1776
1841
  const baselineOffset = lines && lines.length > 0 ? 0 : projection.baseline === void 0 ? 0 : projection.baseline - _chunk2Z23LTH3js.cssLineBoxBaseline.call(void 0, font, _nullishCoalesce(projection.lineHeight, () => ( 16)));
@@ -1780,42 +1845,7 @@ var Scene = (_class2 = class _Scene {
1780
1845
  if (node.width > 0) el.style.width = `${node.width}px`;
1781
1846
  if (node.height > 0) el.style.height = `${node.height}px`;
1782
1847
  el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
1783
- let visible = true;
1784
- if (node.width > 0 && node.height > 0) {
1785
- const worldCorners = [];
1786
- let minX = Infinity;
1787
- let minY = Infinity;
1788
- let maxX = -Infinity;
1789
- let maxY = -Infinity;
1790
- for (let i = 0; i < 4; i++) {
1791
- const lx = i & 1 ? node.width : 0;
1792
- const ly = i & 2 ? node.height : 0;
1793
- const wx = a * lx + c * ly + e;
1794
- const wy = b * lx + d * ly + f;
1795
- worldCorners.push({ x: wx, y: wy });
1796
- if (wx < minX) minX = wx;
1797
- if (wx > maxX) maxX = wx;
1798
- if (wy < minY) minY = wy;
1799
- if (wy > maxY) maxY = wy;
1800
- }
1801
- visible = maxX >= 0 && minX <= this.width && maxY >= 0 && minY <= this.height;
1802
- for (let ancestor = node.parent; visible && ancestor; ancestor = ancestor.parent) {
1803
- if (!ancestor.clipChildren || ancestor.width <= 0 || ancestor.height <= 0) continue;
1804
- let localMinX = Infinity;
1805
- let localMinY = Infinity;
1806
- let localMaxX = -Infinity;
1807
- let localMaxY = -Infinity;
1808
- for (const corner of worldCorners) {
1809
- const local = ancestor.worldToLocal(corner.x, corner.y);
1810
- if (!local) continue;
1811
- localMinX = Math.min(localMinX, local.x);
1812
- localMinY = Math.min(localMinY, local.y);
1813
- localMaxX = Math.max(localMaxX, local.x);
1814
- localMaxY = Math.max(localMaxY, local.y);
1815
- }
1816
- visible = localMaxX >= 0 && localMinX <= ancestor.width && localMaxY >= 0 && localMinY <= ancestor.height;
1817
- }
1818
- }
1848
+ const visible = this.projectionBoxVisible(node, worldTf, 0);
1819
1849
  const display = visible ? "" : "none";
1820
1850
  if (el.style.display !== display) el.style.display = display;
1821
1851
  }
@@ -2818,15 +2848,15 @@ var TextEntity = (_class3 = class extends _chunk2Z23LTH3js.Entity {
2818
2848
 
2819
2849
 
2820
2850
 
2821
- __init70() {this.nodes = []}
2851
+ __init71() {this.nodes = []}
2822
2852
 
2823
- __init71() {this.fillStyle = "#94a3b8"}
2824
- __init72() {this.strokeStyle = null}
2825
- __init73() {this.hoveredFillStyle = "#ffffff"}
2826
- __init74() {this.lineWidth = 1}
2827
- __init75() {this.isHovered = false}
2853
+ __init72() {this.fillStyle = "#94a3b8"}
2854
+ __init73() {this.strokeStyle = null}
2855
+ __init74() {this.hoveredFillStyle = "#ffffff"}
2856
+ __init75() {this.lineWidth = 1}
2857
+ __init76() {this.isHovered = false}
2828
2858
  constructor(text, atlas, maxWidth, fontSize = 24) {
2829
- super();_class3.prototype.__init70.call(this);_class3.prototype.__init71.call(this);_class3.prototype.__init72.call(this);_class3.prototype.__init73.call(this);_class3.prototype.__init74.call(this);_class3.prototype.__init75.call(this);;
2859
+ super();_class3.prototype.__init71.call(this);_class3.prototype.__init72.call(this);_class3.prototype.__init73.call(this);_class3.prototype.__init74.call(this);_class3.prototype.__init75.call(this);_class3.prototype.__init76.call(this);;
2830
2860
  this.text = text;
2831
2861
  this.atlas = atlas;
2832
2862
  this.fontSize = fontSize;
@@ -2941,15 +2971,15 @@ var TextEntity = (_class3 = class extends _chunk2Z23LTH3js.Entity {
2941
2971
  // src/components/GridTextEntity.ts
2942
2972
  var GridTextEntity = (_class4 = class extends _chunk2Z23LTH3js.Entity {
2943
2973
 
2944
- __init76() {this.fillStyle = "#ffffff"}
2945
- __init77() {this.grid = []}
2974
+ __init77() {this.fillStyle = "#ffffff"}
2975
+ __init78() {this.grid = []}
2946
2976
  // Array of rows
2947
- __init78() {this.cols = 0}
2948
- __init79() {this.rows = 0}
2977
+ __init79() {this.cols = 0}
2978
+ __init80() {this.rows = 0}
2949
2979
 
2950
2980
 
2951
2981
  constructor(_atlas, fontSize = 10) {
2952
- super();_class4.prototype.__init76.call(this);_class4.prototype.__init77.call(this);_class4.prototype.__init78.call(this);_class4.prototype.__init79.call(this);;
2982
+ super();_class4.prototype.__init77.call(this);_class4.prototype.__init78.call(this);_class4.prototype.__init79.call(this);_class4.prototype.__init80.call(this);;
2953
2983
  this.fontSize = fontSize;
2954
2984
  this.charWidth = fontSize * 1;
2955
2985
  this.charHeight = fontSize * 1.1;
@@ -3041,23 +3071,23 @@ var SplineEntity = (_class5 = class extends _chunk2Z23LTH3js.Entity {
3041
3071
 
3042
3072
 
3043
3073
 
3044
- __init80() {this.offscreen = null}
3045
- __init81() {this.baked = false}
3074
+ __init81() {this.offscreen = null}
3075
+ __init82() {this.baked = false}
3046
3076
  /** Logical (CSS-pixel) size of the baked bitmap — the blit destination size. */
3047
- __init82() {this.bakedWidth = 0}
3048
- __init83() {this.bakedHeight = 0}
3077
+ __init83() {this.bakedWidth = 0}
3078
+ __init84() {this.bakedHeight = 0}
3049
3079
  /** Gradient strokes can't be baked to a solid-color bitmap; they render per-frame. */
3050
3080
 
3051
3081
  /** Lazily-flattened polylines (one Float32Array of [x,y,...] per segment) for hit-testing. */
3052
- __init84() {this.polylines = null}
3082
+ __init85() {this.polylines = null}
3053
3083
  /**
3054
3084
  * When `true`, the renderer draws a rounded-rect outline of the entity's
3055
3085
  * local bounds after painting the curves. Useful for drag feedback and
3056
3086
  * debugging hit areas. Defaults to `false`.
3057
3087
  */
3058
- __init85() {this.showBounds = false}
3088
+ __init86() {this.showBounds = false}
3059
3089
  constructor(doc, opts = {}) {
3060
- super();_class5.prototype.__init80.call(this);_class5.prototype.__init81.call(this);_class5.prototype.__init82.call(this);_class5.prototype.__init83.call(this);_class5.prototype.__init84.call(this);_class5.prototype.__init85.call(this);;
3090
+ super();_class5.prototype.__init81.call(this);_class5.prototype.__init82.call(this);_class5.prototype.__init83.call(this);_class5.prototype.__init84.call(this);_class5.prototype.__init85.call(this);_class5.prototype.__init86.call(this);;
3061
3091
  this.doc = doc;
3062
3092
  this.lineWidth = _nullishCoalesce(opts.lineWidth, () => ( 2));
3063
3093
  this.cache = _nullishCoalesce(opts.cache, () => ( true));
@@ -3432,9 +3462,9 @@ var Group = class extends _chunk2Z23LTH3js.Entity {
3432
3462
  // src/math/SpatialHashGrid.ts
3433
3463
  var SpatialHashGrid = (_class6 = class {
3434
3464
 
3435
- __init86() {this.grid = /* @__PURE__ */ new Map()}
3436
- __init87() {this.entityCells = /* @__PURE__ */ new Map()}
3437
- constructor(cellSize = 64) {;_class6.prototype.__init86.call(this);_class6.prototype.__init87.call(this);
3465
+ __init87() {this.grid = /* @__PURE__ */ new Map()}
3466
+ __init88() {this.entityCells = /* @__PURE__ */ new Map()}
3467
+ constructor(cellSize = 64) {;_class6.prototype.__init87.call(this);_class6.prototype.__init88.call(this);
3438
3468
  this.cellSize = cellSize;
3439
3469
  }
3440
3470
  hash(cx, cy) {
@@ -3526,18 +3556,18 @@ var SpatialHashGrid = (_class6 = class {
3526
3556
  // src/tree/DOMPortalEntity.ts
3527
3557
  var DOMPortalEntity = (_class7 = class extends _chunk2Z23LTH3js.Entity {
3528
3558
 
3529
- __init88() {this.isDOMPortal = true}
3530
- __init89() {this.domListeners = []}
3531
- __init90() {this.resizeObserver = null}
3532
- __init91() {this.cachedWidth = 100}
3533
- __init92() {this.cachedHeight = 100}
3534
- __init93() {this.lastWidth = ""}
3535
- __init94() {this.lastHeight = ""}
3536
- __init95() {this.lastTransform = ""}
3537
- __init96() {this.lastZIndex = ""}
3538
- __init97() {this.lastOpacity = ""}
3559
+ __init89() {this.isDOMPortal = true}
3560
+ __init90() {this.domListeners = []}
3561
+ __init91() {this.resizeObserver = null}
3562
+ __init92() {this.cachedWidth = 100}
3563
+ __init93() {this.cachedHeight = 100}
3564
+ __init94() {this.lastWidth = ""}
3565
+ __init95() {this.lastHeight = ""}
3566
+ __init96() {this.lastTransform = ""}
3567
+ __init97() {this.lastZIndex = ""}
3568
+ __init98() {this.lastOpacity = ""}
3539
3569
  constructor(domElement, width, height, id) {
3540
- super(id);_class7.prototype.__init88.call(this);_class7.prototype.__init89.call(this);_class7.prototype.__init90.call(this);_class7.prototype.__init91.call(this);_class7.prototype.__init92.call(this);_class7.prototype.__init93.call(this);_class7.prototype.__init94.call(this);_class7.prototype.__init95.call(this);_class7.prototype.__init96.call(this);_class7.prototype.__init97.call(this);;
3570
+ super(id);_class7.prototype.__init89.call(this);_class7.prototype.__init90.call(this);_class7.prototype.__init91.call(this);_class7.prototype.__init92.call(this);_class7.prototype.__init93.call(this);_class7.prototype.__init94.call(this);_class7.prototype.__init95.call(this);_class7.prototype.__init96.call(this);_class7.prototype.__init97.call(this);_class7.prototype.__init98.call(this);;
3541
3571
  this.domElement = domElement;
3542
3572
  this.width = _nullishCoalesce(width, () => ( 0));
3543
3573
  this.height = _nullishCoalesce(height, () => ( 0));
package/dist/index.mjs CHANGED
@@ -720,6 +720,9 @@ var Scene = class _Scene {
720
720
  contentMetricScaleEpoch = -1;
721
721
  contentMetricScaleX = 1;
722
722
  contentProjectionEnabled = true;
723
+ // Virtualization margin (px) for content projection; `undefined` → one
724
+ // viewport height, resolved at sync time. `Infinity` = materialize everything.
725
+ contentProjectionMargin = void 0;
723
726
  /**
724
727
  * True while a text-selection drag that started on a projection's blank
725
728
  * region (no text node under the press) is being driven manually — the
@@ -870,6 +873,7 @@ var Scene = class _Scene {
870
873
  this.particleBackend = options.particleBackend ?? "auto";
871
874
  this.a11ySyncInterval = options.a11ySyncInterval ?? 0;
872
875
  this.contentProjectionEnabled = options.contentProjection ?? true;
876
+ this.contentProjectionMargin = options.contentProjectionMargin;
873
877
  this._devActive = _Scene._devModeDetected();
874
878
  this.reducedMotionQuery = typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-reduced-motion: reduce)") : null;
875
879
  this.root = new class RootEntity extends Entity {
@@ -1661,6 +1665,56 @@ var Scene = class _Scene {
1661
1665
  * hidden (`display: none`) so text-heavy scenes only materialize what is
1662
1666
  * visible to the browser's text machinery anyway.
1663
1667
  */
1668
+ /**
1669
+ * Whether `node`'s world-space box, expanded by `margin` px on every side,
1670
+ * overlaps the scene viewport AND every `clipChildren` ancestor's box. Used
1671
+ * both to virtualize content projection (materialize only near-viewport text,
1672
+ * at `margin = contentProjectionMargin`) and for the exact `display:none`
1673
+ * visibility test (`margin = 0`). Boundless nodes (width/height 0) opt out of
1674
+ * culling and always count as visible, matching the legacy behavior.
1675
+ */
1676
+ projectionBoxVisible(node, tf, margin) {
1677
+ if (!(node.width > 0 && node.height > 0)) return true;
1678
+ const { a, b, c, d, e, f } = tf;
1679
+ const worldCorners = [];
1680
+ let minX = Infinity;
1681
+ let minY = Infinity;
1682
+ let maxX = -Infinity;
1683
+ let maxY = -Infinity;
1684
+ for (let i = 0; i < 4; i++) {
1685
+ const lx = i & 1 ? node.width : 0;
1686
+ const ly = i & 2 ? node.height : 0;
1687
+ const wx = a * lx + c * ly + e;
1688
+ const wy = b * lx + d * ly + f;
1689
+ worldCorners.push({ x: wx, y: wy });
1690
+ if (wx < minX) minX = wx;
1691
+ if (wx > maxX) maxX = wx;
1692
+ if (wy < minY) minY = wy;
1693
+ if (wy > maxY) maxY = wy;
1694
+ }
1695
+ if (!(maxX >= -margin && minX <= this.width + margin && maxY >= -margin && minY <= this.height + margin)) {
1696
+ return false;
1697
+ }
1698
+ for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
1699
+ if (!ancestor.clipChildren || ancestor.width <= 0 || ancestor.height <= 0) continue;
1700
+ let localMinX = Infinity;
1701
+ let localMinY = Infinity;
1702
+ let localMaxX = -Infinity;
1703
+ let localMaxY = -Infinity;
1704
+ for (const corner of worldCorners) {
1705
+ const local = ancestor.worldToLocal(corner.x, corner.y);
1706
+ if (!local) continue;
1707
+ localMinX = Math.min(localMinX, local.x);
1708
+ localMinY = Math.min(localMinY, local.y);
1709
+ localMaxX = Math.max(localMaxX, local.x);
1710
+ localMaxY = Math.max(localMaxY, local.y);
1711
+ }
1712
+ if (!(localMaxX >= -margin && localMinX <= ancestor.width + margin && localMaxY >= -margin && localMinY <= ancestor.height + margin)) {
1713
+ return false;
1714
+ }
1715
+ }
1716
+ return true;
1717
+ }
1664
1718
  syncContentProjection(node) {
1665
1719
  if (!this.contentProjectionEnabled || !this.a11yRoot) return;
1666
1720
  const projection = node.getContentProjection();
@@ -1674,6 +1728,17 @@ var Scene = class _Scene {
1674
1728
  }
1675
1729
  return;
1676
1730
  }
1731
+ const worldTf = node.getWorldTransform();
1732
+ const margin = this.contentProjectionMargin ?? this.height;
1733
+ if (Number.isFinite(margin) && !this.projectionBoxVisible(node, worldTf, margin)) {
1734
+ if (el) {
1735
+ this.clearContentGridState(node.id, el);
1736
+ el.remove();
1737
+ this.contentElements.delete(node.id);
1738
+ this.a11yNeedsReorder = true;
1739
+ }
1740
+ return;
1741
+ }
1677
1742
  if (!el) {
1678
1743
  el = document.createElement("div");
1679
1744
  el.setAttribute("data-vecto-content", node.id);
@@ -1770,7 +1835,7 @@ var Scene = class _Scene {
1770
1835
  el.style.userSelect = selectable ? "text" : "none";
1771
1836
  el.style.cursor = selectable ? "text" : "";
1772
1837
  }
1773
- const { a, b, c, d, e, f } = node.getWorldTransform();
1838
+ const { a, b, c, d, e, f } = worldTf;
1774
1839
  const contentX = projection.contentX ?? 0;
1775
1840
  const contentY = projection.contentY ?? 0;
1776
1841
  const baselineOffset = lines && lines.length > 0 ? 0 : projection.baseline === void 0 ? 0 : projection.baseline - cssLineBoxBaseline(font, projection.lineHeight ?? 16);
@@ -1780,42 +1845,7 @@ var Scene = class _Scene {
1780
1845
  if (node.width > 0) el.style.width = `${node.width}px`;
1781
1846
  if (node.height > 0) el.style.height = `${node.height}px`;
1782
1847
  el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
1783
- let visible = true;
1784
- if (node.width > 0 && node.height > 0) {
1785
- const worldCorners = [];
1786
- let minX = Infinity;
1787
- let minY = Infinity;
1788
- let maxX = -Infinity;
1789
- let maxY = -Infinity;
1790
- for (let i = 0; i < 4; i++) {
1791
- const lx = i & 1 ? node.width : 0;
1792
- const ly = i & 2 ? node.height : 0;
1793
- const wx = a * lx + c * ly + e;
1794
- const wy = b * lx + d * ly + f;
1795
- worldCorners.push({ x: wx, y: wy });
1796
- if (wx < minX) minX = wx;
1797
- if (wx > maxX) maxX = wx;
1798
- if (wy < minY) minY = wy;
1799
- if (wy > maxY) maxY = wy;
1800
- }
1801
- visible = maxX >= 0 && minX <= this.width && maxY >= 0 && minY <= this.height;
1802
- for (let ancestor = node.parent; visible && ancestor; ancestor = ancestor.parent) {
1803
- if (!ancestor.clipChildren || ancestor.width <= 0 || ancestor.height <= 0) continue;
1804
- let localMinX = Infinity;
1805
- let localMinY = Infinity;
1806
- let localMaxX = -Infinity;
1807
- let localMaxY = -Infinity;
1808
- for (const corner of worldCorners) {
1809
- const local = ancestor.worldToLocal(corner.x, corner.y);
1810
- if (!local) continue;
1811
- localMinX = Math.min(localMinX, local.x);
1812
- localMinY = Math.min(localMinY, local.y);
1813
- localMaxX = Math.max(localMaxX, local.x);
1814
- localMaxY = Math.max(localMaxY, local.y);
1815
- }
1816
- visible = localMaxX >= 0 && localMinX <= ancestor.width && localMaxY >= 0 && localMinY <= ancestor.height;
1817
- }
1818
- }
1848
+ const visible = this.projectionBoxVisible(node, worldTf, 0);
1819
1849
  const display = visible ? "" : "none";
1820
1850
  if (el.style.display !== display) el.style.display = display;
1821
1851
  }
@@ -98,6 +98,18 @@ export interface SceneOptions {
98
98
  * scenes to skip the sync walk.
99
99
  */
100
100
  contentProjection?: boolean;
101
+ /**
102
+ * How far outside the viewport (in CSS px, each side) content projections are
103
+ * materialized as DOM. Projections whose box is farther than this are not
104
+ * created — and are removed when they scroll past it — so a document taller
105
+ * than the viewport keeps only a bounded, near-viewport set of DOM nodes
106
+ * instead of one element (plus a `<span>` per line) per block for the whole
107
+ * document. A larger margin keeps more off-screen text ready for native
108
+ * find-in-page / selection at the cost of more DOM; `Infinity` restores the
109
+ * legacy "materialize the entire document" behavior. Default: one viewport
110
+ * height (`undefined` → resolved to `Scene.height` at sync time).
111
+ */
112
+ contentProjectionMargin?: number;
101
113
  }
102
114
  /** Frame-rate the loop is capped to when the OS requests reduced motion. */
103
115
  export declare const REDUCED_MOTION_FPS = 30;
@@ -216,6 +228,7 @@ export declare class Scene {
216
228
  private contentMetricScaleEpoch;
217
229
  private contentMetricScaleX;
218
230
  private contentProjectionEnabled;
231
+ private contentProjectionMargin;
219
232
  /**
220
233
  * True while a text-selection drag that started on a projection's blank
221
234
  * region (no text node under the press) is being driven manually — the
@@ -402,6 +415,15 @@ export declare class Scene {
402
415
  * hidden (`display: none`) so text-heavy scenes only materialize what is
403
416
  * visible to the browser's text machinery anyway.
404
417
  */
418
+ /**
419
+ * Whether `node`'s world-space box, expanded by `margin` px on every side,
420
+ * overlaps the scene viewport AND every `clipChildren` ancestor's box. Used
421
+ * both to virtualize content projection (materialize only near-viewport text,
422
+ * at `margin = contentProjectionMargin`) and for the exact `display:none`
423
+ * visibility test (`margin = 0`). Boundless nodes (width/height 0) opt out of
424
+ * culling and always count as visible, matching the legacy behavior.
425
+ */
426
+ private projectionBoxVisible;
405
427
  private syncContentProjection;
406
428
  /**
407
429
  * Materialize a prepared grid in logical source order while positioning each
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectojs/core",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },