@weasel-js/core 1.0.0 → 1.0.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 (38) hide show
  1. package/CHANGELOG.md +604 -0
  2. package/dist/{chunk-D2C6K6FO.js → chunk-PI56ORHE.js} +2162 -1491
  3. package/dist/chunk-PI56ORHE.js.map +1 -0
  4. package/dist/{chunk-DCIU3SRK.js → chunk-W2WB4352.js} +79 -151
  5. package/dist/chunk-W2WB4352.js.map +1 -0
  6. package/dist/clipboard.d.ts +2 -2
  7. package/dist/clipboard.js +0 -1
  8. package/dist/clone.d.ts +2 -2
  9. package/dist/clone.js +0 -1
  10. package/dist/{geometry-DVeZ2i-c.d.ts → geometry-CnISZ11w.d.ts} +1 -1
  11. package/dist/{grid-XcFQVS0f.d.ts → grid-CBiLBnyc.d.ts} +2 -2
  12. package/dist/index.d.ts +202 -54
  13. package/dist/index.js +2 -3
  14. package/dist/insert.d.ts +3 -3
  15. package/dist/insert.js +0 -1
  16. package/dist/insert.js.map +1 -1
  17. package/dist/move.d.ts +5 -5
  18. package/dist/move.js +0 -1
  19. package/dist/move.js.map +1 -1
  20. package/dist/{options-wAvCnOZd.d.ts → options-DoOvx50U.d.ts} +1 -1
  21. package/dist/patterns-builtin.js +0 -1
  22. package/dist/{pointSnapToGrid-BuDZWBrL.d.ts → pointSnapToGrid-Bj4r_lEQ.d.ts} +3 -3
  23. package/dist/{index-ChM3ZJ2v.d.ts → registry-BQL4oyTg.d.ts} +140 -349
  24. package/dist/renderer.d.ts +63 -8
  25. package/dist/renderer.js +2 -3
  26. package/dist/resize.d.ts +5 -5
  27. package/dist/resize.js +0 -1
  28. package/dist/routing.d.ts +105 -6
  29. package/dist/routing.js +1 -2
  30. package/dist/{types-B_-khFM0.d.ts → types-Ch0Goo-j.d.ts} +12 -0
  31. package/dist/{types-BhJJ2OCM.d.ts → types-Ct4n0Z38.d.ts} +1 -1
  32. package/dist/{types-BJ8_cyT7.d.ts → types-Dtxe7jTS.d.ts} +20 -1
  33. package/dist/{types-DUpI7Apc.d.ts → types-Kh_osAq9.d.ts} +8 -1
  34. package/package.json +6 -6
  35. package/dist/chunk-D2C6K6FO.js.map +0 -1
  36. package/dist/chunk-DCIU3SRK.js.map +0 -1
  37. package/dist/chunk-PZ5AY32C.js +0 -9
  38. package/dist/chunk-PZ5AY32C.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { ActionDisabledReason, defineViewportTool, useOptionalDepRegistry, DepRegistryProvider, useActionsRegistry, ActionsProvider, useDepRegistry, defineTool, reportRouteConflicts, buildDepsFromRequires, useDepSource, actionBindings } from './chunk-DCIU3SRK.js';
1
+ import { ActionDisabledReason, useOptionalDepRegistry, DepRegistryProvider, useActionsRegistry, ActionsProvider, useDepRegistry, reportRouteConflicts, buildDepsFromRequires, useDepSource, actionBindings } from './chunk-W2WB4352.js';
2
2
  import { scratchKey, createTransformOp, snap, getScratch, createReparentOp, setScratch } from './chunk-UB6A6L77.js';
3
3
  import { fixedCornerOf, DEFAULT_RESIZE_BEHAVIORS, CORNER_ANCHORS, cornerPoint } from './chunk-Z3KNFTTS.js';
4
4
  import { AUTO_POSE_DESCRIPTOR, RECT_POSE_DESCRIPTOR, resolveUnit, pathPoseDescriptor, polygonFromPoints, starPath, regularPolygonPath, linePath, ellipsePath, rectPath, strokeHitTest, withinPxRadius, boundsOfPath, PATH_M, PATH_L, PATH_Z, PATH_Q, PATH_C, DEFAULT_FLATTEN_TOLERANCE, flattenQuadratic, flattenCubic, PathBuilder, pointInPath, transformPath, translatePath, pxExtent, remapRotatedLeaf, withinPxBox, flattenCubicWithArcLen, flattenQuadraticWithArcLen, PATH_CMD_LENGTHS, pointToGridCell } from './chunk-6SHZHXXR.js';
@@ -13,13 +13,13 @@ import earcut from 'earcut';
13
13
  import { applyToPoint, rotateAboutPoint, boxToBox, pointInPolygon, segmentsCross } from '@weasel-js/geom';
14
14
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
15
15
  import polygonClipping from 'polygon-clipping';
16
- import { matchSpec, mimeMatchesGlob } from '@weasel-js/gestures';
16
+ import { matchSpec, RESERVED_ID_PREFIXES, RESERVED_ID_NAMES, parseTargetSpec, mimeMatchesGlob } from '@weasel-js/gestures';
17
17
  import { NORMAL, IMPLICIT_TAGS } from '@weasel-js/modes';
18
18
  import { createHistory } from '@weasel-js/history';
19
19
  export * from '@weasel-js/history';
20
20
 
21
21
  // src/version.ts
22
- var VERSION = "1.0.0" ;
22
+ var VERSION = "1.0.2" ;
23
23
 
24
24
  // src/features/grid/roundToCell.ts
25
25
  function roundToCell(value, cellSize) {
@@ -399,6 +399,8 @@ var GLMeshCache = class {
399
399
  gl;
400
400
  aPositionLoc;
401
401
  map = /* @__PURE__ */ new WeakMap();
402
+ /** Meshes this context has drawn at least once; see `uploadRecurring`. */
403
+ seen = /* @__PURE__ */ new WeakSet();
402
404
  finalizer;
403
405
  pendingDeletes = [];
404
406
  /** Transient resources allocated this frame; freed at end of render(). */
@@ -423,6 +425,19 @@ var GLMeshCache = class {
423
425
  this.transientThisFrame.push({ vao: handle.vao, vbo, ibo });
424
426
  return handle;
425
427
  }
428
+ /**
429
+ * Upload a Mesh that may or may not recur across frames. Its first sight in
430
+ * *this* context takes `uploadTransient`; every later one takes the
431
+ * persistent handle. One transient upload to find out is cheaper than
432
+ * stranding a persistent VAO on a mesh that never returns, whose release
433
+ * would wait on GC. The transient upload does not populate the persistent
434
+ * map, so steady-state reuse begins on the third frame.
435
+ */
436
+ uploadRecurring(mesh) {
437
+ if (this.seen.has(mesh)) return this.handleFor(mesh);
438
+ this.seen.add(mesh);
439
+ return this.uploadTransient(mesh);
440
+ }
426
441
  /**
427
442
  * Free all transient resources allocated since the last call. Called by
428
443
  * the renderer at the end of each `render()`. Safe under context loss.
@@ -1532,1269 +1547,1269 @@ function getMesh(path) {
1532
1547
  cache.set(path, mesh);
1533
1548
  return mesh;
1534
1549
  }
1535
-
1536
- // src/features/paths/tessellate/polyline.ts
1537
- function extractPolylines(path, opts = {}) {
1538
- if (path.kind === "rect") return [extractRect(path)];
1539
- return extractPolygon(path, opts);
1540
- }
1541
- function extractRect(p) {
1542
- const { x, y, width: w, height: h } = p;
1543
- return {
1544
- points: [x, y, x + w, y, x + w, y + h, x, y + h],
1545
- closed: true,
1546
- anchorA: new Uint32Array([0, 1, 2, 3]),
1547
- anchorB: new Uint32Array([0, 1, 2, 3]),
1548
- anchorT: new Float32Array([0, 0, 0, 0])
1549
- };
1550
+ var UNDERLINE_OFFSET = 0.1;
1551
+ var STRIKETHROUGH_OFFSET = -0.3;
1552
+ var DECORATION_THICKNESS = 0.05;
1553
+ function fillKey(p) {
1554
+ if ("color" in p) return `s:${p.color}:${p.opacity ?? 1}`;
1555
+ return `nx:${Math.random()}`;
1550
1556
  }
1551
- function extractPolygon(p, opts) {
1552
- const tolerance = opts.flattenTolerance ?? DEFAULT_FLATTEN_TOLERANCE;
1553
- const { commands, coords } = p;
1554
- const out = [];
1555
- let anchorCounter = 0;
1556
- let pts = null;
1557
- let aA = null;
1558
- let aB = null;
1559
- let aT = null;
1560
- let current = null;
1561
- let coordIdx = 0;
1562
- let prevX = 0;
1563
- let prevY = 0;
1564
- let prevAnchor = -1;
1565
- const beginContour = () => {
1566
- pts = [];
1567
- aA = [];
1568
- aB = [];
1569
- aT = [];
1570
- const next = { points: pts, closed: false };
1571
- out.push(next);
1572
- return next;
1573
- };
1574
- const dropDuplicateClosingPoint = () => {
1575
- if (!pts || !aA || !aB || !aT) return;
1576
- const n = pts.length / 2;
1577
- if (n < 2) return;
1578
- if (pts[0] !== pts[(n - 1) * 2] || pts[1] !== pts[(n - 1) * 2 + 1]) return;
1579
- pts.length -= 2;
1580
- aA.length -= 1;
1581
- aB.length -= 1;
1582
- aT.length -= 1;
1583
- };
1584
- const commit = (target) => {
1585
- if (!pts || !aA || !aB || !aT) return;
1586
- target.anchorA = new Uint32Array(aA);
1587
- target.anchorB = new Uint32Array(aB);
1588
- target.anchorT = new Float32Array(aT);
1589
- };
1590
- for (let cmdIdx = 0; cmdIdx < commands.length; cmdIdx++) {
1591
- const cmd = commands[cmdIdx];
1592
- switch (cmd) {
1593
- case PATH_M: {
1594
- if (current) commit(current);
1595
- current = beginContour();
1596
- prevX = coords[coordIdx];
1597
- prevY = coords[coordIdx + 1];
1598
- pts.push(prevX, prevY);
1599
- aA.push(anchorCounter);
1600
- aB.push(anchorCounter);
1601
- aT.push(0);
1602
- prevAnchor = anchorCounter;
1603
- anchorCounter++;
1604
- coordIdx += 2;
1605
- break;
1606
- }
1607
- case PATH_L: {
1608
- prevX = coords[coordIdx];
1609
- prevY = coords[coordIdx + 1];
1610
- pts.push(prevX, prevY);
1611
- aA.push(anchorCounter);
1612
- aB.push(anchorCounter);
1613
- aT.push(0);
1614
- prevAnchor = anchorCounter;
1615
- anchorCounter++;
1616
- coordIdx += 2;
1617
- break;
1618
- }
1619
- case PATH_Q: {
1620
- const cx = coords[coordIdx], cy = coords[coordIdx + 1];
1621
- const ex = coords[coordIdx + 2], ey = coords[coordIdx + 3];
1622
- const targetAnchor = anchorCounter;
1623
- const segStart = pts.length / 2;
1624
- const arcAccum = [];
1625
- const total = flattenQuadraticWithArcLen(prevX, prevY, cx, cy, ex, ey, tolerance, pts, arcAccum);
1626
- for (let k = 0; k < arcAccum.length; k++) {
1627
- aA.push(prevAnchor);
1628
- aB.push(targetAnchor);
1629
- aT.push(total > 0 ? arcAccum[k] / total : 0);
1630
- }
1631
- const lastIdx = segStart + arcAccum.length - 1;
1632
- aA[lastIdx] = targetAnchor;
1633
- aB[lastIdx] = targetAnchor;
1634
- aT[lastIdx] = 0;
1635
- prevX = ex;
1636
- prevY = ey;
1637
- prevAnchor = targetAnchor;
1638
- anchorCounter++;
1639
- coordIdx += 4;
1640
- break;
1641
- }
1642
- case PATH_C: {
1643
- const c1x = coords[coordIdx], c1y = coords[coordIdx + 1];
1644
- const c2x = coords[coordIdx + 2], c2y = coords[coordIdx + 3];
1645
- const ex = coords[coordIdx + 4], ey = coords[coordIdx + 5];
1646
- const targetAnchor = anchorCounter;
1647
- const segStart = pts.length / 2;
1648
- const arcAccum = [];
1649
- const total = flattenCubicWithArcLen(prevX, prevY, c1x, c1y, c2x, c2y, ex, ey, tolerance, pts, arcAccum);
1650
- for (let k = 0; k < arcAccum.length; k++) {
1651
- aA.push(prevAnchor);
1652
- aB.push(targetAnchor);
1653
- aT.push(total > 0 ? arcAccum[k] / total : 0);
1654
- }
1655
- const lastIdx = segStart + arcAccum.length - 1;
1656
- aA[lastIdx] = targetAnchor;
1657
- aB[lastIdx] = targetAnchor;
1658
- aT[lastIdx] = 0;
1659
- prevX = ex;
1660
- prevY = ey;
1661
- prevAnchor = targetAnchor;
1662
- anchorCounter++;
1663
- coordIdx += 6;
1664
- break;
1665
- }
1666
- case PATH_Z: {
1667
- if (current) {
1668
- current.closed = true;
1669
- dropDuplicateClosingPoint();
1670
- }
1671
- break;
1672
- }
1673
- default:
1674
- throw new Error(`extractPolylines: unknown command code ${cmd}`);
1675
- }
1557
+ function sameFill(a, b) {
1558
+ if (a === b) return true;
1559
+ if ("color" in a && "color" in b) {
1560
+ return a.color === b.color && (a.opacity ?? 1) === (b.opacity ?? 1);
1676
1561
  }
1677
- if (current) commit(current);
1678
- return out;
1562
+ return false;
1679
1563
  }
1680
-
1681
- // src/features/paths/tessellate/stroke.ts
1682
- var EMPTY_MESH = {
1683
- vertices: new Float32Array(0),
1684
- indices: new Uint32Array(0),
1685
- anchorA: new Uint32Array(0),
1686
- anchorB: new Uint32Array(0),
1687
- anchorT: new Float32Array(0)
1688
- };
1689
- function tessellateStroke(path, stroke, opts = {}) {
1690
- const width = stroke.width ?? 1;
1691
- if (width <= 0) return EMPTY_MESH;
1692
- const join = stroke.join ?? "miter";
1693
- const cap = stroke.cap ?? "butt";
1694
- const align = stroke.align ?? "center";
1695
- const miterLimit = stroke.miterLimit ?? DEFAULT_MITER_LIMIT;
1696
- const varyingThreshold = stroke.varyingWidthJoinThreshold ?? DEFAULT_VARYING_WIDTH_JOIN_THRESHOLD;
1697
- let workingPath = path;
1698
- if (path.kind === "rect" && align !== "center") {
1699
- const aligned = alignedStrokeRect(path, align, width);
1700
- workingPath = {
1701
- kind: "rect",
1702
- x: aligned.x,
1703
- y: aligned.y,
1704
- width: aligned.width,
1705
- height: aligned.height
1564
+ function strokeKey(s) {
1565
+ if (!s) return "-";
1566
+ return [
1567
+ fillKey(s.paint),
1568
+ s.width ?? 1,
1569
+ s.join ?? "miter",
1570
+ s.cap ?? "butt",
1571
+ s.miterLimit ?? "",
1572
+ s.align ?? "center",
1573
+ (s.dash ?? []).join(",")
1574
+ ].join(":");
1575
+ }
1576
+ function groupKey(family, weight, style, synthetic, fill, stroke, source, page) {
1577
+ return `${family}|${weight}|${style}|${synthetic.bold ? 1 : 0}${synthetic.italic ? 1 : 0}|${fillKey(fill)}|${strokeKey(stroke)}|${source}|${page}`;
1578
+ }
1579
+ function getOrCreateGroup(ctx, run, resolved, page, sourceOverride) {
1580
+ const atlasFamily = resolved.resolved.family;
1581
+ const resolvedWeight = resolved.resolved.weight;
1582
+ const resolvedStyle = resolved.resolved.style;
1583
+ const source = sourceOverride ?? resolved.source;
1584
+ const key = groupKey(
1585
+ atlasFamily,
1586
+ resolvedWeight,
1587
+ resolvedStyle,
1588
+ resolved.synthetic,
1589
+ run.fill,
1590
+ source === "outline" ? run.stroke : void 0,
1591
+ source,
1592
+ page
1593
+ );
1594
+ let g = ctx.groups.get(key);
1595
+ if (!g) {
1596
+ g = {
1597
+ family: atlasFamily,
1598
+ weight: resolvedWeight,
1599
+ style: resolvedStyle,
1600
+ synthetic: { ...resolved.synthetic },
1601
+ source,
1602
+ page,
1603
+ fill: run.fill,
1604
+ // Only the outline tier can paint it, and carrying it on an SDF group
1605
+ // would be a promise the renderer cannot keep.
1606
+ ...source === "outline" && run.stroke !== void 0 ? { stroke: run.stroke } : {},
1607
+ quads: [],
1608
+ glyphs: []
1706
1609
  };
1610
+ ctx.groups.set(key, g);
1707
1611
  }
1708
- const polylines = extractPolylines(workingPath, opts);
1709
- if (stroke.vertexWidths && stroke.vertexWidths.length > 0) {
1710
- for (const pl of polylines) populatePolylineWidths(pl, stroke.vertexWidths, width);
1711
- }
1712
- const dash = stroke.dash ?? [];
1713
- const verts = [];
1714
- const idx = [];
1715
- const anchorA = [];
1716
- const anchorB = [];
1717
- const anchorT = [];
1718
- for (const pl of polylines) {
1719
- const subs = dash.length > 0 ? splitForDash(pl, dash) : [pl];
1720
- for (const sub2 of subs) {
1721
- expandPolyline(sub2, width, join, cap, miterLimit, varyingThreshold, verts, idx, anchorA, anchorB, anchorT);
1612
+ return g;
1613
+ }
1614
+ function resolveGlyph(run, font, resolved, cp) {
1615
+ const direct = font.charMap.get(cp);
1616
+ if (direct) return { glyph: direct, font, resolved };
1617
+ const fallback = resolveGlyphFallback(run.fontFamily, run.fontWeight, run.fontStyle);
1618
+ const face = fallback?.dynamicFace;
1619
+ if (fallback && face) {
1620
+ const glyph = face.requestGlyph(cp);
1621
+ if (glyph.xadvance > 0 || glyph.width > 0) {
1622
+ return { glyph, font: face.font, resolved: fallback };
1722
1623
  }
1723
1624
  }
1724
- return {
1725
- vertices: new Float32Array(verts),
1726
- indices: new Uint32Array(idx),
1727
- anchorA: new Uint32Array(anchorA),
1728
- anchorB: new Uint32Array(anchorB),
1729
- anchorT: new Float32Array(anchorT)
1730
- };
1625
+ warnMissingGlyphOnce(resolved.resolved.family, cp);
1626
+ return null;
1731
1627
  }
1732
- function expandPolyline(pl, width, join, cap, miterLimit, varyingThreshold, verts, idx, anchorA, anchorB, anchorT) {
1733
- const half = width / 2;
1734
- const pts = pl.points;
1735
- const ptCount = pts.length / 2;
1736
- const segCount = ptCount - 1;
1737
- if (segCount < 1) return;
1738
- const plA = pl.anchorA ?? new Uint32Array(ptCount);
1739
- const plB = pl.anchorB ?? new Uint32Array(ptCount);
1740
- const plT = pl.anchorT ?? new Float32Array(ptCount);
1741
- const plHalf = pl.widths ? pl.widths.map((w) => w / 2) : null;
1742
- const segs = [];
1743
- const segSrcIdx = [];
1744
- for (let s = 0; s < segCount; s++) {
1745
- const ha = plHalf ? plHalf[s] : half;
1746
- const hb = plHalf ? plHalf[s + 1] : half;
1747
- const seg = makeSeg(pts[s * 2], pts[s * 2 + 1], pts[(s + 1) * 2], pts[(s + 1) * 2 + 1], ha, hb);
1748
- if (seg) {
1749
- segs.push(seg);
1750
- segSrcIdx.push(s);
1628
+ var warnedMissingGlyphs = /* @__PURE__ */ new Set();
1629
+ function warnMissingGlyphOnce(family, cp) {
1630
+ const key = `${family}|${cp}`;
1631
+ if (warnedMissingGlyphs.has(key)) return;
1632
+ warnedMissingGlyphs.add(key);
1633
+ const ch = String.fromCodePoint(cp);
1634
+ console.warn(
1635
+ `weasel layoutRuns: no glyph for U+${cp.toString(16).toUpperCase().padStart(4, "0")} (${JSON.stringify(ch)}) in "${family}", and the dynamic tier could not rasterize it \u2014 skipping the character. Bake it into the atlas, or call registerCanvasFont("${family}") to serve missing codepoints from installed fonts.`
1636
+ );
1637
+ }
1638
+ function layoutRuns(runs, opts) {
1639
+ const ctx = { groups: /* @__PURE__ */ new Map() };
1640
+ const entries = [];
1641
+ let prevCp;
1642
+ let prevFont;
1643
+ let prevFontSize;
1644
+ for (const run of runs) {
1645
+ const resolved = resolveFontVariant(run.fontFamily, run.fontWeight, run.fontStyle);
1646
+ const font = resolved.entry?.font ?? resolved.dynamicFace?.font;
1647
+ if (!font) {
1648
+ prevCp = void 0;
1649
+ prevFont = void 0;
1650
+ prevFontSize = void 0;
1651
+ continue;
1751
1652
  }
1752
- }
1753
- let closerSrcIdx = -1;
1754
- if (pl.closed && segs.length >= 1) {
1755
- const last = segs[segs.length - 1];
1756
- const first = segs[0];
1757
- const haCloser = plHalf ? plHalf[ptCount - 1] : half;
1758
- const hbCloser = plHalf ? plHalf[0] : half;
1759
- const closer = makeSeg(last.bx, last.by, first.ax, first.ay, haCloser, hbCloser);
1760
- if (closer) {
1761
- segs.push(closer);
1762
- closerSrcIdx = ptCount - 1;
1763
- segSrcIdx.push(closerSrcIdx);
1653
+ const scale2 = run.fontSize / font.info.size;
1654
+ const tracking = run.letterSpacing;
1655
+ for (const ch of [...run.text]) {
1656
+ const cp = ch.codePointAt(0);
1657
+ const isNewline = cp === 10;
1658
+ const isSpace = cp === 32;
1659
+ if (isNewline) {
1660
+ entries.push({
1661
+ run,
1662
+ font,
1663
+ glyph: { id: cp, x: 0, y: 0, width: 0, height: 0, xoffset: 0, yoffset: 0, xadvance: 0, page: 0 },
1664
+ // A newline consumes no advance, so it takes no tracking either.
1665
+ cp,
1666
+ advance: 0,
1667
+ tracking: 0,
1668
+ kerningBefore: 0,
1669
+ isSpace: false,
1670
+ isNewline: true,
1671
+ resolved,
1672
+ fontSize: run.fontSize
1673
+ });
1674
+ prevCp = void 0;
1675
+ prevFont = void 0;
1676
+ prevFontSize = void 0;
1677
+ continue;
1678
+ }
1679
+ if (isSpace) {
1680
+ const spaceGlyph = resolved.dynamicFace ? resolved.dynamicFace.requestGlyph(32) : font.charMap.get(32);
1681
+ const advance = spaceGlyph ? spaceGlyph.xadvance * scale2 : run.fontSize * 0.25;
1682
+ let kerningBefore2 = 0;
1683
+ if (prevCp !== void 0 && prevFont !== void 0 && prevFontSize !== void 0) {
1684
+ const kAtlas = prevFont.kerningMap.get(prevCp)?.get(cp) ?? 0;
1685
+ kerningBefore2 = kAtlas * (prevFontSize / prevFont.info.size);
1686
+ }
1687
+ entries.push({
1688
+ run,
1689
+ font,
1690
+ glyph: spaceGlyph ?? { id: 32, x: 0, y: 0, width: 0, height: 0, xoffset: 0, yoffset: 0, xadvance: 0, page: 0 },
1691
+ cp,
1692
+ advance,
1693
+ tracking,
1694
+ kerningBefore: kerningBefore2,
1695
+ isSpace: true,
1696
+ isNewline: false,
1697
+ resolved,
1698
+ fontSize: run.fontSize
1699
+ });
1700
+ prevCp = cp;
1701
+ prevFont = font;
1702
+ prevFontSize = run.fontSize;
1703
+ continue;
1704
+ }
1705
+ const hit = resolved.dynamicFace ? { glyph: resolved.dynamicFace.requestGlyph(cp), font, resolved } : resolveGlyph(run, font, resolved, cp);
1706
+ if (!hit) {
1707
+ prevCp = cp;
1708
+ prevFont = font;
1709
+ prevFontSize = run.fontSize;
1710
+ continue;
1711
+ }
1712
+ const glyphFont = hit.font;
1713
+ const glyphScale = run.fontSize / glyphFont.info.size;
1714
+ let kerningBefore = 0;
1715
+ if (prevCp !== void 0 && prevFont !== void 0 && prevFontSize !== void 0) {
1716
+ const kAtlas = prevFont.kerningMap.get(prevCp)?.get(cp) ?? 0;
1717
+ kerningBefore = kAtlas * (prevFontSize / prevFont.info.size);
1718
+ }
1719
+ entries.push({
1720
+ run,
1721
+ font: glyphFont,
1722
+ glyph: hit.glyph,
1723
+ cp,
1724
+ advance: hit.glyph.xadvance * glyphScale,
1725
+ tracking,
1726
+ kerningBefore,
1727
+ isSpace,
1728
+ isNewline: false,
1729
+ resolved: hit.resolved,
1730
+ fontSize: run.fontSize
1731
+ });
1732
+ prevCp = cp;
1733
+ prevFont = glyphFont;
1734
+ prevFontSize = run.fontSize;
1764
1735
  }
1765
1736
  }
1766
- if (segs.length === 0) return;
1767
- const segBaseIdx = [];
1768
- for (let s = 0; s < segs.length; s++) {
1769
- const seg = segs[s];
1770
- const startSrc = segSrcIdx[s];
1771
- const endSrc = s === segs.length - 1 && pl.closed && closerSrcIdx >= 0 ? 0 : startSrc + 1;
1772
- const base = verts.length / 2;
1773
- segBaseIdx.push(base);
1774
- const nxA = seg.nxUnit * seg.halfA, nyA = seg.nyUnit * seg.halfA;
1775
- const nxB = seg.nxUnit * seg.halfB, nyB = seg.nyUnit * seg.halfB;
1776
- verts.push(seg.ax + nxA, seg.ay + nyA);
1777
- anchorA.push(plA[startSrc]);
1778
- anchorB.push(plB[startSrc]);
1779
- anchorT.push(plT[startSrc]);
1780
- verts.push(seg.ax - nxA, seg.ay - nyA);
1781
- anchorA.push(plA[startSrc]);
1782
- anchorB.push(plB[startSrc]);
1783
- anchorT.push(plT[startSrc]);
1784
- verts.push(seg.bx + nxB, seg.by + nyB);
1785
- anchorA.push(plA[endSrc]);
1786
- anchorB.push(plB[endSrc]);
1787
- anchorT.push(plT[endSrc]);
1788
- verts.push(seg.bx - nxB, seg.by - nyB);
1789
- anchorA.push(plA[endSrc]);
1790
- anchorB.push(plB[endSrc]);
1791
- anchorT.push(plT[endSrc]);
1792
- idx.push(base, base + 1, base + 2, base + 1, base + 3, base + 2);
1793
- }
1794
- const joinCount = pl.closed ? segs.length : segs.length - 1;
1795
- for (let j = 0; j < joinCount; j++) {
1796
- emitJoin(segs, segBaseIdx, j, join, miterLimit, varyingThreshold, verts, idx, anchorA, anchorB, anchorT, segSrcIdx, plA, plB, plT);
1797
- }
1798
- if (!pl.closed && cap !== "butt") {
1799
- const first = segs[0];
1800
- const firstBase = segBaseIdx[0];
1801
- emitCap(first, firstBase + 0, firstBase + 1, false, first.halfA, cap, verts, idx, anchorA, anchorB, anchorT, plA[0], plB[0], plT[0]);
1802
- const last = segs[segs.length - 1];
1803
- const lastBase = segBaseIdx[segs.length - 1];
1804
- const lastSrc = segSrcIdx[segs.length - 1] + 1;
1805
- emitCap(last, lastBase + 2, lastBase + 3, true, last.halfB, cap, verts, idx, anchorA, anchorB, anchorT, plA[lastSrc], plB[lastSrc], plT[lastSrc]);
1737
+ const lines = [];
1738
+ let cur = { entries: [], width: 0, height: 0 };
1739
+ function commitLine() {
1740
+ lines.push(cur);
1741
+ cur = { entries: [], width: 0, height: 0 };
1806
1742
  }
1807
- }
1808
- function populatePolylineWidths(pl, vertexWidths, fallbackWidth) {
1809
- const ptCount = pl.points.length / 2;
1810
- if (ptCount === 0) return;
1811
- const aA = pl.anchorA ?? new Uint32Array(ptCount);
1812
- const aB = pl.anchorB ?? new Uint32Array(ptCount);
1813
- const aT = pl.anchorT ?? new Float32Array(ptCount);
1814
- const widths = new Float32Array(ptCount);
1815
- const read = (i) => {
1816
- const v = vertexWidths[i];
1817
- return typeof v === "number" && isFinite(v) && v > 0 ? v : fallbackWidth;
1818
- };
1819
- for (let i = 0; i < ptCount; i++) {
1820
- const a = aA[i], b = aB[i], t = aT[i];
1821
- if (a === b) {
1822
- widths[i] = read(a);
1823
- } else {
1824
- const wa = read(a);
1825
- const wb = read(b);
1826
- widths[i] = wa + (wb - wa) * t;
1743
+ let i = 0;
1744
+ while (i < entries.length) {
1745
+ const e = entries[i];
1746
+ if (e.isNewline) {
1747
+ if (cur.entries.length === 0) {
1748
+ cur.height = Math.max(cur.height, e.fontSize * opts.lineHeight);
1749
+ cur.blank = e;
1750
+ }
1751
+ commitLine();
1752
+ i++;
1753
+ continue;
1827
1754
  }
1828
- }
1829
- pl.widths = widths;
1830
- }
1831
- var ROUND_CAP_STEP_RAD = 10 * Math.PI / 180;
1832
- function emitCap(seg, leftIdx, rightIdx, atEnd, half, cap, verts, idx, anchorA, anchorB, anchorT, endA, endB, endT) {
1833
- const sign = atEnd ? 1 : -1;
1834
- const dx = (seg.bx - seg.ax) / seg.len * sign;
1835
- const dy = (seg.by - seg.ay) / seg.len * sign;
1836
- const cx = atEnd ? seg.bx : seg.ax;
1837
- const cy = atEnd ? seg.by : seg.ay;
1838
- const nx = seg.nxUnit * half;
1839
- const ny = seg.nyUnit * half;
1840
- if (cap === "square") {
1841
- const ox = cx + dx * half;
1842
- const oy = cy + dy * half;
1843
- const lOut = verts.length / 2;
1844
- verts.push(ox + nx, oy + ny);
1845
- anchorA.push(endA);
1846
- anchorB.push(endB);
1847
- anchorT.push(endT);
1848
- const rOut = verts.length / 2;
1849
- verts.push(ox - nx, oy - ny);
1850
- anchorA.push(endA);
1851
- anchorB.push(endB);
1852
- anchorT.push(endT);
1853
- if (atEnd) {
1854
- idx.push(leftIdx, lOut, rOut, leftIdx, rOut, rightIdx);
1855
- } else {
1856
- idx.push(leftIdx, rOut, lOut, leftIdx, rightIdx, rOut);
1755
+ if (e.isSpace) {
1756
+ if (cur.entries.length > 0) {
1757
+ cur.entries.push(e);
1758
+ cur.width += e.kerningBefore + e.advance + e.tracking;
1759
+ cur.height = Math.max(cur.height, e.fontSize * opts.lineHeight);
1760
+ }
1761
+ i++;
1762
+ continue;
1857
1763
  }
1858
- return;
1859
- }
1860
- if (cap === "round") {
1861
- const pivotIdx = verts.length / 2;
1862
- verts.push(cx, cy);
1863
- anchorA.push(endA);
1864
- anchorB.push(endB);
1865
- anchorT.push(endT);
1866
- const startAngle = Math.atan2(ny, nx);
1867
- const sweep = atEnd ? -Math.PI : Math.PI;
1868
- const steps = Math.max(1, Math.ceil(Math.abs(sweep) / ROUND_CAP_STEP_RAD));
1869
- const stepAngle = sweep / steps;
1870
- let prevIdx = leftIdx;
1871
- for (let i = 1; i < steps; i++) {
1872
- const ang = startAngle + i * stepAngle;
1873
- const fx = cx + Math.cos(ang) * half;
1874
- const fy = cy + Math.sin(ang) * half;
1875
- const newIdx = verts.length / 2;
1876
- verts.push(fx, fy);
1877
- anchorA.push(endA);
1878
- anchorB.push(endB);
1879
- anchorT.push(endT);
1880
- if (atEnd) idx.push(prevIdx, newIdx, pivotIdx);
1881
- else idx.push(prevIdx, pivotIdx, newIdx);
1882
- prevIdx = newIdx;
1764
+ let j = i;
1765
+ let wordWidth = 0;
1766
+ while (j < entries.length && !entries[j].isSpace && !entries[j].isNewline) {
1767
+ const w = entries[j];
1768
+ wordWidth += w.kerningBefore + w.advance + w.tracking;
1769
+ j++;
1883
1770
  }
1884
- if (atEnd) idx.push(prevIdx, rightIdx, pivotIdx);
1885
- else idx.push(prevIdx, pivotIdx, rightIdx);
1771
+ if (Number.isFinite(opts.maxWidth) && cur.width + wordWidth > opts.maxWidth && cur.entries.length > 0) {
1772
+ commitLine();
1773
+ }
1774
+ for (let k = i; k < j; k++) {
1775
+ const w = entries[k];
1776
+ const kerningBefore = cur.entries.length === 0 ? 0 : w.kerningBefore;
1777
+ cur.entries.push({ ...w, kerningBefore });
1778
+ cur.width += kerningBefore + w.advance + w.tracking;
1779
+ cur.height = Math.max(cur.height, w.fontSize * opts.lineHeight);
1780
+ }
1781
+ i = j;
1886
1782
  }
1887
- }
1888
- function splitForDash(pl, dash) {
1889
- const out = [];
1890
- const plA = pl.anchorA ?? new Uint32Array(pl.points.length / 2);
1891
- const plB = pl.anchorB ?? new Uint32Array(pl.points.length / 2);
1892
- const plT = pl.anchorT ?? new Float32Array(pl.points.length / 2);
1893
- let dashIdx = 0;
1894
- let dashRemaining = dash[0];
1895
- let onPhase = true;
1896
- let curPts = onPhase ? [pl.points[0], pl.points[1]] : null;
1897
- let curA = onPhase ? [plA[0]] : null;
1898
- let curB = onPhase ? [plB[0]] : null;
1899
- let curT = onPhase ? [plT[0]] : null;
1900
- const flushAndAdvance = () => {
1901
- if (curPts && curPts.length >= 4) {
1902
- out.push({
1903
- points: curPts,
1904
- closed: false,
1905
- anchorA: new Uint32Array(curA),
1906
- anchorB: new Uint32Array(curB),
1907
- anchorT: new Float32Array(curT)
1908
- });
1783
+ if (cur.entries.length > 0) commitLine();
1784
+ function outlineFor(e) {
1785
+ const min = opts.outlineMinSize;
1786
+ if (min === void 0 || e.fontSize < min) return null;
1787
+ if (e.resolved.synthetic.bold) return null;
1788
+ const r = e.resolved.resolved;
1789
+ return glyphOutline(r.family, r.weight, r.style, e.cp);
1790
+ }
1791
+ const decorations = [];
1792
+ let span = null;
1793
+ function flushSpan() {
1794
+ const s = span;
1795
+ span = null;
1796
+ if (!s || s.x1 <= s.x0) return;
1797
+ const thickness = s.fontSize * DECORATION_THICKNESS;
1798
+ if (s.underline) {
1799
+ const y0 = s.baselineY + s.fontSize * UNDERLINE_OFFSET;
1800
+ decorations.push({ kind: "underline", x0: s.x0, y0, x1: s.x1, y1: y0 + thickness, fill: s.fill });
1909
1801
  }
1910
- curPts = null;
1911
- curA = null;
1912
- curB = null;
1913
- curT = null;
1914
- dashIdx = (dashIdx + 1) % dash.length;
1915
- dashRemaining = dash[dashIdx];
1916
- onPhase = !onPhase;
1917
- };
1918
- let prevX = pl.points[0], prevY = pl.points[1];
1919
- const ptCount = pl.points.length / 2;
1920
- const segCount = pl.closed ? ptCount : ptCount - 1;
1921
- for (let i = 0; i < segCount; i++) {
1922
- const nextIdx = (i + 1) % ptCount;
1923
- const cx = pl.points[nextIdx * 2], cy = pl.points[nextIdx * 2 + 1];
1924
- const startA = plA[i], startB = plB[i], startT = plT[i];
1925
- const endA = plA[nextIdx], endB = plB[nextIdx], endT = plT[nextIdx];
1926
- const segFullDx = cx - prevX, segFullDy = cy - prevY;
1927
- const segFullLen = Math.hypot(segFullDx, segFullDy);
1928
- let segDx = segFullDx, segDy = segFullDy;
1929
- let segLen = segFullLen;
1930
- let traveled = 0;
1931
- while (segLen > 1e-9) {
1932
- if (segLen <= dashRemaining) {
1933
- if (onPhase && curPts) {
1934
- curPts.push(cx, cy);
1935
- curA.push(endA);
1936
- curB.push(endB);
1937
- curT.push(endT);
1938
- }
1939
- dashRemaining -= segLen;
1940
- prevX = cx;
1941
- prevY = cy;
1942
- traveled = segFullLen;
1943
- segLen = 0;
1944
- if (dashRemaining <= 1e-9) {
1945
- flushAndAdvance();
1946
- if (onPhase) {
1947
- curPts = [prevX, prevY];
1948
- curA = [endA];
1949
- curB = [endB];
1950
- curT = [endT];
1951
- }
1952
- }
1953
- } else {
1954
- const tConsume = dashRemaining / segLen;
1955
- const ix = prevX + segDx * tConsume;
1956
- const iy = prevY + segDy * tConsume;
1957
- traveled += dashRemaining;
1958
- const frac = traveled / segFullLen;
1959
- let mA, mB, mT;
1960
- if (startA === endA && startB === endB) {
1961
- mA = startA;
1962
- mB = startB;
1963
- mT = startT + (endT - startT) * frac;
1802
+ if (s.strikethrough) {
1803
+ const y0 = s.baselineY + s.fontSize * STRIKETHROUGH_OFFSET;
1804
+ decorations.push({ kind: "strikethrough", x0: s.x0, y0, x1: s.x1, y1: y0 + thickness, fill: s.fill });
1805
+ }
1806
+ }
1807
+ const lineBoxes = [];
1808
+ let penY = 0;
1809
+ let maxLineWidth = 0;
1810
+ const finiteWidth = Number.isFinite(opts.maxWidth) ? opts.maxWidth : 0;
1811
+ for (const line of lines) {
1812
+ const alignShift = (() => {
1813
+ if (opts.align === "left") return 0;
1814
+ if (!Number.isFinite(opts.maxWidth)) {
1815
+ return opts.align === "center" ? -line.width / 2 : -line.width;
1816
+ }
1817
+ const slack = finiteWidth - line.width;
1818
+ return opts.align === "center" ? slack / 2 : slack;
1819
+ })();
1820
+ const lineX0 = alignShift;
1821
+ const baselineSource = line.entries[0] ?? line.blank;
1822
+ const lineBaselineY = baselineSource ? penY + baselineSource.font.common.base * (baselineSource.fontSize / baselineSource.font.info.size) : penY;
1823
+ let penX = lineX0;
1824
+ for (const e of line.entries) {
1825
+ penX += e.kerningBefore;
1826
+ const step = e.advance + e.tracking;
1827
+ const scale2 = e.fontSize / e.font.info.size;
1828
+ const baselineY = penY + e.font.common.base * scale2;
1829
+ if (e.run.underline || e.run.strikethrough) {
1830
+ if (span !== null && span.underline === e.run.underline && span.strikethrough === e.run.strikethrough && span.fontSize === e.fontSize && span.baselineY === baselineY && sameFill(span.fill, e.run.fill)) {
1831
+ span.x1 = penX + step;
1964
1832
  } else {
1965
- if (frac < 0.5) {
1966
- mA = startA;
1967
- mB = startB;
1968
- mT = startT;
1969
- } else {
1970
- mA = endA;
1971
- mB = endB;
1972
- mT = endT;
1973
- }
1974
- }
1975
- if (onPhase && curPts) {
1976
- curPts.push(ix, iy);
1977
- curA.push(mA);
1978
- curB.push(mB);
1979
- curT.push(mT);
1980
- }
1981
- prevX = ix;
1982
- prevY = iy;
1983
- segDx = cx - prevX;
1984
- segDy = cy - prevY;
1985
- segLen = Math.hypot(segDx, segDy);
1986
- flushAndAdvance();
1987
- if (onPhase) {
1988
- curPts = [prevX, prevY];
1989
- curA = [mA];
1990
- curB = [mB];
1991
- curT = [mT];
1833
+ flushSpan();
1834
+ span = {
1835
+ underline: e.run.underline,
1836
+ strikethrough: e.run.strikethrough,
1837
+ fill: e.run.fill,
1838
+ fontSize: e.fontSize,
1839
+ baselineY,
1840
+ x0: penX,
1841
+ x1: penX + step
1842
+ };
1992
1843
  }
1844
+ } else {
1845
+ flushSpan();
1846
+ }
1847
+ const outlineD = outlineFor(e);
1848
+ if (outlineD !== null) {
1849
+ const group2 = getOrCreateGroup(ctx, e.run, e.resolved, 0, "outline");
1850
+ group2.glyphs.push({
1851
+ d: outlineD,
1852
+ key: `${e.resolved.resolved.family}|${e.resolved.resolved.weight}|${e.resolved.resolved.style}|${e.cp}`,
1853
+ x: penX,
1854
+ baselineY,
1855
+ // Em space is unit-scale, so world units per em is just the size.
1856
+ scale: e.fontSize
1857
+ });
1858
+ penX += step;
1859
+ continue;
1860
+ }
1861
+ if (e.advance === 0 || e.glyph.width === 0 || e.glyph.page < 0) {
1862
+ penX += step;
1863
+ continue;
1993
1864
  }
1865
+ const group = getOrCreateGroup(ctx, e.run, e.resolved, e.glyph.page);
1866
+ const atlasW = e.font.common.scaleW;
1867
+ const atlasH = e.font.common.scaleH;
1868
+ const qx0 = penX + e.glyph.xoffset * scale2;
1869
+ const qy0 = penY + e.glyph.yoffset * scale2;
1870
+ const qx1 = qx0 + e.glyph.width * scale2;
1871
+ const qy1 = qy0 + e.glyph.height * scale2;
1872
+ const u0 = e.glyph.x / atlasW;
1873
+ const v0 = e.glyph.y / atlasH;
1874
+ const u1 = (e.glyph.x + e.glyph.width) / atlasW;
1875
+ const v1 = (e.glyph.y + e.glyph.height) / atlasH;
1876
+ group.quads.push({ x0: qx0, y0: qy0, x1: qx1, y1: qy1, u0, v0, u1, v1, baselineY });
1877
+ penX += step;
1994
1878
  }
1995
- }
1996
- if (curPts && curPts.length >= 4) {
1997
- out.push({
1998
- points: curPts,
1999
- closed: false,
2000
- anchorA: new Uint32Array(curA),
2001
- anchorB: new Uint32Array(curB),
2002
- anchorT: new Float32Array(curT)
1879
+ flushSpan();
1880
+ lineBoxes.push({
1881
+ x0: lineX0,
1882
+ y0: penY,
1883
+ x1: lineX0 + line.width,
1884
+ y1: penY + line.height,
1885
+ baselineY: lineBaselineY
2003
1886
  });
1887
+ maxLineWidth = Math.max(maxLineWidth, line.width);
1888
+ penY += line.height;
2004
1889
  }
2005
- return out;
2006
- }
2007
- function makeSeg(ax, ay, bx, by, halfA, halfB) {
2008
- const dx = bx - ax, dy = by - ay;
2009
- const len = Math.hypot(dx, dy);
2010
- if (len === 0) return null;
2011
1890
  return {
2012
- ax,
2013
- ay,
2014
- bx,
2015
- by,
2016
- nxUnit: -dy / len,
2017
- nyUnit: dx / len,
2018
- halfA,
2019
- halfB,
2020
- len
1891
+ groups: [...ctx.groups.values()],
1892
+ decorations,
1893
+ lines: lineBoxes,
1894
+ bounds: { width: maxLineWidth, height: penY }
2021
1895
  };
2022
1896
  }
2023
- var DEFAULT_MITER_LIMIT = 10;
2024
- var DEFAULT_VARYING_WIDTH_JOIN_THRESHOLD = 1.5;
2025
- function emitJoin(segs, segBaseIdx, j, join, miterLimit, varyingThreshold, verts, idx, anchorA, anchorB, anchorT, segSrcIdx, plA, plB, plT) {
2026
- const a = segs[j];
2027
- const b = segs[(j + 1) % segs.length];
2028
- const aBase = segBaseIdx[j];
2029
- const bBase = segBaseIdx[(j + 1) % segs.length];
2030
- const adx = a.bx - a.ax, ady = a.by - a.ay;
2031
- const bdx = b.bx - b.ax, bdy = b.by - b.ay;
2032
- const cross = adx * bdy - ady * bdx;
2033
- if (cross === 0) return;
2034
- const onPositive = cross > 0;
2035
- const aOuterEnd = onPositive ? aBase + 3 : aBase + 2;
2036
- const bOuterStart = onPositive ? bBase + 1 : bBase + 0;
2037
- const nextSeg = (j + 1) % segs.length;
2038
- const cornerSrc = segSrcIdx[nextSeg];
2039
- const cornerA = plA[cornerSrc];
2040
- const cornerB = plB[cornerSrc];
2041
- const cornerT = plT[cornerSrc];
2042
- const halfAtCorner = a.halfB;
2043
- const taperA = Math.max(a.halfA, a.halfB) / Math.max(1e-9, Math.min(a.halfA, a.halfB));
2044
- const taperB = Math.max(b.halfA, b.halfB) / Math.max(1e-9, Math.min(b.halfA, b.halfB));
2045
- const forceBevelForVarying = taperA > varyingThreshold || taperB > varyingThreshold;
2046
- if (join === "bevel" || forceBevelForVarying) {
2047
- emitBevel(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT);
2048
- return;
2049
- }
2050
- if (join === "miter") {
2051
- const aOX = verts[aOuterEnd * 2], aOY = verts[aOuterEnd * 2 + 1];
2052
- const bOX = verts[bOuterStart * 2], bOY = verts[bOuterStart * 2 + 1];
2053
- const apex = lineLineIntersect(aOX, aOY, adx, ady, bOX, bOY, -bdx, -bdy);
2054
- if (!apex) {
2055
- emitBevel(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT);
2056
- return;
2057
- }
2058
- const miterLen = Math.hypot(apex[0] - a.bx, apex[1] - a.by);
2059
- if (miterLen > miterLimit * halfAtCorner) {
2060
- emitBevel(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT);
2061
- return;
2062
- }
2063
- const apexIdx = verts.length / 2;
2064
- verts.push(apex[0], apex[1]);
2065
- anchorA.push(cornerA);
2066
- anchorB.push(cornerB);
2067
- anchorT.push(cornerT);
2068
- const jIdx = verts.length / 2;
2069
- verts.push(a.bx, a.by);
2070
- anchorA.push(cornerA);
2071
- anchorB.push(cornerB);
2072
- anchorT.push(cornerT);
2073
- if (onPositive) {
2074
- idx.push(aOuterEnd, apexIdx, bOuterStart);
2075
- idx.push(aOuterEnd, bOuterStart, jIdx);
2076
- } else {
2077
- idx.push(aOuterEnd, bOuterStart, apexIdx);
2078
- idx.push(aOuterEnd, jIdx, bOuterStart);
2079
- }
2080
- return;
2081
- }
2082
- if (join === "round") {
2083
- emitRoundJoin(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT);
2084
- return;
2085
- }
2086
- }
2087
- var ROUND_STEP_RAD = 10 * Math.PI / 180;
2088
- function emitRoundJoin(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT) {
2089
- const cx = a.bx, cy = a.by;
2090
- const pivotIdx = verts.length / 2;
2091
- verts.push(cx, cy);
2092
- anchorA.push(cornerA);
2093
- anchorB.push(cornerB);
2094
- anchorT.push(cornerT);
2095
- const startX = verts[aOuterEnd * 2] - cx;
2096
- const startY = verts[aOuterEnd * 2 + 1] - cy;
2097
- const endX = verts[bOuterStart * 2] - cx;
2098
- const endY = verts[bOuterStart * 2 + 1] - cy;
2099
- const r = Math.hypot(startX, startY);
2100
- const startAngle = Math.atan2(startY, startX);
2101
- const endAngle = Math.atan2(endY, endX);
2102
- let sweep = endAngle - startAngle;
2103
- if (onPositive && sweep < 0) sweep += 2 * Math.PI;
2104
- else if (!onPositive && sweep > 0) sweep -= 2 * Math.PI;
2105
- const steps = Math.max(1, Math.ceil(Math.abs(sweep) / ROUND_STEP_RAD));
2106
- const stepAngle = sweep / steps;
2107
- let prevIdx = aOuterEnd;
2108
- for (let i = 1; i < steps; i++) {
2109
- const ang = startAngle + i * stepAngle;
2110
- const fx = cx + Math.cos(ang) * r;
2111
- const fy = cy + Math.sin(ang) * r;
2112
- const newIdx = verts.length / 2;
2113
- verts.push(fx, fy);
2114
- anchorA.push(cornerA);
2115
- anchorB.push(cornerB);
2116
- anchorT.push(cornerT);
2117
- if (onPositive) idx.push(prevIdx, pivotIdx, newIdx);
2118
- else idx.push(prevIdx, newIdx, pivotIdx);
2119
- prevIdx = newIdx;
2120
- }
2121
- if (onPositive) idx.push(prevIdx, pivotIdx, bOuterStart);
2122
- else idx.push(prevIdx, bOuterStart, pivotIdx);
2123
- }
2124
- function emitBevel(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT) {
2125
- const jIdx = verts.length / 2;
2126
- verts.push(a.bx, a.by);
2127
- anchorA.push(cornerA);
2128
- anchorB.push(cornerB);
2129
- anchorT.push(cornerT);
2130
- if (onPositive) idx.push(aOuterEnd, jIdx, bOuterStart);
2131
- else idx.push(aOuterEnd, bOuterStart, jIdx);
2132
- }
2133
- function lineLineIntersect(apx, apy, adx, ady, bpx, bpy, bdx, bdy) {
2134
- const denom = adx * bdy - ady * bdx;
2135
- if (Math.abs(denom) < 1e-9) return null;
2136
- const t = ((bpx - apx) * bdy - (bpy - apy) * bdx) / denom;
2137
- return [apx + t * adx, apy + t * ady];
1897
+ var LAYOUT_CACHE_VARIANT_LIMIT = 8;
1898
+ var cache2 = /* @__PURE__ */ new WeakMap();
1899
+ function outlineBucket(runs, min) {
1900
+ if (min === void 0) return -1;
1901
+ const sizes = /* @__PURE__ */ new Set();
1902
+ for (const r of runs) sizes.add(r.fontSize);
1903
+ let n = 0;
1904
+ for (const size of sizes) if (size >= min) n++;
1905
+ return n;
2138
1906
  }
2139
- var UNDERLINE_OFFSET = 0.1;
2140
- var STRIKETHROUGH_OFFSET = -0.3;
2141
- var DECORATION_THICKNESS = 0.05;
2142
- function fillKey(p) {
2143
- if ("color" in p) return `s:${p.color}:${p.opacity ?? 1}`;
2144
- return `nx:${Math.random()}`;
1907
+ function variantKey(runs, opts) {
1908
+ return `${opts.maxWidth}|${opts.lineHeight}|${opts.align}|${outlineBucket(runs, opts.outlineMinSize)}`;
2145
1909
  }
2146
- function sameFill(a, b) {
2147
- if (a === b) return true;
2148
- if ("color" in a && "color" in b) {
2149
- return a.color === b.color && (a.opacity ?? 1) === (b.opacity ?? 1);
1910
+ function cachedLayoutRuns(runs, opts) {
1911
+ const fonts = glyphGeneration();
1912
+ let entry = cache2.get(runs);
1913
+ if (entry === void 0) {
1914
+ entry = { generation: fonts, byVariant: /* @__PURE__ */ new Map() };
1915
+ cache2.set(runs, entry);
1916
+ } else if (entry.generation !== fonts) {
1917
+ entry.generation = fonts;
1918
+ entry.byVariant.clear();
2150
1919
  }
2151
- return false;
2152
- }
2153
- function strokeKey(s) {
2154
- if (!s) return "-";
2155
- return [
2156
- fillKey(s.paint),
2157
- s.width ?? 1,
2158
- s.join ?? "miter",
2159
- s.cap ?? "butt",
2160
- s.miterLimit ?? "",
2161
- s.align ?? "center",
2162
- (s.dash ?? []).join(",")
2163
- ].join(":");
1920
+ const key = variantKey(runs, opts);
1921
+ const hit = entry.byVariant.get(key);
1922
+ if (hit !== void 0) return hit;
1923
+ const laid = layoutRuns(runs, opts);
1924
+ if (entry.byVariant.size >= LAYOUT_CACHE_VARIANT_LIMIT) entry.byVariant.clear();
1925
+ entry.byVariant.set(key, laid);
1926
+ return laid;
2164
1927
  }
2165
- function groupKey(family, weight, style, synthetic, fill, stroke, source, page) {
2166
- return `${family}|${weight}|${style}|${synthetic.bold ? 1 : 0}${synthetic.italic ? 1 : 0}|${fillKey(fill)}|${strokeKey(stroke)}|${source}|${page}`;
1928
+
1929
+ // src/features/text/verticalAlign.ts
1930
+ function verticalAlignOffset(align, boxHeight, textHeight) {
1931
+ if (align === void 0 || align === "top" || boxHeight === void 0) return 0;
1932
+ const slack = boxHeight - textHeight;
1933
+ return align === "center" ? slack / 2 : slack;
2167
1934
  }
2168
- function getOrCreateGroup(ctx, run, resolved, page, sourceOverride) {
2169
- const atlasFamily = resolved.resolved.family;
2170
- const resolvedWeight = resolved.resolved.weight;
2171
- const resolvedStyle = resolved.resolved.style;
2172
- const source = sourceOverride ?? resolved.source;
2173
- const key = groupKey(
2174
- atlasFamily,
2175
- resolvedWeight,
2176
- resolvedStyle,
2177
- resolved.synthetic,
2178
- run.fill,
2179
- source === "outline" ? run.stroke : void 0,
2180
- source,
2181
- page
2182
- );
2183
- let g = ctx.groups.get(key);
2184
- if (!g) {
2185
- g = {
2186
- family: atlasFamily,
2187
- weight: resolvedWeight,
2188
- style: resolvedStyle,
2189
- synthetic: { ...resolved.synthetic },
2190
- source,
2191
- page,
2192
- fill: run.fill,
2193
- // Only the outline tier can paint it, and carrying it on an SDF group
2194
- // would be a promise the renderer cannot keep.
2195
- ...source === "outline" && run.stroke !== void 0 ? { stroke: run.stroke } : {},
2196
- quads: [],
2197
- glyphs: []
2198
- };
2199
- ctx.groups.set(key, g);
1935
+
1936
+ // src/features/paths/pathFromD.ts
1937
+ var NUM_RE = /[+-]?(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?/g;
1938
+ function tokenize(d) {
1939
+ const out = [];
1940
+ let i = 0;
1941
+ while (i < d.length) {
1942
+ const ch = d[i];
1943
+ if (/[A-Za-z]/.test(ch)) {
1944
+ let j = i + 1;
1945
+ while (j < d.length && !/[A-Za-z]/.test(d[j])) j++;
1946
+ const argSlice = d.slice(i + 1, j);
1947
+ out.push({ cmd: ch, args: readNumbers(argSlice) });
1948
+ i = j;
1949
+ } else {
1950
+ i++;
1951
+ }
2200
1952
  }
2201
- return g;
1953
+ return out;
2202
1954
  }
2203
- function resolveGlyph(run, font, resolved, cp) {
2204
- const direct = font.charMap.get(cp);
2205
- if (direct) return { glyph: direct, font, resolved };
2206
- const fallback = resolveGlyphFallback(run.fontFamily, run.fontWeight, run.fontStyle);
2207
- const face = fallback?.dynamicFace;
2208
- if (fallback && face) {
2209
- const glyph = face.requestGlyph(cp);
2210
- if (glyph.xadvance > 0 || glyph.width > 0) {
2211
- return { glyph, font: face.font, resolved: fallback };
2212
- }
1955
+ function readNumbers(s) {
1956
+ const out = [];
1957
+ let m;
1958
+ NUM_RE.lastIndex = 0;
1959
+ while ((m = NUM_RE.exec(s)) !== null) {
1960
+ const n = Number(m[0]);
1961
+ if (Number.isFinite(n)) out.push(n);
2213
1962
  }
2214
- warnMissingGlyphOnce(resolved.resolved.family, cp);
2215
- return null;
1963
+ return out;
2216
1964
  }
2217
- var warnedMissingGlyphs = /* @__PURE__ */ new Set();
2218
- function warnMissingGlyphOnce(family, cp) {
2219
- const key = `${family}|${cp}`;
2220
- if (warnedMissingGlyphs.has(key)) return;
2221
- warnedMissingGlyphs.add(key);
2222
- const ch = String.fromCodePoint(cp);
2223
- console.warn(
2224
- `weasel layoutRuns: no glyph for U+${cp.toString(16).toUpperCase().padStart(4, "0")} (${JSON.stringify(ch)}) in "${family}", and the dynamic tier could not rasterize it \u2014 skipping the character. Bake it into the atlas, or call registerCanvasFont("${family}") to serve missing codepoints from installed fonts.`
2225
- );
2226
- }
2227
- function layoutRuns(runs, opts, origin) {
2228
- const ctx = { groups: /* @__PURE__ */ new Map() };
2229
- const entries = [];
2230
- let prevCp;
2231
- let prevFont;
2232
- let prevFontSize;
2233
- for (const run of runs) {
2234
- const resolved = resolveFontVariant(run.fontFamily, run.fontWeight, run.fontStyle);
2235
- const font = resolved.entry?.font ?? resolved.dynamicFace?.font;
2236
- if (!font) {
2237
- prevCp = void 0;
2238
- prevFont = void 0;
2239
- prevFontSize = void 0;
1965
+ var ARG_COUNTS = {
1966
+ M: 2,
1967
+ L: 2,
1968
+ H: 1,
1969
+ V: 1,
1970
+ C: 6,
1971
+ S: 4,
1972
+ Q: 4,
1973
+ T: 2,
1974
+ A: 7,
1975
+ Z: 0
1976
+ };
1977
+ function pathFromD(d, onWarn) {
1978
+ const tokens = tokenize(d);
1979
+ const b = new PathBuilder();
1980
+ const st = {
1981
+ x: 0,
1982
+ y: 0,
1983
+ startX: 0,
1984
+ startY: 0,
1985
+ lastCubicCtrl: null,
1986
+ lastQuadCtrl: null
1987
+ };
1988
+ for (const { cmd, args } of tokens) {
1989
+ const upper = cmd.toUpperCase();
1990
+ const relative = cmd !== upper;
1991
+ const argCount = ARG_COUNTS[upper];
1992
+ if (argCount === void 0) {
1993
+ onWarn?.(`unknown path command: ${cmd}`);
2240
1994
  continue;
2241
1995
  }
2242
- const scale2 = run.fontSize / font.info.size;
2243
- const tracking = run.letterSpacing;
2244
- for (const ch of [...run.text]) {
2245
- const cp = ch.codePointAt(0);
2246
- const isNewline = cp === 10;
2247
- const isSpace = cp === 32;
2248
- if (isNewline) {
2249
- entries.push({
2250
- run,
2251
- font,
2252
- glyph: { id: cp, x: 0, y: 0, width: 0, height: 0, xoffset: 0, yoffset: 0, xadvance: 0, page: 0 },
2253
- // A newline consumes no advance, so it takes no tracking either.
2254
- cp,
2255
- advance: 0,
2256
- tracking: 0,
2257
- kerningBefore: 0,
2258
- isSpace: false,
2259
- isNewline: true,
2260
- resolved,
2261
- fontSize: run.fontSize
2262
- });
2263
- prevCp = void 0;
2264
- prevFont = void 0;
2265
- prevFontSize = void 0;
2266
- continue;
2267
- }
2268
- if (isSpace) {
2269
- const spaceGlyph = resolved.dynamicFace ? resolved.dynamicFace.requestGlyph(32) : font.charMap.get(32);
2270
- const advance = spaceGlyph ? spaceGlyph.xadvance * scale2 : run.fontSize * 0.25;
2271
- let kerningBefore2 = 0;
2272
- if (prevCp !== void 0 && prevFont !== void 0 && prevFontSize !== void 0) {
2273
- const kAtlas = prevFont.kerningMap.get(prevCp)?.get(cp) ?? 0;
2274
- kerningBefore2 = kAtlas * (prevFontSize / prevFont.info.size);
2275
- }
2276
- entries.push({
2277
- run,
2278
- font,
2279
- glyph: spaceGlyph ?? { id: 32, x: 0, y: 0, width: 0, height: 0, xoffset: 0, yoffset: 0, xadvance: 0, page: 0 },
2280
- cp,
2281
- advance,
2282
- tracking,
2283
- kerningBefore: kerningBefore2,
2284
- isSpace: true,
2285
- isNewline: false,
2286
- resolved,
2287
- fontSize: run.fontSize
2288
- });
2289
- prevCp = cp;
2290
- prevFont = font;
2291
- prevFontSize = run.fontSize;
2292
- continue;
2293
- }
2294
- const hit = resolved.dynamicFace ? { glyph: resolved.dynamicFace.requestGlyph(cp), font, resolved } : resolveGlyph(run, font, resolved, cp);
2295
- if (!hit) {
2296
- prevCp = cp;
2297
- prevFont = font;
2298
- prevFontSize = run.fontSize;
2299
- continue;
2300
- }
2301
- const glyphFont = hit.font;
2302
- const glyphScale = run.fontSize / glyphFont.info.size;
2303
- let kerningBefore = 0;
2304
- if (prevCp !== void 0 && prevFont !== void 0 && prevFontSize !== void 0) {
2305
- const kAtlas = prevFont.kerningMap.get(prevCp)?.get(cp) ?? 0;
2306
- kerningBefore = kAtlas * (prevFontSize / prevFont.info.size);
2307
- }
2308
- entries.push({
2309
- run,
2310
- font: glyphFont,
2311
- glyph: hit.glyph,
2312
- cp,
2313
- advance: hit.glyph.xadvance * glyphScale,
2314
- tracking,
2315
- kerningBefore,
2316
- isSpace,
2317
- isNewline: false,
2318
- resolved: hit.resolved,
2319
- fontSize: run.fontSize
2320
- });
2321
- prevCp = cp;
2322
- prevFont = glyphFont;
2323
- prevFontSize = run.fontSize;
2324
- }
2325
- }
2326
- const lines = [];
2327
- let cur = { entries: [], width: 0, height: 0 };
2328
- function commitLine() {
2329
- lines.push(cur);
2330
- cur = { entries: [], width: 0, height: 0 };
2331
- }
2332
- let i = 0;
2333
- while (i < entries.length) {
2334
- const e = entries[i];
2335
- if (e.isNewline) {
2336
- if (cur.entries.length === 0) {
2337
- cur.height = Math.max(cur.height, e.fontSize * opts.lineHeight);
2338
- cur.blank = e;
2339
- }
2340
- commitLine();
2341
- i++;
1996
+ if (upper === "Z") {
1997
+ b.close();
1998
+ st.x = st.startX;
1999
+ st.y = st.startY;
2000
+ st.lastCubicCtrl = null;
2001
+ st.lastQuadCtrl = null;
2342
2002
  continue;
2343
2003
  }
2344
- if (e.isSpace) {
2345
- if (cur.entries.length > 0) {
2346
- cur.entries.push(e);
2347
- cur.width += e.kerningBefore + e.advance + e.tracking;
2348
- cur.height = Math.max(cur.height, e.fontSize * opts.lineHeight);
2349
- }
2350
- i++;
2351
- continue;
2004
+ const groups = argCount === 0 ? 1 : Math.floor(args.length / argCount);
2005
+ for (let g = 0; g < groups; g++) {
2006
+ const a = args.slice(g * argCount, (g + 1) * argCount);
2007
+ const effectiveUpper = upper === "M" && g > 0 ? "L" : upper;
2008
+ runCommand(effectiveUpper, relative, a, st, b);
2352
2009
  }
2353
- let j = i;
2354
- let wordWidth = 0;
2355
- while (j < entries.length && !entries[j].isSpace && !entries[j].isNewline) {
2356
- const w = entries[j];
2357
- wordWidth += w.kerningBefore + w.advance + w.tracking;
2358
- j++;
2010
+ }
2011
+ return b.build();
2012
+ }
2013
+ function runCommand(upper, relative, a, st, b) {
2014
+ const rx = relative ? st.x : 0;
2015
+ const ry = relative ? st.y : 0;
2016
+ switch (upper) {
2017
+ case "M": {
2018
+ const x = a[0] + rx;
2019
+ const y = a[1] + ry;
2020
+ b.moveTo(x, y);
2021
+ st.x = x;
2022
+ st.y = y;
2023
+ st.startX = x;
2024
+ st.startY = y;
2025
+ st.lastCubicCtrl = null;
2026
+ st.lastQuadCtrl = null;
2027
+ return;
2359
2028
  }
2360
- if (Number.isFinite(opts.maxWidth) && cur.width + wordWidth > opts.maxWidth && cur.entries.length > 0) {
2361
- commitLine();
2029
+ case "L": {
2030
+ const x = a[0] + rx;
2031
+ const y = a[1] + ry;
2032
+ b.lineTo(x, y);
2033
+ st.x = x;
2034
+ st.y = y;
2035
+ st.lastCubicCtrl = null;
2036
+ st.lastQuadCtrl = null;
2037
+ return;
2362
2038
  }
2363
- for (let k = i; k < j; k++) {
2364
- const w = entries[k];
2365
- const kerningBefore = cur.entries.length === 0 ? 0 : w.kerningBefore;
2366
- cur.entries.push({ ...w, kerningBefore });
2367
- cur.width += kerningBefore + w.advance + w.tracking;
2368
- cur.height = Math.max(cur.height, w.fontSize * opts.lineHeight);
2039
+ case "H": {
2040
+ const x = a[0] + (relative ? st.x : 0);
2041
+ b.lineTo(x, st.y);
2042
+ st.x = x;
2043
+ st.lastCubicCtrl = null;
2044
+ st.lastQuadCtrl = null;
2045
+ return;
2369
2046
  }
2370
- i = j;
2371
- }
2372
- if (cur.entries.length > 0) commitLine();
2373
- function outlineFor(e) {
2374
- const min = opts.outlineMinSize;
2375
- if (min === void 0 || e.fontSize < min) return null;
2376
- if (e.resolved.synthetic.bold) return null;
2377
- const r = e.resolved.resolved;
2378
- return glyphOutline(r.family, r.weight, r.style, e.cp);
2379
- }
2380
- const decorations = [];
2381
- let span = null;
2382
- function flushSpan() {
2383
- const s = span;
2384
- span = null;
2385
- if (!s || s.x1 <= s.x0) return;
2386
- const thickness = s.fontSize * DECORATION_THICKNESS;
2387
- if (s.underline) {
2388
- const y0 = s.baselineY + s.fontSize * UNDERLINE_OFFSET;
2389
- decorations.push({ kind: "underline", x0: s.x0, y0, x1: s.x1, y1: y0 + thickness, fill: s.fill });
2047
+ case "V": {
2048
+ const y = a[0] + (relative ? st.y : 0);
2049
+ b.lineTo(st.x, y);
2050
+ st.y = y;
2051
+ st.lastCubicCtrl = null;
2052
+ st.lastQuadCtrl = null;
2053
+ return;
2390
2054
  }
2391
- if (s.strikethrough) {
2392
- const y0 = s.baselineY + s.fontSize * STRIKETHROUGH_OFFSET;
2393
- decorations.push({ kind: "strikethrough", x0: s.x0, y0, x1: s.x1, y1: y0 + thickness, fill: s.fill });
2055
+ case "C": {
2056
+ const x1 = a[0] + rx, y1 = a[1] + ry;
2057
+ const x2 = a[2] + rx, y2 = a[3] + ry;
2058
+ const x = a[4] + rx, y = a[5] + ry;
2059
+ b.curveTo(x1, y1, x2, y2, x, y);
2060
+ st.x = x;
2061
+ st.y = y;
2062
+ st.lastCubicCtrl = { x: x2, y: y2 };
2063
+ st.lastQuadCtrl = null;
2064
+ return;
2394
2065
  }
2395
- }
2396
- const lineBoxes = [];
2397
- let penY = origin.y;
2398
- let maxLineWidth = 0;
2399
- const finiteWidth = Number.isFinite(opts.maxWidth) ? opts.maxWidth : 0;
2400
- for (const line of lines) {
2401
- const alignShift = (() => {
2402
- if (opts.align === "left") return 0;
2403
- if (!Number.isFinite(opts.maxWidth)) {
2404
- return opts.align === "center" ? -line.width / 2 : -line.width;
2066
+ case "S": {
2067
+ const ref = st.lastCubicCtrl ?? { x: st.x, y: st.y };
2068
+ const x1 = 2 * st.x - ref.x;
2069
+ const y1 = 2 * st.y - ref.y;
2070
+ const x2 = a[0] + rx, y2 = a[1] + ry;
2071
+ const x = a[2] + rx, y = a[3] + ry;
2072
+ b.curveTo(x1, y1, x2, y2, x, y);
2073
+ st.x = x;
2074
+ st.y = y;
2075
+ st.lastCubicCtrl = { x: x2, y: y2 };
2076
+ st.lastQuadCtrl = null;
2077
+ return;
2078
+ }
2079
+ case "Q": {
2080
+ const x1 = a[0] + rx, y1 = a[1] + ry;
2081
+ const x = a[2] + rx, y = a[3] + ry;
2082
+ b.quadTo(x1, y1, x, y);
2083
+ st.x = x;
2084
+ st.y = y;
2085
+ st.lastQuadCtrl = { x: x1, y: y1 };
2086
+ st.lastCubicCtrl = null;
2087
+ return;
2088
+ }
2089
+ case "T": {
2090
+ const ref = st.lastQuadCtrl ?? { x: st.x, y: st.y };
2091
+ const x1 = 2 * st.x - ref.x;
2092
+ const y1 = 2 * st.y - ref.y;
2093
+ const x = a[0] + rx, y = a[1] + ry;
2094
+ b.quadTo(x1, y1, x, y);
2095
+ st.x = x;
2096
+ st.y = y;
2097
+ st.lastQuadCtrl = { x: x1, y: y1 };
2098
+ st.lastCubicCtrl = null;
2099
+ return;
2100
+ }
2101
+ case "A": {
2102
+ const rxA = Math.abs(a[0]);
2103
+ const ryA = Math.abs(a[1]);
2104
+ const xRot = a[2];
2105
+ const largeArc = a[3] !== 0;
2106
+ const sweep = a[4] !== 0;
2107
+ const ex = a[5] + rx;
2108
+ const ey = a[6] + ry;
2109
+ arcToCubics(st.x, st.y, ex, ey, rxA, ryA, xRot, largeArc, sweep, b);
2110
+ st.x = ex;
2111
+ st.y = ey;
2112
+ st.lastCubicCtrl = null;
2113
+ st.lastQuadCtrl = null;
2114
+ return;
2115
+ }
2116
+ default:
2117
+ return;
2118
+ }
2119
+ }
2120
+ function arcToCubics(x1, y1, x2, y2, rx, ry, xAxisRotationDeg, largeArc, sweep, b) {
2121
+ if (x1 === x2 && y1 === y2) return;
2122
+ if (rx === 0 || ry === 0) {
2123
+ b.lineTo(x2, y2);
2124
+ return;
2125
+ }
2126
+ const phi = xAxisRotationDeg * Math.PI / 180;
2127
+ const cosPhi = Math.cos(phi);
2128
+ const sinPhi = Math.sin(phi);
2129
+ const dx = (x1 - x2) / 2;
2130
+ const dy = (y1 - y2) / 2;
2131
+ const x1p = cosPhi * dx + sinPhi * dy;
2132
+ const y1p = -sinPhi * dx + cosPhi * dy;
2133
+ let rxs = rx * rx;
2134
+ let rys = ry * ry;
2135
+ const x1ps = x1p * x1p;
2136
+ const y1ps = y1p * y1p;
2137
+ const lambda = x1ps / rxs + y1ps / rys;
2138
+ let rxFixed = rx;
2139
+ let ryFixed = ry;
2140
+ if (lambda > 1) {
2141
+ const sqrtLambda = Math.sqrt(lambda);
2142
+ rxFixed = sqrtLambda * rx;
2143
+ ryFixed = sqrtLambda * ry;
2144
+ rxs = rxFixed * rxFixed;
2145
+ rys = ryFixed * ryFixed;
2146
+ }
2147
+ const sign = largeArc === sweep ? -1 : 1;
2148
+ const num = rxs * rys - rxs * y1ps - rys * x1ps;
2149
+ const den = rxs * y1ps + rys * x1ps;
2150
+ const factor = sign * Math.sqrt(Math.max(0, num / den));
2151
+ const cxp = factor * (rxFixed * y1p) / ryFixed;
2152
+ const cyp = factor * -(ryFixed * x1p) / rxFixed;
2153
+ const cx = cosPhi * cxp - sinPhi * cyp + (x1 + x2) / 2;
2154
+ const cy = sinPhi * cxp + cosPhi * cyp + (y1 + y2) / 2;
2155
+ const angle = (ux, uy, vx, vy) => {
2156
+ const dot = ux * vx + uy * vy;
2157
+ const len = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
2158
+ let a = Math.acos(Math.max(-1, Math.min(1, dot / len)));
2159
+ if (ux * vy - uy * vx < 0) a = -a;
2160
+ return a;
2161
+ };
2162
+ const theta1 = angle(1, 0, (x1p - cxp) / rxFixed, (y1p - cyp) / ryFixed);
2163
+ let dTheta = angle(
2164
+ (x1p - cxp) / rxFixed,
2165
+ (y1p - cyp) / ryFixed,
2166
+ (-x1p - cxp) / rxFixed,
2167
+ (-y1p - cyp) / ryFixed
2168
+ );
2169
+ if (!sweep && dTheta > 0) dTheta -= 2 * Math.PI;
2170
+ else if (sweep && dTheta < 0) dTheta += 2 * Math.PI;
2171
+ const segCount = Math.max(1, Math.ceil(Math.abs(dTheta) / (Math.PI / 2)));
2172
+ const segAngle = dTheta / segCount;
2173
+ const t = 4 / 3 * Math.tan(segAngle / 4);
2174
+ let prevX = x1;
2175
+ let prevY = y1;
2176
+ let prevDx = -Math.sin(theta1) * rxFixed;
2177
+ let prevDy = Math.cos(theta1) * ryFixed;
2178
+ for (let i = 1; i <= segCount; i++) {
2179
+ const theta = theta1 + i * segAngle;
2180
+ const cosT = Math.cos(theta);
2181
+ const sinT = Math.sin(theta);
2182
+ const px = cosPhi * (cosT * rxFixed) - sinPhi * (sinT * ryFixed) + cx;
2183
+ const py = sinPhi * (cosT * rxFixed) + cosPhi * (sinT * ryFixed) + cy;
2184
+ const dxNew = -sinT * rxFixed;
2185
+ const dyNew = cosT * ryFixed;
2186
+ const c1x = prevX + cosPhi * (t * prevDx) - sinPhi * (t * prevDy);
2187
+ const c1y = prevY + sinPhi * (t * prevDx) + cosPhi * (t * prevDy);
2188
+ const c2x = px - cosPhi * (t * dxNew) + sinPhi * (t * dyNew);
2189
+ const c2y = py - sinPhi * (t * dxNew) - cosPhi * (t * dyNew);
2190
+ b.curveTo(c1x, c1y, c2x, c2y, px, py);
2191
+ prevX = px;
2192
+ prevY = py;
2193
+ prevDx = dxNew;
2194
+ prevDy = dyNew;
2195
+ }
2196
+ }
2197
+
2198
+ // src/renderer/cache/outlineMeshCache.ts
2199
+ var OUTLINE_FLATTEN_TOLERANCE = 1 / 4096;
2200
+ var OUTLINE_MESH_CACHE_LIMIT = 2048;
2201
+ var cache3 = /* @__PURE__ */ new Map();
2202
+ function outlineMesh(key, d) {
2203
+ const cached = cache3.get(key);
2204
+ if (cached !== void 0) return cached;
2205
+ if (cache3.size >= OUTLINE_MESH_CACHE_LIMIT) cache3 = /* @__PURE__ */ new Map();
2206
+ const mesh = tessellate(pathFromD(d), { flattenTolerance: OUTLINE_FLATTEN_TOLERANCE });
2207
+ cache3.set(key, mesh);
2208
+ return mesh;
2209
+ }
2210
+
2211
+ // src/features/paths/tessellate/polyline.ts
2212
+ function extractPolylines(path, opts = {}) {
2213
+ if (path.kind === "rect") return [extractRect(path)];
2214
+ return extractPolygon(path, opts);
2215
+ }
2216
+ function extractRect(p) {
2217
+ const { x, y, width: w, height: h } = p;
2218
+ return {
2219
+ points: [x, y, x + w, y, x + w, y + h, x, y + h],
2220
+ closed: true,
2221
+ anchorA: new Uint32Array([0, 1, 2, 3]),
2222
+ anchorB: new Uint32Array([0, 1, 2, 3]),
2223
+ anchorT: new Float32Array([0, 0, 0, 0])
2224
+ };
2225
+ }
2226
+ function extractPolygon(p, opts) {
2227
+ const tolerance = opts.flattenTolerance ?? DEFAULT_FLATTEN_TOLERANCE;
2228
+ const { commands, coords } = p;
2229
+ const out = [];
2230
+ let anchorCounter = 0;
2231
+ let pts = null;
2232
+ let aA = null;
2233
+ let aB = null;
2234
+ let aT = null;
2235
+ let current = null;
2236
+ let coordIdx = 0;
2237
+ let prevX = 0;
2238
+ let prevY = 0;
2239
+ let prevAnchor = -1;
2240
+ const beginContour = () => {
2241
+ pts = [];
2242
+ aA = [];
2243
+ aB = [];
2244
+ aT = [];
2245
+ const next = { points: pts, closed: false };
2246
+ out.push(next);
2247
+ return next;
2248
+ };
2249
+ const dropDuplicateClosingPoint = () => {
2250
+ if (!pts || !aA || !aB || !aT) return;
2251
+ const n = pts.length / 2;
2252
+ if (n < 2) return;
2253
+ if (pts[0] !== pts[(n - 1) * 2] || pts[1] !== pts[(n - 1) * 2 + 1]) return;
2254
+ pts.length -= 2;
2255
+ aA.length -= 1;
2256
+ aB.length -= 1;
2257
+ aT.length -= 1;
2258
+ };
2259
+ const commit = (target) => {
2260
+ if (!pts || !aA || !aB || !aT) return;
2261
+ target.anchorA = new Uint32Array(aA);
2262
+ target.anchorB = new Uint32Array(aB);
2263
+ target.anchorT = new Float32Array(aT);
2264
+ };
2265
+ for (let cmdIdx = 0; cmdIdx < commands.length; cmdIdx++) {
2266
+ const cmd = commands[cmdIdx];
2267
+ switch (cmd) {
2268
+ case PATH_M: {
2269
+ if (current) commit(current);
2270
+ current = beginContour();
2271
+ prevX = coords[coordIdx];
2272
+ prevY = coords[coordIdx + 1];
2273
+ pts.push(prevX, prevY);
2274
+ aA.push(anchorCounter);
2275
+ aB.push(anchorCounter);
2276
+ aT.push(0);
2277
+ prevAnchor = anchorCounter;
2278
+ anchorCounter++;
2279
+ coordIdx += 2;
2280
+ break;
2405
2281
  }
2406
- const slack = finiteWidth - line.width;
2407
- return opts.align === "center" ? slack / 2 : slack;
2408
- })();
2409
- const lineX0 = origin.x + alignShift;
2410
- const baselineSource = line.entries[0] ?? line.blank;
2411
- const lineBaselineY = baselineSource ? penY + baselineSource.font.common.base * (baselineSource.fontSize / baselineSource.font.info.size) : penY;
2412
- let penX = lineX0;
2413
- for (const e of line.entries) {
2414
- penX += e.kerningBefore;
2415
- const step = e.advance + e.tracking;
2416
- const scale2 = e.fontSize / e.font.info.size;
2417
- const baselineY = penY + e.font.common.base * scale2;
2418
- if (e.run.underline || e.run.strikethrough) {
2419
- if (span !== null && span.underline === e.run.underline && span.strikethrough === e.run.strikethrough && span.fontSize === e.fontSize && span.baselineY === baselineY && sameFill(span.fill, e.run.fill)) {
2420
- span.x1 = penX + step;
2421
- } else {
2422
- flushSpan();
2423
- span = {
2424
- underline: e.run.underline,
2425
- strikethrough: e.run.strikethrough,
2426
- fill: e.run.fill,
2427
- fontSize: e.fontSize,
2428
- baselineY,
2429
- x0: penX,
2430
- x1: penX + step
2431
- };
2282
+ case PATH_L: {
2283
+ prevX = coords[coordIdx];
2284
+ prevY = coords[coordIdx + 1];
2285
+ pts.push(prevX, prevY);
2286
+ aA.push(anchorCounter);
2287
+ aB.push(anchorCounter);
2288
+ aT.push(0);
2289
+ prevAnchor = anchorCounter;
2290
+ anchorCounter++;
2291
+ coordIdx += 2;
2292
+ break;
2293
+ }
2294
+ case PATH_Q: {
2295
+ const cx = coords[coordIdx], cy = coords[coordIdx + 1];
2296
+ const ex = coords[coordIdx + 2], ey = coords[coordIdx + 3];
2297
+ const targetAnchor = anchorCounter;
2298
+ const segStart = pts.length / 2;
2299
+ const arcAccum = [];
2300
+ const total = flattenQuadraticWithArcLen(prevX, prevY, cx, cy, ex, ey, tolerance, pts, arcAccum);
2301
+ for (let k = 0; k < arcAccum.length; k++) {
2302
+ aA.push(prevAnchor);
2303
+ aB.push(targetAnchor);
2304
+ aT.push(total > 0 ? arcAccum[k] / total : 0);
2432
2305
  }
2433
- } else {
2434
- flushSpan();
2306
+ const lastIdx = segStart + arcAccum.length - 1;
2307
+ aA[lastIdx] = targetAnchor;
2308
+ aB[lastIdx] = targetAnchor;
2309
+ aT[lastIdx] = 0;
2310
+ prevX = ex;
2311
+ prevY = ey;
2312
+ prevAnchor = targetAnchor;
2313
+ anchorCounter++;
2314
+ coordIdx += 4;
2315
+ break;
2435
2316
  }
2436
- const outlineD = outlineFor(e);
2437
- if (outlineD !== null) {
2438
- const group2 = getOrCreateGroup(ctx, e.run, e.resolved, 0, "outline");
2439
- group2.glyphs.push({
2440
- d: outlineD,
2441
- key: `${e.resolved.resolved.family}|${e.resolved.resolved.weight}|${e.resolved.resolved.style}|${e.cp}`,
2442
- x: penX,
2443
- baselineY,
2444
- // Em space is unit-scale, so world units per em is just the size.
2445
- scale: e.fontSize
2446
- });
2447
- penX += step;
2448
- continue;
2317
+ case PATH_C: {
2318
+ const c1x = coords[coordIdx], c1y = coords[coordIdx + 1];
2319
+ const c2x = coords[coordIdx + 2], c2y = coords[coordIdx + 3];
2320
+ const ex = coords[coordIdx + 4], ey = coords[coordIdx + 5];
2321
+ const targetAnchor = anchorCounter;
2322
+ const segStart = pts.length / 2;
2323
+ const arcAccum = [];
2324
+ const total = flattenCubicWithArcLen(prevX, prevY, c1x, c1y, c2x, c2y, ex, ey, tolerance, pts, arcAccum);
2325
+ for (let k = 0; k < arcAccum.length; k++) {
2326
+ aA.push(prevAnchor);
2327
+ aB.push(targetAnchor);
2328
+ aT.push(total > 0 ? arcAccum[k] / total : 0);
2329
+ }
2330
+ const lastIdx = segStart + arcAccum.length - 1;
2331
+ aA[lastIdx] = targetAnchor;
2332
+ aB[lastIdx] = targetAnchor;
2333
+ aT[lastIdx] = 0;
2334
+ prevX = ex;
2335
+ prevY = ey;
2336
+ prevAnchor = targetAnchor;
2337
+ anchorCounter++;
2338
+ coordIdx += 6;
2339
+ break;
2449
2340
  }
2450
- if (e.advance === 0 || e.glyph.width === 0 || e.glyph.page < 0) {
2451
- penX += step;
2452
- continue;
2341
+ case PATH_Z: {
2342
+ if (current) {
2343
+ current.closed = true;
2344
+ dropDuplicateClosingPoint();
2345
+ }
2346
+ break;
2453
2347
  }
2454
- const group = getOrCreateGroup(ctx, e.run, e.resolved, e.glyph.page);
2455
- const atlasW = e.font.common.scaleW;
2456
- const atlasH = e.font.common.scaleH;
2457
- const qx0 = penX + e.glyph.xoffset * scale2;
2458
- const qy0 = penY + e.glyph.yoffset * scale2;
2459
- const qx1 = qx0 + e.glyph.width * scale2;
2460
- const qy1 = qy0 + e.glyph.height * scale2;
2461
- const u0 = e.glyph.x / atlasW;
2462
- const v0 = e.glyph.y / atlasH;
2463
- const u1 = (e.glyph.x + e.glyph.width) / atlasW;
2464
- const v1 = (e.glyph.y + e.glyph.height) / atlasH;
2465
- group.quads.push({ x0: qx0, y0: qy0, x1: qx1, y1: qy1, u0, v0, u1, v1, baselineY });
2466
- penX += step;
2348
+ default:
2349
+ throw new Error(`extractPolylines: unknown command code ${cmd}`);
2350
+ }
2351
+ }
2352
+ if (current) commit(current);
2353
+ return out;
2354
+ }
2355
+
2356
+ // src/features/paths/tessellate/stroke.ts
2357
+ var EMPTY_MESH = {
2358
+ vertices: new Float32Array(0),
2359
+ indices: new Uint32Array(0),
2360
+ anchorA: new Uint32Array(0),
2361
+ anchorB: new Uint32Array(0),
2362
+ anchorT: new Float32Array(0)
2363
+ };
2364
+ function tessellateStroke(path, stroke, opts = {}) {
2365
+ const width = stroke.width ?? 1;
2366
+ if (width <= 0) return EMPTY_MESH;
2367
+ const join = stroke.join ?? "miter";
2368
+ const cap = stroke.cap ?? "butt";
2369
+ const align = stroke.align ?? "center";
2370
+ const miterLimit = stroke.miterLimit ?? DEFAULT_MITER_LIMIT;
2371
+ const varyingThreshold = stroke.varyingWidthJoinThreshold ?? DEFAULT_VARYING_WIDTH_JOIN_THRESHOLD;
2372
+ let workingPath = path;
2373
+ if (path.kind === "rect" && align !== "center") {
2374
+ const aligned = alignedStrokeRect(path, align, width);
2375
+ workingPath = {
2376
+ kind: "rect",
2377
+ x: aligned.x,
2378
+ y: aligned.y,
2379
+ width: aligned.width,
2380
+ height: aligned.height
2381
+ };
2382
+ }
2383
+ const polylines = extractPolylines(workingPath, opts);
2384
+ if (stroke.vertexWidths && stroke.vertexWidths.length > 0) {
2385
+ for (const pl of polylines) populatePolylineWidths(pl, stroke.vertexWidths, width);
2386
+ }
2387
+ const dash = stroke.dash ?? [];
2388
+ const verts = [];
2389
+ const idx = [];
2390
+ const anchorA = [];
2391
+ const anchorB = [];
2392
+ const anchorT = [];
2393
+ for (const pl of polylines) {
2394
+ const subs = dash.length > 0 ? splitForDash(pl, dash) : [pl];
2395
+ for (const sub2 of subs) {
2396
+ expandPolyline(sub2, width, join, cap, miterLimit, varyingThreshold, verts, idx, anchorA, anchorB, anchorT);
2467
2397
  }
2468
- flushSpan();
2469
- lineBoxes.push({
2470
- x0: lineX0,
2471
- y0: penY,
2472
- x1: lineX0 + line.width,
2473
- y1: penY + line.height,
2474
- baselineY: lineBaselineY
2475
- });
2476
- maxLineWidth = Math.max(maxLineWidth, line.width);
2477
- penY += line.height;
2478
2398
  }
2479
2399
  return {
2480
- groups: [...ctx.groups.values()],
2481
- decorations,
2482
- lines: lineBoxes,
2483
- bounds: { width: maxLineWidth, height: penY - origin.y }
2400
+ vertices: new Float32Array(verts),
2401
+ indices: new Uint32Array(idx),
2402
+ anchorA: new Uint32Array(anchorA),
2403
+ anchorB: new Uint32Array(anchorB),
2404
+ anchorT: new Float32Array(anchorT)
2484
2405
  };
2485
2406
  }
2486
- var LAYOUT_CACHE_VARIANT_LIMIT = 8;
2487
- var cache2 = /* @__PURE__ */ new WeakMap();
2488
- function outlineBucket(runs, min) {
2489
- if (min === void 0) return -1;
2490
- const sizes = /* @__PURE__ */ new Set();
2491
- for (const r of runs) sizes.add(r.fontSize);
2492
- let n = 0;
2493
- for (const size of sizes) if (size >= min) n++;
2494
- return n;
2495
- }
2496
- function variantKey(runs, opts, origin) {
2497
- return `${opts.maxWidth}|${opts.lineHeight}|${opts.align}|${outlineBucket(runs, opts.outlineMinSize)}|${origin.x}|${origin.y}`;
2498
- }
2499
- function cachedLayoutRuns(runs, opts, origin) {
2500
- const fonts = glyphGeneration();
2501
- let entry = cache2.get(runs);
2502
- if (entry === void 0) {
2503
- entry = { generation: fonts, byVariant: /* @__PURE__ */ new Map() };
2504
- cache2.set(runs, entry);
2505
- } else if (entry.generation !== fonts) {
2506
- entry.generation = fonts;
2507
- entry.byVariant.clear();
2407
+ function expandPolyline(pl, width, join, cap, miterLimit, varyingThreshold, verts, idx, anchorA, anchorB, anchorT) {
2408
+ const half = width / 2;
2409
+ const pts = pl.points;
2410
+ const ptCount = pts.length / 2;
2411
+ const segCount = ptCount - 1;
2412
+ if (segCount < 1) return;
2413
+ const plA = pl.anchorA ?? new Uint32Array(ptCount);
2414
+ const plB = pl.anchorB ?? new Uint32Array(ptCount);
2415
+ const plT = pl.anchorT ?? new Float32Array(ptCount);
2416
+ const plHalf = pl.widths ? pl.widths.map((w) => w / 2) : null;
2417
+ const segs = [];
2418
+ const segSrcIdx = [];
2419
+ for (let s = 0; s < segCount; s++) {
2420
+ const ha = plHalf ? plHalf[s] : half;
2421
+ const hb = plHalf ? plHalf[s + 1] : half;
2422
+ const seg = makeSeg(pts[s * 2], pts[s * 2 + 1], pts[(s + 1) * 2], pts[(s + 1) * 2 + 1], ha, hb);
2423
+ if (seg) {
2424
+ segs.push(seg);
2425
+ segSrcIdx.push(s);
2426
+ }
2508
2427
  }
2509
- const key = variantKey(runs, opts, origin);
2510
- const hit = entry.byVariant.get(key);
2511
- if (hit !== void 0) return hit;
2512
- const laid = layoutRuns(runs, opts, origin);
2513
- if (entry.byVariant.size >= LAYOUT_CACHE_VARIANT_LIMIT) entry.byVariant.clear();
2514
- entry.byVariant.set(key, laid);
2515
- return laid;
2516
- }
2517
-
2518
- // src/features/text/verticalAlign.ts
2519
- function verticalAlignOffset(align, boxHeight, textHeight) {
2520
- if (align === void 0 || align === "top" || boxHeight === void 0) return 0;
2521
- const slack = boxHeight - textHeight;
2522
- return align === "center" ? slack / 2 : slack;
2523
- }
2524
-
2525
- // src/features/paths/pathFromD.ts
2526
- var NUM_RE = /[+-]?(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?/g;
2527
- function tokenize(d) {
2528
- const out = [];
2529
- let i = 0;
2530
- while (i < d.length) {
2531
- const ch = d[i];
2532
- if (/[A-Za-z]/.test(ch)) {
2533
- let j = i + 1;
2534
- while (j < d.length && !/[A-Za-z]/.test(d[j])) j++;
2535
- const argSlice = d.slice(i + 1, j);
2536
- out.push({ cmd: ch, args: readNumbers(argSlice) });
2537
- i = j;
2538
- } else {
2539
- i++;
2428
+ let closerSrcIdx = -1;
2429
+ if (pl.closed && segs.length >= 1) {
2430
+ const last = segs[segs.length - 1];
2431
+ const first = segs[0];
2432
+ const haCloser = plHalf ? plHalf[ptCount - 1] : half;
2433
+ const hbCloser = plHalf ? plHalf[0] : half;
2434
+ const closer = makeSeg(last.bx, last.by, first.ax, first.ay, haCloser, hbCloser);
2435
+ if (closer) {
2436
+ segs.push(closer);
2437
+ closerSrcIdx = ptCount - 1;
2438
+ segSrcIdx.push(closerSrcIdx);
2540
2439
  }
2541
2440
  }
2542
- return out;
2543
- }
2544
- function readNumbers(s) {
2545
- const out = [];
2546
- let m;
2547
- NUM_RE.lastIndex = 0;
2548
- while ((m = NUM_RE.exec(s)) !== null) {
2549
- const n = Number(m[0]);
2550
- if (Number.isFinite(n)) out.push(n);
2441
+ if (segs.length === 0) return;
2442
+ const segBaseIdx = [];
2443
+ for (let s = 0; s < segs.length; s++) {
2444
+ const seg = segs[s];
2445
+ const startSrc = segSrcIdx[s];
2446
+ const endSrc = s === segs.length - 1 && pl.closed && closerSrcIdx >= 0 ? 0 : startSrc + 1;
2447
+ const base = verts.length / 2;
2448
+ segBaseIdx.push(base);
2449
+ const nxA = seg.nxUnit * seg.halfA, nyA = seg.nyUnit * seg.halfA;
2450
+ const nxB = seg.nxUnit * seg.halfB, nyB = seg.nyUnit * seg.halfB;
2451
+ verts.push(seg.ax + nxA, seg.ay + nyA);
2452
+ anchorA.push(plA[startSrc]);
2453
+ anchorB.push(plB[startSrc]);
2454
+ anchorT.push(plT[startSrc]);
2455
+ verts.push(seg.ax - nxA, seg.ay - nyA);
2456
+ anchorA.push(plA[startSrc]);
2457
+ anchorB.push(plB[startSrc]);
2458
+ anchorT.push(plT[startSrc]);
2459
+ verts.push(seg.bx + nxB, seg.by + nyB);
2460
+ anchorA.push(plA[endSrc]);
2461
+ anchorB.push(plB[endSrc]);
2462
+ anchorT.push(plT[endSrc]);
2463
+ verts.push(seg.bx - nxB, seg.by - nyB);
2464
+ anchorA.push(plA[endSrc]);
2465
+ anchorB.push(plB[endSrc]);
2466
+ anchorT.push(plT[endSrc]);
2467
+ idx.push(base, base + 1, base + 2, base + 1, base + 3, base + 2);
2468
+ }
2469
+ const joinCount = pl.closed ? segs.length : segs.length - 1;
2470
+ for (let j = 0; j < joinCount; j++) {
2471
+ emitJoin(segs, segBaseIdx, j, join, miterLimit, varyingThreshold, verts, idx, anchorA, anchorB, anchorT, segSrcIdx, plA, plB, plT);
2472
+ }
2473
+ if (!pl.closed && cap !== "butt") {
2474
+ const first = segs[0];
2475
+ const firstBase = segBaseIdx[0];
2476
+ emitCap(first, firstBase + 0, firstBase + 1, false, first.halfA, cap, verts, idx, anchorA, anchorB, anchorT, plA[0], plB[0], plT[0]);
2477
+ const last = segs[segs.length - 1];
2478
+ const lastBase = segBaseIdx[segs.length - 1];
2479
+ const lastSrc = segSrcIdx[segs.length - 1] + 1;
2480
+ emitCap(last, lastBase + 2, lastBase + 3, true, last.halfB, cap, verts, idx, anchorA, anchorB, anchorT, plA[lastSrc], plB[lastSrc], plT[lastSrc]);
2551
2481
  }
2552
- return out;
2553
2482
  }
2554
- var ARG_COUNTS = {
2555
- M: 2,
2556
- L: 2,
2557
- H: 1,
2558
- V: 1,
2559
- C: 6,
2560
- S: 4,
2561
- Q: 4,
2562
- T: 2,
2563
- A: 7,
2564
- Z: 0
2565
- };
2566
- function pathFromD(d, onWarn) {
2567
- const tokens = tokenize(d);
2568
- const b = new PathBuilder();
2569
- const st = {
2570
- x: 0,
2571
- y: 0,
2572
- startX: 0,
2573
- startY: 0,
2574
- lastCubicCtrl: null,
2575
- lastQuadCtrl: null
2483
+ function populatePolylineWidths(pl, vertexWidths, fallbackWidth) {
2484
+ const ptCount = pl.points.length / 2;
2485
+ if (ptCount === 0) return;
2486
+ const aA = pl.anchorA ?? new Uint32Array(ptCount);
2487
+ const aB = pl.anchorB ?? new Uint32Array(ptCount);
2488
+ const aT = pl.anchorT ?? new Float32Array(ptCount);
2489
+ const widths = new Float32Array(ptCount);
2490
+ const read = (i) => {
2491
+ const v = vertexWidths[i];
2492
+ return typeof v === "number" && isFinite(v) && v > 0 ? v : fallbackWidth;
2576
2493
  };
2577
- for (const { cmd, args } of tokens) {
2578
- const upper = cmd.toUpperCase();
2579
- const relative = cmd !== upper;
2580
- const argCount = ARG_COUNTS[upper];
2581
- if (argCount === void 0) {
2582
- onWarn?.(`unknown path command: ${cmd}`);
2583
- continue;
2584
- }
2585
- if (upper === "Z") {
2586
- b.close();
2587
- st.x = st.startX;
2588
- st.y = st.startY;
2589
- st.lastCubicCtrl = null;
2590
- st.lastQuadCtrl = null;
2591
- continue;
2592
- }
2593
- const groups = argCount === 0 ? 1 : Math.floor(args.length / argCount);
2594
- for (let g = 0; g < groups; g++) {
2595
- const a = args.slice(g * argCount, (g + 1) * argCount);
2596
- const effectiveUpper = upper === "M" && g > 0 ? "L" : upper;
2597
- runCommand(effectiveUpper, relative, a, st, b);
2494
+ for (let i = 0; i < ptCount; i++) {
2495
+ const a = aA[i], b = aB[i], t = aT[i];
2496
+ if (a === b) {
2497
+ widths[i] = read(a);
2498
+ } else {
2499
+ const wa = read(a);
2500
+ const wb = read(b);
2501
+ widths[i] = wa + (wb - wa) * t;
2598
2502
  }
2599
2503
  }
2600
- return b.build();
2504
+ pl.widths = widths;
2601
2505
  }
2602
- function runCommand(upper, relative, a, st, b) {
2603
- const rx = relative ? st.x : 0;
2604
- const ry = relative ? st.y : 0;
2605
- switch (upper) {
2606
- case "M": {
2607
- const x = a[0] + rx;
2608
- const y = a[1] + ry;
2609
- b.moveTo(x, y);
2610
- st.x = x;
2611
- st.y = y;
2612
- st.startX = x;
2613
- st.startY = y;
2614
- st.lastCubicCtrl = null;
2615
- st.lastQuadCtrl = null;
2616
- return;
2617
- }
2618
- case "L": {
2619
- const x = a[0] + rx;
2620
- const y = a[1] + ry;
2621
- b.lineTo(x, y);
2622
- st.x = x;
2623
- st.y = y;
2624
- st.lastCubicCtrl = null;
2625
- st.lastQuadCtrl = null;
2626
- return;
2627
- }
2628
- case "H": {
2629
- const x = a[0] + (relative ? st.x : 0);
2630
- b.lineTo(x, st.y);
2631
- st.x = x;
2632
- st.lastCubicCtrl = null;
2633
- st.lastQuadCtrl = null;
2634
- return;
2635
- }
2636
- case "V": {
2637
- const y = a[0] + (relative ? st.y : 0);
2638
- b.lineTo(st.x, y);
2639
- st.y = y;
2640
- st.lastCubicCtrl = null;
2641
- st.lastQuadCtrl = null;
2642
- return;
2506
+ var ROUND_CAP_STEP_RAD = 10 * Math.PI / 180;
2507
+ function emitCap(seg, leftIdx, rightIdx, atEnd, half, cap, verts, idx, anchorA, anchorB, anchorT, endA, endB, endT) {
2508
+ const sign = atEnd ? 1 : -1;
2509
+ const dx = (seg.bx - seg.ax) / seg.len * sign;
2510
+ const dy = (seg.by - seg.ay) / seg.len * sign;
2511
+ const cx = atEnd ? seg.bx : seg.ax;
2512
+ const cy = atEnd ? seg.by : seg.ay;
2513
+ const nx = seg.nxUnit * half;
2514
+ const ny = seg.nyUnit * half;
2515
+ if (cap === "square") {
2516
+ const ox = cx + dx * half;
2517
+ const oy = cy + dy * half;
2518
+ const lOut = verts.length / 2;
2519
+ verts.push(ox + nx, oy + ny);
2520
+ anchorA.push(endA);
2521
+ anchorB.push(endB);
2522
+ anchorT.push(endT);
2523
+ const rOut = verts.length / 2;
2524
+ verts.push(ox - nx, oy - ny);
2525
+ anchorA.push(endA);
2526
+ anchorB.push(endB);
2527
+ anchorT.push(endT);
2528
+ if (atEnd) {
2529
+ idx.push(leftIdx, lOut, rOut, leftIdx, rOut, rightIdx);
2530
+ } else {
2531
+ idx.push(leftIdx, rOut, lOut, leftIdx, rightIdx, rOut);
2643
2532
  }
2644
- case "C": {
2645
- const x1 = a[0] + rx, y1 = a[1] + ry;
2646
- const x2 = a[2] + rx, y2 = a[3] + ry;
2647
- const x = a[4] + rx, y = a[5] + ry;
2648
- b.curveTo(x1, y1, x2, y2, x, y);
2649
- st.x = x;
2650
- st.y = y;
2651
- st.lastCubicCtrl = { x: x2, y: y2 };
2652
- st.lastQuadCtrl = null;
2653
- return;
2533
+ return;
2534
+ }
2535
+ if (cap === "round") {
2536
+ const pivotIdx = verts.length / 2;
2537
+ verts.push(cx, cy);
2538
+ anchorA.push(endA);
2539
+ anchorB.push(endB);
2540
+ anchorT.push(endT);
2541
+ const startAngle = Math.atan2(ny, nx);
2542
+ const sweep = atEnd ? -Math.PI : Math.PI;
2543
+ const steps = Math.max(1, Math.ceil(Math.abs(sweep) / ROUND_CAP_STEP_RAD));
2544
+ const stepAngle = sweep / steps;
2545
+ let prevIdx = leftIdx;
2546
+ for (let i = 1; i < steps; i++) {
2547
+ const ang = startAngle + i * stepAngle;
2548
+ const fx = cx + Math.cos(ang) * half;
2549
+ const fy = cy + Math.sin(ang) * half;
2550
+ const newIdx = verts.length / 2;
2551
+ verts.push(fx, fy);
2552
+ anchorA.push(endA);
2553
+ anchorB.push(endB);
2554
+ anchorT.push(endT);
2555
+ if (atEnd) idx.push(prevIdx, newIdx, pivotIdx);
2556
+ else idx.push(prevIdx, pivotIdx, newIdx);
2557
+ prevIdx = newIdx;
2654
2558
  }
2655
- case "S": {
2656
- const ref = st.lastCubicCtrl ?? { x: st.x, y: st.y };
2657
- const x1 = 2 * st.x - ref.x;
2658
- const y1 = 2 * st.y - ref.y;
2659
- const x2 = a[0] + rx, y2 = a[1] + ry;
2660
- const x = a[2] + rx, y = a[3] + ry;
2661
- b.curveTo(x1, y1, x2, y2, x, y);
2662
- st.x = x;
2663
- st.y = y;
2664
- st.lastCubicCtrl = { x: x2, y: y2 };
2665
- st.lastQuadCtrl = null;
2666
- return;
2559
+ if (atEnd) idx.push(prevIdx, rightIdx, pivotIdx);
2560
+ else idx.push(prevIdx, pivotIdx, rightIdx);
2561
+ }
2562
+ }
2563
+ function splitForDash(pl, dash) {
2564
+ const out = [];
2565
+ const plA = pl.anchorA ?? new Uint32Array(pl.points.length / 2);
2566
+ const plB = pl.anchorB ?? new Uint32Array(pl.points.length / 2);
2567
+ const plT = pl.anchorT ?? new Float32Array(pl.points.length / 2);
2568
+ let dashIdx = 0;
2569
+ let dashRemaining = dash[0];
2570
+ let onPhase = true;
2571
+ let curPts = onPhase ? [pl.points[0], pl.points[1]] : null;
2572
+ let curA = onPhase ? [plA[0]] : null;
2573
+ let curB = onPhase ? [plB[0]] : null;
2574
+ let curT = onPhase ? [plT[0]] : null;
2575
+ const flushAndAdvance = () => {
2576
+ if (curPts && curPts.length >= 4) {
2577
+ out.push({
2578
+ points: curPts,
2579
+ closed: false,
2580
+ anchorA: new Uint32Array(curA),
2581
+ anchorB: new Uint32Array(curB),
2582
+ anchorT: new Float32Array(curT)
2583
+ });
2667
2584
  }
2668
- case "Q": {
2669
- const x1 = a[0] + rx, y1 = a[1] + ry;
2670
- const x = a[2] + rx, y = a[3] + ry;
2671
- b.quadTo(x1, y1, x, y);
2672
- st.x = x;
2673
- st.y = y;
2674
- st.lastQuadCtrl = { x: x1, y: y1 };
2675
- st.lastCubicCtrl = null;
2676
- return;
2585
+ curPts = null;
2586
+ curA = null;
2587
+ curB = null;
2588
+ curT = null;
2589
+ dashIdx = (dashIdx + 1) % dash.length;
2590
+ dashRemaining = dash[dashIdx];
2591
+ onPhase = !onPhase;
2592
+ };
2593
+ let prevX = pl.points[0], prevY = pl.points[1];
2594
+ const ptCount = pl.points.length / 2;
2595
+ const segCount = pl.closed ? ptCount : ptCount - 1;
2596
+ for (let i = 0; i < segCount; i++) {
2597
+ const nextIdx = (i + 1) % ptCount;
2598
+ const cx = pl.points[nextIdx * 2], cy = pl.points[nextIdx * 2 + 1];
2599
+ const startA = plA[i], startB = plB[i], startT = plT[i];
2600
+ const endA = plA[nextIdx], endB = plB[nextIdx], endT = plT[nextIdx];
2601
+ const segFullDx = cx - prevX, segFullDy = cy - prevY;
2602
+ const segFullLen = Math.hypot(segFullDx, segFullDy);
2603
+ let segDx = segFullDx, segDy = segFullDy;
2604
+ let segLen = segFullLen;
2605
+ let traveled = 0;
2606
+ while (segLen > 1e-9) {
2607
+ if (segLen <= dashRemaining) {
2608
+ if (onPhase && curPts) {
2609
+ curPts.push(cx, cy);
2610
+ curA.push(endA);
2611
+ curB.push(endB);
2612
+ curT.push(endT);
2613
+ }
2614
+ dashRemaining -= segLen;
2615
+ prevX = cx;
2616
+ prevY = cy;
2617
+ traveled = segFullLen;
2618
+ segLen = 0;
2619
+ if (dashRemaining <= 1e-9) {
2620
+ flushAndAdvance();
2621
+ if (onPhase) {
2622
+ curPts = [prevX, prevY];
2623
+ curA = [endA];
2624
+ curB = [endB];
2625
+ curT = [endT];
2626
+ }
2627
+ }
2628
+ } else {
2629
+ const tConsume = dashRemaining / segLen;
2630
+ const ix = prevX + segDx * tConsume;
2631
+ const iy = prevY + segDy * tConsume;
2632
+ traveled += dashRemaining;
2633
+ const frac = traveled / segFullLen;
2634
+ let mA, mB, mT;
2635
+ if (startA === endA && startB === endB) {
2636
+ mA = startA;
2637
+ mB = startB;
2638
+ mT = startT + (endT - startT) * frac;
2639
+ } else {
2640
+ if (frac < 0.5) {
2641
+ mA = startA;
2642
+ mB = startB;
2643
+ mT = startT;
2644
+ } else {
2645
+ mA = endA;
2646
+ mB = endB;
2647
+ mT = endT;
2648
+ }
2649
+ }
2650
+ if (onPhase && curPts) {
2651
+ curPts.push(ix, iy);
2652
+ curA.push(mA);
2653
+ curB.push(mB);
2654
+ curT.push(mT);
2655
+ }
2656
+ prevX = ix;
2657
+ prevY = iy;
2658
+ segDx = cx - prevX;
2659
+ segDy = cy - prevY;
2660
+ segLen = Math.hypot(segDx, segDy);
2661
+ flushAndAdvance();
2662
+ if (onPhase) {
2663
+ curPts = [prevX, prevY];
2664
+ curA = [mA];
2665
+ curB = [mB];
2666
+ curT = [mT];
2667
+ }
2668
+ }
2677
2669
  }
2678
- case "T": {
2679
- const ref = st.lastQuadCtrl ?? { x: st.x, y: st.y };
2680
- const x1 = 2 * st.x - ref.x;
2681
- const y1 = 2 * st.y - ref.y;
2682
- const x = a[0] + rx, y = a[1] + ry;
2683
- b.quadTo(x1, y1, x, y);
2684
- st.x = x;
2685
- st.y = y;
2686
- st.lastQuadCtrl = { x: x1, y: y1 };
2687
- st.lastCubicCtrl = null;
2670
+ }
2671
+ if (curPts && curPts.length >= 4) {
2672
+ out.push({
2673
+ points: curPts,
2674
+ closed: false,
2675
+ anchorA: new Uint32Array(curA),
2676
+ anchorB: new Uint32Array(curB),
2677
+ anchorT: new Float32Array(curT)
2678
+ });
2679
+ }
2680
+ return out;
2681
+ }
2682
+ function makeSeg(ax, ay, bx, by, halfA, halfB) {
2683
+ const dx = bx - ax, dy = by - ay;
2684
+ const len = Math.hypot(dx, dy);
2685
+ if (len === 0) return null;
2686
+ return {
2687
+ ax,
2688
+ ay,
2689
+ bx,
2690
+ by,
2691
+ nxUnit: -dy / len,
2692
+ nyUnit: dx / len,
2693
+ halfA,
2694
+ halfB,
2695
+ len
2696
+ };
2697
+ }
2698
+ var DEFAULT_MITER_LIMIT = 10;
2699
+ var DEFAULT_VARYING_WIDTH_JOIN_THRESHOLD = 1.5;
2700
+ function emitJoin(segs, segBaseIdx, j, join, miterLimit, varyingThreshold, verts, idx, anchorA, anchorB, anchorT, segSrcIdx, plA, plB, plT) {
2701
+ const a = segs[j];
2702
+ const b = segs[(j + 1) % segs.length];
2703
+ const aBase = segBaseIdx[j];
2704
+ const bBase = segBaseIdx[(j + 1) % segs.length];
2705
+ const adx = a.bx - a.ax, ady = a.by - a.ay;
2706
+ const bdx = b.bx - b.ax, bdy = b.by - b.ay;
2707
+ const cross = adx * bdy - ady * bdx;
2708
+ if (cross === 0) return;
2709
+ const onPositive = cross > 0;
2710
+ const aOuterEnd = onPositive ? aBase + 3 : aBase + 2;
2711
+ const bOuterStart = onPositive ? bBase + 1 : bBase + 0;
2712
+ const nextSeg = (j + 1) % segs.length;
2713
+ const cornerSrc = segSrcIdx[nextSeg];
2714
+ const cornerA = plA[cornerSrc];
2715
+ const cornerB = plB[cornerSrc];
2716
+ const cornerT = plT[cornerSrc];
2717
+ const halfAtCorner = a.halfB;
2718
+ const taperA = Math.max(a.halfA, a.halfB) / Math.max(1e-9, Math.min(a.halfA, a.halfB));
2719
+ const taperB = Math.max(b.halfA, b.halfB) / Math.max(1e-9, Math.min(b.halfA, b.halfB));
2720
+ const forceBevelForVarying = taperA > varyingThreshold || taperB > varyingThreshold;
2721
+ if (join === "bevel" || forceBevelForVarying) {
2722
+ emitBevel(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT);
2723
+ return;
2724
+ }
2725
+ if (join === "miter") {
2726
+ const aOX = verts[aOuterEnd * 2], aOY = verts[aOuterEnd * 2 + 1];
2727
+ const bOX = verts[bOuterStart * 2], bOY = verts[bOuterStart * 2 + 1];
2728
+ const apex = lineLineIntersect(aOX, aOY, adx, ady, bOX, bOY, -bdx, -bdy);
2729
+ if (!apex) {
2730
+ emitBevel(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT);
2688
2731
  return;
2689
2732
  }
2690
- case "A": {
2691
- const rxA = Math.abs(a[0]);
2692
- const ryA = Math.abs(a[1]);
2693
- const xRot = a[2];
2694
- const largeArc = a[3] !== 0;
2695
- const sweep = a[4] !== 0;
2696
- const ex = a[5] + rx;
2697
- const ey = a[6] + ry;
2698
- arcToCubics(st.x, st.y, ex, ey, rxA, ryA, xRot, largeArc, sweep, b);
2699
- st.x = ex;
2700
- st.y = ey;
2701
- st.lastCubicCtrl = null;
2702
- st.lastQuadCtrl = null;
2733
+ const miterLen = Math.hypot(apex[0] - a.bx, apex[1] - a.by);
2734
+ if (miterLen > miterLimit * halfAtCorner) {
2735
+ emitBevel(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT);
2703
2736
  return;
2704
2737
  }
2705
- default:
2706
- return;
2707
- }
2708
- }
2709
- function arcToCubics(x1, y1, x2, y2, rx, ry, xAxisRotationDeg, largeArc, sweep, b) {
2710
- if (x1 === x2 && y1 === y2) return;
2711
- if (rx === 0 || ry === 0) {
2712
- b.lineTo(x2, y2);
2738
+ const apexIdx = verts.length / 2;
2739
+ verts.push(apex[0], apex[1]);
2740
+ anchorA.push(cornerA);
2741
+ anchorB.push(cornerB);
2742
+ anchorT.push(cornerT);
2743
+ const jIdx = verts.length / 2;
2744
+ verts.push(a.bx, a.by);
2745
+ anchorA.push(cornerA);
2746
+ anchorB.push(cornerB);
2747
+ anchorT.push(cornerT);
2748
+ if (onPositive) {
2749
+ idx.push(aOuterEnd, apexIdx, bOuterStart);
2750
+ idx.push(aOuterEnd, bOuterStart, jIdx);
2751
+ } else {
2752
+ idx.push(aOuterEnd, bOuterStart, apexIdx);
2753
+ idx.push(aOuterEnd, jIdx, bOuterStart);
2754
+ }
2713
2755
  return;
2714
2756
  }
2715
- const phi = xAxisRotationDeg * Math.PI / 180;
2716
- const cosPhi = Math.cos(phi);
2717
- const sinPhi = Math.sin(phi);
2718
- const dx = (x1 - x2) / 2;
2719
- const dy = (y1 - y2) / 2;
2720
- const x1p = cosPhi * dx + sinPhi * dy;
2721
- const y1p = -sinPhi * dx + cosPhi * dy;
2722
- let rxs = rx * rx;
2723
- let rys = ry * ry;
2724
- const x1ps = x1p * x1p;
2725
- const y1ps = y1p * y1p;
2726
- const lambda = x1ps / rxs + y1ps / rys;
2727
- let rxFixed = rx;
2728
- let ryFixed = ry;
2729
- if (lambda > 1) {
2730
- const sqrtLambda = Math.sqrt(lambda);
2731
- rxFixed = sqrtLambda * rx;
2732
- ryFixed = sqrtLambda * ry;
2733
- rxs = rxFixed * rxFixed;
2734
- rys = ryFixed * ryFixed;
2757
+ if (join === "round") {
2758
+ emitRoundJoin(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT);
2759
+ return;
2735
2760
  }
2736
- const sign = largeArc === sweep ? -1 : 1;
2737
- const num = rxs * rys - rxs * y1ps - rys * x1ps;
2738
- const den = rxs * y1ps + rys * x1ps;
2739
- const factor = sign * Math.sqrt(Math.max(0, num / den));
2740
- const cxp = factor * (rxFixed * y1p) / ryFixed;
2741
- const cyp = factor * -(ryFixed * x1p) / rxFixed;
2742
- const cx = cosPhi * cxp - sinPhi * cyp + (x1 + x2) / 2;
2743
- const cy = sinPhi * cxp + cosPhi * cyp + (y1 + y2) / 2;
2744
- const angle = (ux, uy, vx, vy) => {
2745
- const dot = ux * vx + uy * vy;
2746
- const len = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
2747
- let a = Math.acos(Math.max(-1, Math.min(1, dot / len)));
2748
- if (ux * vy - uy * vx < 0) a = -a;
2749
- return a;
2750
- };
2751
- const theta1 = angle(1, 0, (x1p - cxp) / rxFixed, (y1p - cyp) / ryFixed);
2752
- let dTheta = angle(
2753
- (x1p - cxp) / rxFixed,
2754
- (y1p - cyp) / ryFixed,
2755
- (-x1p - cxp) / rxFixed,
2756
- (-y1p - cyp) / ryFixed
2757
- );
2758
- if (!sweep && dTheta > 0) dTheta -= 2 * Math.PI;
2759
- else if (sweep && dTheta < 0) dTheta += 2 * Math.PI;
2760
- const segCount = Math.max(1, Math.ceil(Math.abs(dTheta) / (Math.PI / 2)));
2761
- const segAngle = dTheta / segCount;
2762
- const t = 4 / 3 * Math.tan(segAngle / 4);
2763
- let prevX = x1;
2764
- let prevY = y1;
2765
- let prevDx = -Math.sin(theta1) * rxFixed;
2766
- let prevDy = Math.cos(theta1) * ryFixed;
2767
- for (let i = 1; i <= segCount; i++) {
2768
- const theta = theta1 + i * segAngle;
2769
- const cosT = Math.cos(theta);
2770
- const sinT = Math.sin(theta);
2771
- const px = cosPhi * (cosT * rxFixed) - sinPhi * (sinT * ryFixed) + cx;
2772
- const py = sinPhi * (cosT * rxFixed) + cosPhi * (sinT * ryFixed) + cy;
2773
- const dxNew = -sinT * rxFixed;
2774
- const dyNew = cosT * ryFixed;
2775
- const c1x = prevX + cosPhi * (t * prevDx) - sinPhi * (t * prevDy);
2776
- const c1y = prevY + sinPhi * (t * prevDx) + cosPhi * (t * prevDy);
2777
- const c2x = px - cosPhi * (t * dxNew) + sinPhi * (t * dyNew);
2778
- const c2y = py - sinPhi * (t * dxNew) - cosPhi * (t * dyNew);
2779
- b.curveTo(c1x, c1y, c2x, c2y, px, py);
2780
- prevX = px;
2781
- prevY = py;
2782
- prevDx = dxNew;
2783
- prevDy = dyNew;
2761
+ }
2762
+ var ROUND_STEP_RAD = 10 * Math.PI / 180;
2763
+ function emitRoundJoin(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT) {
2764
+ const cx = a.bx, cy = a.by;
2765
+ const pivotIdx = verts.length / 2;
2766
+ verts.push(cx, cy);
2767
+ anchorA.push(cornerA);
2768
+ anchorB.push(cornerB);
2769
+ anchorT.push(cornerT);
2770
+ const startX = verts[aOuterEnd * 2] - cx;
2771
+ const startY = verts[aOuterEnd * 2 + 1] - cy;
2772
+ const endX = verts[bOuterStart * 2] - cx;
2773
+ const endY = verts[bOuterStart * 2 + 1] - cy;
2774
+ const r = Math.hypot(startX, startY);
2775
+ const startAngle = Math.atan2(startY, startX);
2776
+ const endAngle = Math.atan2(endY, endX);
2777
+ let sweep = endAngle - startAngle;
2778
+ if (onPositive && sweep < 0) sweep += 2 * Math.PI;
2779
+ else if (!onPositive && sweep > 0) sweep -= 2 * Math.PI;
2780
+ const steps = Math.max(1, Math.ceil(Math.abs(sweep) / ROUND_STEP_RAD));
2781
+ const stepAngle = sweep / steps;
2782
+ let prevIdx = aOuterEnd;
2783
+ for (let i = 1; i < steps; i++) {
2784
+ const ang = startAngle + i * stepAngle;
2785
+ const fx = cx + Math.cos(ang) * r;
2786
+ const fy = cy + Math.sin(ang) * r;
2787
+ const newIdx = verts.length / 2;
2788
+ verts.push(fx, fy);
2789
+ anchorA.push(cornerA);
2790
+ anchorB.push(cornerB);
2791
+ anchorT.push(cornerT);
2792
+ if (onPositive) idx.push(prevIdx, pivotIdx, newIdx);
2793
+ else idx.push(prevIdx, newIdx, pivotIdx);
2794
+ prevIdx = newIdx;
2784
2795
  }
2796
+ if (onPositive) idx.push(prevIdx, pivotIdx, bOuterStart);
2797
+ else idx.push(prevIdx, bOuterStart, pivotIdx);
2785
2798
  }
2786
-
2787
- // src/renderer/cache/outlineMeshCache.ts
2788
- var OUTLINE_FLATTEN_TOLERANCE = 1 / 4096;
2789
- var OUTLINE_MESH_CACHE_LIMIT = 2048;
2790
- var cache3 = /* @__PURE__ */ new Map();
2791
- function outlineMesh(key, d) {
2792
- const cached = cache3.get(key);
2793
- if (cached !== void 0) return cached;
2794
- if (cache3.size >= OUTLINE_MESH_CACHE_LIMIT) cache3 = /* @__PURE__ */ new Map();
2795
- const mesh = tessellate(pathFromD(d), { flattenTolerance: OUTLINE_FLATTEN_TOLERANCE });
2796
- cache3.set(key, mesh);
2797
- return mesh;
2799
+ function emitBevel(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT) {
2800
+ const jIdx = verts.length / 2;
2801
+ verts.push(a.bx, a.by);
2802
+ anchorA.push(cornerA);
2803
+ anchorB.push(cornerB);
2804
+ anchorT.push(cornerT);
2805
+ if (onPositive) idx.push(aOuterEnd, jIdx, bOuterStart);
2806
+ else idx.push(aOuterEnd, bOuterStart, jIdx);
2807
+ }
2808
+ function lineLineIntersect(apx, apy, adx, ady, bpx, bpy, bdx, bdy) {
2809
+ const denom = adx * bdy - ady * bdx;
2810
+ if (Math.abs(denom) < 1e-9) return null;
2811
+ const t = ((bpx - apx) * bdy - (bpy - apy) * bdx) / denom;
2812
+ return [apx + t * adx, apy + t * ady];
2798
2813
  }
2799
2814
 
2800
2815
  // src/renderer/cache/outlineStrokeMeshCache.ts
@@ -2827,26 +2842,299 @@ function outlineStrokeMesh(glyphKey, d, emWidth, stroke) {
2827
2842
  return mesh.indices.length === 0 ? null : mesh;
2828
2843
  }
2829
2844
 
2845
+ // src/renderer/cache/strokeMeshCache.ts
2846
+ var STROKE_CONFIGS_PER_PATH = 8;
2847
+ var cache5 = /* @__PURE__ */ new WeakMap();
2848
+ function configKey(stroke, flattenTolerance) {
2849
+ return [
2850
+ stroke.width ?? 1,
2851
+ stroke.cap ?? "butt",
2852
+ stroke.join ?? "miter",
2853
+ stroke.miterLimit ?? "",
2854
+ stroke.align ?? "center",
2855
+ (stroke.dash ?? []).join(","),
2856
+ stroke.varyingWidthJoinThreshold ?? "",
2857
+ flattenTolerance ?? ""
2858
+ ].join("|");
2859
+ }
2860
+ function strokeMesh(path, stroke, flattenTolerance) {
2861
+ let byConfig = cache5.get(path);
2862
+ if (byConfig === void 0) {
2863
+ byConfig = /* @__PURE__ */ new Map();
2864
+ cache5.set(path, byConfig);
2865
+ }
2866
+ const key = configKey(stroke, flattenTolerance);
2867
+ const entry = byConfig.get(key);
2868
+ if (entry !== void 0 && entry.vertexWidths === stroke.vertexWidths) {
2869
+ return entry.mesh;
2870
+ }
2871
+ const mesh = tessellateStroke(path, stroke, { flattenTolerance });
2872
+ if (entry === void 0 && byConfig.size >= STROKE_CONFIGS_PER_PATH) byConfig.clear();
2873
+ byConfig.set(key, { mesh, vertexWidths: stroke.vertexWidths });
2874
+ return mesh;
2875
+ }
2876
+
2877
+ // src/renderer/solidBatch.ts
2878
+ var MAX_VERTICES_PER_BATCH = 32768;
2879
+ var FLOATS_PER_VERTEX = 6;
2880
+ var INITIAL_VERTICES = 256;
2881
+ var INITIAL_INDICES = 384;
2882
+ var SolidBatch = class {
2883
+ gl;
2884
+ vao;
2885
+ vbo;
2886
+ ibo;
2887
+ verts = new Float32Array(INITIAL_VERTICES * FLOATS_PER_VERTEX);
2888
+ idx = new Uint32Array(INITIAL_INDICES);
2889
+ nVerts = 0;
2890
+ nIdx = 0;
2891
+ /** What the GPU buffers are currently sized for. */
2892
+ vboVerts = 0;
2893
+ iboIdx = 0;
2894
+ constructor(gl, prog) {
2895
+ const aPos = prog.attribute("a_position");
2896
+ const aColor = prog.attribute("a_vertexColor");
2897
+ if (aPos === void 0 || aColor === void 0) {
2898
+ throw new Error("SolidBatch: vertex-color program is missing a_position / a_vertexColor");
2899
+ }
2900
+ const vao = gl.createVertexArray();
2901
+ const vbo = gl.createBuffer();
2902
+ const ibo = gl.createBuffer();
2903
+ if (!vao || !vbo || !ibo) throw new Error("SolidBatch: failed to create GL objects");
2904
+ this.gl = gl;
2905
+ this.vao = vao;
2906
+ this.vbo = vbo;
2907
+ this.ibo = ibo;
2908
+ const stride = FLOATS_PER_VERTEX * 4;
2909
+ gl.bindVertexArray(vao);
2910
+ gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
2911
+ gl.enableVertexAttribArray(aPos);
2912
+ gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, stride, 0);
2913
+ gl.enableVertexAttribArray(aColor);
2914
+ gl.vertexAttribPointer(aColor, 4, gl.FLOAT, false, stride, 8);
2915
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
2916
+ gl.bindVertexArray(null);
2917
+ this.growGpu(INITIAL_VERTICES, INITIAL_INDICES);
2918
+ }
2919
+ get length() {
2920
+ return this.nIdx;
2921
+ }
2922
+ /** Whether staging `vertices` more would put the run past the per-flush cap. */
2923
+ wouldOverflow(vertices) {
2924
+ return this.nVerts + vertices > MAX_VERTICES_PER_BATCH;
2925
+ }
2926
+ /**
2927
+ * Append one rect's four corners through `m`, all carrying `rgba` (straight
2928
+ * alpha). An affine maps a rect to a parallelogram, so two triangles still
2929
+ * cover it and the batch draws at `u_model` identity.
2930
+ */
2931
+ pushRect(x, y, w, h, m, r, g, b, a) {
2932
+ this.reserve(4, 6);
2933
+ const v = this.verts;
2934
+ let i = this.nVerts * FLOATS_PER_VERTEX;
2935
+ const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
2936
+ const x1 = x + w;
2937
+ const y1 = y + h;
2938
+ const ax = ma * x + mc * y + mtx, ay = mb * x + md * y + mty;
2939
+ const bx = ma * x1 + mc * y + mtx, by = mb * x1 + md * y + mty;
2940
+ const cx = ma * x1 + mc * y1 + mtx, cy = mb * x1 + md * y1 + mty;
2941
+ const dx = ma * x + mc * y1 + mtx, dy = mb * x + md * y1 + mty;
2942
+ v[i++] = ax;
2943
+ v[i++] = ay;
2944
+ v[i++] = r;
2945
+ v[i++] = g;
2946
+ v[i++] = b;
2947
+ v[i++] = a;
2948
+ v[i++] = bx;
2949
+ v[i++] = by;
2950
+ v[i++] = r;
2951
+ v[i++] = g;
2952
+ v[i++] = b;
2953
+ v[i++] = a;
2954
+ v[i++] = cx;
2955
+ v[i++] = cy;
2956
+ v[i++] = r;
2957
+ v[i++] = g;
2958
+ v[i++] = b;
2959
+ v[i++] = a;
2960
+ v[i++] = dx;
2961
+ v[i++] = dy;
2962
+ v[i++] = r;
2963
+ v[i++] = g;
2964
+ v[i++] = b;
2965
+ v[i++] = a;
2966
+ const base = this.nVerts;
2967
+ let j = this.nIdx;
2968
+ this.idx[j++] = base;
2969
+ this.idx[j++] = base + 1;
2970
+ this.idx[j++] = base + 2;
2971
+ this.idx[j++] = base;
2972
+ this.idx[j++] = base + 2;
2973
+ this.idx[j++] = base + 3;
2974
+ this.nVerts += 4;
2975
+ this.nIdx += 6;
2976
+ }
2977
+ /**
2978
+ * Append a tessellated mesh through `m`, all vertices carrying `rgba`. The
2979
+ * mesh's own indices are rebased onto the staged vertices, which is why the
2980
+ * index buffer is uploaded per flush rather than written once.
2981
+ */
2982
+ pushMesh(mesh, m, r, g, b, a) {
2983
+ const src = mesh.vertices;
2984
+ const srcIdx = mesh.indices;
2985
+ const n = src.length >> 1;
2986
+ this.reserve(n, srcIdx.length);
2987
+ const v = this.verts;
2988
+ let i = this.nVerts * FLOATS_PER_VERTEX;
2989
+ const ma = m[0], mb = m[1], mc = m[3], md = m[4], mtx = m[6], mty = m[7];
2990
+ for (let k = 0; k < n; k++) {
2991
+ const x = src[k * 2];
2992
+ const y = src[k * 2 + 1];
2993
+ v[i++] = ma * x + mc * y + mtx;
2994
+ v[i++] = mb * x + md * y + mty;
2995
+ v[i++] = r;
2996
+ v[i++] = g;
2997
+ v[i++] = b;
2998
+ v[i++] = a;
2999
+ }
3000
+ const base = this.nVerts;
3001
+ const out = this.idx;
3002
+ let j = this.nIdx;
3003
+ for (let k = 0; k < srcIdx.length; k++) out[j++] = base + srcIdx[k];
3004
+ this.nVerts += n;
3005
+ this.nIdx += srcIdx.length;
3006
+ }
3007
+ /** Upload the staged geometry and bind the batch VAO. Returns the index
3008
+ * count for the caller's `drawElements`. */
3009
+ uploadAndBind() {
3010
+ const gl = this.gl;
3011
+ if (this.nVerts > this.vboVerts || this.nIdx > this.iboIdx) {
3012
+ this.growGpu(this.nVerts, this.nIdx);
3013
+ }
3014
+ gl.bindVertexArray(this.vao);
3015
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo);
3016
+ gl.bufferSubData(gl.ARRAY_BUFFER, 0, this.verts, 0, this.nVerts * FLOATS_PER_VERTEX);
3017
+ gl.bufferSubData(gl.ELEMENT_ARRAY_BUFFER, 0, this.idx, 0, this.nIdx);
3018
+ return this.nIdx;
3019
+ }
3020
+ reset() {
3021
+ this.nVerts = 0;
3022
+ this.nIdx = 0;
3023
+ }
3024
+ dispose() {
3025
+ const gl = this.gl;
3026
+ gl.deleteBuffer(this.vbo);
3027
+ gl.deleteBuffer(this.ibo);
3028
+ gl.deleteVertexArray(this.vao);
3029
+ }
3030
+ /** Grow the CPU arrays so `vertices` / `indices` more fit. */
3031
+ reserve(vertices, indices) {
3032
+ const needVerts = (this.nVerts + vertices) * FLOATS_PER_VERTEX;
3033
+ if (needVerts > this.verts.length) {
3034
+ let len = this.verts.length;
3035
+ while (len < needVerts) len *= 2;
3036
+ const grown = new Float32Array(len);
3037
+ grown.set(this.verts);
3038
+ this.verts = grown;
3039
+ }
3040
+ const needIdx = this.nIdx + indices;
3041
+ if (needIdx > this.idx.length) {
3042
+ let len = this.idx.length;
3043
+ while (len < needIdx) len *= 2;
3044
+ const grown = new Uint32Array(len);
3045
+ grown.set(this.idx);
3046
+ this.idx = grown;
3047
+ }
3048
+ }
3049
+ /** Size both GPU buffers for at least what is staged, doubling to amortize. */
3050
+ growGpu(vertices, indices) {
3051
+ const gl = this.gl;
3052
+ if (vertices > this.vboVerts) {
3053
+ let capacity = Math.max(this.vboVerts, INITIAL_VERTICES);
3054
+ while (capacity < vertices) capacity *= 2;
3055
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo);
3056
+ gl.bufferData(gl.ARRAY_BUFFER, capacity * FLOATS_PER_VERTEX * 4, gl.DYNAMIC_DRAW);
3057
+ this.vboVerts = capacity;
3058
+ }
3059
+ if (indices > this.iboIdx) {
3060
+ let capacity = Math.max(this.iboIdx, INITIAL_INDICES);
3061
+ while (capacity < indices) capacity *= 2;
3062
+ gl.bindVertexArray(this.vao);
3063
+ gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ibo);
3064
+ gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, capacity * 4, gl.DYNAMIC_DRAW);
3065
+ gl.bindVertexArray(null);
3066
+ this.iboIdx = capacity;
3067
+ }
3068
+ }
3069
+ };
3070
+
2830
3071
  // src/renderer/draw.ts
2831
- function setColorMatrixUniforms(ctx, prog) {
3072
+ var FRAME_UPLOADS = /* @__PURE__ */ new WeakMap();
3073
+ var FRAME_PROJ = /* @__PURE__ */ new WeakMap();
3074
+ function uploadedFor(ctx, prog) {
3075
+ let byProgram = FRAME_UPLOADS.get(ctx);
3076
+ if (!byProgram) {
3077
+ byProgram = /* @__PURE__ */ new WeakMap();
3078
+ FRAME_UPLOADS.set(ctx, byProgram);
3079
+ }
3080
+ let uploaded = byProgram.get(prog);
3081
+ if (!uploaded) {
3082
+ uploaded = {};
3083
+ byProgram.set(prog, uploaded);
3084
+ }
3085
+ return uploaded;
3086
+ }
3087
+ function sameValues(prev, next) {
3088
+ if (prev === void 0 || prev.length !== next.length) return false;
3089
+ for (let i = 0; i < prev.length; i++) if (prev[i] !== next[i]) return false;
3090
+ return true;
3091
+ }
3092
+ var BATCH_MODEL = mat3.identity();
3093
+ function projFor(ctx) {
3094
+ let proj = FRAME_PROJ.get(ctx);
3095
+ if (!proj) {
3096
+ proj = mat3.screenToClip(ctx.widthCss, ctx.heightCss);
3097
+ FRAME_PROJ.set(ctx, proj);
3098
+ }
3099
+ return proj;
3100
+ }
3101
+ var COLOR_MATRIX_SCRATCH = new Float32Array(16);
3102
+ function setColorMatrixUniforms(ctx, prog, cm = ctx.state.colorMatrix) {
2832
3103
  const gl = ctx.gl;
2833
- const cm = ctx.state.colorMatrix;
2834
- const m4 = new Float32Array(16);
2835
- for (let row = 0; row < 4; row++) {
2836
- for (let col = 0; col < 4; col++) {
2837
- m4[col * 4 + row] = cm[row * 5 + col];
3104
+ const uploaded = uploadedFor(ctx, prog);
3105
+ const mLoc = prog.uniform("u_colorMatrix");
3106
+ if (mLoc !== void 0) {
3107
+ for (let row = 0; row < 4; row++) {
3108
+ for (let col = 0; col < 4; col++) {
3109
+ COLOR_MATRIX_SCRATCH[col * 4 + row] = cm[row * 5 + col];
3110
+ }
3111
+ }
3112
+ if (!sameValues(uploaded.colorMatrix, COLOR_MATRIX_SCRATCH)) {
3113
+ gl.uniformMatrix4fv(mLoc, false, COLOR_MATRIX_SCRATCH);
3114
+ uploaded.colorMatrix = Float32Array.from(COLOR_MATRIX_SCRATCH);
2838
3115
  }
2839
3116
  }
2840
- const mLoc = prog.uniform("u_colorMatrix");
2841
3117
  const bLoc = prog.uniform("u_colorBias");
2842
- if (mLoc !== void 0) gl.uniformMatrix4fv(mLoc, false, m4);
2843
- if (bLoc !== void 0) gl.uniform4f(bLoc, cm[4], cm[9], cm[14], cm[19]);
3118
+ if (bLoc !== void 0) {
3119
+ const bias = [cm[4], cm[9], cm[14], cm[19]];
3120
+ if (!sameValues(uploaded.colorBias, bias)) {
3121
+ gl.uniform4f(bLoc, bias[0], bias[1], bias[2], bias[3]);
3122
+ uploaded.colorBias = Float32Array.from(bias);
3123
+ }
3124
+ }
2844
3125
  }
2845
- function fillMeshHandle(ctx, path) {
3126
+ function fillMesh(ctx, path) {
2846
3127
  if (ctx.flattenTolerance !== void 0) {
2847
- return ctx.meshCache.uploadTransient(tessellate(path, { flattenTolerance: ctx.flattenTolerance }));
3128
+ return tessellate(path, { flattenTolerance: ctx.flattenTolerance });
2848
3129
  }
2849
- return ctx.meshCache.handleFor(getMesh(path));
3130
+ return getMesh(path);
3131
+ }
3132
+ function meshHandle(ctx, mesh) {
3133
+ if (ctx.flattenTolerance !== void 0) return ctx.meshCache.uploadTransient(mesh);
3134
+ return ctx.meshCache.handleFor(mesh);
3135
+ }
3136
+ function fillMeshHandle(ctx, path) {
3137
+ return meshHandle(ctx, fillMesh(ctx, path));
2850
3138
  }
2851
3139
  function dispatch(ctx, cmd) {
2852
3140
  switch (cmd.kind) {
@@ -2855,10 +3143,13 @@ function dispatch(ctx, cmd) {
2855
3143
  case "path":
2856
3144
  return drawPath(ctx, cmd);
2857
3145
  case "text":
3146
+ flushSolids(ctx);
2858
3147
  return drawText(ctx, cmd);
2859
3148
  case "image":
3149
+ flushSolids(ctx);
2860
3150
  return drawImage(ctx, cmd);
2861
3151
  case "shader":
3152
+ flushSolids(ctx);
2862
3153
  return drawShader(ctx, cmd);
2863
3154
  }
2864
3155
  }
@@ -2998,40 +3289,106 @@ function drawGroup(ctx, cmd) {
2998
3289
  ctx.state.pop();
2999
3290
  }
3000
3291
  function drawPath(ctx, cmd) {
3001
- if (!cmd.fill && !cmd.stroke) return;
3002
- if (cmd.fill) {
3003
- const isSolidRectFast = cmd.path.kind === "rect" && (cmd.fill.fill === void 0 || cmd.fill.fill === "solid") && (!cmd.vertexColors || cmd.vertexColors.length === 0) && ctx.rectVao !== null && ctx.rectVbo !== null;
3004
- if (isSolidRectFast && cmd.path.kind === "rect") {
3005
- drawRectFast(ctx, cmd.path, cmd.fill);
3006
- } else {
3007
- const handle = fillMeshHandle(ctx, cmd.path);
3008
- if (cmd.vertexColors && cmd.vertexColors.length > 0 && (cmd.fill.fill === void 0 || cmd.fill.fill === "solid")) {
3009
- drawPathFillVColor(ctx, cmd, cmd.fill, handle);
3010
- } else if (handle.requiresStencil) {
3011
- drawPathFillStencil(ctx, cmd.fill, handle);
3012
- } else {
3013
- drawPathFillByKind(ctx, cmd.fill, handle);
3014
- }
3015
- }
3292
+ if (cmd.fill) drawPathFill(ctx, cmd);
3293
+ if (cmd.stroke) drawPathStroke(ctx, cmd);
3294
+ }
3295
+ function drawPathFill(ctx, cmd) {
3296
+ const fill = cmd.fill;
3297
+ const isSolid = fill.fill === void 0 || fill.fill === "solid";
3298
+ const hasVColors = !!(cmd.vertexColors && cmd.vertexColors.length > 0);
3299
+ const solid = fill;
3300
+ if (isSolid && !hasVColors && cmd.path.kind === "rect") {
3301
+ pushRect(ctx, cmd.path, solid);
3302
+ return;
3303
+ }
3304
+ const mesh = fillMesh(ctx, cmd.path);
3305
+ if (tryStageSolid(ctx, mesh, isSolid && !hasVColors ? solid : void 0)) return;
3306
+ const handle = meshHandle(ctx, mesh);
3307
+ if (isSolid && hasVColors) {
3308
+ drawPathFillVColor(ctx, cmd, solid, handle);
3309
+ } else if (handle.requiresStencil) {
3310
+ drawPathFillStencil(ctx, fill, handle);
3311
+ } else {
3312
+ drawPathFillByKind(ctx, fill, handle);
3313
+ }
3314
+ }
3315
+ function isIdentityColorMatrix(cm) {
3316
+ return cm === IDENTITY_COLOR_MATRIX || sameValues(IDENTITY_COLOR_MATRIX, cm);
3317
+ }
3318
+ function stagedStateIsLive(ctx, staged) {
3319
+ if (staged.clipDepth !== ctx.clipDepth) return false;
3320
+ if (!staged.foldsAlpha && staged.alpha !== ctx.state.alpha) return false;
3321
+ const colorMatrix = ctx.state.colorMatrix;
3322
+ return staged.colorMatrix === colorMatrix || sameValues(staged.colorMatrix, colorMatrix);
3323
+ }
3324
+ var MAX_BATCHED_MESH_VERTICES = 256;
3325
+ function canBatchMesh(mesh) {
3326
+ return !mesh.requiresStencil && mesh.vertices.length >> 1 <= MAX_BATCHED_MESH_VERTICES;
3327
+ }
3328
+ function stageSolid(ctx, vertices) {
3329
+ if (ctx.solidState !== void 0 && !stagedStateIsLive(ctx, ctx.solidState)) flushSolids(ctx);
3330
+ if (ctx.solidBatch.wouldOverflow(vertices)) flushSolids(ctx);
3331
+ if (ctx.solidState === void 0) {
3332
+ const colorMatrix = ctx.state.colorMatrix;
3333
+ const foldsAlpha = isIdentityColorMatrix(colorMatrix);
3334
+ ctx.solidState = {
3335
+ alpha: foldsAlpha ? 1 : ctx.state.alpha,
3336
+ colorMatrix,
3337
+ clipDepth: ctx.clipDepth,
3338
+ foldsAlpha
3339
+ };
3016
3340
  }
3017
- if (cmd.stroke) {
3018
- drawPathStroke(ctx, cmd);
3341
+ return ctx.solidState;
3342
+ }
3343
+ function stagedColor(ctx, staged, paint) {
3344
+ const [r, g, b, a] = resolveColor(paint.color);
3345
+ return [r, g, b, a * (paint.opacity ?? 1) * (staged.foldsAlpha ? ctx.state.alpha : 1)];
3346
+ }
3347
+ function pushRect(ctx, rect, fill) {
3348
+ const staged = stageSolid(ctx, 4);
3349
+ const [r, g, b, a] = stagedColor(ctx, staged, fill);
3350
+ ctx.solidBatch.pushRect(
3351
+ rect.x,
3352
+ rect.y,
3353
+ rect.width,
3354
+ rect.height,
3355
+ ctx.state.transform,
3356
+ r,
3357
+ g,
3358
+ b,
3359
+ a
3360
+ );
3361
+ }
3362
+ function pushMesh(ctx, mesh, paint) {
3363
+ const staged = stageSolid(ctx, mesh.vertices.length >> 1);
3364
+ const [r, g, b, a] = stagedColor(ctx, staged, paint);
3365
+ ctx.solidBatch.pushMesh(mesh, ctx.state.transform, r, g, b, a);
3366
+ }
3367
+ function tryStageSolid(ctx, mesh, paint) {
3368
+ if (paint !== void 0 && canBatchMesh(mesh)) {
3369
+ pushMesh(ctx, mesh, paint);
3370
+ return true;
3019
3371
  }
3372
+ flushSolids(ctx);
3373
+ return false;
3020
3374
  }
3021
- function drawRectFast(ctx, rect, fill) {
3375
+ function flushSolids(ctx) {
3376
+ const batch = ctx.solidBatch;
3377
+ const staged = ctx.solidState;
3378
+ if (batch.length === 0 || staged === void 0) return;
3022
3379
  const gl = ctx.gl;
3023
- const { x, y, width: w, height: h } = rect;
3024
- const corners = new Float32Array([x, y, x + w, y, x + w, y + h, x, y + h]);
3025
- gl.useProgram(ctx.pathFill.handle);
3026
- gl.bindVertexArray(ctx.rectVao);
3027
- gl.bindBuffer(gl.ARRAY_BUFFER, ctx.rectVbo);
3028
- gl.bufferSubData(gl.ARRAY_BUFFER, 0, corners);
3029
- setProjAndModel(ctx, ctx.pathFill);
3030
- setSolidPaintUniforms(ctx, ctx.pathFill, fill.color, fill.opacity);
3031
- setColorMatrixUniforms(ctx, ctx.pathFill);
3032
- applyClipTest(ctx);
3033
- gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, 0);
3380
+ const prog = ctx.pathFillVColor;
3381
+ gl.useProgram(prog.handle);
3382
+ const indexCount = batch.uploadAndBind();
3383
+ setProjAndModel(ctx, prog, BATCH_MODEL);
3384
+ gl.uniform4f(prog.uniform("u_color"), 1, 1, 1, 1);
3385
+ gl.uniform1f(prog.uniform("u_alpha"), staged.alpha);
3386
+ setColorMatrixUniforms(ctx, prog, staged.colorMatrix);
3387
+ applyClipTest(ctx, staged.clipDepth);
3388
+ gl.drawElements(gl.TRIANGLES, indexCount, gl.UNSIGNED_INT, 0);
3034
3389
  gl.bindVertexArray(null);
3390
+ batch.reset();
3391
+ ctx.solidState = void 0;
3035
3392
  }
3036
3393
  function expandAnchorColors(perAnchor, handle) {
3037
3394
  const aA = handle.anchorA;
@@ -3076,11 +3433,18 @@ function drawPathFillVColor(ctx, cmd, fill, handle) {
3076
3433
  gl.bindVertexArray(null);
3077
3434
  gl.deleteBuffer(colorVbo);
3078
3435
  }
3079
- function setProjAndModel(ctx, prog) {
3436
+ function setProjAndModel(ctx, prog, model = ctx.state.transform) {
3080
3437
  const gl = ctx.gl;
3081
- const proj = mat3.screenToClip(ctx.widthCss, ctx.heightCss);
3082
- gl.uniformMatrix3fv(prog.uniform("u_proj"), false, proj);
3083
- gl.uniformMatrix3fv(prog.uniform("u_model"), false, ctx.state.transform);
3438
+ const uploaded = uploadedFor(ctx, prog);
3439
+ const proj = projFor(ctx);
3440
+ if (!sameValues(uploaded.proj, proj)) {
3441
+ gl.uniformMatrix3fv(prog.uniform("u_proj"), false, proj);
3442
+ uploaded.proj = Float32Array.from(proj);
3443
+ }
3444
+ if (!sameValues(uploaded.model, model)) {
3445
+ gl.uniformMatrix3fv(prog.uniform("u_model"), false, model);
3446
+ uploaded.model = Float32Array.from(model);
3447
+ }
3084
3448
  }
3085
3449
  function setSolidPaintUniforms(ctx, prog, color, opacity) {
3086
3450
  const gl = ctx.gl;
@@ -3187,9 +3551,8 @@ function drawPathFillGradient(ctx, fill, handle) {
3187
3551
  gl.drawElements(gl.TRIANGLES, handle.indexCount, gl.UNSIGNED_INT, 0);
3188
3552
  gl.bindVertexArray(null);
3189
3553
  }
3190
- function applyClipTest(ctx) {
3554
+ function applyClipTest(ctx, depth = ctx.clipDepth) {
3191
3555
  const gl = ctx.gl;
3192
- const depth = ctx.clipDepth;
3193
3556
  if (depth === 0) {
3194
3557
  gl.disable(gl.STENCIL_TEST);
3195
3558
  return;
@@ -3212,6 +3575,7 @@ function rasterizePathToStencil(ctx, path) {
3212
3575
  gl.bindVertexArray(null);
3213
3576
  }
3214
3577
  function pushClip(ctx, path, newDepth) {
3578
+ flushSolids(ctx);
3215
3579
  const gl = ctx.gl;
3216
3580
  const ancestors = ancestorMask(newDepth - 1);
3217
3581
  const newBit = 1 << newDepth;
@@ -3226,6 +3590,7 @@ function pushClip(ctx, path, newDepth) {
3226
3590
  gl.stencilMask(255);
3227
3591
  }
3228
3592
  function popClip(ctx, path, oldDepth) {
3593
+ flushSolids(ctx);
3229
3594
  const gl = ctx.gl;
3230
3595
  gl.enable(gl.STENCIL_TEST);
3231
3596
  const oldBit = 1 << oldDepth + 1;
@@ -3273,6 +3638,7 @@ function drawPathStroke(ctx, cmd) {
3273
3638
  }
3274
3639
  const align = stroke.align ?? "center";
3275
3640
  if (cmd.path.kind === "polygon" && align !== "center") {
3641
+ flushSolids(ctx);
3276
3642
  drawPathStrokeStenciled(ctx, cmd, align);
3277
3643
  return;
3278
3644
  }
@@ -3281,11 +3647,13 @@ function drawPathStroke(ctx, cmd) {
3281
3647
  function drawPathStrokeUnclipped(ctx, cmd) {
3282
3648
  const stroke = cmd.stroke;
3283
3649
  const solid = stroke.paint;
3284
- const mesh = tessellateStroke(cmd.path, stroke, { flattenTolerance: ctx.flattenTolerance });
3650
+ const mesh = strokeMesh(cmd.path, stroke, ctx.flattenTolerance);
3285
3651
  if (mesh.indices.length === 0) return;
3286
- const handle = ctx.meshCache.uploadTransient(mesh);
3652
+ const hasVColors = !!(stroke.vertexColors && stroke.vertexColors.length > 0);
3653
+ if (tryStageSolid(ctx, mesh, hasVColors ? void 0 : solid)) return;
3654
+ const handle = hasVColors ? ctx.meshCache.uploadTransient(mesh) : ctx.meshCache.uploadRecurring(mesh);
3287
3655
  const gl = ctx.gl;
3288
- if (stroke.vertexColors && stroke.vertexColors.length > 0) {
3656
+ if (hasVColors) {
3289
3657
  const prog = ctx.pathFillVColor;
3290
3658
  gl.useProgram(prog.handle);
3291
3659
  gl.bindVertexArray(handle.vao);
@@ -3316,16 +3684,30 @@ function drawPathStrokeUnclipped(ctx, cmd) {
3316
3684
  gl.drawElements(gl.TRIANGLES, handle.indexCount, gl.UNSIGNED_INT, 0);
3317
3685
  gl.bindVertexArray(null);
3318
3686
  }
3687
+ var DOUBLED_VERTEX_WIDTHS = /* @__PURE__ */ new WeakMap();
3688
+ function doubledVertexWidths(widths) {
3689
+ let doubled = DOUBLED_VERTEX_WIDTHS.get(widths);
3690
+ if (doubled === void 0) {
3691
+ doubled = widths.map((w) => w * 2);
3692
+ DOUBLED_VERTEX_WIDTHS.set(widths, doubled);
3693
+ }
3694
+ return doubled;
3695
+ }
3319
3696
  function drawPathStrokeStenciled(ctx, cmd, align) {
3320
3697
  const stroke = cmd.stroke;
3321
3698
  const solid = stroke.paint;
3322
- const widerStroke = { ...stroke, width: (stroke.width ?? 1) * 2, align: "center" };
3699
+ const widerStroke = {
3700
+ ...stroke,
3701
+ width: (stroke.width ?? 1) * 2,
3702
+ ...stroke.vertexWidths ? { vertexWidths: doubledVertexWidths(stroke.vertexWidths) } : {},
3703
+ align: "center"
3704
+ };
3705
+ const useVColor = !!(stroke.vertexColors && stroke.vertexColors.length > 0);
3323
3706
  const fillHandle = fillMeshHandle(ctx, cmd.path);
3324
- const ribbonMesh = tessellateStroke(cmd.path, widerStroke, { flattenTolerance: ctx.flattenTolerance });
3707
+ const ribbonMesh = strokeMesh(cmd.path, widerStroke, ctx.flattenTolerance);
3325
3708
  if (ribbonMesh.indices.length === 0) return;
3326
- const ribbonHandle = ctx.meshCache.uploadTransient(ribbonMesh);
3709
+ const ribbonHandle = useVColor ? ctx.meshCache.uploadTransient(ribbonMesh) : ctx.meshCache.uploadRecurring(ribbonMesh);
3327
3710
  const gl = ctx.gl;
3328
- const useVColor = !!(stroke.vertexColors && stroke.vertexColors.length > 0);
3329
3711
  const prog = useVColor ? ctx.pathFillVColor : ctx.pathFill;
3330
3712
  gl.useProgram(prog.handle);
3331
3713
  setProjAndModel(ctx, prog);
@@ -3361,11 +3743,11 @@ function drawPathStrokeStenciled(ctx, cmd, align) {
3361
3743
  }
3362
3744
  }
3363
3745
  gl.drawElements(gl.TRIANGLES, ribbonHandle.indexCount, gl.UNSIGNED_INT, 0);
3364
- if (colorVbo) gl.deleteBuffer(colorVbo);
3365
3746
  gl.stencilMask(1);
3366
3747
  gl.clear(gl.STENCIL_BUFFER_BIT);
3367
3748
  gl.disable(gl.STENCIL_TEST);
3368
3749
  gl.bindVertexArray(null);
3750
+ if (colorVbo) gl.deleteBuffer(colorVbo);
3369
3751
  }
3370
3752
  var OUTLINE_MIN_SCREEN_PX = 48;
3371
3753
  function modelScale(m) {
@@ -3380,20 +3762,17 @@ function drawText(ctx, cmd) {
3380
3762
  const maxWidth = cmd.maxWidth ?? Infinity;
3381
3763
  const minScreen = ctx.textOutlineMinScreenSize ?? OUTLINE_MIN_SCREEN_PX;
3382
3764
  const outlineMinSize = Number.isFinite(minScreen) ? minScreen / modelScale(ctx.state.transform) : void 0;
3383
- const laid = cachedLayoutRuns(
3384
- cmd.runs,
3385
- { maxWidth, lineHeight, align, outlineMinSize },
3386
- { x: cmd.x, y: cmd.y }
3387
- );
3765
+ const laid = cachedLayoutRuns(cmd.runs, { maxWidth, lineHeight, align, outlineMinSize });
3388
3766
  if (laid.groups.length === 0 && laid.decorations.length === 0) return;
3389
- const dy = verticalAlignOffset(cmd.verticalAlign, cmd.height, laid.bounds.height);
3767
+ const dx = cmd.x;
3768
+ const dy = cmd.y + verticalAlignOffset(cmd.verticalAlign, cmd.height, laid.bounds.height);
3390
3769
  const gl = ctx.gl;
3391
3770
  applyClipTest(ctx);
3392
3771
  const preparedPrograms = /* @__PURE__ */ new Set();
3393
3772
  let currentProg = null;
3394
3773
  for (const group of laid.groups) {
3395
3774
  if (group.source === "outline") {
3396
- drawTextOutlineGroup(ctx, group, dy);
3775
+ drawTextOutlineGroup(ctx, group, dx, dy);
3397
3776
  currentProg = null;
3398
3777
  continue;
3399
3778
  }
@@ -3408,31 +3787,31 @@ function drawText(ctx, cmd) {
3408
3787
  setColorMatrixUniforms(ctx, prog);
3409
3788
  gl.uniform1f(prog.uniform("u_alpha"), ctx.state.alpha);
3410
3789
  }
3411
- drawTextGroup(ctx, group, prog, dy);
3790
+ drawTextGroup(ctx, group, prog, dx, dy);
3412
3791
  }
3413
- drawTextDecorations(ctx, laid.decorations, dy);
3792
+ drawTextDecorations(ctx, laid.decorations, dx, dy);
3414
3793
  }
3415
3794
  var SYNTHETIC_ITALIC_RADIANS = 0.2094;
3416
- function drawTextOutlineGroup(ctx, group, dy) {
3417
- const mesh = outlineGroupMesh(group, dy);
3795
+ function drawTextOutlineGroup(ctx, group, dx, dy) {
3796
+ const mesh = outlineGroupMesh(group, dx, dy);
3418
3797
  if (mesh) drawPathFillByKind(ctx, group.fill, ctx.meshCache.uploadTransient(mesh));
3419
3798
  if (!group.stroke) return;
3420
- const strokeMesh = outlineGroupStrokeMesh(group, dy);
3421
- if (strokeMesh) {
3422
- drawPathFillByKind(ctx, group.stroke.paint, ctx.meshCache.uploadTransient(strokeMesh));
3799
+ const ribbon = outlineGroupStrokeMesh(group, dx, dy);
3800
+ if (ribbon) {
3801
+ drawPathFillByKind(ctx, group.stroke.paint, ctx.meshCache.uploadTransient(ribbon));
3423
3802
  }
3424
3803
  }
3425
- function outlineGroupMesh(group, dy) {
3426
- return mergeGlyphMeshes(group, dy, (glyph) => outlineMesh(glyph.key, glyph.d));
3804
+ function outlineGroupMesh(group, dx, dy) {
3805
+ return mergeGlyphMeshes(group, dx, dy, (glyph) => outlineMesh(glyph.key, glyph.d));
3427
3806
  }
3428
- function outlineGroupStrokeMesh(group, dy) {
3807
+ function outlineGroupStrokeMesh(group, dx, dy) {
3429
3808
  const stroke = group.stroke;
3430
3809
  if (!stroke) return null;
3431
3810
  const width = stroke.width ?? 1;
3432
3811
  if (!(width > 0)) return null;
3433
- return mergeGlyphMeshes(group, dy, (glyph) => glyph.scale > 0 ? outlineStrokeMesh(glyph.key, glyph.d, quantizeEmWidth(width / glyph.scale), stroke) : null);
3812
+ return mergeGlyphMeshes(group, dx, dy, (glyph) => glyph.scale > 0 ? outlineStrokeMesh(glyph.key, glyph.d, quantizeEmWidth(width / glyph.scale), stroke) : null);
3434
3813
  }
3435
- function mergeGlyphMeshes(group, dy, meshFor) {
3814
+ function mergeGlyphMeshes(group, dx, dy, meshFor) {
3436
3815
  const parts = [];
3437
3816
  let vertexFloats = 0;
3438
3817
  let indexCount = 0;
@@ -3455,7 +3834,7 @@ function mergeGlyphMeshes(group, dy, meshFor) {
3455
3834
  for (let k = 0; k < mesh.vertices.length; k += 2) {
3456
3835
  const ex = mesh.vertices[k];
3457
3836
  const ey = mesh.vertices[k + 1];
3458
- vertices[vi++] = x + (ex - ey * shear) * scale2;
3837
+ vertices[vi++] = x + dx + (ex - ey * shear) * scale2;
3459
3838
  vertices[vi++] = baselineY + dy + ey * scale2;
3460
3839
  }
3461
3840
  for (let k = 0; k < mesh.indices.length; k++) indices[ii++] = base + mesh.indices[k];
@@ -3463,7 +3842,7 @@ function mergeGlyphMeshes(group, dy, meshFor) {
3463
3842
  }
3464
3843
  return { vertices, indices };
3465
3844
  }
3466
- function drawTextDecorations(ctx, decorations, dy) {
3845
+ function drawTextDecorations(ctx, decorations, dx, dy) {
3467
3846
  if (decorations.length === 0) return;
3468
3847
  const batches = /* @__PURE__ */ new Map();
3469
3848
  for (const d of decorations) {
@@ -3486,14 +3865,15 @@ function drawTextDecorations(ctx, decorations, dy) {
3486
3865
  const vertices = new Float32Array(rects.length * 4 * 2);
3487
3866
  let vi = 0;
3488
3867
  for (const d of rects) {
3868
+ const dx0 = d.x0 + dx, dx1 = d.x1 + dx;
3489
3869
  const dy0 = d.y0 + dy, dy1 = d.y1 + dy;
3490
- vertices[vi++] = d.x0;
3870
+ vertices[vi++] = dx0;
3491
3871
  vertices[vi++] = dy0;
3492
- vertices[vi++] = d.x1;
3872
+ vertices[vi++] = dx1;
3493
3873
  vertices[vi++] = dy0;
3494
- vertices[vi++] = d.x0;
3874
+ vertices[vi++] = dx0;
3495
3875
  vertices[vi++] = dy1;
3496
- vertices[vi++] = d.x1;
3876
+ vertices[vi++] = dx1;
3497
3877
  vertices[vi++] = dy1;
3498
3878
  }
3499
3879
  const indices = new Uint32Array(rects.length * 6);
@@ -3532,7 +3912,7 @@ function drawTextDecorations(ctx, decorations, dy) {
3532
3912
  gl.deleteBuffer(ibo);
3533
3913
  }
3534
3914
  }
3535
- function drawTextGroup(ctx, group, prog, dy) {
3915
+ function drawTextGroup(ctx, group, prog, dx, dy) {
3536
3916
  if (group.source === "canvas") {
3537
3917
  if (!syncDynamicPageTexture(ctx.textureCache, group.page)) return;
3538
3918
  } else {
@@ -3543,23 +3923,24 @@ function drawTextGroup(ctx, group, prog, dy) {
3543
3923
  const vertices = new Float32Array(group.quads.length * 4 * 5);
3544
3924
  let vi = 0;
3545
3925
  for (const q of group.quads) {
3926
+ const x0 = q.x0 + dx, x1 = q.x1 + dx;
3546
3927
  const y0 = q.y0 + dy, y1 = q.y1 + dy, by = q.baselineY + dy;
3547
- vertices[vi++] = q.x0;
3928
+ vertices[vi++] = x0;
3548
3929
  vertices[vi++] = y0;
3549
3930
  vertices[vi++] = q.u0;
3550
3931
  vertices[vi++] = q.v0;
3551
3932
  vertices[vi++] = by;
3552
- vertices[vi++] = q.x1;
3933
+ vertices[vi++] = x1;
3553
3934
  vertices[vi++] = y0;
3554
3935
  vertices[vi++] = q.u1;
3555
3936
  vertices[vi++] = q.v0;
3556
3937
  vertices[vi++] = by;
3557
- vertices[vi++] = q.x0;
3938
+ vertices[vi++] = x0;
3558
3939
  vertices[vi++] = y1;
3559
3940
  vertices[vi++] = q.u0;
3560
3941
  vertices[vi++] = q.v1;
3561
3942
  vertices[vi++] = by;
3562
- vertices[vi++] = q.x1;
3943
+ vertices[vi++] = x1;
3563
3944
  vertices[vi++] = y1;
3564
3945
  vertices[vi++] = q.u1;
3565
3946
  vertices[vi++] = q.v1;
@@ -3757,16 +4138,28 @@ function registerProgram(id, vert, frag) {
3757
4138
  }
3758
4139
 
3759
4140
  // src/renderer/WeaselRenderer.ts
4141
+ var UNIFORM_QUALIFIER = /^(?:lowp|mediump|highp|precise|invariant|centroid|flat|smooth|noperspective)$/;
3760
4142
  function extractUniformNames(glsl) {
3761
- const reScalar = /\buniform\s+\S+\s+(\w+)\s*;/g;
3762
- const reArray = /\buniform\s+\S+\s+(\w+)\s*\[\s*(\d+)\s*\]\s*;/g;
4143
+ const re = /\buniform\s+([^;{]+);/g;
3763
4144
  const names = [];
3764
4145
  let m;
3765
- while ((m = reScalar.exec(glsl)) !== null) names.push(m[1]);
3766
- while ((m = reArray.exec(glsl)) !== null) {
3767
- const name = m[1];
3768
- const size = parseInt(m[2], 10);
3769
- for (let i = 0; i < size; i++) names.push(`${name}[${i}]`);
4146
+ while ((m = re.exec(glsl)) !== null) {
4147
+ const words = m[1].trim().split(/\s+/);
4148
+ let i = 0;
4149
+ while (i < words.length && UNIFORM_QUALIFIER.test(words[i])) i++;
4150
+ i++;
4151
+ const declarators = words.slice(i).join(" ");
4152
+ if (!declarators) continue;
4153
+ for (const raw of declarators.split(",")) {
4154
+ const d = raw.trim();
4155
+ const arr = /^(\w+)\s*\[\s*(\d+)\s*\]$/.exec(d);
4156
+ if (arr) {
4157
+ const size = parseInt(arr[2], 10);
4158
+ for (let k = 0; k < size; k++) names.push(`${arr[1]}[${k}]`);
4159
+ } else if (/^\w+$/.test(d)) {
4160
+ names.push(d);
4161
+ }
4162
+ }
3770
4163
  }
3771
4164
  return names;
3772
4165
  }
@@ -3786,10 +4179,7 @@ var WeaselRenderer = class {
3786
4179
  programRegistry = /* @__PURE__ */ new Map();
3787
4180
  quadVbo = null;
3788
4181
  quadIbo = null;
3789
- /** Shared rect-fill geometry: 4 verts × 2 floats (dynamic), 6-index static IBO. */
3790
- rectVao = null;
3791
- rectVbo = null;
3792
- rectIbo = null;
4182
+ solidBatch;
3793
4183
  groupState = new GroupState();
3794
4184
  widthCss;
3795
4185
  heightCss;
@@ -3864,31 +4254,7 @@ var WeaselRenderer = class {
3864
4254
  this.imageCache = new GLImageCache(this.gl, this.imageMinification);
3865
4255
  this.gradRampCache = new GradientRampCache(this.gl);
3866
4256
  this.uploadQuadGeometry();
3867
- this.uploadRectGeometry(aPos);
3868
- }
3869
- /** Allocate the shared rect VAO + dynamic VBO (4 verts × 2 floats) + static
3870
- * IBO (6 indices). drawRectFast bufferSubData's the 4 corner coords each
3871
- * draw, avoiding per-rect buffer allocation in animated demos. */
3872
- uploadRectGeometry(aPositionLoc) {
3873
- const gl = this.gl;
3874
- this.rectVao = gl.createVertexArray();
3875
- this.rectVbo = gl.createBuffer();
3876
- this.rectIbo = gl.createBuffer();
3877
- if (!this.rectVao || !this.rectVbo || !this.rectIbo) {
3878
- throw new Error("WeaselRenderer: failed to create rect-fast geometry");
3879
- }
3880
- gl.bindVertexArray(this.rectVao);
3881
- gl.bindBuffer(gl.ARRAY_BUFFER, this.rectVbo);
3882
- gl.bufferData(gl.ARRAY_BUFFER, 8 * 4, gl.DYNAMIC_DRAW);
3883
- gl.enableVertexAttribArray(aPositionLoc);
3884
- gl.vertexAttribPointer(aPositionLoc, 2, gl.FLOAT, false, 0, 0);
3885
- gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.rectIbo);
3886
- gl.bufferData(
3887
- gl.ELEMENT_ARRAY_BUFFER,
3888
- new Uint32Array([0, 1, 2, 0, 2, 3]),
3889
- gl.STATIC_DRAW
3890
- );
3891
- gl.bindVertexArray(null);
4257
+ this.solidBatch = new SolidBatch(this.gl, this.pathFillVColor);
3892
4258
  }
3893
4259
  uploadQuadGeometry() {
3894
4260
  const gl = this.gl;
@@ -3976,7 +4342,7 @@ var WeaselRenderer = class {
3976
4342
  this.gradRampCache = new GradientRampCache(this.gl);
3977
4343
  markAllFontsNotUploaded();
3978
4344
  this.uploadQuadGeometry();
3979
- this.uploadRectGeometry(aPos);
4345
+ this.solidBatch = new SolidBatch(this.gl, this.pathFillVColor);
3980
4346
  for (const id of this.programRegistry.keys()) {
3981
4347
  const src = getProgramSource(id);
3982
4348
  if (!src) continue;
@@ -4036,9 +4402,7 @@ var WeaselRenderer = class {
4036
4402
  this.gradRampCache.free();
4037
4403
  if (this.quadVbo) gl.deleteBuffer(this.quadVbo);
4038
4404
  if (this.quadIbo) gl.deleteBuffer(this.quadIbo);
4039
- if (this.rectVbo) gl.deleteBuffer(this.rectVbo);
4040
- if (this.rectIbo) gl.deleteBuffer(this.rectIbo);
4041
- if (this.rectVao) gl.deleteVertexArray(this.rectVao);
4405
+ this.solidBatch.dispose();
4042
4406
  }
4043
4407
  /**
4044
4408
  * Draw one frame.
@@ -4071,8 +4435,7 @@ var WeaselRenderer = class {
4071
4435
  programRegistry: this.programRegistry,
4072
4436
  quadVbo: this.quadVbo,
4073
4437
  quadIbo: this.quadIbo,
4074
- rectVao: this.rectVao,
4075
- rectVbo: this.rectVbo,
4438
+ solidBatch: this.solidBatch,
4076
4439
  state: this.groupState,
4077
4440
  widthCss: this.widthCss,
4078
4441
  heightCss: this.heightCss,
@@ -4082,6 +4445,7 @@ var WeaselRenderer = class {
4082
4445
  viewMatrix
4083
4446
  };
4084
4447
  for (const cmd of commands) dispatch(ctx, cmd);
4448
+ flushSolids(ctx);
4085
4449
  this.meshCache.freeTransient();
4086
4450
  }
4087
4451
  resize(dims) {
@@ -4111,6 +4475,10 @@ var WeaselRenderer = class {
4111
4475
  return this.pathFillVColor;
4112
4476
  }
4113
4477
  /** @internal */
4478
+ _solidBatch() {
4479
+ return this.solidBatch;
4480
+ }
4481
+ /** @internal */
4114
4482
  _textSdf() {
4115
4483
  return this.textSdf;
4116
4484
  }
@@ -4320,6 +4688,14 @@ function createViewportLayer(opts) {
4320
4688
  id,
4321
4689
  label,
4322
4690
  space: "screen",
4691
+ reproject(outer, dims, screen) {
4692
+ const b = bounds(outer, dims);
4693
+ if (screen.x < b.x || screen.x >= b.x + b.w || screen.y < b.y || screen.y >= b.y + b.h) return null;
4694
+ return {
4695
+ x: (screen.x - b.x) / view.scale.x + view.x,
4696
+ y: (screen.y - b.y) / view.scale.y + view.y
4697
+ };
4698
+ },
4323
4699
  draw: (data, outerView, dims) => {
4324
4700
  const b = bounds(outerView, dims);
4325
4701
  const children = [];
@@ -4346,6 +4722,14 @@ function createViewportLayer(opts) {
4346
4722
  }
4347
4723
  };
4348
4724
  }
4725
+ function viewportsAt(layers, outer, dims, screen) {
4726
+ for (let i = layers.length - 1; i >= 0; i--) {
4727
+ const layer = layers[i];
4728
+ const point = layer.reproject(outer, dims, screen);
4729
+ if (point) return { layer, point };
4730
+ }
4731
+ return null;
4732
+ }
4349
4733
 
4350
4734
  // src/features/parallax/deriveParallaxView.ts
4351
4735
  function asXY(v) {
@@ -5380,12 +5764,7 @@ function moveGestureAdapter(scene) {
5380
5764
  return {
5381
5765
  getNode: (id) => scene.get(asNodeId(id)),
5382
5766
  getNodes: () => {
5383
- const out = [];
5384
- for (const id of scene.renderOrder()) {
5385
- const n = scene.get(id);
5386
- if (n) out.push(n);
5387
- }
5388
- return out;
5767
+ return [...scene.renderOrderNodes()];
5389
5768
  },
5390
5769
  getPose: (id) => scene.get(asNodeId(id)).pose,
5391
5770
  getParent: (id) => scene.get(asNodeId(id))?.parent ?? null,
@@ -5484,6 +5863,12 @@ function runLayoutPass(scratch, moveCtx) {
5484
5863
  if (layout2.contains) return layout2.contains(cPose, draggedCenter);
5485
5864
  return draggedCenter.x >= aabb.x && draggedCenter.x < aabb.x + aabb.width && draggedCenter.y >= aabb.y && draggedCenter.y < aabb.y + aabb.height;
5486
5865
  };
5866
+ const draggedArg = {
5867
+ id: draggedId,
5868
+ originPose: startWorld,
5869
+ pose: draggedWorld,
5870
+ sourceContainerId
5871
+ };
5487
5872
  const consider = (id, zPath) => {
5488
5873
  if (id === draggedId) return;
5489
5874
  const layout2 = layoutDep.getLayout(id);
@@ -5493,6 +5878,7 @@ function runLayoutPass(scratch, moveCtx) {
5493
5878
  const worldPose = composeWorldPose(poseAdapter, id, pc.compose);
5494
5879
  const worldAABB = boundsOf2(worldPose);
5495
5880
  if (!testInside(worldPose, worldAABB, layout2)) return;
5881
+ if (layout2.acceptsDrop && !layout2.acceptsDrop({ id, bounds: worldAABB }, draggedArg)) return;
5496
5882
  candidates.push({
5497
5883
  id,
5498
5884
  bounds: worldAABB,
@@ -5541,20 +5927,16 @@ function runLayoutPass(scratch, moveCtx) {
5541
5927
  id: cid,
5542
5928
  pose: composeWorldPose(poseAdapter, cid, pc.compose)
5543
5929
  }));
5544
- const draggedArg = {
5545
- id: draggedId,
5546
- originPose: startWorld,
5547
- pose: draggedWorld,
5548
- sourceContainerId
5549
- };
5550
5930
  const targets = layout.getDropTargets(container, children, draggedArg);
5551
5931
  const target = layout.snap.pickTarget(targets, { x: moveCtx.drag.current.x, y: moveCtx.drag.current.y });
5552
5932
  if (!target) return;
5933
+ const reflowIds = /* @__PURE__ */ new Set();
5553
5934
  for (const [cid, pose] of layout.reflowPoses(container, children, draggedArg, target)) {
5554
5935
  if (asNodeId(cid) === draggedId) continue;
5555
5936
  const parent = scene.get(asNodeId(cid))?.parent ?? null;
5556
5937
  const local = rebaseLocalPose(poseAdapter, pose, parent, pc.compose, pc.decompose);
5557
5938
  scratch.previews.set(asNodeId(cid), local);
5939
+ reflowIds.add(asNodeId(cid));
5558
5940
  }
5559
5941
  const sourceReflow = /* @__PURE__ */ new Map();
5560
5942
  if (sourceContainerId && sourceContainerId !== dest.id) {
@@ -5577,10 +5959,11 @@ function runLayoutPass(scratch, moveCtx) {
5577
5959
  sourceReflow.set(cid, pose);
5578
5960
  const parent = scene.get(asNodeId(cid))?.parent ?? null;
5579
5961
  scratch.previews.set(asNodeId(cid), rebaseLocalPose(poseAdapter, pose, parent, pc.compose, pc.decompose));
5962
+ reflowIds.add(asNodeId(cid));
5580
5963
  }
5581
5964
  }
5582
5965
  }
5583
- scratch.layoutPass = { layout, container, children, target, sourceReflow };
5966
+ scratch.layoutPass = { layout, container, children, target, sourceReflow, reflowIds };
5584
5967
  }
5585
5968
  function scenePoseAdapter(scene) {
5586
5969
  return {
@@ -5888,7 +6271,8 @@ var moveAction = {
5888
6271
  scratch.previews.clear();
5889
6272
  },
5890
6273
  previewIds: () => scratch.previews.keys(),
5891
- previewPose: (id) => scratch.previews.get(id) ?? null
6274
+ previewPose: (id) => scratch.previews.get(id) ?? null,
6275
+ previewOpaqueIds: () => scratch.layoutPass?.reflowIds ?? null
5892
6276
  };
5893
6277
  }
5894
6278
  },
@@ -6001,12 +6385,7 @@ function defaultCommitAdapter(scene) {
6001
6385
  return {
6002
6386
  getNode: (id) => scene.get(asNodeId(id)),
6003
6387
  getNodes: () => {
6004
- const out = [];
6005
- for (const id of scene.renderOrder()) {
6006
- const n = scene.get(id);
6007
- if (n) out.push(n);
6008
- }
6009
- return out;
6388
+ return [...scene.renderOrderNodes()];
6010
6389
  },
6011
6390
  getPose: (id) => scene.get(asNodeId(id)).pose,
6012
6391
  getParent: (id) => scene.get(asNodeId(id))?.parent ?? null,
@@ -6662,7 +7041,8 @@ var KIT_INSERT_KINDS = /* @__PURE__ */ new Set([
6662
7041
  "line",
6663
7042
  "polygon",
6664
7043
  "star",
6665
- "pencil"
7044
+ "pencil",
7045
+ "image"
6666
7046
  ]);
6667
7047
  var liveInsertScratch = null;
6668
7048
  function effectiveOriginMode(paramsOrigin, altHeld) {
@@ -7791,7 +8171,7 @@ var pinchZoomAction = {
7791
8171
  const multiTouch = ctx.multiTouch;
7792
8172
  if (!multiTouch) return {};
7793
8173
  const startSpread = multiTouch.spread > 0 ? multiTouch.spread : 1;
7794
- const scratch = { view, startSpread };
8174
+ const scratch = { view, startSpread, centroid: multiTouch.centroid };
7795
8175
  return {
7796
8176
  kind: "pinch",
7797
8177
  onMove(moveCtx) {
@@ -7801,8 +8181,15 @@ var pinchZoomAction = {
7801
8181
  if (currentSpread <= 0 || scratch.startSpread <= 0) return;
7802
8182
  const factor = currentSpread / scratch.startSpread;
7803
8183
  scratch.startSpread = currentSpread;
8184
+ const prev = scratch.centroid;
8185
+ scratch.centroid = centroid;
7804
8186
  const current = scratch.view.get();
7805
- scratch.view.set(zoomAt(current, centroid, factor));
8187
+ const zoomed = zoomAt(current, prev, factor);
8188
+ scratch.view.set({
8189
+ scale: zoomed.scale,
8190
+ x: zoomed.x - (centroid.x - prev.x) / zoomed.scale.x,
8191
+ y: zoomed.y - (centroid.y - prev.y) / zoomed.scale.y
8192
+ });
7806
8193
  }
7807
8194
  // onEnd: nothing to do — zoom was applied each frame.
7808
8195
  };
@@ -8286,10 +8673,12 @@ function normalizeNegativeExtent(pose) {
8286
8673
  }
8287
8674
 
8288
8675
  // src/interactions/actions/defaults/flip.ts
8289
- function flipSelection(selection, scene, axis, applyOps, geometryProjection) {
8676
+ function flipSelection(selection, scene, axis, pivot, applyOps, geometryProjection) {
8290
8677
  const ids = selection.get();
8291
8678
  if (ids.length === 0) return;
8292
8679
  const geom = AUTO_POSE_DESCRIPTOR;
8680
+ const nodes = ids.map((id) => scene.get(id)).filter((n) => n != null);
8681
+ const unionPivot = pivot === "union" ? unionBounds(nodes.map((n) => geom.getBounds(n.pose))) : null;
8293
8682
  const ops = [];
8294
8683
  for (const id of ids) {
8295
8684
  const node = scene.get(id);
@@ -8297,10 +8686,10 @@ function flipSelection(selection, scene, axis, applyOps, geometryProjection) {
8297
8686
  ops.push(createTransformOp({
8298
8687
  id,
8299
8688
  from: node.pose,
8300
- to: flipPoseViaDescriptor(node.pose, axis, geom),
8689
+ to: unionPivot ? flipPoseAboutBounds(node.pose, axis, geom, unionPivot) : flipPoseViaDescriptor(node.pose, axis, geom),
8301
8690
  label: "Flip"
8302
8691
  }));
8303
- const g = geom.getBounds(node.pose);
8692
+ const g = unionPivot ?? geom.getBounds(node.pose);
8304
8693
  const cx = g.x + g.width / 2;
8305
8694
  const cy = g.y + g.height / 2;
8306
8695
  const m = axis === "x" ? [-1, 0, 0, 1, 2 * cx, 0] : [1, 0, 0, -1, 0, 2 * cy];
@@ -8329,12 +8718,13 @@ var flipAction = {
8329
8718
  timing: "immediate",
8330
8719
  run: (deps, params) => {
8331
8720
  const axis = params?.axis ?? "x";
8721
+ const pivot = params?.pivot ?? "each";
8332
8722
  const selection = deps.selection;
8333
8723
  const scene = deps.scene;
8334
8724
  const applyOps = deps.applyOps;
8335
8725
  const geometryProjection = deps.geometryProjection;
8336
8726
  if (!selection || !scene) return;
8337
- flipSelection(selection, scene, axis, applyOps, geometryProjection);
8727
+ flipSelection(selection, scene, axis, pivot, applyOps, geometryProjection);
8338
8728
  }
8339
8729
  },
8340
8730
  enabled: requiresSelection
@@ -10283,6 +10673,12 @@ var DEFAULT_DEBUG_THEME = {
10283
10673
  fpsText: "#e0e0e0",
10284
10674
  fpsTextBg: "rgba(0, 0, 0, 0.6)"
10285
10675
  };
10676
+ var DEFAULT_DEBUG_STROKES = {
10677
+ hitbox: { width: 1, dash: [2, 2] },
10678
+ bounds: { width: 1 },
10679
+ handle: { width: 1 },
10680
+ snap: { width: 1 }
10681
+ };
10286
10682
 
10287
10683
  // src/debug/createDebugOverlayLayer.ts
10288
10684
  function createDebugOverlayLayer({
@@ -10290,6 +10686,7 @@ function createDebugOverlayLayer({
10290
10686
  config
10291
10687
  }) {
10292
10688
  const theme = { ...DEFAULT_DEBUG_THEME, ...config.theme ?? {} };
10689
+ const strokes = { ...DEFAULT_DEBUG_STROKES, ...config.strokes ?? {} };
10293
10690
  const fpsHistory = [];
10294
10691
  const FPS_WINDOW = 60;
10295
10692
  return {
@@ -10301,11 +10698,11 @@ function createDebugOverlayLayer({
10301
10698
  const s = sink.snapshot();
10302
10699
  const t = viewToTransform(view);
10303
10700
  const out = [];
10304
- if (config.hitboxes) emitHitboxes(out, s, view, t, theme);
10305
- if (config.bounds) emitBounds(out, s, view, t, theme);
10306
- if (config.handles) emitHandles(out, s, t, theme);
10701
+ if (config.hitboxes) emitHitboxes(out, s, view, t, theme, strokes);
10702
+ if (config.bounds) emitBounds(out, s, view, t, theme, strokes);
10703
+ if (config.handles) emitHandles(out, s, t, theme, strokes);
10307
10704
  if (config.origins) emitOrigins(out, s, t, theme);
10308
- if (config.snap) emitSnap(out, s, t, theme);
10705
+ if (config.snap) emitSnap(out, s, t, theme, strokes);
10309
10706
  if (config.ids) emitIds(out, s, t, theme);
10310
10707
  if (config.layers) emitLayersPanel(out, s, dims, theme);
10311
10708
  if (config.fps) {
@@ -10338,9 +10735,16 @@ function approxCircleScreen(cx, cy, r, segments = 24) {
10338
10735
  function rectPath2(x, y, w, h) {
10339
10736
  return { kind: "rect", x, y, width: w, height: h };
10340
10737
  }
10341
- function emitHitboxes(out, s, view, t, theme) {
10738
+ function strokeOf(s, color) {
10739
+ return {
10740
+ paint: { fill: "solid", color },
10741
+ width: s.width,
10742
+ ...s.dash && s.dash.length > 0 ? { dash: [...s.dash] } : {}
10743
+ };
10744
+ }
10745
+ function emitHitboxes(out, s, view, t, theme, strokes) {
10342
10746
  const fill = { fill: "solid", color: theme.hitboxFill };
10343
- const stroke = { paint: { fill: "solid", color: theme.hitboxStroke }, width: 1, dash: [2, 2] };
10747
+ const stroke = strokeOf(strokes.hitbox, theme.hitboxStroke);
10344
10748
  for (const h of s.hitboxes) {
10345
10749
  if (h.shape.kind === "rect") {
10346
10750
  const [sx, sy] = worldToScreen(h.shape.x, h.shape.y, t);
@@ -10354,8 +10758,8 @@ function emitHitboxes(out, s, view, t, theme) {
10354
10758
  }
10355
10759
  }
10356
10760
  }
10357
- function emitBounds(out, s, view, t, theme) {
10358
- const stroke = { paint: { fill: "solid", color: theme.bounds }, width: 1 };
10761
+ function emitBounds(out, s, view, t, theme, strokes) {
10762
+ const stroke = strokeOf(strokes.bounds, theme.bounds);
10359
10763
  for (const b of s.bounds) {
10360
10764
  const [sx, sy] = worldToScreen(b.bounds.x, b.bounds.y, t);
10361
10765
  const sw = b.bounds.width * view.scale.x;
@@ -10363,8 +10767,8 @@ function emitBounds(out, s, view, t, theme) {
10363
10767
  out.push({ kind: "path", path: rectPath2(sx, sy, sw, sh), stroke });
10364
10768
  }
10365
10769
  }
10366
- function emitHandles(out, s, t, theme) {
10367
- const stroke = { paint: { fill: "solid", color: theme.handle }, width: 1 };
10770
+ function emitHandles(out, s, t, theme, strokes) {
10771
+ const stroke = strokeOf(strokes.handle, theme.handle);
10368
10772
  for (const h of s.handles) {
10369
10773
  const [cx, cy] = worldToScreen(h.position.x, h.position.y, t);
10370
10774
  const horiz = {
@@ -10390,14 +10794,14 @@ function emitOrigins(out, s, t, theme) {
10390
10794
  out.push({ kind: "path", path: approxCircleScreen(cx, cy, 3), fill });
10391
10795
  }
10392
10796
  }
10393
- function emitSnap(out, s, t, theme) {
10797
+ function emitSnap(out, s, t, theme, strokes) {
10394
10798
  const color = theme.snap;
10395
10799
  for (const c of s.snap) {
10396
10800
  const [cx, cy] = worldToScreen(c.point.x, c.point.y, t);
10397
10801
  const cmd = {
10398
10802
  kind: "path",
10399
10803
  path: approxCircleScreen(cx, cy, 4),
10400
- ...c.accepted ? { fill: { fill: "solid", color } } : { stroke: { paint: { fill: "solid", color }, width: 1 } }
10804
+ ...c.accepted ? { fill: { fill: "solid", color } } : { stroke: strokeOf(strokes.snap, color) }
10401
10805
  };
10402
10806
  out.push(cmd);
10403
10807
  }
@@ -10706,18 +11110,39 @@ function modsCount(mods) {
10706
11110
  }
10707
11111
  function targetRank(target) {
10708
11112
  if (target === void 0) return 0;
10709
- if (typeof target !== "string") return 1;
10710
- if (target.startsWith("kind:")) return target.endsWith(":selected") ? 3 : 2;
10711
- if (target.startsWith("affordance:")) return 2;
10712
- return 1;
11113
+ const form = parseTargetSpec(target);
11114
+ if (form === null) return 1;
11115
+ switch (form.form) {
11116
+ case "kind":
11117
+ return form.requireSelected ? 3 : 2;
11118
+ case "affordance":
11119
+ return 2;
11120
+ case "body":
11121
+ return 1;
11122
+ case "predicate":
11123
+ return 1;
11124
+ default: {
11125
+ return 1;
11126
+ }
11127
+ }
10713
11128
  }
10714
11129
  function targetConsultsAffordance(specTarget) {
10715
11130
  if (specTarget === void 0) return false;
10716
- if (typeof specTarget === "object" && specTarget !== null && "kindOf" in specTarget) {
10717
- const kindOf2 = specTarget.kindOf;
10718
- return kindOf2?.readsAffordance !== false;
11131
+ const form = parseTargetSpec(specTarget);
11132
+ if (form === null) return false;
11133
+ switch (form.form) {
11134
+ case "predicate":
11135
+ return form.kindOf.readsAffordance !== false;
11136
+ case "affordance":
11137
+ return true;
11138
+ case "body":
11139
+ return false;
11140
+ case "kind":
11141
+ return false;
11142
+ default: {
11143
+ return false;
11144
+ }
10719
11145
  }
10720
- return typeof specTarget === "string" && specTarget.startsWith("affordance:");
10721
11146
  }
10722
11147
  function specTargetOf(spec) {
10723
11148
  return "target" in spec ? spec.target : void 0;
@@ -10759,11 +11184,22 @@ function reportDeadClaim(owner, warn) {
10759
11184
  `[weasel] exclusive claim by "${key}" matched no binding: no \`affordance:\` or \`kindOf\` target resolved against it, so the press was dropped.`
10760
11185
  );
10761
11186
  }
11187
+ function phaseRank(spec) {
11188
+ if (spec === void 0) return 0;
11189
+ const atoms = typeof spec === "string" ? [{ channel: "&", phase: spec }] : spec;
11190
+ if (atoms.length === 0) return 0;
11191
+ let min = 2;
11192
+ for (const a of atoms) {
11193
+ const rank = (a.channel === "*" ? 0 : 1) + (a.phase === "*" ? 0 : 1);
11194
+ if (rank < min) min = rank;
11195
+ }
11196
+ return min;
11197
+ }
10762
11198
  function specificity(spec) {
10763
- const t = targetRank("target" in spec ? spec.target : void 0);
11199
+ const t = targetRank(specTargetOf(spec));
10764
11200
  const mods = "mods" in spec ? spec.mods : void 0;
10765
11201
  const m = modsCount(mods);
10766
- const p = "phase" in spec && spec.phase !== void 0 ? 1 : 0;
11202
+ const p = phaseRank("phase" in spec ? spec.phase : void 0);
10767
11203
  const typed = (spec.kind === "drop" || spec.kind === "paste") && spec.types !== void 0 && spec.types.length > 0 ? 2 : 1;
10768
11204
  return [t, m, p, typed];
10769
11205
  }
@@ -12423,8 +12859,7 @@ function useGestureDispatcher(opts) {
12423
12859
  if (!cd) return;
12424
12860
  const items = itemsFromClipboardData(cd);
12425
12861
  if (items.length === 0) return;
12426
- e.preventDefault();
12427
- dispatch2({
12862
+ const result = dispatch2({
12428
12863
  kind: "paste",
12429
12864
  items,
12430
12865
  // ClipboardEvent carries no modifier state.
@@ -12433,6 +12868,7 @@ function useGestureDispatcher(opts) {
12433
12868
  metaKey: false,
12434
12869
  shiftKey: false
12435
12870
  });
12871
+ if (result === "handled") e.preventDefault();
12436
12872
  };
12437
12873
  if (keyboard) {
12438
12874
  window.addEventListener("keydown", onKeyDown);
@@ -12776,6 +13212,52 @@ function useViewAnimation(setView) {
12776
13212
  );
12777
13213
  return { animateTo, animateToBounds, cancel };
12778
13214
  }
13215
+ function validateId(id, kind) {
13216
+ if (id.length === 0) {
13217
+ throw new Error(`weasel: ${kind} id may not be empty`);
13218
+ }
13219
+ if (RESERVED_ID_PREFIXES.has(id[0])) {
13220
+ throw new Error(
13221
+ `weasel: ${kind} id "${id}" starts with reserved sigil "${id[0]}" (reserved set: ${[...RESERVED_ID_PREFIXES].join(" ")})`
13222
+ );
13223
+ }
13224
+ if (RESERVED_ID_NAMES.has(id)) {
13225
+ throw new Error(
13226
+ `weasel: ${kind} id "${id}" collides with a reserved phase keyword (reserved: ${[...RESERVED_ID_NAMES].join(", ")})`
13227
+ );
13228
+ }
13229
+ }
13230
+ function defineTool(def) {
13231
+ validateId(def.id, "tool");
13232
+ const resolveCursor = (ctx) => {
13233
+ if (def.cursor == null) return "";
13234
+ return typeof def.cursor === "function" ? def.cursor(ctx) : def.cursor;
13235
+ };
13236
+ return {
13237
+ id: def.id,
13238
+ eligibility: {
13239
+ focus: true,
13240
+ capabilities: def.capabilities,
13241
+ ...def.hotkey ? { offhand: def.hotkey } : {}
13242
+ },
13243
+ actions: def.actions,
13244
+ def,
13245
+ presentation: def.presentation,
13246
+ keybinding: def.keybinding,
13247
+ onActivate: def.onActivate,
13248
+ onDeactivate: def.onDeactivate,
13249
+ initScratch: def.initScratch ?? (() => null),
13250
+ cursor: resolveCursor,
13251
+ bindings: def.bindings,
13252
+ overlay: def.overlay,
13253
+ overlayPosition: def.overlayPosition
13254
+ };
13255
+ }
13256
+
13257
+ // src/tools/defineViewportTool.ts
13258
+ function defineViewportTool(def) {
13259
+ return defineTool(def);
13260
+ }
12779
13261
 
12780
13262
  // src/interactions/actions/defaults/toolOffhand.ts
12781
13263
  var TOOL_OFFHAND_ID = "tool.offhand";
@@ -12870,17 +13352,19 @@ function useContributions(opts) {
12870
13352
  const { ordered, state } = snapshot();
12871
13353
  return scopeBindings(ordered, state);
12872
13354
  }, [snapshot]);
12873
- const overlays = useCallback(() => {
13355
+ const overlays = useCallback((position = "top") => {
12874
13356
  const { ordered, state } = snapshot();
12875
13357
  const active = [];
12876
13358
  const hotkey = [];
12877
13359
  const ambient = [];
12878
13360
  for (const entry of ordered) {
12879
13361
  if (!entry.overlay) continue;
13362
+ if ((entry.overlayPosition ?? "top") !== position) continue;
13363
+ const layers = Array.isArray(entry.overlay) ? entry.overlay : [entry.overlay];
12880
13364
  const scope = liveScope(entry.id, entry.eligibility ?? {}, state);
12881
- if (scope === "active") active.push(entry.overlay);
12882
- else if (scope === "hotkey") hotkey.push(entry.overlay);
12883
- else if (scope === "ambient") ambient.push(entry.overlay);
13365
+ if (scope === "active") active.push(...layers);
13366
+ else if (scope === "hotkey") hotkey.push(...layers);
13367
+ else if (scope === "ambient") ambient.push(...layers);
12884
13368
  }
12885
13369
  return [...active, ...hotkey, ...ambient];
12886
13370
  }, [snapshot]);
@@ -13633,21 +14117,18 @@ function textLineBoxes(pose, opts = {}) {
13633
14117
  const style = resolveTextStyle(pose.style);
13634
14118
  const source = pose.runs && pose.runs.length > 0 ? pose.runs : pose.text;
13635
14119
  const runs = resolveRuns(toRuns(source), style);
13636
- const laid = layoutRuns(
13637
- runs,
13638
- {
13639
- maxWidth: opts.maxWidth ?? pose.width,
13640
- lineHeight: style.lineHeight,
13641
- align: style.align
13642
- },
13643
- { x: pose.x, y: pose.y }
13644
- );
13645
- const dy = verticalAlignOffset(pose.verticalAlign, pose.height, laid.bounds.height);
14120
+ const laid = layoutRuns(runs, {
14121
+ maxWidth: opts.maxWidth ?? pose.width,
14122
+ lineHeight: style.lineHeight,
14123
+ align: style.align
14124
+ });
14125
+ const dx = pose.x;
14126
+ const dy = pose.y + verticalAlignOffset(pose.verticalAlign, pose.height, laid.bounds.height);
13646
14127
  const out = [];
13647
14128
  for (const line of laid.lines) {
13648
14129
  if (!opts.includeEmpty && line.x1 <= line.x0) continue;
13649
14130
  out.push({
13650
- x: line.x0 - padding,
14131
+ x: line.x0 + dx - padding,
13651
14132
  y: line.y0 + dy - padding,
13652
14133
  width: line.x1 - line.x0 + padding * 2,
13653
14134
  height: line.y1 - line.y0 + padding * 2
@@ -13710,7 +14191,7 @@ function isBoundsGradient(fill) {
13710
14191
  }
13711
14192
 
13712
14193
  // src/features/patterns/resolveSpec.ts
13713
- var cache5 = /* @__PURE__ */ new Map();
14194
+ var cache6 = /* @__PURE__ */ new Map();
13714
14195
  function specKey(spec) {
13715
14196
  const keys = Object.keys(spec).sort();
13716
14197
  const bag = spec;
@@ -13718,10 +14199,10 @@ function specKey(spec) {
13718
14199
  }
13719
14200
  function resolvePatternSpec(spec) {
13720
14201
  const key = specKey(spec);
13721
- const hit = cache5.get(key);
14202
+ const hit = cache6.get(key);
13722
14203
  if (hit !== void 0) return hit;
13723
14204
  const built = buildTile(spec);
13724
- cache5.set(key, built);
14205
+ cache6.set(key, built);
13725
14206
  return built;
13726
14207
  }
13727
14208
  function buildTile(spec) {
@@ -13757,7 +14238,7 @@ function resolveFillPattern(fill) {
13757
14238
  }
13758
14239
 
13759
14240
  // src/features/images/imageCache.ts
13760
- var cache6 = /* @__PURE__ */ new Map();
14241
+ var cache7 = /* @__PURE__ */ new Map();
13761
14242
  var subscribers = /* @__PURE__ */ new Set();
13762
14243
  var defaultLoader = async (src) => {
13763
14244
  if (typeof Image === "undefined" || typeof createImageBitmap === "undefined") {
@@ -13775,28 +14256,28 @@ function notify() {
13775
14256
  }
13776
14257
  function startLoad(src) {
13777
14258
  const entry = { status: "loading" };
13778
- cache6.set(src, entry);
14259
+ cache7.set(src, entry);
13779
14260
  loader(src).then(
13780
14261
  (bitmap) => {
13781
- if (cache6.get(src) !== entry) return;
13782
- cache6.set(src, { status: "ready", bitmap });
14262
+ if (cache7.get(src) !== entry) return;
14263
+ cache7.set(src, { status: "ready", bitmap });
13783
14264
  notify();
13784
14265
  },
13785
14266
  () => {
13786
- if (cache6.get(src) !== entry) return;
13787
- cache6.set(src, { status: "error" });
14267
+ if (cache7.get(src) !== entry) return;
14268
+ cache7.set(src, { status: "error" });
13788
14269
  notify();
13789
14270
  }
13790
14271
  );
13791
14272
  }
13792
14273
  function getImageBitmap(src) {
13793
- const entry = cache6.get(src);
14274
+ const entry = cache7.get(src);
13794
14275
  if (entry) return entry.bitmap;
13795
14276
  startLoad(src);
13796
14277
  return void 0;
13797
14278
  }
13798
14279
  function imageStatus(src) {
13799
- return cache6.get(src)?.status ?? "idle";
14280
+ return cache7.get(src)?.status ?? "idle";
13800
14281
  }
13801
14282
  function subscribeImageReady(cb) {
13802
14283
  subscribers.add(cb);
@@ -14163,6 +14644,7 @@ registerBuiltInShapePainters();
14163
14644
  function pickTopMostHit(ids, adapter) {
14164
14645
  if (ids.length === 0) return null;
14165
14646
  if (ids.length === 1) return ids[0];
14647
+ let candidates = ids;
14166
14648
  const getParent = adapter?.getParent;
14167
14649
  if (typeof getParent === "function") {
14168
14650
  const set = new Set(ids);
@@ -14177,15 +14659,40 @@ function pickTopMostHit(ids, adapter) {
14177
14659
  }
14178
14660
  if (dropped.size > 0) {
14179
14661
  const filtered = ids.filter((id) => !dropped.has(id));
14180
- if (filtered.length === 0) ; else if (filtered.length === 1) {
14181
- return filtered[0];
14182
- } else {
14183
- return filtered[filtered.length - 1];
14662
+ if (filtered.length === 1) return filtered[0];
14663
+ if (filtered.length > 1) candidates = filtered;
14664
+ }
14665
+ }
14666
+ return pickHighestZ(candidates, adapter);
14667
+ }
14668
+ function pickHighestZ(ids, adapter) {
14669
+ const compareZ = adapter?.compareZ;
14670
+ if (typeof compareZ === "function") {
14671
+ let best = ids[0];
14672
+ for (let i = 1; i < ids.length; i++) {
14673
+ if (compareZ(ids[i], best) >= 0) best = ids[i];
14674
+ }
14675
+ return best;
14676
+ }
14677
+ const getZIndex = adapter?.getZIndex;
14678
+ if (typeof getZIndex === "function") {
14679
+ let best = ids[0];
14680
+ let bestZ = zOf(getZIndex, best);
14681
+ for (let i = 1; i < ids.length; i++) {
14682
+ const z = zOf(getZIndex, ids[i]);
14683
+ if (z >= bestZ) {
14684
+ best = ids[i];
14685
+ bestZ = z;
14184
14686
  }
14185
14687
  }
14688
+ return best;
14186
14689
  }
14187
14690
  return ids[ids.length - 1];
14188
14691
  }
14692
+ function zOf(getZIndex, id) {
14693
+ const z = getZIndex(id);
14694
+ return typeof z === "number" && Number.isFinite(z) ? z : -Infinity;
14695
+ }
14189
14696
 
14190
14697
  // src/core/selection/selectionTarget.ts
14191
14698
  var MULTI_RESIZE_TARGET_ID = "__weasel:multi-selection";
@@ -14590,9 +15097,19 @@ function composeAffordanceLayer(id, label, affordances) {
14590
15097
  },
14591
15098
  // The walk itself lives in `hitAffordanceRegions` so this layer and
14592
15099
  // `buildAffordanceAt` share one implementation rather than two that can
14593
- // drift. This wrapper exists because `RenderLayer.hitTest` wants only the
14594
- // binding back.
14595
- hitTest: (wx, wy, state, view, _dims, isVisible) => hitAffordanceRegions(affordances, wx, wy, state, view, isVisible)?.binding ?? null
15100
+ // drift. This wrapper lifts the region's declared cursor and claim onto
15101
+ // the binding, the way `buildAffordanceAt` lifts them onto `AffordanceHit`.
15102
+ hitTest: (wx, wy, state, view, _dims, isVisible) => {
15103
+ const hit = hitAffordanceRegions(affordances, wx, wy, state, view, isVisible);
15104
+ if (!hit) return null;
15105
+ const { cursor, strength, claimedKinds } = hit.region;
15106
+ return {
15107
+ ...hit.binding,
15108
+ ...cursor !== void 0 ? { cursor } : {},
15109
+ ...strength !== void 0 ? { strength } : {},
15110
+ ...claimedKinds !== void 0 ? { claimedKinds } : {}
15111
+ };
15112
+ }
14596
15113
  };
14597
15114
  }
14598
15115
  function asChromeState(data) {
@@ -15375,7 +15892,7 @@ function useEllipseTool() {
15375
15892
  );
15376
15893
  }
15377
15894
  function useImageTool(options) {
15378
- const { src, label = "Image" } = options;
15895
+ const { src, label = "Image", preview = "bitmap" } = options;
15379
15896
  return useMemo(
15380
15897
  () => defineTool({
15381
15898
  id: "image",
@@ -15394,11 +15911,11 @@ function useImageTool(options) {
15394
15911
  {
15395
15912
  spec: { kind: "drag" },
15396
15913
  actionId: "insert",
15397
- opts: { params: { kind: "image", src } }
15914
+ opts: { params: { kind: "image", src, preview } }
15398
15915
  }
15399
15916
  ]
15400
15917
  }),
15401
- [src, label]
15918
+ [src, label, preview]
15402
15919
  );
15403
15920
  }
15404
15921
  function useEyedropperTool(opts) {
@@ -15865,6 +16382,19 @@ function composeOrderedLayers(layersMap, standardLayers, sceneLayers) {
15865
16382
  }
15866
16383
  return out;
15867
16384
  }
16385
+ function placeToolOverlays(ordered, selectionOverlay, overlays) {
16386
+ const before = overlays("before-selection");
16387
+ const after = overlays("after-selection");
16388
+ const at = selectionOverlay ? ordered.indexOf(selectionOverlay) : -1;
16389
+ if (at >= 0) {
16390
+ ordered.splice(at + 1, 0, ...after);
16391
+ ordered.splice(at, 0, ...before);
16392
+ } else {
16393
+ ordered.push(...before, ...after);
16394
+ }
16395
+ ordered.push(...overlays("top"));
16396
+ return ordered;
16397
+ }
15868
16398
 
15869
16399
  // src/canvas/toolPreview.ts
15870
16400
  function* toolsInPriorityOrder(tools) {
@@ -16630,6 +17160,15 @@ function wrapWithPoseRotation(cmds, pose) {
16630
17160
  children: cmds
16631
17161
  }];
16632
17162
  }
17163
+
17164
+ // src/canvas/wrapNodeOutput.ts
17165
+ function wrapNodeOutput(cmds, pose, alpha) {
17166
+ const rotated = wrapWithPoseRotation(cmds, pose);
17167
+ if (alpha !== 1 && rotated.length > 0) {
17168
+ return [{ kind: "group", alpha, children: rotated }];
17169
+ }
17170
+ return rotated;
17171
+ }
16633
17172
  function CursorCoordsHud({ canvasRef, viewRef, offset }) {
16634
17173
  const [state, setState] = useState({
16635
17174
  client: { x: 0, y: 0 },
@@ -16881,12 +17420,7 @@ function buildSceneLayer(cfg, adapter, debugSink, boundsOfFn, hideIds, slot) {
16881
17420
  const oy = pose.y ?? (b ? b.y : 0);
16882
17421
  debugSink.recordOrigin(obj.id, { x: ox, y: oy });
16883
17422
  }
16884
- const rotated = wrapWithPoseRotation(cmds, pose);
16885
- const alpha = cfg.alphaFor ? cfg.alphaFor(obj.id) : 1;
16886
- if (alpha !== 1 && rotated.length > 0) {
16887
- return [{ kind: "group", alpha, children: rotated }];
16888
- }
16889
- return rotated;
17423
+ return wrapNodeOutput(cmds, pose, cfg.alphaFor ? cfg.alphaFor(obj.id) : 1);
16890
17424
  };
16891
17425
  const hierarchicalAdapter = cfg.toPose ? {
16892
17426
  ...a,
@@ -16910,13 +17444,8 @@ function buildSceneLayer(cfg, adapter, debugSink, boundsOfFn, hideIds, slot) {
16910
17444
  const pose = toPose(obj);
16911
17445
  if (drawOne) {
16912
17446
  const cmds = drawOne(obj, pose, view);
16913
- const rotated = wrapWithPoseRotation(cmds, pose);
16914
- const alpha = cfg.alphaFor ? cfg.alphaFor(obj.id) : 1;
16915
- if (alpha !== 1 && rotated.length > 0) {
16916
- children.push({ kind: "group", alpha, children: rotated });
16917
- } else {
16918
- for (const cmd of rotated) children.push(cmd);
16919
- }
17447
+ const wrapped = wrapNodeOutput(cmds, pose, cfg.alphaFor ? cfg.alphaFor(obj.id) : 1);
17448
+ for (const cmd of wrapped) children.push(cmd);
16920
17449
  }
16921
17450
  if (debugSink) {
16922
17451
  const b = boundsOfFn ? boundsOfFn(obj.id) : null;
@@ -17412,7 +17941,7 @@ function CanvasInner(props, ref) {
17412
17941
  const out = composeOrderedLayers(effectiveLayersMap, standardLayers, sceneLayers);
17413
17942
  if (decorationLayer) out.push(decorationLayer);
17414
17943
  if (tools) {
17415
- out.push(...tools.getActiveOverlays());
17944
+ placeToolOverlays(out, standardLayers.selectionOverlay, tools.getActiveOverlays);
17416
17945
  }
17417
17946
  return out;
17418
17947
  }, [layersMap, adapter, selectedIds, effectiveBoundsOf, multiActive2, debugSink, tools, backgroundLayer, decorationLayer]);
@@ -17630,6 +18159,7 @@ function createScene(options) {
17630
18159
  return i;
17631
18160
  }
17632
18161
  function rebuildLayerIndex() {
18162
+ invalidateOrder();
17633
18163
  state.layerIndex.clear();
17634
18164
  for (let i = 0; i < state.layers.length; i++) {
17635
18165
  state.layerIndex.set(state.layers[i].id, i);
@@ -17655,6 +18185,7 @@ function createScene(options) {
17655
18185
  const idx = sibs.indexOf(id);
17656
18186
  if (idx < 0) throw new Error(`Scene: node "${id}" not found in its parent's children`);
17657
18187
  sibs.splice(idx, 1);
18188
+ invalidateOrder();
17658
18189
  return { parent: node.parent, index: idx };
17659
18190
  }
17660
18191
  function attach(id, parent, index) {
@@ -17663,6 +18194,7 @@ function createScene(options) {
17663
18194
  sibs.splice(i, 0, id);
17664
18195
  const node = requireNode(id);
17665
18196
  node.parent = parent;
18197
+ invalidateOrder();
17666
18198
  }
17667
18199
  function descendants(id, out) {
17668
18200
  const n = state.nodes.get(id);
@@ -17742,9 +18274,11 @@ function createScene(options) {
17742
18274
  apply: (p) => {
17743
18275
  requireLayerIndex(p.to);
17744
18276
  requireNode(p.id).layer = p.to;
18277
+ invalidateOrder();
17745
18278
  },
17746
18279
  revert: (p) => {
17747
18280
  requireNode(p.id).layer = p.from;
18281
+ invalidateOrder();
17748
18282
  }
17749
18283
  });
17750
18284
  registerKitOp("kit:move", {
@@ -17883,21 +18417,98 @@ function createScene(options) {
17883
18417
  registered.set(k, h);
17884
18418
  }
17885
18419
  }
17886
- function* renderOrderInternal() {
17887
- for (const layer of state.layers) {
17888
- const stack = [...state.roots].reverse();
17889
- while (stack.length > 0) {
17890
- const id = stack.pop();
17891
- const node = state.nodes.get(id);
17892
- if (!node) continue;
17893
- if (node.layer === layer.id) yield id;
17894
- if (node.kind === "container") {
17895
- for (let i = node.children.length - 1; i >= 0; i--) {
17896
- stack.push(node.children[i]);
17897
- }
18420
+ function renderOrderInternal() {
18421
+ return state.layers.length === 1 ? renderOrderFlat() : renderOrderBucketed();
18422
+ }
18423
+ function renderOrderNodesInternal() {
18424
+ return state.layers.length === 1 ? renderOrderNodesFlat() : renderOrderNodesBucketed();
18425
+ }
18426
+ let structureGeneration = 0;
18427
+ let orderCache = { gen: 0 };
18428
+ function invalidateOrder() {
18429
+ structureGeneration++;
18430
+ }
18431
+ function currentOrderCache() {
18432
+ if (orderCache.gen !== structureGeneration) {
18433
+ orderCache = { gen: structureGeneration };
18434
+ }
18435
+ return orderCache;
18436
+ }
18437
+ function renderOrderFlat() {
18438
+ const only = state.layers[0].id;
18439
+ const out = [];
18440
+ const stack = [...state.roots].reverse();
18441
+ while (stack.length > 0) {
18442
+ const id = stack.pop();
18443
+ const node = state.nodes.get(id);
18444
+ if (!node) continue;
18445
+ if (node.layer === only) out.push(id);
18446
+ if (node.kind === "container") {
18447
+ for (let i = node.children.length - 1; i >= 0; i--) {
18448
+ stack.push(node.children[i]);
17898
18449
  }
17899
18450
  }
17900
18451
  }
18452
+ return out;
18453
+ }
18454
+ function renderOrderNodesFlat() {
18455
+ const only = state.layers[0].id;
18456
+ const out = [];
18457
+ const stack = [...state.roots].reverse();
18458
+ while (stack.length > 0) {
18459
+ const id = stack.pop();
18460
+ const node = state.nodes.get(id);
18461
+ if (!node) continue;
18462
+ if (node.layer === only) out.push(node);
18463
+ if (node.kind === "container") {
18464
+ for (let i = node.children.length - 1; i >= 0; i--) {
18465
+ stack.push(node.children[i]);
18466
+ }
18467
+ }
18468
+ }
18469
+ return out;
18470
+ }
18471
+ function renderOrderBucketed() {
18472
+ const buckets = state.layers.map(() => []);
18473
+ const stack = [...state.roots].reverse();
18474
+ while (stack.length > 0) {
18475
+ const id = stack.pop();
18476
+ const node = state.nodes.get(id);
18477
+ if (!node) continue;
18478
+ const bucket = buckets[state.layerIndex.get(node.layer) ?? -1];
18479
+ if (bucket) bucket.push(id);
18480
+ if (node.kind === "container") {
18481
+ for (let i = node.children.length - 1; i >= 0; i--) {
18482
+ stack.push(node.children[i]);
18483
+ }
18484
+ }
18485
+ }
18486
+ const out = [];
18487
+ for (const bucket of buckets) {
18488
+ for (const id of bucket) out.push(id);
18489
+ }
18490
+ return out;
18491
+ }
18492
+ function renderOrderNodesBucketed() {
18493
+ const buckets = state.layers.map(() => []);
18494
+ const stack = [...state.roots].reverse();
18495
+ while (stack.length > 0) {
18496
+ const id = stack.pop();
18497
+ const node = state.nodes.get(id);
18498
+ if (!node) continue;
18499
+ const bucket = buckets[state.layerIndex.get(node.layer) ?? -1];
18500
+ if (bucket) bucket.push(node);
18501
+ if (node.kind === "container") {
18502
+ for (let i = node.children.length - 1; i >= 0; i--) {
18503
+ stack.push(node.children[i]);
18504
+ }
18505
+ }
18506
+ }
18507
+ const out = [];
18508
+ for (const bucket of buckets) {
18509
+ for (const node of bucket) out.push(node);
18510
+ }
18511
+ return out;
17901
18512
  }
17902
18513
  const scene = {
17903
18514
  get nodes() {
@@ -17926,7 +18537,12 @@ function createScene(options) {
17926
18537
  return out;
17927
18538
  },
17928
18539
  renderOrder() {
17929
- return renderOrderInternal();
18540
+ const c = currentOrderCache();
18541
+ return c.ids ??= renderOrderInternal();
18542
+ },
18543
+ renderOrderNodes() {
18544
+ const c = currentOrderCache();
18545
+ return c.nodes ??= renderOrderNodesInternal();
17930
18546
  },
17931
18547
  add(spec) {
17932
18548
  const id = spec.id ?? generateId();
@@ -18234,9 +18850,8 @@ function createScene(options) {
18234
18850
  },
18235
18851
  toJSON() {
18236
18852
  const nodes = [];
18237
- for (const id of renderOrderInternal()) {
18238
- const n = state.nodes.get(id);
18239
- if (!n) continue;
18853
+ for (const n of renderOrderNodesInternal()) {
18854
+ const id = n.id;
18240
18855
  const out = {
18241
18856
  id,
18242
18857
  kind: n.kind,
@@ -18270,6 +18885,7 @@ function createScene(options) {
18270
18885
  state.roots.length = 0;
18271
18886
  state.layers.length = 0;
18272
18887
  state.layerIndex.clear();
18888
+ invalidateOrder();
18273
18889
  for (let i = 0; i < json.systemLayers.length; i++) {
18274
18890
  const spec = json.systemLayers[i];
18275
18891
  if (state.layerIndex.has(spec.id)) {
@@ -18567,9 +19183,8 @@ function sceneToAdapter(scene, options = {}) {
18567
19183
  getNodes() {
18568
19184
  const visible = visibleLayers();
18569
19185
  const out = [];
18570
- for (const id of scene.renderOrder()) {
18571
- const n = scene.get(id);
18572
- if (n && visible.has(n.layer)) out.push(n);
19186
+ for (const n of scene.renderOrderNodes()) {
19187
+ if (visible.has(n.layer)) out.push(n);
18573
19188
  }
18574
19189
  return out;
18575
19190
  },
@@ -18909,9 +19524,7 @@ function useSceneSelectTool(args) {
18909
19524
  // path descriptor for a tighter test.
18910
19525
  hitTestArea: (rect) => {
18911
19526
  const hits = [];
18912
- for (const nid of scene.renderOrder()) {
18913
- const n = scene.get(nid);
18914
- if (!n) continue;
19527
+ for (const n of scene.renderOrderNodes()) {
18915
19528
  if (isPathLike(n.pose) && pathPoseDescriptor.intersectsRect) {
18916
19529
  if (pathPoseDescriptor.intersectsRect(n.pose, rect)) hits.push(n.id);
18917
19530
  continue;
@@ -18949,9 +19562,8 @@ function useSceneSelectTool(args) {
18949
19562
  }
18950
19563
  const tolerance = pickTolerancePx / meanScale(getView?.()?.scale ?? { x: 1, y: 1 });
18951
19564
  const out = [];
18952
- for (const id of scene.renderOrder()) {
18953
- const n = scene.get(id);
18954
- if (!n || !poseContainsRotated(n.pose, wx, wy, tolerance)) continue;
19565
+ for (const n of scene.renderOrderNodes()) {
19566
+ if (!poseContainsRotated(n.pose, wx, wy, tolerance)) continue;
18955
19567
  if (shapePicking && !shapeCoversPoint(n, n.pose, wx, wy, { tolerance })) continue;
18956
19568
  out.push(n.id);
18957
19569
  }
@@ -18996,6 +19608,7 @@ function useSceneSelectTool(args) {
18996
19608
  boundsOf: wiredBoundsOf
18997
19609
  };
18998
19610
  }
19611
+ var GHOST_ALPHA = 0.85;
18999
19612
  function usePreviewGhostLayer(args) {
19000
19613
  const { scene, tools, sceneSlot, dispatcher } = args;
19001
19614
  const sceneRef = useRef(scene);
@@ -19082,15 +19695,26 @@ function usePreviewGhostLayer(args) {
19082
19695
  }
19083
19696
  return [group];
19084
19697
  };
19698
+ const opaqueSet = /* @__PURE__ */ new Set();
19699
+ for (const source of sources) {
19700
+ const ids = source.previewOpaqueIds?.();
19701
+ if (!ids) continue;
19702
+ for (const id of ids) opaqueSet.add(id);
19703
+ }
19085
19704
  const children = [];
19705
+ const opaque = [];
19086
19706
  for (const id of idSet) {
19087
19707
  const node = sc.get(asNodeId(id));
19088
19708
  const parent = node?.parent;
19089
19709
  if (parent != null && idSet.has(parent)) continue;
19090
- for (const cmd of buildSubtree(id)) children.push(cmd);
19710
+ const sink = opaqueSet.has(id) ? opaque : children;
19711
+ for (const cmd of buildSubtree(id)) sink.push(cmd);
19091
19712
  }
19092
- if (children.length === 0) return [];
19093
- return [{ kind: "group", alpha: 0.85, children }];
19713
+ if (children.length === 0 && opaque.length === 0) return [];
19714
+ const out = [];
19715
+ if (opaque.length > 0) out.push({ kind: "group", children: opaque });
19716
+ if (children.length > 0) out.push({ kind: "group", alpha: GHOST_ALPHA, children });
19717
+ return out;
19094
19718
  }
19095
19719
  }), []);
19096
19720
  }
@@ -19100,6 +19724,7 @@ var DEFAULT_STYLE = {
19100
19724
  dash: [3, 3],
19101
19725
  lineWidth: 1
19102
19726
  };
19727
+ var PREVIEW_CONTENT_OPACITY = 0.85;
19103
19728
  function useDispatcherOverlayLayer(args) {
19104
19729
  const { dispatcher, style } = args;
19105
19730
  const dispatcherRef = useRef(dispatcher);
@@ -19162,6 +19787,26 @@ function useDispatcherOverlayLayer(args) {
19162
19787
  pathCmd = rectPath(sx, sy, b.width * view.scale.x, b.height * view.scale.y);
19163
19788
  break;
19164
19789
  }
19790
+ case "image": {
19791
+ const e = ov.extras;
19792
+ const [sx, sy] = worldToScreen(b.x, b.y, t);
19793
+ const sw = b.width * view.scale.x;
19794
+ const sh = b.height * view.scale.y;
19795
+ const bmp = e.preview === "outline" || !e.src ? void 0 : getImageBitmap(e.src);
19796
+ if (bmp) {
19797
+ out.push({
19798
+ kind: "image",
19799
+ image: bmp,
19800
+ x: sx,
19801
+ y: sy,
19802
+ w: sw,
19803
+ h: sh,
19804
+ opacity: PREVIEW_CONTENT_OPACITY
19805
+ });
19806
+ }
19807
+ pathCmd = rectPath(sx, sy, sw, sh);
19808
+ break;
19809
+ }
19165
19810
  case "ellipse": {
19166
19811
  const [sx, sy] = worldToScreen(b.x, b.y, t);
19167
19812
  pathCmd = ellipsePath({
@@ -19220,7 +19865,7 @@ function useDispatcherOverlayLayer(args) {
19220
19865
  const cmd = {
19221
19866
  kind: "path",
19222
19867
  path: pathCmd,
19223
- fill: ov.shape === "line" || ov.shape === "pencil" ? void 0 : { color: cfg.fill },
19868
+ fill: ov.shape === "line" || ov.shape === "pencil" || ov.shape === "image" ? void 0 : { color: cfg.fill },
19224
19869
  stroke: { paint: { color: cfg.stroke }, width: cfg.lineWidth, dash: cfg.dash }
19225
19870
  };
19226
19871
  out.push(cmd);
@@ -19933,9 +20578,6 @@ function buildClassifyTarget(getSelection, pickBest, kindOfNode) {
19933
20578
  function w2s3(wx, wy, view) {
19934
20579
  return [(wx - view.x) * view.scale.x, (wy - view.y) * view.scale.y];
19935
20580
  }
19936
- function r2s(radiusWorld, view) {
19937
- return radiusWorld * meanScale(view.scale);
19938
- }
19939
20581
  var SLOP_FILL = "rgba(255, 80, 140, 0.18)";
19940
20582
  var SLOP_STROKE = "rgba(255, 80, 140, 0.55)";
19941
20583
  function createSlopsDebugLayer(opts) {
@@ -19946,7 +20588,7 @@ function createSlopsDebugLayer(opts) {
19946
20588
  draw: (_data, view) => {
19947
20589
  const out = [];
19948
20590
  const sel = opts.selectionRef.current?.current ?? [];
19949
- const r = r2s(HANDLE_HIT_RADIUS, view);
20591
+ const r = HANDLE_HIT_RADIUS;
19950
20592
  for (const id of sel) {
19951
20593
  const b = opts.boundsOf(id);
19952
20594
  if (!b) continue;
@@ -19983,6 +20625,7 @@ function createSlopsDebugLayer(opts) {
19983
20625
  }
19984
20626
  const editingId = opts.getEditingId();
19985
20627
  if (editingId) {
20628
+ const anchorR = ANCHOR_HIT_RADIUS;
19986
20629
  const pose = opts.getPose(editingId);
19987
20630
  if (pose && pose.kind === "polygon") {
19988
20631
  const anchors = enumerateAnchors(pose);
@@ -19990,7 +20633,7 @@ function createSlopsDebugLayer(opts) {
19990
20633
  const [sx, sy] = w2s3(a.x, a.y, view);
19991
20634
  out.push({
19992
20635
  kind: "path",
19993
- path: circlePath(sx, sy, r),
20636
+ path: circlePath(sx, sy, anchorR),
19994
20637
  fill: { fill: "solid", color: SLOP_FILL },
19995
20638
  stroke: { paint: { fill: "solid", color: SLOP_STROKE }, width: 1 }
19996
20639
  });
@@ -19998,7 +20641,7 @@ function createSlopsDebugLayer(opts) {
19998
20641
  const [csx, csy] = w2s3(a.controlIn.x, a.controlIn.y, view);
19999
20642
  out.push({
20000
20643
  kind: "path",
20001
- path: circlePath(csx, csy, r),
20644
+ path: circlePath(csx, csy, anchorR),
20002
20645
  fill: { fill: "solid", color: SLOP_FILL },
20003
20646
  stroke: { paint: { fill: "solid", color: SLOP_STROKE }, width: 1 }
20004
20647
  });
@@ -20007,7 +20650,7 @@ function createSlopsDebugLayer(opts) {
20007
20650
  const [csx, csy] = w2s3(a.controlOut.x, a.controlOut.y, view);
20008
20651
  out.push({
20009
20652
  kind: "path",
20010
- path: circlePath(csx, csy, r),
20653
+ path: circlePath(csx, csy, anchorR),
20011
20654
  fill: { fill: "solid", color: SLOP_FILL },
20012
20655
  stroke: { paint: { fill: "solid", color: SLOP_STROKE }, width: 1 }
20013
20656
  });
@@ -20089,30 +20732,41 @@ function DepRegistryProviderIfRoot({ children }) {
20089
20732
  function hitTestArea(scene, bounds) {
20090
20733
  const { x, y, width: w, height: h } = bounds;
20091
20734
  const area = [x, y, x + w, y, x + w, y + h, x, y + h];
20092
- return hitTestAreaPolygon(scene, area, { x, y, width: w, height: h });
20735
+ return hitTestAreaPolygon(scene, area, { x, y, width: w, height: h }, true);
20093
20736
  }
20094
- function hitTestAreaPolygon(scene, area, areaBounds) {
20737
+ function hitTestAreaPolygon(scene, area, areaBounds, areaIsRect = false) {
20095
20738
  const ab = areaBounds ?? boundsOf(area);
20096
20739
  if (!ab) return [];
20097
20740
  const hits = [];
20098
- for (const id of scene.renderOrder()) {
20099
- const node = scene.get(id);
20100
- if (!node || node.kind === "container") continue;
20741
+ const order = scene.renderOrderNodes();
20742
+ for (let i = 0; i < order.length; i++) {
20743
+ const node = order[i];
20744
+ if (node.kind === "container") continue;
20101
20745
  const pose = node.pose;
20102
- const b = aabbOfPose(pose);
20746
+ const silhouette = pose !== null && typeof pose === "object" && pose.kind === "polygon";
20747
+ const b = silhouette ? nodeMemo(node, "aabb", pose, () => aabbOfPose(pose)) : aabbOfPose(pose);
20103
20748
  if (!Number.isFinite(b.x) || !Number.isFinite(b.y) || !Number.isFinite(b.width) || !Number.isFinite(b.height)) {
20104
20749
  continue;
20105
20750
  }
20106
20751
  if (b.x >= ab.x + ab.width || b.x + b.width <= ab.x || b.y >= ab.y + ab.height || b.y + b.height <= ab.y) {
20107
20752
  continue;
20108
20753
  }
20109
- if (!isPathLike(pose) || pose.kind === "rect") {
20110
- hits.push(id);
20754
+ if (areaIsRect && b.x >= ab.x && b.y >= ab.y && b.x + b.width <= ab.x + ab.width && b.y + b.height <= ab.y + ab.height) {
20755
+ hits.push(node.id);
20111
20756
  continue;
20112
20757
  }
20113
- if (silhouetteOverlapsArea(pose.coords, area)) {
20114
- hits.push(id);
20758
+ if (silhouette) {
20759
+ if (silhouetteOverlapsArea(pose.coords, area)) {
20760
+ hits.push(node.id);
20761
+ }
20762
+ continue;
20763
+ }
20764
+ const drawn = findShapeSilhouette(node, pose);
20765
+ if (drawn?.kind === "polygon") {
20766
+ if (silhouetteOverlapsArea(drawn.coords, area)) hits.push(node.id);
20767
+ continue;
20115
20768
  }
20769
+ hits.push(node.id);
20116
20770
  }
20117
20771
  return hits;
20118
20772
  }
@@ -22345,14 +22999,25 @@ var inferredNodeProperties = inferredNodeRouting.map((e) => ({
22345
22999
 
22346
23000
  // src/canvas/sceneViewRender.ts
22347
23001
  var RENDERER_CACHE = /* @__PURE__ */ new WeakMap();
22348
- function buildSceneViewCommands(scene, view, drawOne, extraCommands) {
22349
- const children = [];
22350
- for (const id of scene.renderOrder()) {
22351
- const node = scene.get(id);
22352
- if (!node) continue;
22353
- const cmds = drawOne(node, node.pose, view);
22354
- for (const cmd of cmds) children.push(cmd);
22355
- }
23002
+ function sceneAsHierarchy(scene) {
23003
+ return {
23004
+ getLayers: () => scene.layers.map((l) => ({ id: l.id, visible: l.visible })),
23005
+ getNode: (id) => scene.get(id),
23006
+ getChildren: (parentId) => {
23007
+ if (parentId === null) return scene.roots;
23008
+ const node = scene.get(parentId);
23009
+ return node && node.kind === "container" ? node.children : [];
23010
+ },
23011
+ getPose: (id) => scene.get(id).pose
23012
+ };
23013
+ }
23014
+ function buildSceneViewCommands(scene, view, drawOne, extraCommands, alphaFor) {
23015
+ const wrappedDrawOne = (node, pose, v) => wrapNodeOutput(drawOne(node, pose, v), pose, alphaFor ? alphaFor(node.id) : 1);
23016
+ const children = buildSceneTree(
23017
+ sceneAsHierarchy(scene),
23018
+ wrappedDrawOne,
23019
+ view
23020
+ );
22356
23021
  if (extraCommands && extraCommands.length > 0) {
22357
23022
  for (const cmd of extraCommands) children.push(cmd);
22358
23023
  }
@@ -22363,7 +23028,7 @@ function buildSceneViewCommands(scene, view, drawOne, extraCommands) {
22363
23028
  }];
22364
23029
  }
22365
23030
  function renderSceneToCanvas(args) {
22366
- const { canvas, scene, view, width, height, drawOne, extraCommands } = args;
23031
+ const { canvas, scene, view, width, height, drawOne, extraCommands, alphaFor } = args;
22367
23032
  const dpr = args.dpr ?? (typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1);
22368
23033
  let entry = RENDERER_CACHE.get(canvas);
22369
23034
  if (!entry) {
@@ -22391,11 +23056,11 @@ function renderSceneToCanvas(args) {
22391
23056
  entry.height = height;
22392
23057
  entry.dpr = dpr;
22393
23058
  }
22394
- const commands = buildSceneViewCommands(scene, view, drawOne, extraCommands);
23059
+ const commands = buildSceneViewCommands(scene, view, drawOne, extraCommands, alphaFor);
22395
23060
  entry.renderer.render(commands, viewToMat3(view));
22396
23061
  }
22397
23062
  function SceneViewCanvasInner(props) {
22398
- const { scene, view, width, height, drawOne, extraCommands, className, canvasRef } = props;
23063
+ const { scene, view, width, height, drawOne, extraCommands, alphaFor, className, canvasRef } = props;
22399
23064
  useSyncExternalStore(scene.subscribe, scene.getVersion, scene.getVersion);
22400
23065
  const localRef = useRef(null);
22401
23066
  const setCanvasRef = (el) => {
@@ -22416,7 +23081,8 @@ function SceneViewCanvasInner(props) {
22416
23081
  width,
22417
23082
  height,
22418
23083
  drawOne,
22419
- extraCommands
23084
+ extraCommands,
23085
+ alphaFor
22420
23086
  });
22421
23087
  });
22422
23088
  return /* @__PURE__ */ jsx(
@@ -22515,6 +23181,7 @@ function MinimapCanvasInner(props) {
22515
23181
  width,
22516
23182
  height,
22517
23183
  drawOne,
23184
+ alphaFor,
22518
23185
  fit = "scene",
22519
23186
  poseBounds,
22520
23187
  indicatorStyle,
@@ -22626,6 +23293,7 @@ function MinimapCanvasInner(props) {
22626
23293
  height,
22627
23294
  drawOne,
22628
23295
  extraCommands,
23296
+ alphaFor,
22629
23297
  className,
22630
23298
  canvasRef: setCanvasRef
22631
23299
  }
@@ -22667,7 +23335,7 @@ function planPixelRender(args) {
22667
23335
  fill: { fill: "solid", color: args.background }
22668
23336
  });
22669
23337
  }
22670
- commands.push(...buildSceneViewCommands(args.scene, view, drawOne));
23338
+ commands.push(...buildSceneViewCommands(args.scene, view, drawOne, void 0, args.alphaFor));
22671
23339
  return { width, height, view, commands };
22672
23340
  }
22673
23341
  function defaultCreateCanvas(width, height) {
@@ -23019,15 +23687,11 @@ function measureText(ctx, text, maxWidth, style) {
23019
23687
  function measureTextBounds(text, style, opts) {
23020
23688
  const resolved = resolveTextStyle(style);
23021
23689
  const runs = resolveRuns([{ text }], resolved);
23022
- const { bounds } = layoutRuns(
23023
- runs,
23024
- {
23025
- maxWidth: opts?.maxWidth ?? Infinity,
23026
- lineHeight: opts?.lineHeight ?? resolved.lineHeight,
23027
- align: resolved.align
23028
- },
23029
- { x: 0, y: 0 }
23030
- );
23690
+ const { bounds } = layoutRuns(runs, {
23691
+ maxWidth: opts?.maxWidth ?? Infinity,
23692
+ lineHeight: opts?.lineHeight ?? resolved.lineHeight,
23693
+ align: resolved.align
23694
+ });
23031
23695
  return bounds;
23032
23696
  }
23033
23697
 
@@ -23929,6 +24593,14 @@ function useSceneTextEdit(scene, container, options = {}) {
23929
24593
  const setter = optsRef.current.setRuns;
23930
24594
  const data = setter ? setter(node.data, runs) : { ...node.data, runs };
23931
24595
  sceneRef.current.update(nid, { data });
24596
+ },
24597
+ setStyle: (id, style) => {
24598
+ const nid = asNodeId(id);
24599
+ const node = sceneRef.current.get(nid);
24600
+ if (!node) return;
24601
+ const setter = optsRef.current.setStyle;
24602
+ const data = setter ? setter(node.data, style) : { ...node.data, style };
24603
+ sceneRef.current.update(nid, { data });
23932
24604
  }
23933
24605
  });
23934
24606
  const onDoubleClick = useCallback((e) => {
@@ -23940,10 +24612,9 @@ function useSceneTextEdit(scene, container, options = {}) {
23940
24612
  const cy = view ? canvasY / view.scale.y + view.y : canvasY;
23941
24613
  const readText = (data) => optsRef.current.getText ? optsRef.current.getText(data) : data.text ?? "";
23942
24614
  const readStyle = (data) => optsRef.current.getStyle ? optsRef.current.getStyle(data) : data.style;
23943
- const order = [...sceneRef.current.renderOrder()];
24615
+ const order = sceneRef.current.renderOrderNodes();
23944
24616
  for (let i = order.length - 1; i >= 0; i--) {
23945
- const node = sceneRef.current.get(order[i]);
23946
- if (!node) continue;
24617
+ const node = order[i];
23947
24618
  const text = readText(node.data);
23948
24619
  const pose = {
23949
24620
  x: node.pose.x,
@@ -27638,6 +28309,6 @@ function mergeContributions(...bundles) {
27638
28309
  return out;
27639
28310
  }
27640
28311
 
27641
- export { ALWAYS, ANCHOR_HIT_BASE_PX, ActionsProviderIfRoot, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, BUNDLE_TOOLS, COARSE_TARGET_SCALE, CURVE_REPS, ColorOverrideRegistry, CropIcon, CursorCoordsHud, DEFAULT_ALPHA_DECAY, DEFAULT_ALPHA_MIN, DEFAULT_DEBUG_THEME, DEFAULT_DEVICE_PROFILE, DEFAULT_FILL_COLOR, DEFAULT_HANDLE_SIZE2 as DEFAULT_HANDLE_SIZE, DEFAULT_PALETTE, DEFAULT_ROTATION_HANDLE_DISTANCE, DEFAULT_STROKE_COLOR, DEFAULT_TEXT_STYLE, DEFAULT_VELOCITY_DECAY, DepRegistryProviderIfRoot, DeviceProfileProvider, DivideIcon, EASINGS, EllipseIcon, ExcludeIcon, EyedropperIcon, FALLBACK_FIT_VIEW, GHOST_STROKE, HANDLE_BASE_PX, HandIcon, IDENTITY_COLOR_MATRIX, IDENTITY_POSE_COMPOSITION, IntersectIcon, KIT_SHAPE_KINDS, LassoIcon, LineIcon, MIXED, MOVE_ANCHORS, MinimapCanvas, NEVER, OUTLINE_MIN_SCREEN_PX, PATH_ANCHOR_CHROME_ID, PenIcon, PencilIcon, PickHud, PointerProviderIfRoot, PolygonIcon, RECT_ALIGN_PROJECTION, ROTATION_HANDLE_BASE_PX, RectIcon, SPRING_PRESETS, SceneCanvas, SceneViewCanvas, SelectIcon, SelectionContextProvider, SelectionContextProviderIfRoot, ShaderCompileError, StarIcon, SubtractIcon, TextIcon, UnionIcon, UnknownIcon, VERSION, WeaselProvider, WeaselRenderer, aabbCenter, actionIs, alignDeltaFor, alignInsertBehavior, alignMoveBehavior, alignResizeBehavior, alignedStrokeRect, always, and, animateLifecycle, animateOnSetPose, annulusSemiAxes, applyBooleanOp, applyHitExistingGate, applyStyleToRange, areaSelectAction, arrayAdapter, asNodeId, bezierCubic, bezierQuadratic, buildChromeCtx, buildGradientRamp, buildRuleCtx, buildSceneViewCommands, canBringForward, canHover, canSendBackward, capabilityAll, capabilityIn, capabilityIs, capabilityNot, caretIndexAt, cellAt, charOffsetToDomPosition, clampView, clearSelectionAction, clientToCanvas, clipboardCopyAction, clipboardCutAction, cloneAction, coarsePointer, composeAffordanceLayer, composePath, composeRectPose, composeSelectionPose, composeWorldPose, computeFitView2 as computeFitView, computeIndicatorCommand, computeWheelAction, cond, constrainTo45, containedThenNearest, countPathAnchors, createCellHighlightLayer, createChildrenLayer, createCornerResizeAffordance, createDebugOverlayLayer, createDebugSink, createDispatcher, createGridLayer, createGuidesLayer, createHistory2 as createHistory, createMarkdownRenderer, createMoveToIndexOp, createNodeProperties, createNodeRouting, createParallaxLayer, createPathAnchorAffordances, createPathEditingOverlayLayer, createPathLayer, createPenPreviewLayer, createReorderOp, createRotationAffordance, createScene, createSelectionHandlesLayer, createSelectionOutlineLayer, createSelectionOverlayLayer, createSetDataOp, createSetLayerOp, createSetPathOp, createSetTextOp, createTextLayer, createViewportLayer, cycleVertexColors, decomposePath, decomposeRectPose, defaultCommitAdapter, defaultDrawOne, defaultLabelTextRenderer, defaultNodeProperties, defaultNodeRouting, defaultVisibilityRules, deriveAlignmentGuides, deriveParallaxView, describeRule, domPositionToCharOffset, domToRuns, drawLayers, easeIn, easeInBack, easeInBounce, easeInCirc, easeInCubic, easeInElastic, easeInExpo, easeInOut, easeInOutBack, easeInOutBounce, easeInOutCirc, easeInOutCubic, easeInOutElastic, easeInOutExpo, easeInOutQuad, easeInOutQuart, easeInOutQuint, easeInOutSine, easeInQuad, easeInQuart, easeInQuint, easeInSine, easeOut, easeOutBack, easeOutBounce, easeOutCirc, easeOutCubic2 as easeOutCubic, easeOutElastic, easeOutExpo, easeOutQuad, easeOutQuart, easeOutQuint, easeOutSine, editAnchorsAction, enterTextEditAction, enumerateAnchors, evaluate, fillInPoseFrame, fillToBoundsFrame, findNodeShape, findShapeInk, findShapeSilhouette, fitTextPose, fitToBounds, fitViewToBounds, fitZoom, focused, fontString, forEachCoalesced, freeform, gateLayer, gesturing, getAlpha01, getImageBitmap, getNodeShapes, getStylusData, gradientForBounds, gradientGeometry, hexToRgba, hitAffordanceRegions, hitAnchor, hitRotationHandle, hovering, hoveringSelected, imageStatus, inferredNodeProperties, inferredNodeRouting, insertAction, isEditableTarget2 as isEditableTarget, isPatternSpec, lassoSelectAction, layoutMarkdown, lerpColorArray, lerpOklab, lerpOklch, linear, liveScope, makeViewportZoomAction, markdownToRuns, mat3, matchAlignment, matchesKeyBinding, meanScale, measureText, measureTextBounds, measuredWidth, mergeAlphaFromPrev, mergeContributions, modeIn, modeIs, modeNot, modifierHeld, momentum, moveAction, multiActive, nearest, nearestWithin, nestedHitTester, never, nodeHasFlag, none, normalizeHex, not, nurbs, oklabToOklch, oklabToSrgbU8, oklchToOklab, openFilePicker, or, parseColor, parseColorToRgba255, parseDebugFlags, pathContainsPoint, pathContainsPolygon, pathContainsRect, pathDistanceToPoint, pathDivide, pathExclude, pathFromD, pathInPoseFrame, pathInWorld, pathIntersect, pathIntersectsPolygon, pathIntersectsRect, pathOriginProjection, pathSubtract, pathToAnchors, pathUnion, pickTopMostHit, pinchZoomAction, planPixelRender, pointInRotatedRect, pointInTextPose, poseRotationOf, pressureToWidth, rainbowVertexColors, rebaseLocalPose, rectCorners, registerContentHandler, registerNodeShape, registerProgram, renderLabel, renderSceneToCanvas, renderSceneToPixels, resizeAction, resolveColor, resolveDeviceProfile, resolveFillPattern, resolvePatternSpec, resolveRuns, resolveTextStyle, resolveVisibility, rgbaToHex, rotateAction, rotateAroundAABBCenter, rotatePathAround, rotatePoint, rotatedRectCorners, rotationDegreesUnit, rotationHandle, roundToCell, runsToDom, runsToMarkdown, runsToPlainText, sampleGradientStops, sceneFromJSON, sceneNodeClientRect, sceneToAdapter, scopeBindings, screenToWorld, selectFromLasso, selectionAtLeast, selectionEmpty, selectionIs, setFlagOverRange, shapeCoversPoint, sliceAction, snapPoint, solidVertexColors, specificity, spiro, splitPathByLine, splitSubpaths, springPose, springVertexColors, srgbU8ToOklab, staggerVertexColors, startThresholdDrag, styleAtRange, subscribeImageReady, tessellate, tessellateStroke, textCommand, textLineBoxes, tileGrid, toHex8, toRuns, translatePoseViaDescriptor, translateRectPose, tweenPose, tweenVertexColors, unionBounds, unionBoundsPath, useActiveToolContext, useAlign, useAnimator, useArrayAdapter, useAutoCenter, useBooleans, useBooleansAdapter, useCanvasFocus, useCanvasSize, useDecayLoop, useDeviceProfile, useDistribute, useDragGesture, useDragHandle, useDragRadial, useDragRect, useDropZone, useEllipseTool, useEyedropperTool, useGestureDispatcher, useGridCellHover, useGuides, useHandTool, useHandleDrag, useHoverTracking, useImageTool, useKeybindings, useLassoTool, useLineTool, useOptionalActiveToolContext, usePenTool, usePencilTool, usePinchGesture, usePinchZoomTool, usePointerStylus, usePolygonTool, usePublishSelection, useRectTool, useResizePolicy, useRotateTool, useScene, useSceneAdapter, useSceneTextEdit, useSelectTool, useSelection, useSelectionContext, useSimulation, useSliceDep, useStandardActions, useStarTool, useTextEdit, useTextTool, useTools, useVelocityTracker, useViewAnimation, useViewTween, useZoom, verticalAlignOffset, viewToMat3, viewToTransform, viewportDragPanAction, viewportZoomAction, when, withAlpha01, withCoord, withGradientKind, worldEditToStorage, worldPoseLookup, worldToScreen, zoomAt, zoomAtLeast };
27642
- //# sourceMappingURL=chunk-D2C6K6FO.js.map
27643
- //# sourceMappingURL=chunk-D2C6K6FO.js.map
28312
+ export { ALWAYS, ANCHOR_HIT_BASE_PX, ActionsProviderIfRoot, ActiveToolContextProvider, ActiveToolContextProviderIfRoot, BUNDLE_TOOLS, COARSE_TARGET_SCALE, CURVE_REPS, ColorOverrideRegistry, CropIcon, CursorCoordsHud, DEFAULT_ALPHA_DECAY, DEFAULT_ALPHA_MIN, DEFAULT_DEBUG_STROKES, DEFAULT_DEBUG_THEME, DEFAULT_DEVICE_PROFILE, DEFAULT_FILL_COLOR, DEFAULT_HANDLE_SIZE2 as DEFAULT_HANDLE_SIZE, DEFAULT_PALETTE, DEFAULT_ROTATION_HANDLE_DISTANCE, DEFAULT_STROKE_COLOR, DEFAULT_TEXT_STYLE, DEFAULT_VELOCITY_DECAY, DepRegistryProviderIfRoot, DeviceProfileProvider, DivideIcon, EASINGS, EllipseIcon, ExcludeIcon, EyedropperIcon, FALLBACK_FIT_VIEW, GHOST_STROKE, HANDLE_BASE_PX, HandIcon, IDENTITY_COLOR_MATRIX, IDENTITY_POSE_COMPOSITION, IntersectIcon, KIT_SHAPE_KINDS, LassoIcon, LineIcon, MIXED, MOVE_ANCHORS, MinimapCanvas, NEVER, OUTLINE_MIN_SCREEN_PX, PATH_ANCHOR_CHROME_ID, PenIcon, PencilIcon, PickHud, PointerProviderIfRoot, PolygonIcon, RECT_ALIGN_PROJECTION, ROTATION_HANDLE_BASE_PX, RectIcon, SPRING_PRESETS, SceneCanvas, SceneViewCanvas, SelectIcon, SelectionContextProvider, SelectionContextProviderIfRoot, ShaderCompileError, StarIcon, SubtractIcon, TextIcon, UnionIcon, UnknownIcon, VERSION, WeaselProvider, WeaselRenderer, aabbCenter, actionIs, alignDeltaFor, alignInsertBehavior, alignMoveBehavior, alignResizeBehavior, alignedStrokeRect, always, and, animateLifecycle, animateOnSetPose, annulusSemiAxes, applyBooleanOp, applyHitExistingGate, applyStyleToRange, areaSelectAction, arrayAdapter, asNodeId, bezierCubic, bezierQuadratic, buildChromeCtx, buildGradientRamp, buildRuleCtx, buildSceneViewCommands, canBringForward, canHover, canSendBackward, capabilityAll, capabilityIn, capabilityIs, capabilityNot, caretIndexAt, cellAt, charOffsetToDomPosition, clampView, clearSelectionAction, clientToCanvas, clipboardCopyAction, clipboardCutAction, cloneAction, coarsePointer, composeAffordanceLayer, composePath, composeRectPose, composeSelectionPose, composeWorldPose, computeFitView2 as computeFitView, computeIndicatorCommand, computeWheelAction, cond, constrainTo45, containedThenNearest, countPathAnchors, createCellHighlightLayer, createChildrenLayer, createCornerResizeAffordance, createDebugOverlayLayer, createDebugSink, createDispatcher, createGridLayer, createGuidesLayer, createHistory2 as createHistory, createMarkdownRenderer, createMoveToIndexOp, createNodeProperties, createNodeRouting, createParallaxLayer, createPathAnchorAffordances, createPathEditingOverlayLayer, createPathLayer, createPenPreviewLayer, createReorderOp, createRotationAffordance, createScene, createSelectionHandlesLayer, createSelectionOutlineLayer, createSelectionOverlayLayer, createSetDataOp, createSetLayerOp, createSetPathOp, createSetTextOp, createTextLayer, createViewportLayer, cycleVertexColors, decomposePath, decomposeRectPose, defaultCommitAdapter, defaultDrawOne, defaultLabelTextRenderer, defaultNodeProperties, defaultNodeRouting, defaultVisibilityRules, defineTool, defineViewportTool, deriveAlignmentGuides, deriveParallaxView, describeRule, domPositionToCharOffset, domToRuns, drawLayers, easeIn, easeInBack, easeInBounce, easeInCirc, easeInCubic, easeInElastic, easeInExpo, easeInOut, easeInOutBack, easeInOutBounce, easeInOutCirc, easeInOutCubic, easeInOutElastic, easeInOutExpo, easeInOutQuad, easeInOutQuart, easeInOutQuint, easeInOutSine, easeInQuad, easeInQuart, easeInQuint, easeInSine, easeOut, easeOutBack, easeOutBounce, easeOutCirc, easeOutCubic2 as easeOutCubic, easeOutElastic, easeOutExpo, easeOutQuad, easeOutQuart, easeOutQuint, easeOutSine, editAnchorsAction, enterTextEditAction, enumerateAnchors, evaluate, fillInPoseFrame, fillToBoundsFrame, findNodeShape, findShapeInk, findShapeSilhouette, fitTextPose, fitToBounds, fitViewToBounds, fitZoom, focused, fontString, forEachCoalesced, freeform, gateLayer, gesturing, getAlpha01, getImageBitmap, getNodeShapes, getStylusData, gradientForBounds, gradientGeometry, hexToRgba, hitAffordanceRegions, hitAnchor, hitRotationHandle, hovering, hoveringSelected, imageStatus, inferredNodeProperties, inferredNodeRouting, insertAction, isEditableTarget2 as isEditableTarget, isPatternSpec, lassoSelectAction, layoutMarkdown, lerpColorArray, lerpOklab, lerpOklch, linear, liveScope, makeViewportZoomAction, markdownToRuns, mat3, matchAlignment, matchesKeyBinding, meanScale, measureText, measureTextBounds, measuredWidth, mergeAlphaFromPrev, mergeContributions, modeIn, modeIs, modeNot, modifierHeld, momentum, moveAction, multiActive, nearest, nearestWithin, nestedHitTester, never, nodeHasFlag, none, normalizeHex, not, nurbs, oklabToOklch, oklabToSrgbU8, oklchToOklab, openFilePicker, or, parseColor, parseColorToRgba255, parseDebugFlags, pathContainsPoint, pathContainsPolygon, pathContainsRect, pathDistanceToPoint, pathDivide, pathExclude, pathFromD, pathInPoseFrame, pathInWorld, pathIntersect, pathIntersectsPolygon, pathIntersectsRect, pathOriginProjection, pathSubtract, pathToAnchors, pathUnion, pickTopMostHit, pinchZoomAction, planPixelRender, pointInRotatedRect, pointInTextPose, poseRotationOf, pressureToWidth, rainbowVertexColors, rebaseLocalPose, rectCorners, registerContentHandler, registerNodeShape, registerProgram, renderLabel, renderSceneToCanvas, renderSceneToPixels, resizeAction, resolveColor, resolveDeviceProfile, resolveFillPattern, resolvePatternSpec, resolveRuns, resolveTextStyle, resolveVisibility, rgbaToHex, rotateAction, rotateAroundAABBCenter, rotatePathAround, rotatePoint, rotatedRectCorners, rotationDegreesUnit, rotationHandle, roundToCell, runsToDom, runsToMarkdown, runsToPlainText, sampleGradientStops, sceneFromJSON, sceneNodeClientRect, sceneToAdapter, scopeBindings, screenToWorld, selectFromLasso, selectionAtLeast, selectionEmpty, selectionIs, setFlagOverRange, shapeCoversPoint, sliceAction, snapPoint, solidVertexColors, specificity, spiro, splitPathByLine, splitSubpaths, springPose, springVertexColors, srgbU8ToOklab, staggerVertexColors, startThresholdDrag, styleAtRange, subscribeImageReady, tessellate, tessellateStroke, textCommand, textLineBoxes, tileGrid, toHex8, toRuns, translatePoseViaDescriptor, translateRectPose, tweenPose, tweenVertexColors, unionBounds, unionBoundsPath, useActiveToolContext, useAlign, useAnimator, useArrayAdapter, useAutoCenter, useBooleans, useBooleansAdapter, useCanvasFocus, useCanvasSize, useDecayLoop, useDeviceProfile, useDistribute, useDragGesture, useDragHandle, useDragRadial, useDragRect, useDropZone, useEllipseTool, useEyedropperTool, useGestureDispatcher, useGridCellHover, useGuides, useHandTool, useHandleDrag, useHoverTracking, useImageTool, useKeybindings, useLassoTool, useLineTool, useOptionalActiveToolContext, usePenTool, usePencilTool, usePinchGesture, usePinchZoomTool, usePointerStylus, usePolygonTool, usePublishSelection, useRectTool, useResizePolicy, useRotateTool, useScene, useSceneAdapter, useSceneTextEdit, useSelectTool, useSelection, useSelectionContext, useSimulation, useSliceDep, useStandardActions, useStarTool, useTextEdit, useTextTool, useTools, useVelocityTracker, useViewAnimation, useViewTween, useZoom, verticalAlignOffset, viewToMat3, viewToTransform, viewportDragPanAction, viewportZoomAction, viewportsAt, when, withAlpha01, withCoord, withGradientKind, worldEditToStorage, worldPoseLookup, worldToScreen, zoomAt, zoomAtLeast };
28313
+ //# sourceMappingURL=chunk-PI56ORHE.js.map
28314
+ //# sourceMappingURL=chunk-PI56ORHE.js.map