@canvas-harness/core 0.1.0 → 0.1.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.
package/dist/index.cjs CHANGED
@@ -635,8 +635,8 @@ var EdgeGeometryCache = class {
635
635
  * Caller is responsible for passing the current store-managed version.
636
636
  */
637
637
  get(edge, version, getNode) {
638
- const cached = this.entries.get(edge.id);
639
- if (cached && cached.version === version) return cached.geom;
638
+ const cached2 = this.entries.get(edge.id);
639
+ if (cached2 && cached2.version === version) return cached2.geom;
640
640
  const geom = computeEdgeGeometry(edge, getNode);
641
641
  if (geom) this.entries.set(edge.id, { version, geom });
642
642
  else this.entries.delete(edge.id);
@@ -848,8 +848,8 @@ var mixHex = (a, b, t) => {
848
848
  };
849
849
  var darkenCache = /* @__PURE__ */ new Map();
850
850
  var darkenHex = (hex) => {
851
- const cached = darkenCache.get(hex);
852
- if (cached !== void 0) return cached;
851
+ const cached2 = darkenCache.get(hex);
852
+ if (cached2 !== void 0) return cached2;
853
853
  const result = mixHex(hex, "#000000", TONE_BLEND);
854
854
  darkenCache.set(hex, result);
855
855
  return result;
@@ -1616,7 +1616,7 @@ var buildPath = (type, x, y, w, h, radius) => {
1616
1616
  };
1617
1617
 
1618
1618
  // src/text/tokens.ts
1619
- var INLINE_PATTERN = /(\*\*[^*]+\*\*|==[^=\s](?:[^=]*?[^=\s])?==|`[^`]+`|\*[^*]+\*|__[^_]+__|~~[^~]+~~|_[^_]+_|\[[^\]]+\]\([^)]+\))/g;
1619
+ var INLINE_PATTERN = /(\$[^$\n]+?\$|\*\*[^*]+\*\*|==[^=\s](?:[^=]*?[^=\s])?==|`[^`]+`|\*[^*]+\*|__[^_]+__|~~[^~]+~~|_[^_]+_|\[[^\]]+\]\([^)]+\))/g;
1620
1620
  var HR_LINE_PATTERN = /^[ \t]*---[ \t]*$/;
1621
1621
  var DOUBLE_HR_LINE_PATTERN = /^[ \t]*===[ \t]*$/;
1622
1622
  var transformSymbols = (value) => value.replace(/<=>|<->|<-|->|\[\]|\[[vx]\]/gi, (match) => {
@@ -1654,6 +1654,8 @@ var tokenizeInline = (segment) => {
1654
1654
  tokens.push({ type: "link", content: transformSymbols(match.slice(1, splitIndex)) });
1655
1655
  } else if (match.startsWith("`") && match.endsWith("`")) {
1656
1656
  tokens.push({ type: "code", content: match.slice(1, -1) });
1657
+ } else if (match.startsWith("$") && match.endsWith("$")) {
1658
+ tokens.push({ type: "math", content: match.slice(1, -1) });
1657
1659
  } else {
1658
1660
  tokens.push({ type: "text", content: transformSymbols(match) });
1659
1661
  }
@@ -1742,6 +1744,225 @@ var DEFAULT_HIGHLIGHT_COLOR_DARK = "#6b5a23";
1742
1744
  var LINK_COLOR = "#2563eb";
1743
1745
  var CODE_BG_COLOR = "rgba(148, 163, 184, 0.18)";
1744
1746
 
1747
+ // src/text/math/loader.ts
1748
+ var cached = null;
1749
+ var loadPromise2 = null;
1750
+ var loadFailed = false;
1751
+ var readyCallbacks2 = /* @__PURE__ */ new Set();
1752
+ var getMathJax = () => {
1753
+ if (cached) return cached;
1754
+ if (loadFailed) return null;
1755
+ if (!loadPromise2) {
1756
+ loadPromise2 = loadMathJax().then((instance) => {
1757
+ if (instance) {
1758
+ cached = instance;
1759
+ for (const cb of readyCallbacks2) cb();
1760
+ } else {
1761
+ loadFailed = true;
1762
+ }
1763
+ readyCallbacks2.clear();
1764
+ return cached;
1765
+ }).catch((err) => {
1766
+ console.warn("[math] failed to load MathJax:", err);
1767
+ loadFailed = true;
1768
+ readyCallbacks2.clear();
1769
+ return null;
1770
+ });
1771
+ }
1772
+ return null;
1773
+ };
1774
+ var onMathJaxReady = (cb) => {
1775
+ if (cached) return;
1776
+ if (loadFailed) return;
1777
+ readyCallbacks2.add(cb);
1778
+ };
1779
+ var loadMathJax = async () => {
1780
+ if (typeof window === "undefined") return null;
1781
+ const winAny = window;
1782
+ winAny.MathJax = {
1783
+ ...winAny.MathJax ?? {},
1784
+ startup: { typeset: false },
1785
+ options: {
1786
+ enableMenu: false,
1787
+ enableEnrichment: false,
1788
+ enableSpeech: false,
1789
+ enableComplexity: false,
1790
+ sre: { speech: "none" }
1791
+ },
1792
+ // `fontCache: 'none'` inlines every glyph as a raw <path>
1793
+ // (slightly bigger SVG, no <use> references). Required for SVGs
1794
+ // we extract to a Blob URL and rasterize via <img> — `<use>`
1795
+ // refs to <defs> elsewhere in the page wouldn't resolve.
1796
+ // `linebreaks: { inline: false }` keeps the whole formula in one
1797
+ // <svg> element (v4 defaults to true for long inline math).
1798
+ svg: {
1799
+ scale: 1,
1800
+ fontCache: "none",
1801
+ linebreaks: { inline: false }
1802
+ }
1803
+ };
1804
+ const VENDOR_URL = "https://cdn.jsdelivr.net/npm/mathjax@4/tex-svg.js";
1805
+ await new Promise((resolve, reject) => {
1806
+ const existing = document.querySelector(
1807
+ `script[src="${VENDOR_URL}"]`
1808
+ );
1809
+ if (existing) {
1810
+ existing.addEventListener("load", () => resolve(), { once: true });
1811
+ existing.addEventListener("error", () => reject(new Error("MathJax CDN load failed")), {
1812
+ once: true
1813
+ });
1814
+ return;
1815
+ }
1816
+ const script = document.createElement("script");
1817
+ script.src = VENDOR_URL;
1818
+ script.async = true;
1819
+ script.onload = () => resolve();
1820
+ script.onerror = () => reject(new Error("MathJax CDN load failed"));
1821
+ document.head.appendChild(script);
1822
+ });
1823
+ const mj = winAny.MathJax;
1824
+ if (!mj) throw new Error("MathJax did not install on window after import");
1825
+ if (typeof mj.tex2svgPromise !== "function") {
1826
+ throw new Error("MathJax loaded but tex2svgPromise is missing \u2014 wrong bundle?");
1827
+ }
1828
+ await mj.startup?.promise;
1829
+ return mj;
1830
+ };
1831
+
1832
+ // src/text/math/cache.ts
1833
+ var normalizeSize = (px) => Math.max(8, Math.round(px));
1834
+ var cache3 = /* @__PURE__ */ new Map();
1835
+ var compileQueue = [];
1836
+ var compileScheduled = false;
1837
+ var mathEpoch = 0;
1838
+ var epochSubscribers = /* @__PURE__ */ new Set();
1839
+ var getMathEpoch = () => mathEpoch;
1840
+ var subscribeMathEpoch = (cb) => {
1841
+ epochSubscribers.add(cb);
1842
+ return () => {
1843
+ epochSubscribers.delete(cb);
1844
+ };
1845
+ };
1846
+ var bumpMathEpoch = () => {
1847
+ mathEpoch += 1;
1848
+ for (const cb of epochSubscribers) cb();
1849
+ };
1850
+ var getMathBitmap = (source, color, sizePx) => {
1851
+ const size = normalizeSize(sizePx);
1852
+ const key = `${size}:${color}:${source}`;
1853
+ const existing = cache3.get(key);
1854
+ if (existing) {
1855
+ if (existing.state === "ready") return existing.bitmap;
1856
+ return null;
1857
+ }
1858
+ cache3.set(key, { state: "pending" });
1859
+ compileQueue.push({ key, source, color, sizePx: size });
1860
+ scheduleCompile();
1861
+ return null;
1862
+ };
1863
+ var scheduleCompile = () => {
1864
+ if (compileScheduled) return;
1865
+ compileScheduled = true;
1866
+ if (typeof window === "undefined" || typeof requestAnimationFrame === "undefined") {
1867
+ void drainQueue();
1868
+ return;
1869
+ }
1870
+ requestAnimationFrame(() => {
1871
+ void drainQueue();
1872
+ });
1873
+ };
1874
+ var drainQueue = async () => {
1875
+ compileScheduled = false;
1876
+ if (compileQueue.length === 0) return;
1877
+ const mj = getMathJax();
1878
+ if (!mj) {
1879
+ onMathJaxReady(() => scheduleCompile());
1880
+ return;
1881
+ }
1882
+ const FRAME_BUDGET_MS = 4;
1883
+ const start = performance.now();
1884
+ let didResolve = false;
1885
+ while (compileQueue.length > 0 && performance.now() - start < FRAME_BUDGET_MS) {
1886
+ const item = compileQueue.shift();
1887
+ if (cache3.get(item.key)?.state !== "pending") continue;
1888
+ try {
1889
+ const bitmap = await compileOne(mj, item.source, item.color, item.sizePx);
1890
+ cache3.set(item.key, { state: "ready", bitmap });
1891
+ didResolve = true;
1892
+ } catch (err) {
1893
+ cache3.set(item.key, { state: "error", err });
1894
+ console.warn(`[math] failed to compile "${item.source}":`, err);
1895
+ }
1896
+ }
1897
+ if (didResolve) bumpMathEpoch();
1898
+ if (compileQueue.length > 0) scheduleCompile();
1899
+ };
1900
+ var compileOne = async (mj, source, color, sizePx) => {
1901
+ const svgElement = await mj.tex2svgPromise(source, { display: false, em: sizePx, ex: sizePx / 2 });
1902
+ let markup = mj.startup.adaptor.serializeXML ? mj.startup.adaptor.serializeXML(svgElement) : mj.startup.adaptor.outerHTML(svgElement);
1903
+ const svgMatch = /<svg[\s\S]*?<\/svg>/.exec(markup);
1904
+ if (svgMatch) markup = svgMatch[0];
1905
+ 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, "");
1906
+ if (!markup.includes('xmlns="http://www.w3.org/2000/svg"')) {
1907
+ markup = markup.replace(/^<svg\b/, '<svg xmlns="http://www.w3.org/2000/svg"');
1908
+ }
1909
+ markup = markup.replace(/currentColor/gi, color);
1910
+ const dims = parseSvgDims(markup, sizePx);
1911
+ const blob = new Blob([markup], { type: "image/svg+xml" });
1912
+ const url = URL.createObjectURL(blob);
1913
+ try {
1914
+ let img;
1915
+ try {
1916
+ img = await loadImage(url);
1917
+ } catch (e) {
1918
+ console.warn(`[math] SVG failed to load for "${source}":
1919
+ ${markup}`);
1920
+ throw e;
1921
+ }
1922
+ const rasterW = Math.max(1, Math.ceil(dims.width * 2));
1923
+ const rasterH = Math.max(1, Math.ceil(dims.height * 2));
1924
+ const bitmap = await createImageBitmap(img, {
1925
+ resizeWidth: rasterW,
1926
+ resizeHeight: rasterH,
1927
+ resizeQuality: "high"
1928
+ });
1929
+ return {
1930
+ bitmap,
1931
+ width: dims.width,
1932
+ height: dims.height,
1933
+ baselineOffset: dims.baselineOffset
1934
+ };
1935
+ } finally {
1936
+ URL.revokeObjectURL(url);
1937
+ }
1938
+ };
1939
+ var loadImage = (src) => new Promise((resolve, reject) => {
1940
+ const img = new Image();
1941
+ img.onload = () => resolve(img);
1942
+ img.onerror = (e) => reject(e);
1943
+ img.src = src;
1944
+ });
1945
+ var parseSvgDims = (markup, sizePx) => {
1946
+ const exToPx = sizePx / 2;
1947
+ const widthMatch = /<svg[^>]*\bwidth="([0-9.]+)ex"/.exec(markup);
1948
+ const heightMatch = /<svg[^>]*\bheight="([0-9.]+)ex"/.exec(markup);
1949
+ const vAlignMatch = /vertical-align:\s*(-?[0-9.]+)ex/.exec(markup);
1950
+ const widthEx = widthMatch ? Number.parseFloat(widthMatch[1]) : 2;
1951
+ const heightEx = heightMatch ? Number.parseFloat(heightMatch[1]) : 2;
1952
+ const vAlignEx = vAlignMatch ? Number.parseFloat(vAlignMatch[1]) : 0;
1953
+ const width = widthEx * exToPx;
1954
+ const height = heightEx * exToPx;
1955
+ const descent = Math.abs(vAlignEx) * exToPx;
1956
+ const baselineOffset = height - descent;
1957
+ return { width, height, baselineOffset };
1958
+ };
1959
+ var clearMathCache = () => {
1960
+ cache3.clear();
1961
+ compileQueue.length = 0;
1962
+ compileScheduled = false;
1963
+ };
1964
+ var getMathCacheSize = () => cache3.size;
1965
+
1745
1966
  // src/text/measure.ts
1746
1967
  var MAX_WIDTH_CACHE_SIZE = 5e3;
1747
1968
  var measureCanvas = typeof document !== "undefined" ? document.createElement("canvas") : null;
@@ -1755,10 +1976,16 @@ var getCanvasFont = (opts) => {
1755
1976
  };
1756
1977
  var measureText = (opts) => {
1757
1978
  if (!opts.text) return 0;
1979
+ if (opts.type === "math") {
1980
+ const fontSizePx = FONT_SIZE_MAP[opts.fontSize];
1981
+ const bitmap = getMathBitmap(opts.text, DEFAULT_TEXT_COLOR, fontSizePx);
1982
+ if (bitmap) return bitmap.width;
1983
+ return Math.max(8, opts.text.length * fontSizePx * 0.55 + fontSizePx);
1984
+ }
1758
1985
  const font = getCanvasFont(opts);
1759
1986
  const key = `${font}|${opts.text}`;
1760
- const cached = widthCache.get(key);
1761
- if (cached !== void 0) return cached;
1987
+ const cached2 = widthCache.get(key);
1988
+ if (cached2 !== void 0) return cached2;
1762
1989
  if (!measureCtx) {
1763
1990
  return opts.text.length * FONT_SIZE_MAP[opts.fontSize] * 0.55;
1764
1991
  }
@@ -1900,6 +2127,8 @@ var layoutTokens = (tokens, opts) => {
1900
2127
  currentRuns.push({ text: chunk, type });
1901
2128
  cursorX += chunkWidth;
1902
2129
  };
2130
+ const fontSizePx = FONT_SIZE_MAP[opts.fontSize];
2131
+ const mathColor = opts.textColor || DEFAULT_TEXT_COLOR;
1903
2132
  for (const token of tokens) {
1904
2133
  if (token.type === "code-block") {
1905
2134
  pushCodeBlock(token.content);
@@ -1917,6 +2146,18 @@ var layoutTokens = (tokens, opts) => {
1917
2146
  pushRule(true);
1918
2147
  continue;
1919
2148
  }
2149
+ if (token.type === "math") {
2150
+ const bitmap = getMathBitmap(token.content, mathColor, fontSizePx);
2151
+ const width = bitmap ? bitmap.width : (
2152
+ // Placeholder: roughly proportional to source length so wrap
2153
+ // doesn't dramatically shift on resolve. Capped at maxWidth.
2154
+ Math.min(maxWidth, token.content.length * fontSizePx * 0.55 + fontSizePx)
2155
+ );
2156
+ if (cursorX > 0 && cursorX + width > maxWidth) pushLine();
2157
+ currentRuns.push({ text: token.content, type: "math" });
2158
+ cursorX += width;
2159
+ continue;
2160
+ }
1920
2161
  for (const chunk of splitChunks(token.content)) pushChunk(chunk, token.type);
1921
2162
  }
1922
2163
  if (currentRuns.length > 0 || lines.length === 0) {
@@ -2083,7 +2324,8 @@ var drawTextToCanvas = (ctx, opts) => {
2083
2324
  width: opts.width,
2084
2325
  fontFamily: opts.fontFamily,
2085
2326
  fontSize: opts.fontSize,
2086
- textStyle: opts.textStyle
2327
+ textStyle: opts.textStyle,
2328
+ textColor: opts.textColor
2087
2329
  });
2088
2330
  ctx.textBaseline = "alphabetic";
2089
2331
  ctx.fillStyle = opts.textColor || DEFAULT_TEXT_COLOR;
@@ -2191,6 +2433,24 @@ var drawTextToCanvas = (ctx, opts) => {
2191
2433
  fontSize: opts.fontSize,
2192
2434
  textStyle: opts.textStyle
2193
2435
  });
2436
+ if (run.type === "math") {
2437
+ const mathColor = opts.textColor || DEFAULT_TEXT_COLOR;
2438
+ const bitmap = getMathBitmap(run.text, mathColor, fontSizePx);
2439
+ if (bitmap) {
2440
+ ctx.drawImage(bitmap.bitmap, x, y - bitmap.baselineOffset, runWidth, bitmap.height);
2441
+ } else {
2442
+ ctx.save();
2443
+ ctx.fillStyle = "rgba(148, 163, 184, 0.18)";
2444
+ ctx.fillRect(x, y - fontSizePx + 2, runWidth, fontSizePx);
2445
+ ctx.fillStyle = "#94a3b8";
2446
+ ctx.font = `italic ${Math.max(8, fontSizePx * 0.75)}px system-ui, sans-serif`;
2447
+ ctx.textBaseline = "alphabetic";
2448
+ ctx.fillText("\u2026", x + runWidth / 2 - 4, y - 2);
2449
+ ctx.restore();
2450
+ }
2451
+ x += runWidth;
2452
+ continue;
2453
+ }
2194
2454
  if (run.type === "highlight") {
2195
2455
  ctx.save();
2196
2456
  ctx.fillStyle = opts.highlightColor || DEFAULT_HIGHLIGHT_COLOR;
@@ -2229,6 +2489,8 @@ subscribeFontEpoch(() => {
2229
2489
  renderCache.clear();
2230
2490
  textHashCache.clear();
2231
2491
  });
2492
+ subscribeMathEpoch(() => {
2493
+ });
2232
2494
  var getOrRenderTextBitmap = (req) => {
2233
2495
  const text = req.text;
2234
2496
  if (!text || !text.trim()) return null;
@@ -2237,10 +2499,10 @@ var getOrRenderTextBitmap = (req) => {
2237
2499
  const renderScale = resolveRenderScale(1, quantZoom, req.isMoving);
2238
2500
  const epoch = getFontEpoch();
2239
2501
  const key = makeKey(req, quantZoom, quantDpr, renderScale, epoch);
2240
- const cached = renderCache.get(key);
2241
- if (cached) {
2242
- cached.lastUsed = Date.now();
2243
- return { canvas: cached.canvas, width: cached.width, height: cached.height };
2502
+ const cached2 = renderCache.get(key);
2503
+ if (cached2) {
2504
+ cached2.lastUsed = Date.now();
2505
+ return { canvas: cached2.canvas, width: cached2.width, height: cached2.height };
2244
2506
  }
2245
2507
  const entry = drawIntoNewCanvas(req, quantDpr, renderScale);
2246
2508
  if (!entry) return null;
@@ -2249,7 +2511,8 @@ var getOrRenderTextBitmap = (req) => {
2249
2511
  return { canvas: entry.canvas, width: entry.width, height: entry.height };
2250
2512
  };
2251
2513
  var makeKey = (req, zoom, dpr, scale, epoch) => {
2252
- return `${epoch}:${cachedTextHash(req.text)}:${req.width}:${req.height}:${zoom}:${dpr}:${scale}:${req.align}:${req.fontFamily}:${req.fontSize}:${req.textStyle}:${req.textColor}:${req.highlightColor}`;
2514
+ const mathSuffix = req.text.includes("$") ? `:m${getMathEpoch()}` : "";
2515
+ 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}`;
2253
2516
  };
2254
2517
  var cachedTextHash = (value) => {
2255
2518
  const hit = textHashCache.get(value);
@@ -2305,12 +2568,12 @@ var clearTextBitmapCache = () => {
2305
2568
  };
2306
2569
  var getTextBitmapCacheSize = () => renderCache.size;
2307
2570
  var FREEHAND_CACHE_MAX = 500;
2308
- var cache3 = /* @__PURE__ */ new Map();
2571
+ var cache4 = /* @__PURE__ */ new Map();
2309
2572
  var remember = (key, path) => {
2310
- cache3.set(key, path);
2311
- if (cache3.size > FREEHAND_CACHE_MAX) {
2312
- const oldest = cache3.keys().next().value;
2313
- if (oldest !== void 0) cache3.delete(oldest);
2573
+ cache4.set(key, path);
2574
+ if (cache4.size > FREEHAND_CACHE_MAX) {
2575
+ const oldest = cache4.keys().next().value;
2576
+ if (oldest !== void 0) cache4.delete(oldest);
2314
2577
  }
2315
2578
  };
2316
2579
  var signaturePoints = (samples) => {
@@ -2362,10 +2625,10 @@ var outlineToPath2D = (ring) => {
2362
2625
  var getOrBuildFreehandPath = (samples, strokeWidth, seed) => {
2363
2626
  if (samples.length < 2) return null;
2364
2627
  const cacheKey = `${seed}|${strokeWidth.toFixed(2)}|${signaturePoints(samples)}`;
2365
- const hit = cache3.get(cacheKey);
2628
+ const hit = cache4.get(cacheKey);
2366
2629
  if (hit) {
2367
- cache3.delete(cacheKey);
2368
- cache3.set(cacheKey, hit);
2630
+ cache4.delete(cacheKey);
2631
+ cache4.set(cacheKey, hit);
2369
2632
  return hit;
2370
2633
  }
2371
2634
  const pts = buildPressurePoints(samples);
@@ -4221,11 +4484,11 @@ var paintPlaceholder = (ctx, w, h, label) => {
4221
4484
  ctx.fillText(label, w / 2, h / 2);
4222
4485
  }
4223
4486
  };
4224
- var paintImageNode = (ctx, node, cache4, theme) => {
4487
+ var paintImageNode = (ctx, node, cache5, theme) => {
4225
4488
  if (node.w <= 0 || node.h <= 0) return;
4226
4489
  const data = node.data;
4227
4490
  if (!data?.src) return;
4228
- const bitmap = cache4.getImage(data.src);
4491
+ const bitmap = cache5.getImage(data.src);
4229
4492
  const opacity = resolveOpacity(node.style, theme);
4230
4493
  const needsScope = opacity !== 1;
4231
4494
  if (needsScope) {
@@ -4239,13 +4502,13 @@ var paintImageNode = (ctx, node, cache4, theme) => {
4239
4502
  }
4240
4503
  if (needsScope) ctx.restore();
4241
4504
  };
4242
- var paintIconNode = (ctx, node, cache4, scale, theme) => {
4505
+ var paintIconNode = (ctx, node, cache5, scale, theme) => {
4243
4506
  if (node.w <= 0 || node.h <= 0) return;
4244
4507
  const data = node.data;
4245
4508
  if (!data?.src) return;
4246
4509
  const sizePx = Math.max(node.w, node.h) * scale;
4247
4510
  const color = node.style?.iconColor;
4248
- const bitmap = cache4.getIcon(data.src, color, sizePx);
4511
+ const bitmap = cache5.getIcon(data.src, color, sizePx);
4249
4512
  const opacity = resolveOpacity(node.style, theme);
4250
4513
  const needsScope = opacity !== 1;
4251
4514
  if (needsScope) {
@@ -4608,16 +4871,18 @@ var applyCameraTransform = (surface, camera) => {
4608
4871
  };
4609
4872
  var worldViewport = (surface, camera) => viewportWorldRect(camera, surface.cssWidth, surface.cssHeight);
4610
4873
  var drawWithNodeTransform = (ctx, node, fn) => {
4611
- ctx.save();
4612
4874
  if (node.angle === 0) {
4613
4875
  ctx.translate(node.x, node.y);
4614
- } else {
4615
- const cx = node.x + node.w / 2;
4616
- const cy = node.y + node.h / 2;
4617
- ctx.translate(cx, cy);
4618
- ctx.rotate(node.angle);
4619
- ctx.translate(-node.w / 2, -node.h / 2);
4876
+ fn();
4877
+ ctx.translate(-node.x, -node.y);
4878
+ return;
4620
4879
  }
4880
+ ctx.save();
4881
+ const cx = node.x + node.w / 2;
4882
+ const cy = node.y + node.h / 2;
4883
+ ctx.translate(cx, cy);
4884
+ ctx.rotate(node.angle);
4885
+ ctx.translate(-node.w / 2, -node.h / 2);
4621
4886
  fn();
4622
4887
  ctx.restore();
4623
4888
  };
@@ -4639,6 +4904,12 @@ var createRenderer = (opts) => {
4639
4904
  let interactiveDirty = false;
4640
4905
  let overlaySet = /* @__PURE__ */ new Set();
4641
4906
  let lastDrawn = 0;
4907
+ let sortedNodeIdsCache = null;
4908
+ let sortedEdgeIdsCache = null;
4909
+ const invalidateSortedCaches = () => {
4910
+ sortedNodeIdsCache = null;
4911
+ sortedEdgeIdsCache = null;
4912
+ };
4642
4913
  const requestRepaint = () => {
4643
4914
  staticDirty = true;
4644
4915
  loop.requestFrame();
@@ -4768,7 +5039,9 @@ var createRenderer = (opts) => {
4768
5039
  }
4769
5040
  if (def.renderCanvas) {
4770
5041
  drawWithNodeTransform(staticSurface.ctx, node, () => {
5042
+ staticSurface.ctx.save();
4771
5043
  def.renderCanvas(staticSurface.ctx, node, renderEnv);
5044
+ staticSurface.ctx.restore();
4772
5045
  });
4773
5046
  drawn++;
4774
5047
  }
@@ -4801,11 +5074,19 @@ var createRenderer = (opts) => {
4801
5074
  }
4802
5075
  }
4803
5076
  if (def.drawPlaceholder) {
4804
- drawWithNodeTransform(ctx, node, () => def.drawPlaceholder(ctx, node, env));
5077
+ drawWithNodeTransform(ctx, node, () => {
5078
+ ctx.save();
5079
+ def.drawPlaceholder(ctx, node, env);
5080
+ ctx.restore();
5081
+ });
4805
5082
  return true;
4806
5083
  }
4807
5084
  if (def.renderCanvas) {
4808
- drawWithNodeTransform(ctx, node, () => def.renderCanvas(ctx, node, env));
5085
+ drawWithNodeTransform(ctx, node, () => {
5086
+ ctx.save();
5087
+ def.renderCanvas(ctx, node, env);
5088
+ ctx.restore();
5089
+ });
4809
5090
  return true;
4810
5091
  }
4811
5092
  return false;
@@ -4872,14 +5153,23 @@ var createRenderer = (opts) => {
4872
5153
  isMoving: isMoving2
4873
5154
  });
4874
5155
  };
5156
+ const getSortedEdgeIds = () => {
5157
+ if (sortedEdgeIdsCache) return sortedEdgeIdsCache;
5158
+ const all = store.getAllEdges();
5159
+ sortedEdgeIdsCache = all.slice().sort((a, b) => a.z - b.z || (a.id < b.id ? -1 : 1)).map((e) => e.id);
5160
+ return sortedEdgeIdsCache;
5161
+ };
4875
5162
  const visibleEdges = (viewport) => {
4876
5163
  const ids = store.querySpatial({ rect: viewport }).edges;
5164
+ if (ids.length === 0) return [];
5165
+ const visibleSet = new Set(ids);
5166
+ const sorted = getSortedEdgeIds();
4877
5167
  const result = [];
4878
- for (const id of ids) {
5168
+ for (const id of sorted) {
5169
+ if (!visibleSet.has(id)) continue;
4879
5170
  const e = store.getEdge(id);
4880
5171
  if (e) result.push(e);
4881
5172
  }
4882
- result.sort((a, b) => a.z - b.z || (a.id < b.id ? -1 : 1));
4883
5173
  return result;
4884
5174
  };
4885
5175
  const paintInteractive = () => {
@@ -5027,21 +5317,31 @@ var createRenderer = (opts) => {
5027
5317
  }
5028
5318
  return m;
5029
5319
  };
5320
+ const getSortedNodeIds = () => {
5321
+ if (sortedNodeIdsCache) return sortedNodeIdsCache;
5322
+ const all = store.getAllNodes();
5323
+ sortedNodeIdsCache = all.slice().sort((a, b) => a.z - b.z || (a.id < b.id ? -1 : 1)).map((n) => n.id);
5324
+ return sortedNodeIdsCache;
5325
+ };
5030
5326
  const visibleNodes = (camera, viewport) => {
5031
5327
  const ids = store.querySpatial({ rect: viewport }).nodes;
5328
+ if (ids.length === 0) return [];
5329
+ const visibleSet = new Set(ids);
5330
+ const sorted = getSortedNodeIds();
5032
5331
  const result = [];
5033
5332
  const minWorldSize = MIN_ON_SCREEN_SIZE_PX / camera.z;
5034
- for (const id of ids) {
5333
+ for (const id of sorted) {
5334
+ if (!visibleSet.has(id)) continue;
5035
5335
  const n = store.getNode(id);
5036
5336
  if (!n) continue;
5037
5337
  if (n.w < minWorldSize && n.h < minWorldSize) continue;
5038
5338
  if (intersectsViewport(n, viewport)) result.push(n);
5039
5339
  }
5040
- result.sort((a, b) => a.z - b.z || (a.id < b.id ? -1 : 1));
5041
5340
  return result;
5042
5341
  };
5043
5342
  const loop = createFrameLoop({ draw: drawFrame });
5044
5343
  const onStoreChange = () => {
5344
+ invalidateSortedCaches();
5045
5345
  staticDirty = true;
5046
5346
  interactiveDirty = true;
5047
5347
  loop.requestFrame();
@@ -5070,6 +5370,10 @@ var createRenderer = (opts) => {
5070
5370
  staticDirty = true;
5071
5371
  loop.requestFrame();
5072
5372
  });
5373
+ const unsubMathEpoch = subscribeMathEpoch(() => {
5374
+ staticDirty = true;
5375
+ loop.requestFrame();
5376
+ });
5073
5377
  return {
5074
5378
  start() {
5075
5379
  loop.start();
@@ -5119,6 +5423,7 @@ var createRenderer = (opts) => {
5119
5423
  unsubSelection();
5120
5424
  unsubInteraction();
5121
5425
  unsubFontEpoch();
5426
+ unsubMathEpoch();
5122
5427
  assetCache.dispose();
5123
5428
  }
5124
5429
  };
@@ -6196,6 +6501,7 @@ exports.autoRouteControls = autoRouteControls;
6196
6501
  exports.blobToDataUri = blobToDataUri;
6197
6502
  exports.clampEffectiveScale = clampEffectiveScale;
6198
6503
  exports.clampZoom = clampZoom;
6504
+ exports.clearMathCache = clearMathCache;
6199
6505
  exports.clearMeasureCache = clearMeasureCache;
6200
6506
  exports.clearSurface = clearSurface;
6201
6507
  exports.clearTextBitmapCache = clearTextBitmapCache;
@@ -6238,6 +6544,10 @@ exports.getContext = getContext;
6238
6544
  exports.getDpr = getDpr;
6239
6545
  exports.getFontEpoch = getFontEpoch;
6240
6546
  exports.getMarkdownLineHeightPx = getMarkdownLineHeightPx;
6547
+ exports.getMathBitmap = getMathBitmap;
6548
+ exports.getMathCacheSize = getMathCacheSize;
6549
+ exports.getMathEpoch = getMathEpoch;
6550
+ exports.getMathJax = getMathJax;
6241
6551
  exports.getOrRenderTextBitmap = getOrRenderTextBitmap;
6242
6552
  exports.getPointAndTangentAtArcLength = getPointAndTangentAtArcLength;
6243
6553
  exports.getTextBitmapCacheSize = getTextBitmapCacheSize;
@@ -6271,6 +6581,7 @@ exports.nodeIntersectsRect = nodeIntersectsRect;
6271
6581
  exports.nodeLocalToWorld = nodeLocalToWorld;
6272
6582
  exports.notePenActive = notePenActive;
6273
6583
  exports.notePenInactive = notePenInactive;
6584
+ exports.onMathJaxReady = onMathJaxReady;
6274
6585
  exports.opSchemas = opSchemas;
6275
6586
  exports.opSchemasAsAnthropicTools = opSchemasAsAnthropicTools;
6276
6587
  exports.paintBackground = paintBackground;
@@ -6309,6 +6620,7 @@ exports.sideOf = sideOf;
6309
6620
  exports.sizeSurface = sizeSurface;
6310
6621
  exports.storeToJSON = storeToJSON;
6311
6622
  exports.subscribeFontEpoch = subscribeFontEpoch;
6623
+ exports.subscribeMathEpoch = subscribeMathEpoch;
6312
6624
  exports.tangentAtArcLength = tangentAtArcLength;
6313
6625
  exports.toImageBlob = toImageBlob;
6314
6626
  exports.toSerialized = toSerialized;