@vectojs/core 1.4.0 → 1.5.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 CHANGED
@@ -112,6 +112,31 @@ This projection is intentionally thin. Applications still own accessible names,
112
112
  focus order, contrast, and correct control semantics. See the
113
113
  [accessibility guide](https://vectojs.org/learn/accessibility/).
114
114
 
115
+ ## Static content projection
116
+
117
+ Canvas-rendered text can opt into browser-native find, translation, selection, and copy without
118
+ turning the application into a DOM layout. Override `getContentProjection()` on the owning Entity;
119
+ the Scene creates a transparent, position-synchronized mirror while the VMT remains authoritative:
120
+
121
+ ```ts
122
+ class SelectableLabel extends Entity {
123
+ getContentProjection() {
124
+ return {
125
+ text: 'Selectable canvas text',
126
+ font: '18px Inter',
127
+ lineHeight: 24,
128
+ selectable: true,
129
+ };
130
+ }
131
+ }
132
+ ```
133
+
134
+ `selectable` controls whether the mirror receives pointer input. The Scene keeps projection order
135
+ aligned with VMT order, removes descendant mirrors with their subtree, and hides mirrors fully
136
+ outside the viewport or a `clipChildren` ancestor. Tooling can inspect the currently materialized
137
+ node with `scene.getContentElement(entityId)`. Virtualized or non-materialized off-viewport text is
138
+ not searchable until the application brings it into the active scene.
139
+
115
140
  ## Performance model
116
141
 
117
142
  Useful levers include on-demand rendering, viewport culling, spatial hashing, prepared text layout,
package/dist/index.js CHANGED
@@ -549,6 +549,12 @@ var Scene = (_class2 = class _Scene {
549
549
  this.activePortalsThisFrame.delete(node.id);
550
550
  this.activePortalsPrevFrame.delete(node.id);
551
551
  }
552
+ const contentEl = this.contentElements.get(node.id);
553
+ if (contentEl) {
554
+ contentEl.remove();
555
+ this.contentElements.delete(node.id);
556
+ this.a11yNeedsReorder = true;
557
+ }
552
558
  const el = this.a11yElements.get(node.id);
553
559
  if (el) {
554
560
  if (el === this.focusedA11yElement) {
@@ -588,11 +594,6 @@ var Scene = (_class2 = class _Scene {
588
594
  * @param entity - The subtree whose shadow nodes should be removed.
589
595
  */
590
596
  detachA11y(entity) {
591
- const contentEl = this.contentElements.get(entity.id);
592
- if (contentEl) {
593
- contentEl.remove();
594
- this.contentElements.delete(entity.id);
595
- }
596
597
  this.removeA11yRecursively(entity);
597
598
  }
598
599
  /**
@@ -1028,6 +1029,7 @@ var Scene = (_class2 = class _Scene {
1028
1029
  if (el) {
1029
1030
  el.remove();
1030
1031
  this.contentElements.delete(node.id);
1032
+ this.a11yNeedsReorder = true;
1031
1033
  }
1032
1034
  return;
1033
1035
  }
@@ -1052,6 +1054,7 @@ var Scene = (_class2 = class _Scene {
1052
1054
  );
1053
1055
  this.a11yRoot.appendChild(el);
1054
1056
  this.contentElements.set(node.id, el);
1057
+ this.a11yNeedsReorder = true;
1055
1058
  }
1056
1059
  if (el.textContent !== projection.text) el.textContent = projection.text;
1057
1060
  const font = _nullishCoalesce(projection.font, () => ( ""));
@@ -1078,6 +1081,7 @@ var Scene = (_class2 = class _Scene {
1078
1081
  el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
1079
1082
  let visible = true;
1080
1083
  if (node.width > 0 && node.height > 0) {
1084
+ const worldCorners = [];
1081
1085
  let minX = Infinity;
1082
1086
  let minY = Infinity;
1083
1087
  let maxX = -Infinity;
@@ -1087,12 +1091,29 @@ var Scene = (_class2 = class _Scene {
1087
1091
  const ly = i & 2 ? node.height : 0;
1088
1092
  const wx = a * lx + c * ly + e;
1089
1093
  const wy = b * lx + d * ly + f;
1094
+ worldCorners.push({ x: wx, y: wy });
1090
1095
  if (wx < minX) minX = wx;
1091
1096
  if (wx > maxX) maxX = wx;
1092
1097
  if (wy < minY) minY = wy;
1093
1098
  if (wy > maxY) maxY = wy;
1094
1099
  }
1095
1100
  visible = maxX >= 0 && minX <= this.width && maxY >= 0 && minY <= this.height;
1101
+ for (let ancestor = node.parent; visible && ancestor; ancestor = ancestor.parent) {
1102
+ if (!ancestor.clipChildren || ancestor.width <= 0 || ancestor.height <= 0) continue;
1103
+ let localMinX = Infinity;
1104
+ let localMinY = Infinity;
1105
+ let localMaxX = -Infinity;
1106
+ let localMaxY = -Infinity;
1107
+ for (const corner of worldCorners) {
1108
+ const local = ancestor.worldToLocal(corner.x, corner.y);
1109
+ if (!local) continue;
1110
+ localMinX = Math.min(localMinX, local.x);
1111
+ localMinY = Math.min(localMinY, local.y);
1112
+ localMaxX = Math.max(localMaxX, local.x);
1113
+ localMaxY = Math.max(localMaxY, local.y);
1114
+ }
1115
+ visible = localMaxX >= 0 && localMinX <= ancestor.width && localMaxY >= 0 && localMinY <= ancestor.height;
1116
+ }
1096
1117
  }
1097
1118
  const display = visible ? "" : "none";
1098
1119
  if (el.style.display !== display) el.style.display = display;
@@ -1640,6 +1661,10 @@ var Scene = (_class2 = class _Scene {
1640
1661
  getA11yElement(entityId) {
1641
1662
  return this.a11yElements.get(entityId);
1642
1663
  }
1664
+ /** Gets the static-content DOM projection for an entity ID, when materialized. */
1665
+ getContentElement(entityId) {
1666
+ return this.contentElements.get(entityId);
1667
+ }
1643
1668
  /**
1644
1669
  * Gets the root entity of the scene.
1645
1670
  */
package/dist/index.mjs CHANGED
@@ -549,6 +549,12 @@ var Scene = class _Scene {
549
549
  this.activePortalsThisFrame.delete(node.id);
550
550
  this.activePortalsPrevFrame.delete(node.id);
551
551
  }
552
+ const contentEl = this.contentElements.get(node.id);
553
+ if (contentEl) {
554
+ contentEl.remove();
555
+ this.contentElements.delete(node.id);
556
+ this.a11yNeedsReorder = true;
557
+ }
552
558
  const el = this.a11yElements.get(node.id);
553
559
  if (el) {
554
560
  if (el === this.focusedA11yElement) {
@@ -588,11 +594,6 @@ var Scene = class _Scene {
588
594
  * @param entity - The subtree whose shadow nodes should be removed.
589
595
  */
590
596
  detachA11y(entity) {
591
- const contentEl = this.contentElements.get(entity.id);
592
- if (contentEl) {
593
- contentEl.remove();
594
- this.contentElements.delete(entity.id);
595
- }
596
597
  this.removeA11yRecursively(entity);
597
598
  }
598
599
  /**
@@ -1028,6 +1029,7 @@ var Scene = class _Scene {
1028
1029
  if (el) {
1029
1030
  el.remove();
1030
1031
  this.contentElements.delete(node.id);
1032
+ this.a11yNeedsReorder = true;
1031
1033
  }
1032
1034
  return;
1033
1035
  }
@@ -1052,6 +1054,7 @@ var Scene = class _Scene {
1052
1054
  );
1053
1055
  this.a11yRoot.appendChild(el);
1054
1056
  this.contentElements.set(node.id, el);
1057
+ this.a11yNeedsReorder = true;
1055
1058
  }
1056
1059
  if (el.textContent !== projection.text) el.textContent = projection.text;
1057
1060
  const font = projection.font ?? "";
@@ -1078,6 +1081,7 @@ var Scene = class _Scene {
1078
1081
  el.style.transform = `matrix(${a}, ${b}, ${c}, ${d}, 0, 0)`;
1079
1082
  let visible = true;
1080
1083
  if (node.width > 0 && node.height > 0) {
1084
+ const worldCorners = [];
1081
1085
  let minX = Infinity;
1082
1086
  let minY = Infinity;
1083
1087
  let maxX = -Infinity;
@@ -1087,12 +1091,29 @@ var Scene = class _Scene {
1087
1091
  const ly = i & 2 ? node.height : 0;
1088
1092
  const wx = a * lx + c * ly + e;
1089
1093
  const wy = b * lx + d * ly + f;
1094
+ worldCorners.push({ x: wx, y: wy });
1090
1095
  if (wx < minX) minX = wx;
1091
1096
  if (wx > maxX) maxX = wx;
1092
1097
  if (wy < minY) minY = wy;
1093
1098
  if (wy > maxY) maxY = wy;
1094
1099
  }
1095
1100
  visible = maxX >= 0 && minX <= this.width && maxY >= 0 && minY <= this.height;
1101
+ for (let ancestor = node.parent; visible && ancestor; ancestor = ancestor.parent) {
1102
+ if (!ancestor.clipChildren || ancestor.width <= 0 || ancestor.height <= 0) continue;
1103
+ let localMinX = Infinity;
1104
+ let localMinY = Infinity;
1105
+ let localMaxX = -Infinity;
1106
+ let localMaxY = -Infinity;
1107
+ for (const corner of worldCorners) {
1108
+ const local = ancestor.worldToLocal(corner.x, corner.y);
1109
+ if (!local) continue;
1110
+ localMinX = Math.min(localMinX, local.x);
1111
+ localMinY = Math.min(localMinY, local.y);
1112
+ localMaxX = Math.max(localMaxX, local.x);
1113
+ localMaxY = Math.max(localMaxY, local.y);
1114
+ }
1115
+ visible = localMaxX >= 0 && localMinX <= ancestor.width && localMaxY >= 0 && localMinY <= ancestor.height;
1116
+ }
1096
1117
  }
1097
1118
  const display = visible ? "" : "none";
1098
1119
  if (el.style.display !== display) el.style.display = display;
@@ -1640,6 +1661,10 @@ var Scene = class _Scene {
1640
1661
  getA11yElement(entityId) {
1641
1662
  return this.a11yElements.get(entityId);
1642
1663
  }
1664
+ /** Gets the static-content DOM projection for an entity ID, when materialized. */
1665
+ getContentElement(entityId) {
1666
+ return this.contentElements.get(entityId);
1667
+ }
1643
1668
  /**
1644
1669
  * Gets the root entity of the scene.
1645
1670
  */
@@ -330,6 +330,8 @@ export declare class Scene {
330
330
  * Gets the accessibility DOM element projected for the given entity ID.
331
331
  */
332
332
  getA11yElement(entityId: string): HTMLElement | undefined;
333
+ /** Gets the static-content DOM projection for an entity ID, when materialized. */
334
+ getContentElement(entityId: string): HTMLElement | undefined;
333
335
  /**
334
336
  * Gets the root entity of the scene.
335
337
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectojs/core",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },