@glissade/backend-dom 0.22.0-pre.1 → 0.22.0-pre.2

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 +35 -10
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -20,17 +20,20 @@ function ellipsePoint(cx, cy, rx, ry, phi, theta) {
20
20
  const sp = Math.sin(phi);
21
21
  return [cx + rx * ct * cp - ry * st * sp, cy + rx * ct * sp + ry * st * cp];
22
22
  }
23
- /** An `['E', cx, cy, rx, ry, rot, a0, a1]` ellipse seg → SVG path commands
24
- * (`M start A [A …]`). SVG can't draw a ≥360° arc in one `A`, so a full
25
- * ellipse splits into two half-arcs. The dominant `E` producer is
23
+ /** An `['E', cx, cy, rx, ry, rot, a0, a1]` ellipse seg → SVG path commands. When
24
+ * `continues` (an open subpath precedes it e.g. a rounded-rect corner after an
25
+ * edge `L`) it leads with `L start` so the contour stays ONE continuous subpath
26
+ * (a stray `M` would break the fill — e1JP5_1IzI2D); standalone (a Circle's `E`
27
+ * is the first seg) it leads with `M start`. SVG can't draw a ≥360° arc in one
28
+ * `A`, so a full ellipse splits into two half-arcs. The dominant `E` producer is
26
29
  * `roundedRectSegs`/`Circle`, whose quarter/full arcs this reconstructs exactly. */
27
- function ellipseToArcs(seg) {
30
+ function ellipseToArcs(seg, continues) {
28
31
  const [, cx, cy, rx, ry, rot, a0, a1] = seg;
29
32
  const rotDeg = rot * 180 / Math.PI;
30
33
  const delta = a1 - a0;
31
34
  const sweep = delta >= 0 ? 1 : 0;
32
35
  const [sx, sy] = ellipsePoint(cx, cy, rx, ry, rot, a0);
33
- const out = [`M${sx} ${sy}`];
36
+ const out = [`${continues ? "L" : "M"}${sx} ${sy}`];
34
37
  if (Math.abs(delta) >= 2 * Math.PI - 1e-9) {
35
38
  const dir = sweep ? Math.PI : -Math.PI;
36
39
  const [mx, my] = ellipsePoint(cx, cy, rx, ry, rot, a0 + dir);
@@ -47,24 +50,31 @@ function ellipseToArcs(seg) {
47
50
  /** Turn a `PathSeg[]` into an SVG `d` attribute (M/L/C/Q/E/Z — the full set). */
48
51
  function segsToD(segs) {
49
52
  const parts = [];
53
+ let open = false;
50
54
  for (const seg of segs) switch (seg[0]) {
51
55
  case "M":
52
56
  parts.push(`M${seg[1]} ${seg[2]}`);
57
+ open = true;
53
58
  break;
54
59
  case "L":
55
60
  parts.push(`L${seg[1]} ${seg[2]}`);
61
+ open = true;
56
62
  break;
57
63
  case "C":
58
64
  parts.push(`C${seg[1]} ${seg[2]} ${seg[3]} ${seg[4]} ${seg[5]} ${seg[6]}`);
65
+ open = true;
59
66
  break;
60
67
  case "Q":
61
68
  parts.push(`Q${seg[1]} ${seg[2]} ${seg[3]} ${seg[4]}`);
69
+ open = true;
62
70
  break;
63
71
  case "E":
64
- parts.push(ellipseToArcs(seg));
72
+ parts.push(ellipseToArcs(seg, open));
73
+ open = true;
65
74
  break;
66
75
  case "Z":
67
76
  parts.push("Z");
77
+ open = false;
68
78
  break;
69
79
  }
70
80
  return parts.join(" ");
@@ -219,11 +229,13 @@ var DomBackend = class {
219
229
  stack.push(cursor);
220
230
  scopeStack.push(scope);
221
231
  break;
222
- case "restore":
223
- this.#pruneCursor(cursor);
224
- cursor = stack.pop() ?? this.root;
232
+ case "restore": {
233
+ const saved = stack.pop() ?? this.root;
234
+ if (cursor !== saved) this.#pruneCursor(cursor);
235
+ cursor = saved;
225
236
  scope = scopeStack.pop() ?? "";
226
237
  break;
238
+ }
227
239
  case "transform": {
228
240
  const key = this.#keyFor(cursor, id, "transform");
229
241
  const o = this.#matchOrCreate(cursor, key, "transform", () => {
@@ -437,6 +449,7 @@ var DomBackend = class {
437
449
  measureText(text, font) {
438
450
  const size = font.size;
439
451
  const span = this.#ensureMeasureSpan();
452
+ if (!span.isConnected) this.#mountMeasureSpan(span);
440
453
  span.style.font = fontString(font);
441
454
  span.style.fontVariationSettings = font.fontVariationSettings ?? "normal";
442
455
  span.style.letterSpacing = font.letterSpacing !== void 0 ? `${font.letterSpacing}px` : "normal";
@@ -658,10 +671,22 @@ var DomBackend = class {
658
671
  span.style.whiteSpace = "pre";
659
672
  span.style.left = "-99999px";
660
673
  span.style.top = "0";
661
- (this.#host ?? this.#doc.body ?? this.root).appendChild(span);
662
674
  this.#measureSpan = span;
675
+ this.#mountMeasureSpan(span);
663
676
  return span;
664
677
  }
678
+ /**
679
+ * Attach the measuring span to a CONNECTED layout tree. A detached element
680
+ * reports a 0-width rect in real browsers too — so a measurer mounted under a
681
+ * not-yet-connected host silently falls back to the coarse estimate, mis-breaks
682
+ * long Text, and captions overflow their `width` (aJsLQp0fSs5L). Prefer the
683
+ * document body (reliably live), then a connected host, else the root (headless
684
+ * jsdom has no layout anyway → 0 → estimate, which is expected there).
685
+ */
686
+ #mountMeasureSpan(span) {
687
+ const body = this.#doc.body;
688
+ ((body && body.isConnected !== false ? body : null) ?? (this.#host?.isConnected ? this.#host : null) ?? this.root).appendChild(span);
689
+ }
665
690
  /** Best-effort `src` for a registered image asset (an `HTMLImageElement` or a
666
691
  * URL string); other shapes have no DOM-loadable src in this preview tier. */
667
692
  #imageSrc(assetId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/backend-dom",
3
- "version": "0.22.0-pre.1",
3
+ "version": "0.22.0-pre.2",
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.1",
22
- "@glissade/scene": "0.22.0-pre.1"
21
+ "@glissade/core": "0.22.0-pre.2",
22
+ "@glissade/scene": "0.22.0-pre.2"
23
23
  },
24
24
  "repository": {
25
25
  "type": "git",