@canvas-harness/core 0.0.5 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -633,8 +633,8 @@ var EdgeGeometryCache = class {
633
633
  * Caller is responsible for passing the current store-managed version.
634
634
  */
635
635
  get(edge, version, getNode) {
636
- const cached = this.entries.get(edge.id);
637
- if (cached && cached.version === version) return cached.geom;
636
+ const cached2 = this.entries.get(edge.id);
637
+ if (cached2 && cached2.version === version) return cached2.geom;
638
638
  const geom = computeEdgeGeometry(edge, getNode);
639
639
  if (geom) this.entries.set(edge.id, { version, geom });
640
640
  else this.entries.delete(edge.id);
@@ -846,8 +846,8 @@ var mixHex = (a, b, t) => {
846
846
  };
847
847
  var darkenCache = /* @__PURE__ */ new Map();
848
848
  var darkenHex = (hex) => {
849
- const cached = darkenCache.get(hex);
850
- if (cached !== void 0) return cached;
849
+ const cached2 = darkenCache.get(hex);
850
+ if (cached2 !== void 0) return cached2;
851
851
  const result = mixHex(hex, "#000000", TONE_BLEND);
852
852
  darkenCache.set(hex, result);
853
853
  return result;
@@ -1614,7 +1614,7 @@ var buildPath = (type, x, y, w, h, radius) => {
1614
1614
  };
1615
1615
 
1616
1616
  // src/text/tokens.ts
1617
- var INLINE_PATTERN = /(\*\*[^*]+\*\*|==[^=\s](?:[^=]*?[^=\s])?==|`[^`]+`|\*[^*]+\*|__[^_]+__|~~[^~]+~~|_[^_]+_|\[[^\]]+\]\([^)]+\))/g;
1617
+ var INLINE_PATTERN = /(\$[^$\n]+?\$|\*\*[^*]+\*\*|==[^=\s](?:[^=]*?[^=\s])?==|`[^`]+`|\*[^*]+\*|__[^_]+__|~~[^~]+~~|_[^_]+_|\[[^\]]+\]\([^)]+\))/g;
1618
1618
  var HR_LINE_PATTERN = /^[ \t]*---[ \t]*$/;
1619
1619
  var DOUBLE_HR_LINE_PATTERN = /^[ \t]*===[ \t]*$/;
1620
1620
  var transformSymbols = (value) => value.replace(/<=>|<->|<-|->|\[\]|\[[vx]\]/gi, (match) => {
@@ -1652,6 +1652,8 @@ var tokenizeInline = (segment) => {
1652
1652
  tokens.push({ type: "link", content: transformSymbols(match.slice(1, splitIndex)) });
1653
1653
  } else if (match.startsWith("`") && match.endsWith("`")) {
1654
1654
  tokens.push({ type: "code", content: match.slice(1, -1) });
1655
+ } else if (match.startsWith("$") && match.endsWith("$")) {
1656
+ tokens.push({ type: "math", content: match.slice(1, -1) });
1655
1657
  } else {
1656
1658
  tokens.push({ type: "text", content: transformSymbols(match) });
1657
1659
  }
@@ -1740,6 +1742,225 @@ var DEFAULT_HIGHLIGHT_COLOR_DARK = "#6b5a23";
1740
1742
  var LINK_COLOR = "#2563eb";
1741
1743
  var CODE_BG_COLOR = "rgba(148, 163, 184, 0.18)";
1742
1744
 
1745
+ // src/text/math/loader.ts
1746
+ var cached = null;
1747
+ var loadPromise2 = null;
1748
+ var loadFailed = false;
1749
+ var readyCallbacks2 = /* @__PURE__ */ new Set();
1750
+ var getMathJax = () => {
1751
+ if (cached) return cached;
1752
+ if (loadFailed) return null;
1753
+ if (!loadPromise2) {
1754
+ loadPromise2 = loadMathJax().then((instance) => {
1755
+ if (instance) {
1756
+ cached = instance;
1757
+ for (const cb of readyCallbacks2) cb();
1758
+ } else {
1759
+ loadFailed = true;
1760
+ }
1761
+ readyCallbacks2.clear();
1762
+ return cached;
1763
+ }).catch((err) => {
1764
+ console.warn("[math] failed to load MathJax:", err);
1765
+ loadFailed = true;
1766
+ readyCallbacks2.clear();
1767
+ return null;
1768
+ });
1769
+ }
1770
+ return null;
1771
+ };
1772
+ var onMathJaxReady = (cb) => {
1773
+ if (cached) return;
1774
+ if (loadFailed) return;
1775
+ readyCallbacks2.add(cb);
1776
+ };
1777
+ var loadMathJax = async () => {
1778
+ if (typeof window === "undefined") return null;
1779
+ const winAny = window;
1780
+ winAny.MathJax = {
1781
+ ...winAny.MathJax ?? {},
1782
+ startup: { typeset: false },
1783
+ options: {
1784
+ enableMenu: false,
1785
+ enableEnrichment: false,
1786
+ enableSpeech: false,
1787
+ enableComplexity: false,
1788
+ sre: { speech: "none" }
1789
+ },
1790
+ // `fontCache: 'none'` inlines every glyph as a raw <path>
1791
+ // (slightly bigger SVG, no <use> references). Required for SVGs
1792
+ // we extract to a Blob URL and rasterize via <img> — `<use>`
1793
+ // refs to <defs> elsewhere in the page wouldn't resolve.
1794
+ // `linebreaks: { inline: false }` keeps the whole formula in one
1795
+ // <svg> element (v4 defaults to true for long inline math).
1796
+ svg: {
1797
+ scale: 1,
1798
+ fontCache: "none",
1799
+ linebreaks: { inline: false }
1800
+ }
1801
+ };
1802
+ const VENDOR_URL = "https://cdn.jsdelivr.net/npm/mathjax@4/tex-svg.js";
1803
+ await new Promise((resolve, reject) => {
1804
+ const existing = document.querySelector(
1805
+ `script[src="${VENDOR_URL}"]`
1806
+ );
1807
+ if (existing) {
1808
+ existing.addEventListener("load", () => resolve(), { once: true });
1809
+ existing.addEventListener("error", () => reject(new Error("MathJax CDN load failed")), {
1810
+ once: true
1811
+ });
1812
+ return;
1813
+ }
1814
+ const script = document.createElement("script");
1815
+ script.src = VENDOR_URL;
1816
+ script.async = true;
1817
+ script.onload = () => resolve();
1818
+ script.onerror = () => reject(new Error("MathJax CDN load failed"));
1819
+ document.head.appendChild(script);
1820
+ });
1821
+ const mj = winAny.MathJax;
1822
+ if (!mj) throw new Error("MathJax did not install on window after import");
1823
+ if (typeof mj.tex2svgPromise !== "function") {
1824
+ throw new Error("MathJax loaded but tex2svgPromise is missing \u2014 wrong bundle?");
1825
+ }
1826
+ await mj.startup?.promise;
1827
+ return mj;
1828
+ };
1829
+
1830
+ // src/text/math/cache.ts
1831
+ var normalizeSize = (px) => Math.max(8, Math.round(px));
1832
+ var cache3 = /* @__PURE__ */ new Map();
1833
+ var compileQueue = [];
1834
+ var compileScheduled = false;
1835
+ var mathEpoch = 0;
1836
+ var epochSubscribers = /* @__PURE__ */ new Set();
1837
+ var getMathEpoch = () => mathEpoch;
1838
+ var subscribeMathEpoch = (cb) => {
1839
+ epochSubscribers.add(cb);
1840
+ return () => {
1841
+ epochSubscribers.delete(cb);
1842
+ };
1843
+ };
1844
+ var bumpMathEpoch = () => {
1845
+ mathEpoch += 1;
1846
+ for (const cb of epochSubscribers) cb();
1847
+ };
1848
+ var getMathBitmap = (source, color, sizePx) => {
1849
+ const size = normalizeSize(sizePx);
1850
+ const key = `${size}:${color}:${source}`;
1851
+ const existing = cache3.get(key);
1852
+ if (existing) {
1853
+ if (existing.state === "ready") return existing.bitmap;
1854
+ return null;
1855
+ }
1856
+ cache3.set(key, { state: "pending" });
1857
+ compileQueue.push({ key, source, color, sizePx: size });
1858
+ scheduleCompile();
1859
+ return null;
1860
+ };
1861
+ var scheduleCompile = () => {
1862
+ if (compileScheduled) return;
1863
+ compileScheduled = true;
1864
+ if (typeof window === "undefined" || typeof requestAnimationFrame === "undefined") {
1865
+ void drainQueue();
1866
+ return;
1867
+ }
1868
+ requestAnimationFrame(() => {
1869
+ void drainQueue();
1870
+ });
1871
+ };
1872
+ var drainQueue = async () => {
1873
+ compileScheduled = false;
1874
+ if (compileQueue.length === 0) return;
1875
+ const mj = getMathJax();
1876
+ if (!mj) {
1877
+ onMathJaxReady(() => scheduleCompile());
1878
+ return;
1879
+ }
1880
+ const FRAME_BUDGET_MS = 4;
1881
+ const start = performance.now();
1882
+ let didResolve = false;
1883
+ while (compileQueue.length > 0 && performance.now() - start < FRAME_BUDGET_MS) {
1884
+ const item = compileQueue.shift();
1885
+ if (cache3.get(item.key)?.state !== "pending") continue;
1886
+ try {
1887
+ const bitmap = await compileOne(mj, item.source, item.color, item.sizePx);
1888
+ cache3.set(item.key, { state: "ready", bitmap });
1889
+ didResolve = true;
1890
+ } catch (err) {
1891
+ cache3.set(item.key, { state: "error", err });
1892
+ console.warn(`[math] failed to compile "${item.source}":`, err);
1893
+ }
1894
+ }
1895
+ if (didResolve) bumpMathEpoch();
1896
+ if (compileQueue.length > 0) scheduleCompile();
1897
+ };
1898
+ var compileOne = async (mj, source, color, sizePx) => {
1899
+ const svgElement = await mj.tex2svgPromise(source, { display: false, em: sizePx, ex: sizePx / 2 });
1900
+ let markup = mj.startup.adaptor.serializeXML ? mj.startup.adaptor.serializeXML(svgElement) : mj.startup.adaptor.outerHTML(svgElement);
1901
+ const svgMatch = /<svg[\s\S]*?<\/svg>/.exec(markup);
1902
+ if (svgMatch) markup = svgMatch[0];
1903
+ markup = markup.replace(/\sdata-semantic-[a-z0-9-]+="[^"]*"/g, "").replace(/\sdata-speech-[a-z0-9-]+="[^"]*"/g, "").replace(/\sdata-mml-node="[^"]*"/g, "").replace(/\sdata-latex="[^"]*"/g, "").replace(/\sdata-braille[a-z0-9-]*="[^"]*"/g, "").replace(/\saria-[a-z0-9-]+="[^"]*"/g, "").replace(/\srole="[^"]*"/g, "").replace(/\sfocusable="[^"]*"/g, "").replace(/\stabindex="[^"]*"/g, "").replace(/\shas-speech="[^"]*"/g, "");
1904
+ if (!markup.includes('xmlns="http://www.w3.org/2000/svg"')) {
1905
+ markup = markup.replace(/^<svg\b/, '<svg xmlns="http://www.w3.org/2000/svg"');
1906
+ }
1907
+ markup = markup.replace(/currentColor/gi, color);
1908
+ const dims = parseSvgDims(markup, sizePx);
1909
+ const blob = new Blob([markup], { type: "image/svg+xml" });
1910
+ const url = URL.createObjectURL(blob);
1911
+ try {
1912
+ let img;
1913
+ try {
1914
+ img = await loadImage(url);
1915
+ } catch (e) {
1916
+ console.warn(`[math] SVG failed to load for "${source}":
1917
+ ${markup}`);
1918
+ throw e;
1919
+ }
1920
+ const rasterW = Math.max(1, Math.ceil(dims.width * 2));
1921
+ const rasterH = Math.max(1, Math.ceil(dims.height * 2));
1922
+ const bitmap = await createImageBitmap(img, {
1923
+ resizeWidth: rasterW,
1924
+ resizeHeight: rasterH,
1925
+ resizeQuality: "high"
1926
+ });
1927
+ return {
1928
+ bitmap,
1929
+ width: dims.width,
1930
+ height: dims.height,
1931
+ baselineOffset: dims.baselineOffset
1932
+ };
1933
+ } finally {
1934
+ URL.revokeObjectURL(url);
1935
+ }
1936
+ };
1937
+ var loadImage = (src) => new Promise((resolve, reject) => {
1938
+ const img = new Image();
1939
+ img.onload = () => resolve(img);
1940
+ img.onerror = (e) => reject(e);
1941
+ img.src = src;
1942
+ });
1943
+ var parseSvgDims = (markup, sizePx) => {
1944
+ const exToPx = sizePx / 2;
1945
+ const widthMatch = /<svg[^>]*\bwidth="([0-9.]+)ex"/.exec(markup);
1946
+ const heightMatch = /<svg[^>]*\bheight="([0-9.]+)ex"/.exec(markup);
1947
+ const vAlignMatch = /vertical-align:\s*(-?[0-9.]+)ex/.exec(markup);
1948
+ const widthEx = widthMatch ? Number.parseFloat(widthMatch[1]) : 2;
1949
+ const heightEx = heightMatch ? Number.parseFloat(heightMatch[1]) : 2;
1950
+ const vAlignEx = vAlignMatch ? Number.parseFloat(vAlignMatch[1]) : 0;
1951
+ const width = widthEx * exToPx;
1952
+ const height = heightEx * exToPx;
1953
+ const descent = Math.abs(vAlignEx) * exToPx;
1954
+ const baselineOffset = height - descent;
1955
+ return { width, height, baselineOffset };
1956
+ };
1957
+ var clearMathCache = () => {
1958
+ cache3.clear();
1959
+ compileQueue.length = 0;
1960
+ compileScheduled = false;
1961
+ };
1962
+ var getMathCacheSize = () => cache3.size;
1963
+
1743
1964
  // src/text/measure.ts
1744
1965
  var MAX_WIDTH_CACHE_SIZE = 5e3;
1745
1966
  var measureCanvas = typeof document !== "undefined" ? document.createElement("canvas") : null;
@@ -1753,10 +1974,16 @@ var getCanvasFont = (opts) => {
1753
1974
  };
1754
1975
  var measureText = (opts) => {
1755
1976
  if (!opts.text) return 0;
1977
+ if (opts.type === "math") {
1978
+ const fontSizePx = FONT_SIZE_MAP[opts.fontSize];
1979
+ const bitmap = getMathBitmap(opts.text, DEFAULT_TEXT_COLOR, fontSizePx);
1980
+ if (bitmap) return bitmap.width;
1981
+ return Math.max(8, opts.text.length * fontSizePx * 0.55 + fontSizePx);
1982
+ }
1756
1983
  const font = getCanvasFont(opts);
1757
1984
  const key = `${font}|${opts.text}`;
1758
- const cached = widthCache.get(key);
1759
- if (cached !== void 0) return cached;
1985
+ const cached2 = widthCache.get(key);
1986
+ if (cached2 !== void 0) return cached2;
1760
1987
  if (!measureCtx) {
1761
1988
  return opts.text.length * FONT_SIZE_MAP[opts.fontSize] * 0.55;
1762
1989
  }
@@ -1898,6 +2125,8 @@ var layoutTokens = (tokens, opts) => {
1898
2125
  currentRuns.push({ text: chunk, type });
1899
2126
  cursorX += chunkWidth;
1900
2127
  };
2128
+ const fontSizePx = FONT_SIZE_MAP[opts.fontSize];
2129
+ const mathColor = opts.textColor || DEFAULT_TEXT_COLOR;
1901
2130
  for (const token of tokens) {
1902
2131
  if (token.type === "code-block") {
1903
2132
  pushCodeBlock(token.content);
@@ -1915,6 +2144,18 @@ var layoutTokens = (tokens, opts) => {
1915
2144
  pushRule(true);
1916
2145
  continue;
1917
2146
  }
2147
+ if (token.type === "math") {
2148
+ const bitmap = getMathBitmap(token.content, mathColor, fontSizePx);
2149
+ const width = bitmap ? bitmap.width : (
2150
+ // Placeholder: roughly proportional to source length so wrap
2151
+ // doesn't dramatically shift on resolve. Capped at maxWidth.
2152
+ Math.min(maxWidth, token.content.length * fontSizePx * 0.55 + fontSizePx)
2153
+ );
2154
+ if (cursorX > 0 && cursorX + width > maxWidth) pushLine();
2155
+ currentRuns.push({ text: token.content, type: "math" });
2156
+ cursorX += width;
2157
+ continue;
2158
+ }
1918
2159
  for (const chunk of splitChunks(token.content)) pushChunk(chunk, token.type);
1919
2160
  }
1920
2161
  if (currentRuns.length > 0 || lines.length === 0) {
@@ -2081,7 +2322,8 @@ var drawTextToCanvas = (ctx, opts) => {
2081
2322
  width: opts.width,
2082
2323
  fontFamily: opts.fontFamily,
2083
2324
  fontSize: opts.fontSize,
2084
- textStyle: opts.textStyle
2325
+ textStyle: opts.textStyle,
2326
+ textColor: opts.textColor
2085
2327
  });
2086
2328
  ctx.textBaseline = "alphabetic";
2087
2329
  ctx.fillStyle = opts.textColor || DEFAULT_TEXT_COLOR;
@@ -2189,6 +2431,24 @@ var drawTextToCanvas = (ctx, opts) => {
2189
2431
  fontSize: opts.fontSize,
2190
2432
  textStyle: opts.textStyle
2191
2433
  });
2434
+ if (run.type === "math") {
2435
+ const mathColor = opts.textColor || DEFAULT_TEXT_COLOR;
2436
+ const bitmap = getMathBitmap(run.text, mathColor, fontSizePx);
2437
+ if (bitmap) {
2438
+ ctx.drawImage(bitmap.bitmap, x, y - bitmap.baselineOffset, runWidth, bitmap.height);
2439
+ } else {
2440
+ ctx.save();
2441
+ ctx.fillStyle = "rgba(148, 163, 184, 0.18)";
2442
+ ctx.fillRect(x, y - fontSizePx + 2, runWidth, fontSizePx);
2443
+ ctx.fillStyle = "#94a3b8";
2444
+ ctx.font = `italic ${Math.max(8, fontSizePx * 0.75)}px system-ui, sans-serif`;
2445
+ ctx.textBaseline = "alphabetic";
2446
+ ctx.fillText("\u2026", x + runWidth / 2 - 4, y - 2);
2447
+ ctx.restore();
2448
+ }
2449
+ x += runWidth;
2450
+ continue;
2451
+ }
2192
2452
  if (run.type === "highlight") {
2193
2453
  ctx.save();
2194
2454
  ctx.fillStyle = opts.highlightColor || DEFAULT_HIGHLIGHT_COLOR;
@@ -2227,6 +2487,8 @@ subscribeFontEpoch(() => {
2227
2487
  renderCache.clear();
2228
2488
  textHashCache.clear();
2229
2489
  });
2490
+ subscribeMathEpoch(() => {
2491
+ });
2230
2492
  var getOrRenderTextBitmap = (req) => {
2231
2493
  const text = req.text;
2232
2494
  if (!text || !text.trim()) return null;
@@ -2235,10 +2497,10 @@ var getOrRenderTextBitmap = (req) => {
2235
2497
  const renderScale = resolveRenderScale(1, quantZoom, req.isMoving);
2236
2498
  const epoch = getFontEpoch();
2237
2499
  const key = makeKey(req, quantZoom, quantDpr, renderScale, epoch);
2238
- const cached = renderCache.get(key);
2239
- if (cached) {
2240
- cached.lastUsed = Date.now();
2241
- return { canvas: cached.canvas, width: cached.width, height: cached.height };
2500
+ const cached2 = renderCache.get(key);
2501
+ if (cached2) {
2502
+ cached2.lastUsed = Date.now();
2503
+ return { canvas: cached2.canvas, width: cached2.width, height: cached2.height };
2242
2504
  }
2243
2505
  const entry = drawIntoNewCanvas(req, quantDpr, renderScale);
2244
2506
  if (!entry) return null;
@@ -2247,7 +2509,8 @@ var getOrRenderTextBitmap = (req) => {
2247
2509
  return { canvas: entry.canvas, width: entry.width, height: entry.height };
2248
2510
  };
2249
2511
  var makeKey = (req, zoom, dpr, scale, epoch) => {
2250
- return `${epoch}:${cachedTextHash(req.text)}:${req.width}:${req.height}:${zoom}:${dpr}:${scale}:${req.align}:${req.fontFamily}:${req.fontSize}:${req.textStyle}:${req.textColor}:${req.highlightColor}`;
2512
+ const mathSuffix = req.text.includes("$") ? `:m${getMathEpoch()}` : "";
2513
+ return `${epoch}:${cachedTextHash(req.text)}:${req.width}:${req.height}:${zoom}:${dpr}:${scale}:${req.align}:${req.fontFamily}:${req.fontSize}:${req.textStyle}:${req.textColor}:${req.highlightColor}${mathSuffix}`;
2251
2514
  };
2252
2515
  var cachedTextHash = (value) => {
2253
2516
  const hit = textHashCache.get(value);
@@ -2303,12 +2566,12 @@ var clearTextBitmapCache = () => {
2303
2566
  };
2304
2567
  var getTextBitmapCacheSize = () => renderCache.size;
2305
2568
  var FREEHAND_CACHE_MAX = 500;
2306
- var cache3 = /* @__PURE__ */ new Map();
2569
+ var cache4 = /* @__PURE__ */ new Map();
2307
2570
  var remember = (key, path) => {
2308
- cache3.set(key, path);
2309
- if (cache3.size > FREEHAND_CACHE_MAX) {
2310
- const oldest = cache3.keys().next().value;
2311
- if (oldest !== void 0) cache3.delete(oldest);
2571
+ cache4.set(key, path);
2572
+ if (cache4.size > FREEHAND_CACHE_MAX) {
2573
+ const oldest = cache4.keys().next().value;
2574
+ if (oldest !== void 0) cache4.delete(oldest);
2312
2575
  }
2313
2576
  };
2314
2577
  var signaturePoints = (samples) => {
@@ -2360,10 +2623,10 @@ var outlineToPath2D = (ring) => {
2360
2623
  var getOrBuildFreehandPath = (samples, strokeWidth, seed) => {
2361
2624
  if (samples.length < 2) return null;
2362
2625
  const cacheKey = `${seed}|${strokeWidth.toFixed(2)}|${signaturePoints(samples)}`;
2363
- const hit = cache3.get(cacheKey);
2626
+ const hit = cache4.get(cacheKey);
2364
2627
  if (hit) {
2365
- cache3.delete(cacheKey);
2366
- cache3.set(cacheKey, hit);
2628
+ cache4.delete(cacheKey);
2629
+ cache4.set(cacheKey, hit);
2367
2630
  return hit;
2368
2631
  }
2369
2632
  const pts = buildPressurePoints(samples);
@@ -3242,8 +3505,15 @@ var createCanvasStore = (opts = {}) => {
3242
3505
  };
3243
3506
  const incidentEdges = /* @__PURE__ */ new Map();
3244
3507
  let topZ = 0;
3245
- for (const n of Object.values(initial.nodes)) if (n.z > topZ) topZ = n.z;
3246
- for (const e of Object.values(initial.edges)) if (e.z > topZ) topZ = e.z;
3508
+ let bottomZ = 0;
3509
+ for (const n of Object.values(initial.nodes)) {
3510
+ if (n.z > topZ) topZ = n.z;
3511
+ if (n.z < bottomZ) bottomZ = n.z;
3512
+ }
3513
+ for (const e of Object.values(initial.edges)) {
3514
+ if (e.z > topZ) topZ = e.z;
3515
+ if (e.z < bottomZ) bottomZ = e.z;
3516
+ }
3247
3517
  const getNodeForGeo = (id) => nodeAtoms.get(id)?.value;
3248
3518
  let currentBatchOps = null;
3249
3519
  let batchDepth = 0;
@@ -3332,6 +3602,8 @@ var createCanvasStore = (opts = {}) => {
3332
3602
  nodeAtoms.set(op.node.id, a);
3333
3603
  nodeIdsAtom.update((ids) => [...ids, op.node.id]);
3334
3604
  reindexNode(op.node);
3605
+ if (op.node.z > topZ) topZ = op.node.z;
3606
+ if (op.node.z < bottomZ) bottomZ = op.node.z;
3335
3607
  if (op.node.type === "frame") {
3336
3608
  frameOrderAtom.update((ids) => ids.includes(op.node.id) ? ids : [...ids, op.node.id]);
3337
3609
  }
@@ -3343,6 +3615,10 @@ var createCanvasStore = (opts = {}) => {
3343
3615
  const next = { ...a.value, ...op.patch };
3344
3616
  a.set(next);
3345
3617
  reindexNode(next);
3618
+ if (op.patch.z !== void 0) {
3619
+ if (op.patch.z > topZ) topZ = op.patch.z;
3620
+ if (op.patch.z < bottomZ) bottomZ = op.patch.z;
3621
+ }
3346
3622
  const incident = incidentEdges.get(op.id);
3347
3623
  if (incident) {
3348
3624
  for (const eid of incident) {
@@ -3371,6 +3647,8 @@ var createCanvasStore = (opts = {}) => {
3371
3647
  trackIncidence(op.edge);
3372
3648
  bumpEdgeVersion(op.edge.id);
3373
3649
  reindexEdge(op.edge);
3650
+ if (op.edge.z > topZ) topZ = op.edge.z;
3651
+ if (op.edge.z < bottomZ) bottomZ = op.edge.z;
3374
3652
  break;
3375
3653
  }
3376
3654
  case "edge.update": {
@@ -3383,6 +3661,10 @@ var createCanvasStore = (opts = {}) => {
3383
3661
  a.set(next);
3384
3662
  bumpEdgeVersion(op.id);
3385
3663
  reindexEdge(next);
3664
+ if (op.patch.z !== void 0) {
3665
+ if (op.patch.z > topZ) topZ = op.patch.z;
3666
+ if (op.patch.z < bottomZ) bottomZ = op.patch.z;
3667
+ }
3386
3668
  break;
3387
3669
  }
3388
3670
  case "edge.remove": {
@@ -3470,9 +3752,8 @@ var createCanvasStore = (opts = {}) => {
3470
3752
  clientId,
3471
3753
  generateId: () => idGenerator(),
3472
3754
  addNode(node) {
3473
- const withZ = node.z === 0 ? { ...node, z: ++topZ } : node;
3474
- if (withZ.z > topZ) topZ = withZ.z;
3475
- const fitted = withAutoFitHeight(withZ);
3755
+ const z = node.z ?? ++topZ;
3756
+ const fitted = withAutoFitHeight({ ...node, z });
3476
3757
  enqueueOp({ type: "node.add", node: fitted });
3477
3758
  return fitted.id;
3478
3759
  },
@@ -3535,7 +3816,6 @@ var createCanvasStore = (opts = {}) => {
3535
3816
  w,
3536
3817
  h,
3537
3818
  angle: 0,
3538
- z: 0,
3539
3819
  groups: [],
3540
3820
  style: opts2.style,
3541
3821
  data: { src, naturalW, naturalH, alt: opts2.alt }
@@ -3558,7 +3838,6 @@ var createCanvasStore = (opts = {}) => {
3558
3838
  w,
3559
3839
  h,
3560
3840
  angle: 0,
3561
- z: 0,
3562
3841
  groups: [],
3563
3842
  ...mergedStyle ? { style: mergedStyle } : {},
3564
3843
  data: { src: sanitized, alt: opts2.alt }
@@ -3566,8 +3845,8 @@ var createCanvasStore = (opts = {}) => {
3566
3845
  return id;
3567
3846
  },
3568
3847
  addEdge(edge) {
3569
- const withZ = edge.z === 0 ? { ...edge, z: ++topZ } : edge;
3570
- if (withZ.z > topZ) topZ = withZ.z;
3848
+ const z = edge.z ?? ++topZ;
3849
+ const withZ = { ...edge, z };
3571
3850
  enqueueOp({ type: "edge.add", edge: withZ });
3572
3851
  return withZ.id;
3573
3852
  },
@@ -3590,29 +3869,10 @@ var createCanvasStore = (opts = {}) => {
3590
3869
  });
3591
3870
  },
3592
3871
  sendToBack(ids) {
3593
- const targets = new Set(ids);
3594
- let minZ = 0;
3595
- let initialized = false;
3596
- for (const a of nodeAtoms.values()) {
3597
- if (targets.has(a.value.id)) continue;
3598
- if (!initialized || a.value.z < minZ) {
3599
- minZ = a.value.z;
3600
- initialized = true;
3601
- }
3602
- }
3603
- for (const a of edgeAtoms.values()) {
3604
- if (targets.has(a.value.id)) continue;
3605
- if (!initialized || a.value.z < minZ) {
3606
- minZ = a.value.z;
3607
- initialized = true;
3608
- }
3609
- }
3610
3872
  this.batch(() => {
3611
- let next = (initialized ? minZ : 0) - 1;
3612
3873
  for (const id of ids) {
3613
- if (nodeAtoms.has(id)) this.updateNode(id, { z: next });
3614
- else if (edgeAtoms.has(id)) this.updateEdge(id, { z: next });
3615
- next -= 1;
3874
+ if (nodeAtoms.has(id)) this.updateNode(id, { z: --bottomZ });
3875
+ else if (edgeAtoms.has(id)) this.updateEdge(id, { z: --bottomZ });
3616
3876
  }
3617
3877
  });
3618
3878
  },
@@ -3630,7 +3890,6 @@ var createCanvasStore = (opts = {}) => {
3630
3890
  if (currentZ === void 0) continue;
3631
3891
  const idx = binaryFirstGreater(allZ, currentZ);
3632
3892
  const nextZ = idx >= 0 ? allZ[idx] + 1 : currentZ + 1;
3633
- if (nextZ > topZ) topZ = nextZ;
3634
3893
  if (node) this.updateNode(id, { z: nextZ });
3635
3894
  else this.updateEdge(id, { z: nextZ });
3636
3895
  }
@@ -4223,11 +4482,11 @@ var paintPlaceholder = (ctx, w, h, label) => {
4223
4482
  ctx.fillText(label, w / 2, h / 2);
4224
4483
  }
4225
4484
  };
4226
- var paintImageNode = (ctx, node, cache4, theme) => {
4485
+ var paintImageNode = (ctx, node, cache5, theme) => {
4227
4486
  if (node.w <= 0 || node.h <= 0) return;
4228
4487
  const data = node.data;
4229
4488
  if (!data?.src) return;
4230
- const bitmap = cache4.getImage(data.src);
4489
+ const bitmap = cache5.getImage(data.src);
4231
4490
  const opacity = resolveOpacity(node.style, theme);
4232
4491
  const needsScope = opacity !== 1;
4233
4492
  if (needsScope) {
@@ -4241,13 +4500,13 @@ var paintImageNode = (ctx, node, cache4, theme) => {
4241
4500
  }
4242
4501
  if (needsScope) ctx.restore();
4243
4502
  };
4244
- var paintIconNode = (ctx, node, cache4, scale, theme) => {
4503
+ var paintIconNode = (ctx, node, cache5, scale, theme) => {
4245
4504
  if (node.w <= 0 || node.h <= 0) return;
4246
4505
  const data = node.data;
4247
4506
  if (!data?.src) return;
4248
4507
  const sizePx = Math.max(node.w, node.h) * scale;
4249
4508
  const color = node.style?.iconColor;
4250
- const bitmap = cache4.getIcon(data.src, color, sizePx);
4509
+ const bitmap = cache5.getIcon(data.src, color, sizePx);
4251
4510
  const opacity = resolveOpacity(node.style, theme);
4252
4511
  const needsScope = opacity !== 1;
4253
4512
  if (needsScope) {
@@ -4291,7 +4550,7 @@ var paintBackground = (ctx, opts) => {
4291
4550
  }
4292
4551
  };
4293
4552
  var paintDots = (ctx, minX, minY, maxX, maxY, gap, color, zoom) => {
4294
- const sizeWorld = Math.max(1, 2.4 / zoom);
4553
+ const sizeWorld = Math.max(1, 1.6 / zoom);
4295
4554
  const half = sizeWorld / 2;
4296
4555
  ctx.save();
4297
4556
  ctx.fillStyle = color;
@@ -5072,6 +5331,10 @@ var createRenderer = (opts) => {
5072
5331
  staticDirty = true;
5073
5332
  loop.requestFrame();
5074
5333
  });
5334
+ const unsubMathEpoch = subscribeMathEpoch(() => {
5335
+ staticDirty = true;
5336
+ loop.requestFrame();
5337
+ });
5075
5338
  return {
5076
5339
  start() {
5077
5340
  loop.start();
@@ -5121,6 +5384,7 @@ var createRenderer = (opts) => {
5121
5384
  unsubSelection();
5122
5385
  unsubInteraction();
5123
5386
  unsubFontEpoch();
5387
+ unsubMathEpoch();
5124
5388
  assetCache.dispose();
5125
5389
  }
5126
5390
  };
@@ -6153,6 +6417,6 @@ var installedExtensions = (store) => {
6153
6417
  // src/index.ts
6154
6418
  var VERSION = "0.0.0";
6155
6419
 
6156
- export { BEZIER_SEGMENTS, CODE_BG_COLOR, CODE_BLOCK_MARGIN_Y, CODE_BLOCK_PADDING_X, CONTENT_HEIGHT_BUFFER, CONTENT_PADDING, DEFAULT_BACKGROUND, DEFAULT_CAMERA, DEFAULT_HIGHLIGHT_COLOR, DEFAULT_HIGHLIGHT_COLOR_DARK, DEFAULT_MINIMAP_MAX_NODES, DEFAULT_STYLE, DEFAULT_TEXT_COLOR, EDGE_HANDLE_SLOP_PX, EDGE_HIT_SLOP_PX, EdgeGeometryCache, FONT_FAMILY_MAP, FONT_SIZE_MAP, LINE_HEIGHT_MAP, LINK_COLOR, MAX_IMAGE_BYTES, MAX_SVG_BYTES, MAX_ZOOM, MIN_ZOOM, PALM_REJECTION_GRACE_MS, RESIZE_HANDLES, RESIZE_HANDLE_SIZE_PX, ROTATE_HANDLE_OFFSET_PX, ROTATE_HANDLE_RADIUS_PX, SCHEMA_VERSION, UniformGrid, VERSION, applyCameraTransform, applySvgColor, arrowheadLength, asBatchId, asClientId, asEdgeId, asGroupId, asNodeId, attachSync, autoRouteControls, blobToDataUri, clampEffectiveScale, clampZoom, clearMeasureCache, clearSurface, clearTextBitmapCache, clipSamples, computeAutoFitHeight, computeEdgeGeometry, copy, createCanvasStore, createDefaultTextareaEditor, createFrameLoop, createPalmRejectionState, createRenderer, cubicBezier, cubicBezierTangent, cut, defineExtension, defineNode, deserializeClipboard, detectConflicts, downscaleImageBlob, drawArrowhead, drawEdge, drawMinimapViewport, drawShape, drawTextToCanvas, drawWithNodeTransform, edgeAABBFromSamples, edgeLabelBoundsWorld, emptyPresenceState, estimateMarkdownContentHeight, exportSelection, exportSelectionSvg, exportViewport, extractSvgDimensions, fromSerialized, fullVisibleClipResult, getCanvasFont, getContentHeight, getContext, getDpr, getFontEpoch, getMarkdownLineHeightPx, getOrRenderTextBitmap, getPointAndTangentAtArcLength, getTextBitmapCacheSize, handleEnter, handleWorldPositions, hitTestAny, hitTestEdge, hitTestHandles, hitTestPoint, hitTestRotateHandle, idleInteractionState, inflateRect, insertLink, installExtension, installedExtensions, inverseBatch, inverseOp, isAttached, isCanvasHarnessClipboard, isDrawablePrimitive, isMoving, isNodeRemoteEditing, layoutTokens, makeIdGenerator, marqueeNodes, measureText, midpointToCubicControls, minimapScreenToWorld, nodeAABB, nodeIntersectsRect, nodeLocalToWorld, notePenActive, notePenInactive, opSchemas, opSchemasAsAnthropicTools, paintBackground, panByScreen, paste, pointInNode, projectEndToWorld, projectToNodeBoundary, quantizeDpr, quantizeZoom, randomClientId, rectContainsPoint, rectFromPoints, rectsIntersect, registerMigrator, renderMinimapContent, resolveColor, resolveOpacity, resolveRenderScale, resolveStrokeWidth, rotateHandleWorldPosition, rotateVecByAngle, sampleBezier, sampleSelfLoop, samplesFor, sanitizeSvg, sceneBounds, screenToWorld, selfLoopGeometry, serializeSelection, setupSurface, shouldAutoFit, shouldRejectTouch, sideNormalLocal, sideOf, sizeSurface, storeToJSON, subscribeFontEpoch, tangentAtArcLength, toImageBlob, toSerialized, toggleBold, toggleCode, toggleItalic, toggleStrike, toggleUnderline, tokenize, unionRects, validateImageInput, validateSvgMarkup, viewportWorldRect, withAutoFitHeight, worldToNodeLocal, worldToScreen, worldViewport, worldViewportFromCamera, zoomAtScreenPoint };
6420
+ export { BEZIER_SEGMENTS, CODE_BG_COLOR, CODE_BLOCK_MARGIN_Y, CODE_BLOCK_PADDING_X, CONTENT_HEIGHT_BUFFER, CONTENT_PADDING, DEFAULT_BACKGROUND, DEFAULT_CAMERA, DEFAULT_HIGHLIGHT_COLOR, DEFAULT_HIGHLIGHT_COLOR_DARK, DEFAULT_MINIMAP_MAX_NODES, DEFAULT_STYLE, DEFAULT_TEXT_COLOR, EDGE_HANDLE_SLOP_PX, EDGE_HIT_SLOP_PX, EdgeGeometryCache, FONT_FAMILY_MAP, FONT_SIZE_MAP, LINE_HEIGHT_MAP, LINK_COLOR, MAX_IMAGE_BYTES, MAX_SVG_BYTES, MAX_ZOOM, MIN_ZOOM, PALM_REJECTION_GRACE_MS, RESIZE_HANDLES, RESIZE_HANDLE_SIZE_PX, ROTATE_HANDLE_OFFSET_PX, ROTATE_HANDLE_RADIUS_PX, SCHEMA_VERSION, UniformGrid, VERSION, applyCameraTransform, applySvgColor, arrowheadLength, asBatchId, asClientId, asEdgeId, asGroupId, asNodeId, attachSync, autoRouteControls, blobToDataUri, clampEffectiveScale, clampZoom, clearMathCache, clearMeasureCache, clearSurface, clearTextBitmapCache, clipSamples, computeAutoFitHeight, computeEdgeGeometry, copy, createCanvasStore, createDefaultTextareaEditor, createFrameLoop, createPalmRejectionState, createRenderer, cubicBezier, cubicBezierTangent, cut, defineExtension, defineNode, deserializeClipboard, detectConflicts, downscaleImageBlob, drawArrowhead, drawEdge, drawMinimapViewport, drawShape, drawTextToCanvas, drawWithNodeTransform, edgeAABBFromSamples, edgeLabelBoundsWorld, emptyPresenceState, estimateMarkdownContentHeight, exportSelection, exportSelectionSvg, exportViewport, extractSvgDimensions, fromSerialized, fullVisibleClipResult, getCanvasFont, getContentHeight, getContext, getDpr, getFontEpoch, getMarkdownLineHeightPx, getMathBitmap, getMathCacheSize, getMathEpoch, getMathJax, getOrRenderTextBitmap, getPointAndTangentAtArcLength, getTextBitmapCacheSize, handleEnter, handleWorldPositions, hitTestAny, hitTestEdge, hitTestHandles, hitTestPoint, hitTestRotateHandle, idleInteractionState, inflateRect, insertLink, installExtension, installedExtensions, inverseBatch, inverseOp, isAttached, isCanvasHarnessClipboard, isDrawablePrimitive, isMoving, isNodeRemoteEditing, layoutTokens, makeIdGenerator, marqueeNodes, measureText, midpointToCubicControls, minimapScreenToWorld, nodeAABB, nodeIntersectsRect, nodeLocalToWorld, notePenActive, notePenInactive, onMathJaxReady, opSchemas, opSchemasAsAnthropicTools, paintBackground, panByScreen, paste, pointInNode, projectEndToWorld, projectToNodeBoundary, quantizeDpr, quantizeZoom, randomClientId, rectContainsPoint, rectFromPoints, rectsIntersect, registerMigrator, renderMinimapContent, resolveColor, resolveOpacity, resolveRenderScale, resolveStrokeWidth, rotateHandleWorldPosition, rotateVecByAngle, sampleBezier, sampleSelfLoop, samplesFor, sanitizeSvg, sceneBounds, screenToWorld, selfLoopGeometry, serializeSelection, setupSurface, shouldAutoFit, shouldRejectTouch, sideNormalLocal, sideOf, sizeSurface, storeToJSON, subscribeFontEpoch, subscribeMathEpoch, tangentAtArcLength, toImageBlob, toSerialized, toggleBold, toggleCode, toggleItalic, toggleStrike, toggleUnderline, tokenize, unionRects, validateImageInput, validateSvgMarkup, viewportWorldRect, withAutoFitHeight, worldToNodeLocal, worldToScreen, worldViewport, worldViewportFromCamera, zoomAtScreenPoint };
6157
6421
  //# sourceMappingURL=index.js.map
6158
6422
  //# sourceMappingURL=index.js.map