@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.
- package/dist/DOMProjection.d.ts +26 -0
- package/dist/eventBridge.d.ts +12 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +125 -3
- package/dist/index.mjs +124 -3
- package/package.json +2 -2
package/dist/DOMProjection.d.ts
CHANGED
|
@@ -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
|
package/dist/eventBridge.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
44
|
+
"@vectojs/core": "^1.40.1"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/node": "^26.4.0",
|