@vectojs/dom 0.2.0 → 0.2.1

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,8 @@ export interface DOMProjectionStats {
12
12
  opacityWrites: number;
13
13
  sizeWrites: number;
14
14
  contentWrites: number;
15
+ /** `clip-path` writes for the r5 `clipChildren` agreement (dirty-checked). */
16
+ clipWrites: number;
15
17
  poolHits: number;
16
18
  poolMisses: number;
17
19
  }
@@ -46,6 +48,24 @@ export interface DOMKindSpec {
46
48
  * field does not re-apply every frame.
47
49
  */
48
50
  export declare function writeField(cached: Map<string, string>, key: string, next: string | undefined, apply: (value: string | undefined) => void, onWrite?: () => void): boolean;
51
+ /**
52
+ * CSS `clip-path` for `node`'s projected element so DOM rendering agrees with
53
+ * canvas / `HitTester.isHitEligible` clipping (r5): the intersection of every
54
+ * `clipChildren` ancestor's local box, expressed in the node's own local
55
+ * pixels (the element's `clip-path` reference box shares that origin, since
56
+ * the element itself is positioned purely by its world-matrix transform).
57
+ *
58
+ * Uses only public core surface (`parent`, `clipChildren`, `width`/`height`,
59
+ * `getWorldTransform`, `worldToLocal`) — no core internals, so no core change
60
+ * was needed. Zero-area clippers are skipped: they clip nothing on canvas and
61
+ * are skipped by `Scene.projectionBoxVisible` and the a11y region walk too.
62
+ *
63
+ * Returns `''` when nothing clips (the element stays unclipped), and a
64
+ * zero-area `polygon()` when the intersection is empty (fully clipped). A
65
+ * degenerate node transform also returns `''` — its singular matrix already
66
+ * paints nothing, so there is nothing to clip.
67
+ */
68
+ export declare function clipPathForNode(node: Entity): string;
49
69
  /**
50
70
  * The `DOMProjection` (`'dom'`) backend (RFC §§4/5/6/7, P1 prototype).
51
71
  *
@@ -74,6 +94,12 @@ export declare class DOMProjection implements ProjectionBackend {
74
94
  /** Nodes owning a live press: `pointerId`s per node id (RFC4 §5 pin source). */
75
95
  private readonly activeGestures;
76
96
  private mountSeq;
97
+ /**
98
+ * First-mount z per node id (r6): an unmount/remount cycle restores the same
99
+ * z instead of taking a fresh `mountSeq` slot that would reorder the pooled
100
+ * element above later siblings. FIFO-capped at {@link MOUNT_ORDER_CAP}.
101
+ */
102
+ private readonly mountOrder;
77
103
  private readonly stats;
78
104
  /**
79
105
  * @param canvas - The scene's canvas. The root is appended to its
@@ -45,10 +45,21 @@ export interface DOMBridgeHandle {
45
45
  setInteractionMode(mode: DOMInteractionMode): void;
46
46
  /** Current policy. */
47
47
  getInteractionMode(): DOMInteractionMode;
48
+ /**
49
+ * Whether this bridge instance currently owns a live press for `pointerId`
50
+ * (r7: per-instance scope — the authoritative answer for one element, where
51
+ * the module-global {@link getGestureOwner} is the cross-scene election
52
+ * view RFC4's arbitration gate will consult).
53
+ */
54
+ hasGesture(pointerId: number): boolean;
48
55
  /** Remove every listener this bridge installed. Idempotent. */
49
56
  release(): void;
50
57
  }
51
- /** Test/contract introspection: who owns `pointerId`, if anyone. */
58
+ /**
59
+ * Test/contract introspection: the node id owning `pointerId`, if anyone.
60
+ * Single-scene contract preserved (still answers "who owns this press");
61
+ * the value is now the owning node id rather than the constant `'dom'`.
62
+ */
52
63
  export declare function getGestureOwner(pointerId: number): string | undefined;
53
64
  /**
54
65
  * Bridge native DOM events on a projected element into the Vecto event system.
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * materialize as live `HTMLElement`s positioned by their world matrix. Depends
7
7
  * on `@vectojs/core`, never the reverse.
8
8
  */
9
- export { DOMProjection, writeField, DOM_VISUAL_Z_INDEX, DOM_ROOT_ATTR } from './DOMProjection';
9
+ export { DOMProjection, writeField, clipPathForNode, DOM_VISUAL_Z_INDEX, DOM_ROOT_ATTR, } from './DOMProjection';
10
10
  export type { DOMProjectionStats, DOMKindSpec, DOMSyncFields } from './DOMProjection';
11
11
  export { attachDOMBridge, getGestureOwner } from './eventBridge';
12
12
  export type { DOMInteractionMode, DOMBridgeOptions, DOMBridgeHandle } from './eventBridge';
package/dist/index.js CHANGED
@@ -30,6 +30,7 @@ __export(index_exports, {
30
30
  DOM_VISUAL_Z_INDEX: () => DOM_VISUAL_Z_INDEX,
31
31
  affineToMatrix3d: () => affineToMatrix3d,
32
32
  attachDOMBridge: () => attachDOMBridge,
33
+ clipPathForNode: () => clipPathForNode,
33
34
  getGestureOwner: () => getGestureOwner,
34
35
  writeField: () => writeField
35
36
  });
@@ -52,6 +53,7 @@ var PRESS_EVENTS = ["pointerdown", "mousedown", "touchstart"];
52
53
  function attachDOMBridge(el, node, options = {}) {
53
54
  const listeners = [];
54
55
  let mode = options.mode ?? "selection";
56
+ const owned = /* @__PURE__ */ new Set();
55
57
  const on = (type, handler, capture = false) => {
56
58
  el.addEventListener(type, handler, capture);
57
59
  listeners.push({ type, handler, capture });
@@ -59,7 +61,8 @@ function attachDOMBridge(el, node, options = {}) {
59
61
  const pressCapture = (e) => {
60
62
  if (e.type === "pointerdown") {
61
63
  const id = e.pointerId ?? 0;
62
- if (!gestureOwner.has(id)) gestureOwner.set(id, "dom");
64
+ if (!gestureOwner.has(id)) gestureOwner.set(id, node.id);
65
+ owned.add(id);
63
66
  options.onGestureStart?.(node.id, id);
64
67
  node.dispatchEvent(new import_core.VectoJSEvent("pointerdown", node, e, true, void 0, "dom"));
65
68
  }
@@ -70,7 +73,8 @@ function attachDOMBridge(el, node, options = {}) {
70
73
  on(type, (e) => {
71
74
  if (type === "pointerup" || type === "pointercancel") {
72
75
  const id = e.pointerId ?? 0;
73
- gestureOwner.delete(id);
76
+ owned.delete(id);
77
+ if (gestureOwner.get(id) === node.id) gestureOwner.delete(id);
74
78
  options.onGestureEnd?.(node.id, id);
75
79
  }
76
80
  node.dispatchEvent(new import_core.VectoJSEvent(type, node, e, true, void 0, "dom"));
@@ -114,6 +118,9 @@ function attachDOMBridge(el, node, options = {}) {
114
118
  getInteractionMode() {
115
119
  return mode;
116
120
  },
121
+ hasGesture(pointerId) {
122
+ return owned.has(pointerId);
123
+ },
117
124
  release() {
118
125
  if (released) return;
119
126
  released = true;
@@ -121,6 +128,10 @@ function attachDOMBridge(el, node, options = {}) {
121
128
  el.removeEventListener(type, handler, capture);
122
129
  }
123
130
  listeners.length = 0;
131
+ for (const id of owned) {
132
+ if (gestureOwner.get(id) === node.id) gestureOwner.delete(id);
133
+ }
134
+ owned.clear();
124
135
  }
125
136
  };
126
137
  }
@@ -134,6 +145,7 @@ function affineToMatrix3d(m) {
134
145
  var DOM_VISUAL_Z_INDEX = "9";
135
146
  var DOM_ROOT_ATTR = "data-vecto-dom-root";
136
147
  var POOL_CAP_PER_TAG = 8;
148
+ var MOUNT_ORDER_CAP = 2048;
137
149
  function strProp(node, key) {
138
150
  const value = node[key];
139
151
  return typeof value === "string" ? value : void 0;
@@ -198,6 +210,90 @@ var passthroughSpec = (tag) => ({
198
210
  }
199
211
  });
200
212
  var fallbackSpec = passthroughSpec("div");
213
+ function roundClip(v) {
214
+ if (!Number.isFinite(v)) return 0;
215
+ const r = Math.round(v * 1e3) / 1e3;
216
+ return r === 0 ? 0 : r;
217
+ }
218
+ function signedArea(poly) {
219
+ let area = 0;
220
+ for (let i = 0; i < poly.length; i++) {
221
+ const p = poly[i];
222
+ const q = poly[(i + 1) % poly.length];
223
+ area += p.x * q.y - q.x * p.y;
224
+ }
225
+ return area / 2;
226
+ }
227
+ function intersectConvexPolygons(subject, clip) {
228
+ const ccw = signedArea(clip) >= 0;
229
+ let output = subject;
230
+ for (let i = 0; i < clip.length; i++) {
231
+ const a = clip[i];
232
+ const b = clip[(i + 1) % clip.length];
233
+ const inside = (p) => {
234
+ const cross = (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x);
235
+ return ccw ? cross >= -1e-9 : cross <= 1e-9;
236
+ };
237
+ const crossing = (p, q) => {
238
+ const dx = q.x - p.x;
239
+ const dy = q.y - p.y;
240
+ const ex = b.x - a.x;
241
+ const ey = b.y - a.y;
242
+ const denom = dx * ey - dy * ex;
243
+ if (Math.abs(denom) < 1e-12) return { x: p.x, y: p.y };
244
+ const t = ((a.x - p.x) * ey - (a.y - p.y) * ex) / denom;
245
+ return { x: p.x + t * dx, y: p.y + t * dy };
246
+ };
247
+ const input = output;
248
+ output = [];
249
+ if (input.length === 0) break;
250
+ for (let j = 0; j < input.length; j++) {
251
+ const current = input[j];
252
+ const previous = input[(j + input.length - 1) % input.length];
253
+ const currentIn = inside(current);
254
+ const previousIn = inside(previous);
255
+ if (currentIn) {
256
+ if (!previousIn) output.push(crossing(previous, current));
257
+ output.push(current);
258
+ } else if (previousIn) {
259
+ output.push(crossing(previous, current));
260
+ }
261
+ }
262
+ }
263
+ return output;
264
+ }
265
+ function clipPathForNode(node) {
266
+ let poly = null;
267
+ for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
268
+ if (!ancestor.clipChildren || ancestor.width <= 0 || ancestor.height <= 0) continue;
269
+ const t = ancestor.getWorldTransform();
270
+ const corners = [
271
+ { x: t.e, y: t.f },
272
+ { x: t.a * ancestor.width + t.e, y: t.b * ancestor.width + t.f },
273
+ {
274
+ x: t.a * ancestor.width + t.c * ancestor.height + t.e,
275
+ y: t.b * ancestor.width + t.d * ancestor.height + t.f
276
+ },
277
+ { x: t.c * ancestor.height + t.e, y: t.d * ancestor.height + t.f }
278
+ ];
279
+ const quad = [];
280
+ let degenerate = false;
281
+ for (const corner of corners) {
282
+ const local = node.worldToLocal(corner.x, corner.y);
283
+ if (!local) {
284
+ degenerate = true;
285
+ break;
286
+ }
287
+ quad.push({ x: local.x, y: local.y });
288
+ }
289
+ if (degenerate) return "";
290
+ poly = poly ? intersectConvexPolygons(poly, quad) : quad;
291
+ if (poly.length === 0) break;
292
+ }
293
+ if (!poly) return "";
294
+ if (poly.length === 0) return "polygon(0px 0px, 0px 0px, 0px 0px)";
295
+ return `polygon(${poly.map((p) => `${roundClip(p.x)}px ${roundClip(p.y)}px`).join(", ")})`;
296
+ }
201
297
  var DOMProjection = class {
202
298
  kind = "dom";
203
299
  root = null;
@@ -209,6 +305,12 @@ var DOMProjection = class {
209
305
  /** Nodes owning a live press: `pointerId`s per node id (RFC4 §5 pin source). */
210
306
  activeGestures = /* @__PURE__ */ new Map();
211
307
  mountSeq = 0;
308
+ /**
309
+ * First-mount z per node id (r6): an unmount/remount cycle restores the same
310
+ * z instead of taking a fresh `mountSeq` slot that would reorder the pooled
311
+ * element above later siblings. FIFO-capped at {@link MOUNT_ORDER_CAP}.
312
+ */
313
+ mountOrder = /* @__PURE__ */ new Map();
212
314
  stats = {
213
315
  mounts: 0,
214
316
  unmounts: 0,
@@ -216,6 +318,7 @@ var DOMProjection = class {
216
318
  opacityWrites: 0,
217
319
  sizeWrites: 0,
218
320
  contentWrites: 0,
321
+ clipWrites: 0,
219
322
  poolHits: 0,
220
323
  poolMisses: 0
221
324
  };
@@ -302,6 +405,7 @@ var DOMProjection = class {
302
405
  this.stats.poolHits += 1;
303
406
  if (node.domKind === "text") el.style.userSelect = "text";
304
407
  el.style.display = "";
408
+ el.style.clipPath = "";
305
409
  } else {
306
410
  this.stats.poolMisses += 1;
307
411
  el = spec.create(node);
@@ -347,6 +451,7 @@ var DOMProjection = class {
347
451
  lastOpacity: "",
348
452
  lastWidth: "",
349
453
  lastHeight: "",
454
+ lastClip: "",
350
455
  intrinsicWidth: 0,
351
456
  intrinsicHeight: 0
352
457
  };
@@ -363,7 +468,16 @@ var DOMProjection = class {
363
468
  state.observer = observer;
364
469
  }
365
470
  this.states.set(node.id, state);
366
- el.style.zIndex = String(1 + this.mountSeq++);
471
+ let z = this.mountOrder.get(node.id);
472
+ if (z === void 0) {
473
+ z = 1 + this.mountSeq++;
474
+ if (this.mountOrder.size >= MOUNT_ORDER_CAP) {
475
+ const oldest = this.mountOrder.keys().next();
476
+ if (!oldest.done) this.mountOrder.delete(oldest.value);
477
+ }
478
+ this.mountOrder.set(node.id, z);
479
+ }
480
+ el.style.zIndex = String(z);
367
481
  node.domResident = true;
368
482
  this.stats.mounts += 1;
369
483
  }
@@ -405,6 +519,12 @@ var DOMProjection = class {
405
519
  this.stats.sizeWrites += 1;
406
520
  }
407
521
  }
522
+ const clip = clipPathForNode(node);
523
+ if (state.lastClip !== clip) {
524
+ state.el.style.clipPath = clip;
525
+ state.lastClip = clip;
526
+ this.stats.clipWrites += 1;
527
+ }
408
528
  const spec = this.specFor(node);
409
529
  const cached = state.cached;
410
530
  const fields = {
@@ -448,6 +568,7 @@ var DOMProjection = class {
448
568
  this.states.clear();
449
569
  this.pools.clear();
450
570
  this.activeGestures.clear();
571
+ this.mountOrder.clear();
451
572
  this.root?.remove();
452
573
  this.root = null;
453
574
  this.sentinel = null;
@@ -584,6 +705,7 @@ var DOMTransform = class extends import_core2.Entity {
584
705
  DOM_VISUAL_Z_INDEX,
585
706
  affineToMatrix3d,
586
707
  attachDOMBridge,
708
+ clipPathForNode,
587
709
  getGestureOwner,
588
710
  writeField
589
711
  });
package/dist/index.mjs CHANGED
@@ -15,6 +15,7 @@ var PRESS_EVENTS = ["pointerdown", "mousedown", "touchstart"];
15
15
  function attachDOMBridge(el, node, options = {}) {
16
16
  const listeners = [];
17
17
  let mode = options.mode ?? "selection";
18
+ const owned = /* @__PURE__ */ new Set();
18
19
  const on = (type, handler, capture = false) => {
19
20
  el.addEventListener(type, handler, capture);
20
21
  listeners.push({ type, handler, capture });
@@ -22,7 +23,8 @@ function attachDOMBridge(el, node, options = {}) {
22
23
  const pressCapture = (e) => {
23
24
  if (e.type === "pointerdown") {
24
25
  const id = e.pointerId ?? 0;
25
- if (!gestureOwner.has(id)) gestureOwner.set(id, "dom");
26
+ if (!gestureOwner.has(id)) gestureOwner.set(id, node.id);
27
+ owned.add(id);
26
28
  options.onGestureStart?.(node.id, id);
27
29
  node.dispatchEvent(new VectoJSEvent("pointerdown", node, e, true, void 0, "dom"));
28
30
  }
@@ -33,7 +35,8 @@ function attachDOMBridge(el, node, options = {}) {
33
35
  on(type, (e) => {
34
36
  if (type === "pointerup" || type === "pointercancel") {
35
37
  const id = e.pointerId ?? 0;
36
- gestureOwner.delete(id);
38
+ owned.delete(id);
39
+ if (gestureOwner.get(id) === node.id) gestureOwner.delete(id);
37
40
  options.onGestureEnd?.(node.id, id);
38
41
  }
39
42
  node.dispatchEvent(new VectoJSEvent(type, node, e, true, void 0, "dom"));
@@ -77,6 +80,9 @@ function attachDOMBridge(el, node, options = {}) {
77
80
  getInteractionMode() {
78
81
  return mode;
79
82
  },
83
+ hasGesture(pointerId) {
84
+ return owned.has(pointerId);
85
+ },
80
86
  release() {
81
87
  if (released) return;
82
88
  released = true;
@@ -84,6 +90,10 @@ function attachDOMBridge(el, node, options = {}) {
84
90
  el.removeEventListener(type, handler, capture);
85
91
  }
86
92
  listeners.length = 0;
93
+ for (const id of owned) {
94
+ if (gestureOwner.get(id) === node.id) gestureOwner.delete(id);
95
+ }
96
+ owned.clear();
87
97
  }
88
98
  };
89
99
  }
@@ -97,6 +107,7 @@ function affineToMatrix3d(m) {
97
107
  var DOM_VISUAL_Z_INDEX = "9";
98
108
  var DOM_ROOT_ATTR = "data-vecto-dom-root";
99
109
  var POOL_CAP_PER_TAG = 8;
110
+ var MOUNT_ORDER_CAP = 2048;
100
111
  function strProp(node, key) {
101
112
  const value = node[key];
102
113
  return typeof value === "string" ? value : void 0;
@@ -161,6 +172,90 @@ var passthroughSpec = (tag) => ({
161
172
  }
162
173
  });
163
174
  var fallbackSpec = passthroughSpec("div");
175
+ function roundClip(v) {
176
+ if (!Number.isFinite(v)) return 0;
177
+ const r = Math.round(v * 1e3) / 1e3;
178
+ return r === 0 ? 0 : r;
179
+ }
180
+ function signedArea(poly) {
181
+ let area = 0;
182
+ for (let i = 0; i < poly.length; i++) {
183
+ const p = poly[i];
184
+ const q = poly[(i + 1) % poly.length];
185
+ area += p.x * q.y - q.x * p.y;
186
+ }
187
+ return area / 2;
188
+ }
189
+ function intersectConvexPolygons(subject, clip) {
190
+ const ccw = signedArea(clip) >= 0;
191
+ let output = subject;
192
+ for (let i = 0; i < clip.length; i++) {
193
+ const a = clip[i];
194
+ const b = clip[(i + 1) % clip.length];
195
+ const inside = (p) => {
196
+ const cross = (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x);
197
+ return ccw ? cross >= -1e-9 : cross <= 1e-9;
198
+ };
199
+ const crossing = (p, q) => {
200
+ const dx = q.x - p.x;
201
+ const dy = q.y - p.y;
202
+ const ex = b.x - a.x;
203
+ const ey = b.y - a.y;
204
+ const denom = dx * ey - dy * ex;
205
+ if (Math.abs(denom) < 1e-12) return { x: p.x, y: p.y };
206
+ const t = ((a.x - p.x) * ey - (a.y - p.y) * ex) / denom;
207
+ return { x: p.x + t * dx, y: p.y + t * dy };
208
+ };
209
+ const input = output;
210
+ output = [];
211
+ if (input.length === 0) break;
212
+ for (let j = 0; j < input.length; j++) {
213
+ const current = input[j];
214
+ const previous = input[(j + input.length - 1) % input.length];
215
+ const currentIn = inside(current);
216
+ const previousIn = inside(previous);
217
+ if (currentIn) {
218
+ if (!previousIn) output.push(crossing(previous, current));
219
+ output.push(current);
220
+ } else if (previousIn) {
221
+ output.push(crossing(previous, current));
222
+ }
223
+ }
224
+ }
225
+ return output;
226
+ }
227
+ function clipPathForNode(node) {
228
+ let poly = null;
229
+ for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
230
+ if (!ancestor.clipChildren || ancestor.width <= 0 || ancestor.height <= 0) continue;
231
+ const t = ancestor.getWorldTransform();
232
+ const corners = [
233
+ { x: t.e, y: t.f },
234
+ { x: t.a * ancestor.width + t.e, y: t.b * ancestor.width + t.f },
235
+ {
236
+ x: t.a * ancestor.width + t.c * ancestor.height + t.e,
237
+ y: t.b * ancestor.width + t.d * ancestor.height + t.f
238
+ },
239
+ { x: t.c * ancestor.height + t.e, y: t.d * ancestor.height + t.f }
240
+ ];
241
+ const quad = [];
242
+ let degenerate = false;
243
+ for (const corner of corners) {
244
+ const local = node.worldToLocal(corner.x, corner.y);
245
+ if (!local) {
246
+ degenerate = true;
247
+ break;
248
+ }
249
+ quad.push({ x: local.x, y: local.y });
250
+ }
251
+ if (degenerate) return "";
252
+ poly = poly ? intersectConvexPolygons(poly, quad) : quad;
253
+ if (poly.length === 0) break;
254
+ }
255
+ if (!poly) return "";
256
+ if (poly.length === 0) return "polygon(0px 0px, 0px 0px, 0px 0px)";
257
+ return `polygon(${poly.map((p) => `${roundClip(p.x)}px ${roundClip(p.y)}px`).join(", ")})`;
258
+ }
164
259
  var DOMProjection = class {
165
260
  kind = "dom";
166
261
  root = null;
@@ -172,6 +267,12 @@ var DOMProjection = class {
172
267
  /** Nodes owning a live press: `pointerId`s per node id (RFC4 §5 pin source). */
173
268
  activeGestures = /* @__PURE__ */ new Map();
174
269
  mountSeq = 0;
270
+ /**
271
+ * First-mount z per node id (r6): an unmount/remount cycle restores the same
272
+ * z instead of taking a fresh `mountSeq` slot that would reorder the pooled
273
+ * element above later siblings. FIFO-capped at {@link MOUNT_ORDER_CAP}.
274
+ */
275
+ mountOrder = /* @__PURE__ */ new Map();
175
276
  stats = {
176
277
  mounts: 0,
177
278
  unmounts: 0,
@@ -179,6 +280,7 @@ var DOMProjection = class {
179
280
  opacityWrites: 0,
180
281
  sizeWrites: 0,
181
282
  contentWrites: 0,
283
+ clipWrites: 0,
182
284
  poolHits: 0,
183
285
  poolMisses: 0
184
286
  };
@@ -265,6 +367,7 @@ var DOMProjection = class {
265
367
  this.stats.poolHits += 1;
266
368
  if (node.domKind === "text") el.style.userSelect = "text";
267
369
  el.style.display = "";
370
+ el.style.clipPath = "";
268
371
  } else {
269
372
  this.stats.poolMisses += 1;
270
373
  el = spec.create(node);
@@ -310,6 +413,7 @@ var DOMProjection = class {
310
413
  lastOpacity: "",
311
414
  lastWidth: "",
312
415
  lastHeight: "",
416
+ lastClip: "",
313
417
  intrinsicWidth: 0,
314
418
  intrinsicHeight: 0
315
419
  };
@@ -326,7 +430,16 @@ var DOMProjection = class {
326
430
  state.observer = observer;
327
431
  }
328
432
  this.states.set(node.id, state);
329
- el.style.zIndex = String(1 + this.mountSeq++);
433
+ let z = this.mountOrder.get(node.id);
434
+ if (z === void 0) {
435
+ z = 1 + this.mountSeq++;
436
+ if (this.mountOrder.size >= MOUNT_ORDER_CAP) {
437
+ const oldest = this.mountOrder.keys().next();
438
+ if (!oldest.done) this.mountOrder.delete(oldest.value);
439
+ }
440
+ this.mountOrder.set(node.id, z);
441
+ }
442
+ el.style.zIndex = String(z);
330
443
  node.domResident = true;
331
444
  this.stats.mounts += 1;
332
445
  }
@@ -368,6 +481,12 @@ var DOMProjection = class {
368
481
  this.stats.sizeWrites += 1;
369
482
  }
370
483
  }
484
+ const clip = clipPathForNode(node);
485
+ if (state.lastClip !== clip) {
486
+ state.el.style.clipPath = clip;
487
+ state.lastClip = clip;
488
+ this.stats.clipWrites += 1;
489
+ }
371
490
  const spec = this.specFor(node);
372
491
  const cached = state.cached;
373
492
  const fields = {
@@ -411,6 +530,7 @@ var DOMProjection = class {
411
530
  this.states.clear();
412
531
  this.pools.clear();
413
532
  this.activeGestures.clear();
533
+ this.mountOrder.clear();
414
534
  this.root?.remove();
415
535
  this.root = null;
416
536
  this.sentinel = null;
@@ -546,6 +666,7 @@ export {
546
666
  DOM_VISUAL_Z_INDEX,
547
667
  affineToMatrix3d,
548
668
  attachDOMBridge,
669
+ clipPathForNode,
549
670
  getGestureOwner,
550
671
  writeField
551
672
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vectojs/dom",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -41,7 +41,7 @@
41
41
  "test": "vitest run"
42
42
  },
43
43
  "dependencies": {
44
- "@vectojs/core": "^1.40.0"
44
+ "@vectojs/core": "^1.40.1"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/node": "^26.4.0",