@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 +350 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +136 -2
- package/dist/index.d.ts +136 -2
- package/dist/index.js +344 -39
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
637
|
-
if (
|
|
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
|
|
850
|
-
if (
|
|
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 = /(
|
|
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
|
|
1759
|
-
if (
|
|
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
|
|
2239
|
-
if (
|
|
2240
|
-
|
|
2241
|
-
return { canvas:
|
|
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
|
-
|
|
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
|
|
2569
|
+
var cache4 = /* @__PURE__ */ new Map();
|
|
2307
2570
|
var remember = (key, path) => {
|
|
2308
|
-
|
|
2309
|
-
if (
|
|
2310
|
-
const oldest =
|
|
2311
|
-
if (oldest !== void 0)
|
|
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 =
|
|
2626
|
+
const hit = cache4.get(cacheKey);
|
|
2364
2627
|
if (hit) {
|
|
2365
|
-
|
|
2366
|
-
|
|
2628
|
+
cache4.delete(cacheKey);
|
|
2629
|
+
cache4.set(cacheKey, hit);
|
|
2367
2630
|
return hit;
|
|
2368
2631
|
}
|
|
2369
2632
|
const pts = buildPressurePoints(samples);
|
|
@@ -4219,11 +4482,11 @@ var paintPlaceholder = (ctx, w, h, label) => {
|
|
|
4219
4482
|
ctx.fillText(label, w / 2, h / 2);
|
|
4220
4483
|
}
|
|
4221
4484
|
};
|
|
4222
|
-
var paintImageNode = (ctx, node,
|
|
4485
|
+
var paintImageNode = (ctx, node, cache5, theme) => {
|
|
4223
4486
|
if (node.w <= 0 || node.h <= 0) return;
|
|
4224
4487
|
const data = node.data;
|
|
4225
4488
|
if (!data?.src) return;
|
|
4226
|
-
const bitmap =
|
|
4489
|
+
const bitmap = cache5.getImage(data.src);
|
|
4227
4490
|
const opacity = resolveOpacity(node.style, theme);
|
|
4228
4491
|
const needsScope = opacity !== 1;
|
|
4229
4492
|
if (needsScope) {
|
|
@@ -4237,13 +4500,13 @@ var paintImageNode = (ctx, node, cache4, theme) => {
|
|
|
4237
4500
|
}
|
|
4238
4501
|
if (needsScope) ctx.restore();
|
|
4239
4502
|
};
|
|
4240
|
-
var paintIconNode = (ctx, node,
|
|
4503
|
+
var paintIconNode = (ctx, node, cache5, scale, theme) => {
|
|
4241
4504
|
if (node.w <= 0 || node.h <= 0) return;
|
|
4242
4505
|
const data = node.data;
|
|
4243
4506
|
if (!data?.src) return;
|
|
4244
4507
|
const sizePx = Math.max(node.w, node.h) * scale;
|
|
4245
4508
|
const color = node.style?.iconColor;
|
|
4246
|
-
const bitmap =
|
|
4509
|
+
const bitmap = cache5.getIcon(data.src, color, sizePx);
|
|
4247
4510
|
const opacity = resolveOpacity(node.style, theme);
|
|
4248
4511
|
const needsScope = opacity !== 1;
|
|
4249
4512
|
if (needsScope) {
|
|
@@ -4606,16 +4869,18 @@ var applyCameraTransform = (surface, camera) => {
|
|
|
4606
4869
|
};
|
|
4607
4870
|
var worldViewport = (surface, camera) => viewportWorldRect(camera, surface.cssWidth, surface.cssHeight);
|
|
4608
4871
|
var drawWithNodeTransform = (ctx, node, fn) => {
|
|
4609
|
-
ctx.save();
|
|
4610
4872
|
if (node.angle === 0) {
|
|
4611
4873
|
ctx.translate(node.x, node.y);
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
ctx.translate(cx, cy);
|
|
4616
|
-
ctx.rotate(node.angle);
|
|
4617
|
-
ctx.translate(-node.w / 2, -node.h / 2);
|
|
4874
|
+
fn();
|
|
4875
|
+
ctx.translate(-node.x, -node.y);
|
|
4876
|
+
return;
|
|
4618
4877
|
}
|
|
4878
|
+
ctx.save();
|
|
4879
|
+
const cx = node.x + node.w / 2;
|
|
4880
|
+
const cy = node.y + node.h / 2;
|
|
4881
|
+
ctx.translate(cx, cy);
|
|
4882
|
+
ctx.rotate(node.angle);
|
|
4883
|
+
ctx.translate(-node.w / 2, -node.h / 2);
|
|
4619
4884
|
fn();
|
|
4620
4885
|
ctx.restore();
|
|
4621
4886
|
};
|
|
@@ -4637,6 +4902,12 @@ var createRenderer = (opts) => {
|
|
|
4637
4902
|
let interactiveDirty = false;
|
|
4638
4903
|
let overlaySet = /* @__PURE__ */ new Set();
|
|
4639
4904
|
let lastDrawn = 0;
|
|
4905
|
+
let sortedNodeIdsCache = null;
|
|
4906
|
+
let sortedEdgeIdsCache = null;
|
|
4907
|
+
const invalidateSortedCaches = () => {
|
|
4908
|
+
sortedNodeIdsCache = null;
|
|
4909
|
+
sortedEdgeIdsCache = null;
|
|
4910
|
+
};
|
|
4640
4911
|
const requestRepaint = () => {
|
|
4641
4912
|
staticDirty = true;
|
|
4642
4913
|
loop.requestFrame();
|
|
@@ -4766,7 +5037,9 @@ var createRenderer = (opts) => {
|
|
|
4766
5037
|
}
|
|
4767
5038
|
if (def.renderCanvas) {
|
|
4768
5039
|
drawWithNodeTransform(staticSurface.ctx, node, () => {
|
|
5040
|
+
staticSurface.ctx.save();
|
|
4769
5041
|
def.renderCanvas(staticSurface.ctx, node, renderEnv);
|
|
5042
|
+
staticSurface.ctx.restore();
|
|
4770
5043
|
});
|
|
4771
5044
|
drawn++;
|
|
4772
5045
|
}
|
|
@@ -4799,11 +5072,19 @@ var createRenderer = (opts) => {
|
|
|
4799
5072
|
}
|
|
4800
5073
|
}
|
|
4801
5074
|
if (def.drawPlaceholder) {
|
|
4802
|
-
drawWithNodeTransform(ctx, node, () =>
|
|
5075
|
+
drawWithNodeTransform(ctx, node, () => {
|
|
5076
|
+
ctx.save();
|
|
5077
|
+
def.drawPlaceholder(ctx, node, env);
|
|
5078
|
+
ctx.restore();
|
|
5079
|
+
});
|
|
4803
5080
|
return true;
|
|
4804
5081
|
}
|
|
4805
5082
|
if (def.renderCanvas) {
|
|
4806
|
-
drawWithNodeTransform(ctx, node, () =>
|
|
5083
|
+
drawWithNodeTransform(ctx, node, () => {
|
|
5084
|
+
ctx.save();
|
|
5085
|
+
def.renderCanvas(ctx, node, env);
|
|
5086
|
+
ctx.restore();
|
|
5087
|
+
});
|
|
4807
5088
|
return true;
|
|
4808
5089
|
}
|
|
4809
5090
|
return false;
|
|
@@ -4870,14 +5151,23 @@ var createRenderer = (opts) => {
|
|
|
4870
5151
|
isMoving: isMoving2
|
|
4871
5152
|
});
|
|
4872
5153
|
};
|
|
5154
|
+
const getSortedEdgeIds = () => {
|
|
5155
|
+
if (sortedEdgeIdsCache) return sortedEdgeIdsCache;
|
|
5156
|
+
const all = store.getAllEdges();
|
|
5157
|
+
sortedEdgeIdsCache = all.slice().sort((a, b) => a.z - b.z || (a.id < b.id ? -1 : 1)).map((e) => e.id);
|
|
5158
|
+
return sortedEdgeIdsCache;
|
|
5159
|
+
};
|
|
4873
5160
|
const visibleEdges = (viewport) => {
|
|
4874
5161
|
const ids = store.querySpatial({ rect: viewport }).edges;
|
|
5162
|
+
if (ids.length === 0) return [];
|
|
5163
|
+
const visibleSet = new Set(ids);
|
|
5164
|
+
const sorted = getSortedEdgeIds();
|
|
4875
5165
|
const result = [];
|
|
4876
|
-
for (const id of
|
|
5166
|
+
for (const id of sorted) {
|
|
5167
|
+
if (!visibleSet.has(id)) continue;
|
|
4877
5168
|
const e = store.getEdge(id);
|
|
4878
5169
|
if (e) result.push(e);
|
|
4879
5170
|
}
|
|
4880
|
-
result.sort((a, b) => a.z - b.z || (a.id < b.id ? -1 : 1));
|
|
4881
5171
|
return result;
|
|
4882
5172
|
};
|
|
4883
5173
|
const paintInteractive = () => {
|
|
@@ -5025,21 +5315,31 @@ var createRenderer = (opts) => {
|
|
|
5025
5315
|
}
|
|
5026
5316
|
return m;
|
|
5027
5317
|
};
|
|
5318
|
+
const getSortedNodeIds = () => {
|
|
5319
|
+
if (sortedNodeIdsCache) return sortedNodeIdsCache;
|
|
5320
|
+
const all = store.getAllNodes();
|
|
5321
|
+
sortedNodeIdsCache = all.slice().sort((a, b) => a.z - b.z || (a.id < b.id ? -1 : 1)).map((n) => n.id);
|
|
5322
|
+
return sortedNodeIdsCache;
|
|
5323
|
+
};
|
|
5028
5324
|
const visibleNodes = (camera, viewport) => {
|
|
5029
5325
|
const ids = store.querySpatial({ rect: viewport }).nodes;
|
|
5326
|
+
if (ids.length === 0) return [];
|
|
5327
|
+
const visibleSet = new Set(ids);
|
|
5328
|
+
const sorted = getSortedNodeIds();
|
|
5030
5329
|
const result = [];
|
|
5031
5330
|
const minWorldSize = MIN_ON_SCREEN_SIZE_PX / camera.z;
|
|
5032
|
-
for (const id of
|
|
5331
|
+
for (const id of sorted) {
|
|
5332
|
+
if (!visibleSet.has(id)) continue;
|
|
5033
5333
|
const n = store.getNode(id);
|
|
5034
5334
|
if (!n) continue;
|
|
5035
5335
|
if (n.w < minWorldSize && n.h < minWorldSize) continue;
|
|
5036
5336
|
if (intersectsViewport(n, viewport)) result.push(n);
|
|
5037
5337
|
}
|
|
5038
|
-
result.sort((a, b) => a.z - b.z || (a.id < b.id ? -1 : 1));
|
|
5039
5338
|
return result;
|
|
5040
5339
|
};
|
|
5041
5340
|
const loop = createFrameLoop({ draw: drawFrame });
|
|
5042
5341
|
const onStoreChange = () => {
|
|
5342
|
+
invalidateSortedCaches();
|
|
5043
5343
|
staticDirty = true;
|
|
5044
5344
|
interactiveDirty = true;
|
|
5045
5345
|
loop.requestFrame();
|
|
@@ -5068,6 +5368,10 @@ var createRenderer = (opts) => {
|
|
|
5068
5368
|
staticDirty = true;
|
|
5069
5369
|
loop.requestFrame();
|
|
5070
5370
|
});
|
|
5371
|
+
const unsubMathEpoch = subscribeMathEpoch(() => {
|
|
5372
|
+
staticDirty = true;
|
|
5373
|
+
loop.requestFrame();
|
|
5374
|
+
});
|
|
5071
5375
|
return {
|
|
5072
5376
|
start() {
|
|
5073
5377
|
loop.start();
|
|
@@ -5117,6 +5421,7 @@ var createRenderer = (opts) => {
|
|
|
5117
5421
|
unsubSelection();
|
|
5118
5422
|
unsubInteraction();
|
|
5119
5423
|
unsubFontEpoch();
|
|
5424
|
+
unsubMathEpoch();
|
|
5120
5425
|
assetCache.dispose();
|
|
5121
5426
|
}
|
|
5122
5427
|
};
|
|
@@ -6149,6 +6454,6 @@ var installedExtensions = (store) => {
|
|
|
6149
6454
|
// src/index.ts
|
|
6150
6455
|
var VERSION = "0.0.0";
|
|
6151
6456
|
|
|
6152
|
-
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 };
|
|
6457
|
+
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 };
|
|
6153
6458
|
//# sourceMappingURL=index.js.map
|
|
6154
6459
|
//# sourceMappingURL=index.js.map
|