@vectojs/core 1.29.0 → 1.31.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.
@@ -12,6 +12,10 @@ var ANIMATABLE_PROPS = /* @__PURE__ */ new Set([
12
12
  "rotation",
13
13
  "opacity"
14
14
  ]);
15
+ function contentLineInHint(hint, y, height) {
16
+ if (hint?.minY === void 0 || hint.maxY === void 0) return true;
17
+ return y + height >= hint.minY && y <= hint.maxY;
18
+ }
15
19
  var VectoJSEvent = class {
16
20
  /** The event name. */
17
21
  type;
@@ -251,6 +255,42 @@ var Entity = class {
251
255
  * nodes, so on-top components stay clickable.
252
256
  */
253
257
  a11yFullViewport = false;
258
+ /**
259
+ * When this entity's a11y shadow node is materialized.
260
+ *
261
+ * `'eager'` (the default) keeps today's behaviour: a shadow node exists for as
262
+ * long as the entity is `interactive` with a box. That is right for a button or
263
+ * a link, and wrong for thousands of ephemeral, individually-meaningless
264
+ * entities — particles, danmaku, graph nodes — where it produces one DOM node
265
+ * per entity every frame.
266
+ *
267
+ * Measured on 5,000 moving interactive entities (`benchmarks/lazy-a11y/`):
268
+ * eager costs **72.2 ms/frame on Chrome and 114.3 ms on Firefox**, missing even
269
+ * 60 Hz, against **1.55/1.63 ms** for the same scene with one node projected —
270
+ * within noise of the 1.26/1.65 ms floor of projecting nothing at all.
271
+ *
272
+ * `'onDemand'` projects a node only while {@link Scene} considers the entity
273
+ * *engaged*: it is focused, it is the current pointer target, or it has been
274
+ * explicitly requested via {@link Scene.requestA11yProjection}. Crucially the
275
+ * trigger is not hover alone — a keyboard or assistive-technology user
276
+ * generates no hover, so a hover-only gate would remove exactly those users'
277
+ * access. Engagement therefore includes focus and an explicit request, and the
278
+ * entity stays hit-testable on canvas throughout, so a click still reaches it
279
+ * and promotes it.
280
+ *
281
+ * `'never'` suppresses the node entirely. Prefer `interactive = false` unless
282
+ * the entity genuinely needs pointer events without any semantic presence;
283
+ * this exists so a purely decorative interactive surface can opt out without
284
+ * losing canvas hit-testing.
285
+ *
286
+ * **This does not replace an aggregate description.** A thousand `'onDemand'`
287
+ * danmaku are individually reachable but say nothing collectively. The proven
288
+ * pattern is one aggregate live region (`role: 'status'`, `a11yFullViewport`)
289
+ * plus a small pool of persistent hotspots for the current selection — see
290
+ * `vectojs-native/danmaku`. Use `'onDemand'` to stop paying per entity, not as
291
+ * the whole accessibility story.
292
+ */
293
+ a11yProjection = "eager";
254
294
  /**
255
295
  * Hide this entity AND its whole subtree from the accessibility/automation
256
296
  * projection, regardless of each node's own `interactive` flag.
@@ -1045,9 +1085,42 @@ var Entity = class {
1045
1085
  * `selectable` is set — natively selectable. Returns `null` by default.
1046
1086
  * Read on the a11y sync cadence, so text changes propagate automatically.
1047
1087
  *
1088
+ * @param hint - Optional advice about which part of the entity is worth
1089
+ * describing. Purely an optimization: ignoring it is always correct, which
1090
+ * is why it is a parameter rather than a required contract change. See
1091
+ * {@link ContentProjectionHint}.
1048
1092
  * @returns The projection descriptor, or `null` to project nothing.
1049
1093
  */
1050
- getContentProjection() {
1094
+ getContentProjection(hint) {
1095
+ void hint;
1096
+ return null;
1097
+ }
1098
+ /**
1099
+ * A cheap, monotonically-increasing stamp of this entity's projected content.
1100
+ *
1101
+ * Purely an optimization, and opt-in: returning `null` (the default) means
1102
+ * "I cannot cheaply tell whether my content changed", and the {@link Scene}
1103
+ * then rebuilds the projection every synced frame exactly as before. An
1104
+ * implementation must bump the value whenever anything
1105
+ * {@link getContentProjection} would report changes — text, fonts, line
1106
+ * geometry, `selectable`, grid revision.
1107
+ *
1108
+ * When two consecutive syncs report the same epoch AND the entity's geometry
1109
+ * is unchanged, `Scene` skips the block *before* calling
1110
+ * {@link getContentProjection}. That matters because the projection call is
1111
+ * O(glyphs-in-block) and the DOM diff around it costs about the same again:
1112
+ * measured on a 1500-resident-block document, a sync in which the projected
1113
+ * text was byte-identical before and after still cost 17.875 ms, and skipping
1114
+ * unchanged blocks took that to 0.475 ms (carryctx CTX-0199, vectojs#343).
1115
+ *
1116
+ * Correctness is entirely on the implementer: a stale epoch means stale DOM,
1117
+ * so bump it in the same place the content is invalidated rather than trying
1118
+ * to enumerate mutation sites afterwards. Any monotonic counter works; the
1119
+ * value is only ever compared for equality with the previous sync's.
1120
+ *
1121
+ * @returns The current content epoch, or `null` to disable skipping.
1122
+ */
1123
+ getContentEpoch() {
1051
1124
  return null;
1052
1125
  }
1053
1126
  /**
@@ -1093,6 +1166,8 @@ var MSDFTextEntity = class extends Entity {
1093
1166
  layoutText = "";
1094
1167
  text = "";
1095
1168
  lastRenderedSeqId = 0;
1169
+ /** Bumped by {@link queueLayout}; read by `Scene` to skip an unchanged sync. */
1170
+ contentEpoch = 0;
1096
1171
  // Atlas-decode subscription (see watchAtlasDecode). Held so `destroy()` can
1097
1172
  // release it: the handler closes over `this`, so leaving it attached to a
1098
1173
  // long-lived shared atlas image would retain the whole entity.
@@ -1212,6 +1287,7 @@ var MSDFTextEntity = class extends Entity {
1212
1287
  });
1213
1288
  }
1214
1289
  queueLayout() {
1290
+ this.contentEpoch++;
1215
1291
  LayoutWorkerManager.getInstance().queueLayout(this.id, this.layoutText, {
1216
1292
  fontId: this.font.id,
1217
1293
  fontSize: this.fontSize,
@@ -1241,6 +1317,9 @@ var MSDFTextEntity = class extends Entity {
1241
1317
  lineHeight: this.lineHeight
1242
1318
  };
1243
1319
  }
1320
+ getContentEpoch() {
1321
+ return this.contentEpoch;
1322
+ }
1244
1323
  isPointInside(globalX, globalY) {
1245
1324
  if (!this.layoutResult) return false;
1246
1325
  const local = this.worldToLocal(globalX, globalY);
@@ -1634,6 +1713,7 @@ var SVGEntity = class extends Entity {
1634
1713
  };
1635
1714
 
1636
1715
  export {
1716
+ contentLineInHint,
1637
1717
  VectoJSEvent,
1638
1718
  Entity,
1639
1719
  MSDFTextEntity,