@glissade/backend-dom 0.22.0-pre.0 → 0.22.0-pre.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.
Files changed (2) hide show
  1. package/dist/index.js +97 -5
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -69,6 +69,57 @@ function segsToD(segs) {
69
69
  }
70
70
  return parts.join(" ");
71
71
  }
72
+ /**
73
+ * Axis-aligned bounding box of a path in its LOCAL coordinate space, or null for
74
+ * an empty path. Curve control points give a safe superset (the painted curve
75
+ * never exceeds its hull); `E` uses `max(rx,ry)` so the box contains the ellipse
76
+ * at any rotation (exact for the rounded-rect `rx==ry` case). Used to size each
77
+ * shape's `<svg>` island TIGHTLY around its geometry instead of full-canvas —
78
+ * the paint is unchanged (the viewBox maps local coords 1:1), but the SVG box no
79
+ * longer spans the whole viewport, so shapes don't overlap as giant transparent
80
+ * hit-targets.
81
+ */
82
+ function pathBBox(segs) {
83
+ let minX = Infinity;
84
+ let minY = Infinity;
85
+ let maxX = -Infinity;
86
+ let maxY = -Infinity;
87
+ const pt = (x, y) => {
88
+ if (x < minX) minX = x;
89
+ if (x > maxX) maxX = x;
90
+ if (y < minY) minY = y;
91
+ if (y > maxY) maxY = y;
92
+ };
93
+ for (const seg of segs) switch (seg[0]) {
94
+ case "M":
95
+ case "L":
96
+ pt(seg[1], seg[2]);
97
+ break;
98
+ case "C":
99
+ pt(seg[1], seg[2]);
100
+ pt(seg[3], seg[4]);
101
+ pt(seg[5], seg[6]);
102
+ break;
103
+ case "Q":
104
+ pt(seg[1], seg[2]);
105
+ pt(seg[3], seg[4]);
106
+ break;
107
+ case "E": {
108
+ const r = Math.max(seg[3], seg[4]);
109
+ pt(seg[1] - r, seg[2] - r);
110
+ pt(seg[1] + r, seg[2] + r);
111
+ break;
112
+ }
113
+ case "Z": break;
114
+ }
115
+ if (!Number.isFinite(minX)) return null;
116
+ return {
117
+ x: minX,
118
+ y: minY,
119
+ w: maxX - minX,
120
+ h: maxY - minY
121
+ };
122
+ }
72
123
  /** Short, stable FNV-1a hash (hex) over a string — for deterministic def ids
73
124
  * that survive reorder (same scope+key → same id across frames). */
74
125
  function hashKey(s) {
@@ -141,15 +192,24 @@ var DomBackend = class {
141
192
  const res = list.resources[id];
142
193
  return res && res.kind === "path" ? res.segs : [];
143
194
  };
144
- /** A fresh `<svg>` geometry island absolutely positioned over the cursor. */
195
+ /**
196
+ * A fresh `<svg>` geometry island. Starts collapsed (0×0) — fillPath/
197
+ * strokePath size it TIGHTLY to the path bbox per render (#sizeIsland);
198
+ * clip's defs-only island stays collapsed (it renders nothing, only holds a
199
+ * <clipPath> referenced by id). `overflow:visible` keeps any curve/miter
200
+ * overshoot painted; `pointer-events:none` makes the box's transparent area
201
+ * click-through, so it can't swallow clicks meant for shapes behind it (the
202
+ * painted path re-enables hit-testing with its own `pointer-events`).
203
+ */
145
204
  const island = () => {
146
205
  const svg = doc.createElementNS(SVG_NS, "svg");
147
- svg.setAttribute("width", String(list.size.w));
148
- svg.setAttribute("height", String(list.size.h));
206
+ svg.setAttribute("width", "0");
207
+ svg.setAttribute("height", "0");
149
208
  svg.style.position = "absolute";
150
209
  svg.style.left = "0";
151
210
  svg.style.top = "0";
152
211
  svg.style.overflow = "visible";
212
+ svg.style.pointerEvents = "none";
153
213
  return svg;
154
214
  };
155
215
  list.commands.forEach((cmd, i) => {
@@ -225,6 +285,7 @@ var DomBackend = class {
225
285
  const o = this.#matchOrCreate(cursor, key, "fillPath", () => {
226
286
  const svg = island();
227
287
  const path = doc.createElementNS(SVG_NS, "path");
288
+ path.style.pointerEvents = "auto";
228
289
  svg.appendChild(path);
229
290
  return {
230
291
  op: "fillPath",
@@ -234,8 +295,10 @@ var DomBackend = class {
234
295
  };
235
296
  });
236
297
  const path = o.path;
237
- this.#setAttr(path, o, "d", "d", segsToD(pathSegs(cmd.path)));
298
+ const segs = pathSegs(cmd.path);
299
+ this.#setAttr(path, o, "d", "d", segsToD(segs));
238
300
  this.#setAttr(path, o, "fill", "fill", this.#resolvePaint(cmd.paint, o, scope, key));
301
+ this.#sizeIsland(o.el, o, segs, 0);
239
302
  this.#stamp(o, path, id);
240
303
  break;
241
304
  }
@@ -244,6 +307,7 @@ var DomBackend = class {
244
307
  const o = this.#matchOrCreate(cursor, key, "strokePath", () => {
245
308
  const svg = island();
246
309
  const path = doc.createElementNS(SVG_NS, "path");
310
+ path.style.pointerEvents = "stroke";
247
311
  svg.appendChild(path);
248
312
  return {
249
313
  op: "strokePath",
@@ -253,10 +317,12 @@ var DomBackend = class {
253
317
  };
254
318
  });
255
319
  const path = o.path;
256
- this.#setAttr(path, o, "d", "d", segsToD(pathSegs(cmd.path)));
320
+ const segs = pathSegs(cmd.path);
321
+ this.#setAttr(path, o, "d", "d", segsToD(segs));
257
322
  this.#setAttr(path, o, "fill", "fill", "none");
258
323
  this.#setAttr(path, o, "stroke", "stroke", this.#resolvePaint(cmd.paint, o, scope, key));
259
324
  this.#applyStroke(path, o, cmd.stroke);
325
+ this.#sizeIsland(o.el, o, segs, cmd.stroke.width);
260
326
  this.#stamp(o, path, id);
261
327
  break;
262
328
  }
@@ -523,6 +589,32 @@ var DomBackend = class {
523
589
  this.#setAttr(el, o, "nodeId", "data-node-id", id);
524
590
  }
525
591
  /**
592
+ * Size a geometry island's `<svg>` box tightly to its path bbox (in the
593
+ * cursor's local space) via the `viewBox`, so the painted coordinates are
594
+ * UNCHANGED (1:1 mapping) while the element box shrinks from full-canvas to the
595
+ * shape. Cached writes — only touches the DOM when the bbox moves. `pad`
596
+ * (stroke width) grows the box so it contains a stroke straddling the
597
+ * centerline; `overflow:visible` covers any residual curve/miter overshoot.
598
+ */
599
+ #sizeIsland(svg, o, segs, pad) {
600
+ const bb = pathBBox(segs);
601
+ if (bb === null) {
602
+ this.#setAttr(svg, o, "svgW", "width", "0");
603
+ this.#setAttr(svg, o, "svgH", "height", "0");
604
+ this.#setAttr(svg, o, "svgVB", "viewBox", void 0);
605
+ return;
606
+ }
607
+ const x = bb.x - pad;
608
+ const y = bb.y - pad;
609
+ const w = bb.w + 2 * pad;
610
+ const h = bb.h + 2 * pad;
611
+ this.#setStyle(o, svg, "left", `${x}px`);
612
+ this.#setStyle(o, svg, "top", `${y}px`);
613
+ this.#setAttr(svg, o, "svgW", "width", String(w));
614
+ this.#setAttr(svg, o, "svgH", "height", String(h));
615
+ this.#setAttr(svg, o, "svgVB", "viewBox", `${x} ${y} ${w} ${h}`);
616
+ }
617
+ /**
526
618
  * Caret-preserving text write. RULE B (freeze): never touch the text while the
527
619
  * div (or a descendant) is the focused contentEditable. RULE A (patch-only):
528
620
  * write nothing when unchanged; otherwise mutate the SAME Text node's `.data`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/backend-dom",
3
- "version": "0.22.0-pre.0",
3
+ "version": "0.22.0-pre.1",
4
4
  "description": "glissade DOM render backend: DisplayList -> HTML/SVG elements. A preview / non-parity realtime tier (accessibility, selectable text, CSS-native embedding) — NOT a Skia-export twin.",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -18,8 +18,8 @@
18
18
  "dist"
19
19
  ],
20
20
  "dependencies": {
21
- "@glissade/core": "0.22.0-pre.0",
22
- "@glissade/scene": "0.22.0-pre.0"
21
+ "@glissade/core": "0.22.0-pre.1",
22
+ "@glissade/scene": "0.22.0-pre.1"
23
23
  },
24
24
  "repository": {
25
25
  "type": "git",