@canvas-harness/core 0.1.7 → 0.1.9

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
@@ -824,17 +824,32 @@ var arrowheadLength = (kind, strokeWidth) => {
824
824
  return ARROW_BASE_LENGTH * scale;
825
825
  };
826
826
 
827
- // src/render/rough/constants.ts
828
- var ROUGH_MIN_ZOOM = 0.4;
829
- var ROUGH_MAX_NODES = 800;
830
- var ROUGH_MAX_MOVING_NODES = 5;
831
- var ROUGH_DEFAULTS = {
832
- bowing: 2,
833
- disableMultiStroke: true,
834
- preserveVertices: true
827
+ // src/render/rough/cache.ts
828
+ var cache = /* @__PURE__ */ new Map();
829
+ var MAX_ENTRIES = 1e3;
830
+ var getOrBuildDrawable = (key, build) => {
831
+ const hit = cache.get(key);
832
+ if (hit !== void 0) {
833
+ cache.delete(key);
834
+ cache.set(key, hit);
835
+ return hit;
836
+ }
837
+ const drawable = build();
838
+ if (cache.size >= MAX_ENTRIES) {
839
+ const first = cache.keys().next().value;
840
+ if (first !== void 0) cache.delete(first);
841
+ }
842
+ cache.set(key, drawable);
843
+ return drawable;
844
+ };
845
+ var seedFromId = (id) => {
846
+ let hash = 2166136261;
847
+ for (let i = 0; i < id.length; i += 1) {
848
+ hash ^= id.charCodeAt(i);
849
+ hash = Math.imul(hash, 16777619);
850
+ }
851
+ return hash >>> 0;
835
852
  };
836
- var ROUGH_FILL_MISREGISTER_X = -3;
837
- var ROUGH_FILL_MISREGISTER_Y = -2;
838
853
 
839
854
  // src/render/shapes/defaults.ts
840
855
  var DEFAULT_STYLE = {
@@ -874,1495 +889,706 @@ var isFullyTransparent = (color) => {
874
889
  var dashPatternFor = (strokeStyle, width) => {
875
890
  switch (strokeStyle) {
876
891
  case "dashed":
877
- return [width * 5, width * 4];
892
+ return [width * 3, width * 5];
878
893
  case "dotted":
879
- return [width * 1.5, width * 3];
894
+ return [width * 1.5, width * 4];
880
895
  default:
881
896
  return [];
882
897
  }
883
898
  };
884
899
 
885
- // src/render/color.ts
886
- var TONE_BLEND = 0.2;
887
- var parseHex = (hex) => {
888
- if (!hex.startsWith("#")) return null;
889
- const h = hex.slice(1);
890
- if (h.length === 3) {
891
- return [
892
- Number.parseInt(h[0] + h[0], 16),
893
- Number.parseInt(h[1] + h[1], 16),
894
- Number.parseInt(h[2] + h[2], 16)
895
- ];
900
+ // src/text/tokens.ts
901
+ var INLINE_PATTERN = /(\$\$[^\n]+?\$\$|\*\*[^*]+\*\*|==[^=\s](?:[^=]*?[^=\s])?==|`[^`]+`|\*[^*]+\*|__[^_]+__|~~[^~]+~~|_[^_]+_|\[[^\]]+\]\([^)]+\))/g;
902
+ var HR_LINE_PATTERN = /^[ \t]*---[ \t]*$/;
903
+ var DOUBLE_HR_LINE_PATTERN = /^[ \t]*===[ \t]*$/;
904
+ var transformSymbols = (value) => value.replace(/<=>|<->|<-|->|\[\]|\[[vx]\]/gi, (match) => {
905
+ const normalized = match.toLowerCase();
906
+ if (normalized === "->") return "\u2192";
907
+ if (normalized === "<-") return "\u2190";
908
+ if (normalized === "<->") return "\u2194";
909
+ if (normalized === "<=>") return "\u21D4";
910
+ if (normalized === "[]") return "\u2610";
911
+ if (normalized === "[v]") return "\u2705";
912
+ if (normalized === "[x]") return "\u274E";
913
+ return match;
914
+ });
915
+ var tokenizeInline = (segment) => {
916
+ if (!segment) return [];
917
+ const tokens = [];
918
+ let lastIndex = 0;
919
+ segment.replace(INLINE_PATTERN, (match, _group, offset) => {
920
+ const idx = offset;
921
+ if (idx > lastIndex) {
922
+ tokens.push({ type: "text", content: transformSymbols(segment.slice(lastIndex, idx)) });
923
+ }
924
+ if (match.startsWith("**") && match.endsWith("**") || match.startsWith("__") && match.endsWith("__")) {
925
+ tokens.push({ type: "bold", content: transformSymbols(match.slice(2, -2)) });
926
+ } else if (match.startsWith("*") && match.endsWith("*")) {
927
+ tokens.push({ type: "italic", content: transformSymbols(match.slice(1, -1)) });
928
+ } else if (match.startsWith("~~") && match.endsWith("~~")) {
929
+ tokens.push({ type: "strike", content: transformSymbols(match.slice(2, -2)) });
930
+ } else if (match.startsWith("==") && match.endsWith("==")) {
931
+ tokens.push({ type: "highlight", content: transformSymbols(match.slice(2, -2)) });
932
+ } else if (match.startsWith("_") && match.endsWith("_")) {
933
+ tokens.push({ type: "underline", content: transformSymbols(match.slice(1, -1)) });
934
+ } else if (match.startsWith("[") && match.includes("](") && match.endsWith(")")) {
935
+ const splitIndex = match.indexOf("](");
936
+ tokens.push({ type: "link", content: transformSymbols(match.slice(1, splitIndex)) });
937
+ } else if (match.startsWith("`") && match.endsWith("`")) {
938
+ tokens.push({ type: "code", content: match.slice(1, -1) });
939
+ } else if (match.startsWith("$$") && match.endsWith("$$")) {
940
+ tokens.push({ type: "math", content: match.slice(2, -2) });
941
+ } else {
942
+ tokens.push({ type: "text", content: transformSymbols(match) });
943
+ }
944
+ lastIndex = idx + match.length;
945
+ return match;
946
+ });
947
+ if (lastIndex < segment.length) {
948
+ tokens.push({ type: "text", content: transformSymbols(segment.slice(lastIndex)) });
896
949
  }
897
- if (h.length === 6 || h.length === 8) {
898
- return [
899
- Number.parseInt(h.slice(0, 2), 16),
900
- Number.parseInt(h.slice(2, 4), 16),
901
- Number.parseInt(h.slice(4, 6), 16)
902
- ];
950
+ return tokens;
951
+ };
952
+ var tokenizeLine = (line) => {
953
+ if (DOUBLE_HR_LINE_PATTERN.test(line)) return [{ type: "hr-double" }];
954
+ if (HR_LINE_PATTERN.test(line)) return [{ type: "hr" }];
955
+ return tokenizeInline(line);
956
+ };
957
+ var tokenizeTextBlock = (block) => {
958
+ if (!block) return [];
959
+ const tokens = [];
960
+ const lines = block.split("\n");
961
+ lines.forEach((line, index) => {
962
+ const lineTokens = tokenizeLine(line);
963
+ tokens.push(...lineTokens);
964
+ const isRuleLine = lineTokens.length === 1 && (lineTokens[0]?.type === "hr" || lineTokens[0]?.type === "hr-double");
965
+ if (index < lines.length - 1 && !isRuleLine) tokens.push({ type: "br" });
966
+ });
967
+ return tokens;
968
+ };
969
+ var tokenize = (input) => {
970
+ if (!input) return [];
971
+ const tokens = [];
972
+ let cursor = 0;
973
+ while (cursor < input.length) {
974
+ const fenceStart = input.indexOf("```", cursor);
975
+ if (fenceStart === -1) {
976
+ tokens.push(...tokenizeTextBlock(input.slice(cursor)));
977
+ break;
978
+ }
979
+ if (fenceStart > cursor) {
980
+ tokens.push(...tokenizeTextBlock(input.slice(cursor, fenceStart)));
981
+ }
982
+ const fenceEnd = input.indexOf("```", fenceStart + 3);
983
+ if (fenceEnd === -1) {
984
+ tokens.push(...tokenizeTextBlock(input.slice(fenceStart)));
985
+ break;
986
+ }
987
+ const fenceContent = input.slice(fenceStart + 3, fenceEnd);
988
+ const delimiterIndex = fenceContent.search(/[\r\n]/);
989
+ let codeContent = fenceContent;
990
+ if (delimiterIndex >= 0) {
991
+ codeContent = fenceContent.slice(delimiterIndex).replace(/^\r?\n/, "");
992
+ }
993
+ tokens.push({ type: "code-block", content: codeContent.replace(/\r\n/g, "\n") });
994
+ cursor = fenceEnd + 3;
903
995
  }
904
- return null;
996
+ return tokens;
905
997
  };
906
- var toHexPair = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
907
- var mixHex = (a, b, t) => {
908
- const A = parseHex(a);
909
- const B = parseHex(b);
910
- if (!A || !B) return a;
911
- const p = Math.max(0, Math.min(1, t));
912
- return `#${toHexPair(A[0] * (1 - p) + B[0] * p)}${toHexPair(A[1] * (1 - p) + B[1] * p)}${toHexPair(A[2] * (1 - p) + B[2] * p)}`;
998
+
999
+ // src/text/defaults.ts
1000
+ var FONT_FAMILY_MAP = {
1001
+ handwriting: '"Architects Daughter", cursive',
1002
+ "sans-serif": '"Atkinson Hyperlegible Next", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", ui-sans-serif, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
1003
+ serif: '"Lora", "Source Serif 4", ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
1004
+ monospace: '"Inconsolata", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
1005
+ informal: '"Shantell Sans", ui-handwriting, cursive'
913
1006
  };
914
- var darkenCache = /* @__PURE__ */ new Map();
915
- var darkenHex = (hex) => {
916
- const cached2 = darkenCache.get(hex);
917
- if (cached2 !== void 0) return cached2;
918
- const result = mixHex(hex, "#000000", TONE_BLEND);
919
- darkenCache.set(hex, result);
920
- return result;
1007
+ var FONT_SIZE_MAP = {
1008
+ S: 14,
1009
+ M: 16,
1010
+ L: 24,
1011
+ XL: 36
1012
+ };
1013
+ var LINE_HEIGHT_MAP = {
1014
+ S: 20,
1015
+ M: 24,
1016
+ L: 32,
1017
+ XL: 40
921
1018
  };
1019
+ var CODE_BLOCK_PADDING_X = 6;
1020
+ var CODE_BLOCK_MARGIN_Y = 4;
1021
+ var CONTENT_HEIGHT_BUFFER = 4;
1022
+ var CONTENT_PADDING = 6;
1023
+ var DEFAULT_TEXT_COLOR = "#1f2937";
1024
+ var DEFAULT_HIGHLIGHT_COLOR = "#fde047";
1025
+ var DEFAULT_HIGHLIGHT_COLOR_DARK = "#6b5a23";
1026
+ var LINK_COLOR = "#2563eb";
1027
+ var CODE_BG_COLOR = "rgba(148, 163, 184, 0.18)";
922
1028
 
923
- // src/render/shapes/path-helpers.ts
924
- var buildRectPath = (ctx, w, h, radius) => {
925
- ctx.beginPath();
926
- if (radius <= 0) {
927
- ctx.rect(0, 0, w, h);
928
- return;
1029
+ // src/text/math/loader.ts
1030
+ var cached = null;
1031
+ var loadPromise = null;
1032
+ var loadFailed = false;
1033
+ var readyCallbacks = /* @__PURE__ */ new Set();
1034
+ var getMathJax = () => {
1035
+ if (cached) return cached;
1036
+ if (loadFailed) return null;
1037
+ if (!loadPromise) {
1038
+ loadPromise = loadMathJax().then((instance) => {
1039
+ if (instance) {
1040
+ cached = instance;
1041
+ for (const cb of readyCallbacks) cb();
1042
+ } else {
1043
+ loadFailed = true;
1044
+ }
1045
+ readyCallbacks.clear();
1046
+ return cached;
1047
+ }).catch((err) => {
1048
+ console.warn("[math] failed to load MathJax:", err);
1049
+ loadFailed = true;
1050
+ readyCallbacks.clear();
1051
+ return null;
1052
+ });
929
1053
  }
930
- ctx.roundRect(0, 0, w, h, radius);
1054
+ return null;
931
1055
  };
932
- var buildEllipsePath = (ctx, w, h) => {
933
- const rx = w / 2;
934
- const ry = h / 2;
935
- ctx.beginPath();
936
- ctx.ellipse(rx, ry, rx, ry, 0, 0, Math.PI * 2);
1056
+ var onMathJaxReady = (cb) => {
1057
+ if (cached) return;
1058
+ if (loadFailed) return;
1059
+ readyCallbacks.add(cb);
937
1060
  };
938
- var buildDiamondPath = (ctx, w, h, radius = 0) => {
939
- ctx.beginPath();
940
- if (radius <= 0) {
941
- ctx.moveTo(w / 2, 0);
942
- ctx.lineTo(w, h / 2);
943
- ctx.lineTo(w / 2, h);
944
- ctx.lineTo(0, h / 2);
945
- ctx.closePath();
946
- return;
947
- }
948
- const cx = w / 2;
949
- const cy = h / 2;
950
- const T = { x: cx, y: 0 };
951
- const R = { x: w, y: cy };
952
- const B = { x: cx, y: h };
953
- const L = { x: 0, y: cy };
954
- const edgeLen = Math.hypot(R.x - T.x, R.y - T.y);
955
- const sMax = Math.max(0, edgeLen / 2 - 0.01);
956
- const s = Math.min(radius * Math.SQRT2, sMax);
957
- if (s <= 1e-4) {
958
- ctx.moveTo(T.x, T.y);
959
- ctx.lineTo(R.x, R.y);
960
- ctx.lineTo(B.x, B.y);
961
- ctx.lineTo(L.x, L.y);
962
- ctx.closePath();
963
- return;
964
- }
965
- const along = (a, b, d) => {
966
- const dx = b.x - a.x;
967
- const dy = b.y - a.y;
968
- const len = Math.hypot(dx, dy) || 1;
969
- const t = d / len;
970
- return { x: a.x + dx * t, y: a.y + dy * t };
1061
+ var loadMathJax = async () => {
1062
+ if (typeof window === "undefined") return null;
1063
+ const winAny = window;
1064
+ winAny.MathJax = {
1065
+ ...winAny.MathJax ?? {},
1066
+ startup: { typeset: false },
1067
+ options: {
1068
+ enableMenu: false,
1069
+ enableEnrichment: false,
1070
+ enableSpeech: false,
1071
+ enableComplexity: false,
1072
+ sre: { speech: "none" }
1073
+ },
1074
+ // `fontCache: 'none'` inlines every glyph as a raw <path>
1075
+ // (slightly bigger SVG, no <use> references). Required for SVGs
1076
+ // we extract to a Blob URL and rasterize via <img> — `<use>`
1077
+ // refs to <defs> elsewhere in the page wouldn't resolve.
1078
+ // `linebreaks: { inline: false }` keeps the whole formula in one
1079
+ // <svg> element (v4 defaults to true for long inline math).
1080
+ svg: {
1081
+ scale: 1,
1082
+ fontCache: "none",
1083
+ linebreaks: { inline: false }
1084
+ }
971
1085
  };
972
- const TR = along(T, R, s);
973
- const RT = along(R, T, s);
974
- const RB = along(R, B, s);
975
- const BR = along(B, R, s);
976
- const BL = along(B, L, s);
977
- const LB = along(L, B, s);
978
- const LT = along(L, T, s);
979
- const TL = along(T, L, s);
980
- ctx.moveTo(TR.x, TR.y);
981
- ctx.lineTo(RT.x, RT.y);
982
- ctx.quadraticCurveTo(R.x, R.y, RB.x, RB.y);
983
- ctx.lineTo(BR.x, BR.y);
984
- ctx.quadraticCurveTo(B.x, B.y, BL.x, BL.y);
985
- ctx.lineTo(LB.x, LB.y);
986
- ctx.quadraticCurveTo(L.x, L.y, LT.x, LT.y);
987
- ctx.lineTo(TL.x, TL.y);
988
- ctx.quadraticCurveTo(T.x, T.y, TR.x, TR.y);
989
- ctx.closePath();
1086
+ const VENDOR_URL = "https://cdn.jsdelivr.net/npm/mathjax@4/tex-svg.js";
1087
+ await new Promise((resolve, reject) => {
1088
+ const existing = document.querySelector(
1089
+ `script[src="${VENDOR_URL}"]`
1090
+ );
1091
+ if (existing) {
1092
+ existing.addEventListener("load", () => resolve(), { once: true });
1093
+ existing.addEventListener("error", () => reject(new Error("MathJax CDN load failed")), {
1094
+ once: true
1095
+ });
1096
+ return;
1097
+ }
1098
+ const script = document.createElement("script");
1099
+ script.src = VENDOR_URL;
1100
+ script.async = true;
1101
+ script.onload = () => resolve();
1102
+ script.onerror = () => reject(new Error("MathJax CDN load failed"));
1103
+ document.head.appendChild(script);
1104
+ });
1105
+ const mj = winAny.MathJax;
1106
+ if (!mj) throw new Error("MathJax did not install on window after import");
1107
+ if (typeof mj.tex2svgPromise !== "function") {
1108
+ throw new Error("MathJax loaded but tex2svgPromise is missing \u2014 wrong bundle?");
1109
+ }
1110
+ await mj.startup?.promise;
1111
+ return mj;
990
1112
  };
991
- var thoughtCloudGeometry = (w, h) => {
992
- const domeW = Math.min(w * 0.4, h * 1.2);
993
- const domeH = Math.min(h * 0.45, domeW);
994
- const domeAnchorX = w * 0.3;
995
- const domeX = Math.max(0, Math.min(w - domeW, domeAnchorX - domeW / 2));
996
- return {
997
- domeX,
998
- domeW,
999
- domeH,
1000
- cx: domeX + domeW / 2,
1001
- cy: domeH / 2,
1002
- rx: domeW / 2,
1003
- ry: domeH / 2,
1004
- bodyY: domeH * 0.55
1113
+
1114
+ // src/text/math/cache.ts
1115
+ var normalizeSize = (px) => Math.max(8, Math.round(px));
1116
+ var cache2 = /* @__PURE__ */ new Map();
1117
+ var compileQueue = [];
1118
+ var compileScheduled = false;
1119
+ var mathEpoch = 0;
1120
+ var epochSubscribers = /* @__PURE__ */ new Set();
1121
+ var getMathEpoch = () => mathEpoch;
1122
+ var subscribeMathEpoch = (cb) => {
1123
+ epochSubscribers.add(cb);
1124
+ return () => {
1125
+ epochSubscribers.delete(cb);
1005
1126
  };
1006
1127
  };
1007
- var buildThoughtCloudPath = (ctx, w, h, radius) => {
1008
- const g = thoughtCloudGeometry(w, h);
1009
- const bodyH = h - g.bodyY;
1010
- const r = Math.max(0, Math.min(radius, bodyH / 2, w / 2));
1011
- const t = g.ry > 0 ? (g.bodyY - g.cy) / g.ry : 0;
1012
- const inRange = Math.abs(t) < 1;
1013
- let xL = g.domeX;
1014
- let xR = g.domeX + g.domeW;
1015
- if (inRange) {
1016
- const xOffset = g.rx * Math.sqrt(1 - t * t);
1017
- xL = g.cx - xOffset;
1018
- xR = g.cx + xOffset;
1128
+ var bumpMathEpoch = () => {
1129
+ mathEpoch += 1;
1130
+ for (const cb of epochSubscribers) cb();
1131
+ };
1132
+ var getMathBitmap = (source, color, sizePx) => {
1133
+ const size = normalizeSize(sizePx);
1134
+ const key = `${size}:${color}:${source}`;
1135
+ const existing = cache2.get(key);
1136
+ if (existing) {
1137
+ if (existing.state === "ready") return existing.bitmap;
1138
+ return null;
1019
1139
  }
1020
- xL = Math.max(r, xL);
1021
- xR = Math.min(w - r, xR);
1022
- ctx.beginPath();
1023
- ctx.moveTo(r, g.bodyY);
1024
- ctx.lineTo(xL, g.bodyY);
1025
- const startAngle = Math.atan2((g.bodyY - g.cy) / g.ry, (xL - g.cx) / g.rx);
1026
- let endAngle = Math.atan2((g.bodyY - g.cy) / g.ry, (xR - g.cx) / g.rx);
1027
- if (endAngle <= startAngle) endAngle += 2 * Math.PI;
1028
- ctx.ellipse(g.cx, g.cy, g.rx, g.ry, 0, startAngle, endAngle, false);
1029
- ctx.lineTo(w - r, g.bodyY);
1030
- ctx.quadraticCurveTo(w, g.bodyY, w, g.bodyY + r);
1031
- ctx.lineTo(w, h - r);
1032
- ctx.quadraticCurveTo(w, h, w - r, h);
1033
- ctx.lineTo(r, h);
1034
- ctx.quadraticCurveTo(0, h, 0, h - r);
1035
- ctx.lineTo(0, g.bodyY + r);
1036
- ctx.quadraticCurveTo(0, g.bodyY, r, g.bodyY);
1037
- ctx.closePath();
1140
+ cache2.set(key, { state: "pending" });
1141
+ compileQueue.push({ key, source, color, sizePx: size });
1142
+ scheduleCompile();
1143
+ return null;
1038
1144
  };
1039
- var buildTagPath = (ctx, w, h, radius = 8) => {
1040
- const notch = Math.min(h * 0.5, w * 0.3);
1041
- const tipRadius = 6;
1042
- const tipX = 0;
1043
- const tipY = h / 2;
1044
- const bodyLeft = Math.max(0, Math.min(notch, w));
1045
- const right = w;
1046
- const bottom = h;
1047
- const rBody = Math.min(radius, h / 2, (right - bodyLeft) / 2);
1048
- const rJoin = Math.min(radius, h * 0.45, bodyLeft * 0.8);
1049
- ctx.beginPath();
1050
- if (bodyLeft <= 1e-3) {
1051
- const r = Math.min(radius, h / 2, w / 2);
1052
- ctx.moveTo(r, 0);
1053
- ctx.lineTo(w - r, 0);
1054
- ctx.quadraticCurveTo(w, 0, w, r);
1055
- ctx.lineTo(w, h - r);
1056
- ctx.quadraticCurveTo(w, h, w - r, h);
1057
- ctx.lineTo(r, h);
1058
- ctx.quadraticCurveTo(0, h, 0, h - r);
1059
- ctx.lineTo(0, r);
1060
- ctx.quadraticCurveTo(0, 0, r, 0);
1061
- ctx.closePath();
1145
+ var scheduleCompile = () => {
1146
+ if (compileScheduled) return;
1147
+ compileScheduled = true;
1148
+ if (typeof window === "undefined" || typeof requestAnimationFrame === "undefined") {
1149
+ void drainQueue();
1062
1150
  return;
1063
1151
  }
1064
- const pTop = { x: bodyLeft, y: rJoin };
1065
- const pBot = { x: bodyLeft, y: bottom - rJoin };
1066
- const dirX = tipX - bodyLeft;
1067
- const dirYTop = tipY - rJoin;
1068
- const dirYBot = tipY - (bottom - rJoin);
1069
- const lenTop = Math.hypot(dirX, dirYTop) || 1;
1070
- const lenBot = Math.hypot(dirX, dirYBot) || 1;
1071
- const maxTipRound = Math.min(lenTop, lenBot) * 0.49;
1072
- const t = Math.max(0, Math.min(tipRadius, maxTipRound));
1073
- const tipEnter = { x: tipX - dirX / lenBot * t, y: tipY - dirYBot / lenBot * t };
1074
- const tipExit = { x: tipX - dirX / lenTop * t, y: tipY - dirYTop / lenTop * t };
1075
- const k = rJoin * 0.65;
1076
- const topStart = { x: bodyLeft + rBody, y: 0 };
1077
- const botEnd = { x: bodyLeft + rBody, y: bottom };
1078
- ctx.moveTo(topStart.x, topStart.y);
1079
- ctx.lineTo(right - rBody, 0);
1080
- ctx.quadraticCurveTo(right, 0, right, rBody);
1081
- ctx.lineTo(right, bottom - rBody);
1082
- ctx.quadraticCurveTo(right, bottom, right - rBody, bottom);
1083
- ctx.lineTo(botEnd.x, botEnd.y);
1084
- ctx.bezierCurveTo(
1085
- botEnd.x - k,
1086
- bottom,
1087
- pBot.x - dirX / lenBot * k,
1088
- pBot.y - dirYBot / lenBot * k,
1089
- pBot.x,
1090
- pBot.y
1091
- );
1092
- ctx.lineTo(t > 0 ? tipEnter.x : tipX, t > 0 ? tipEnter.y : tipY);
1093
- if (t > 0) ctx.quadraticCurveTo(tipX, tipY, tipExit.x, tipExit.y);
1094
- ctx.lineTo(pTop.x, pTop.y);
1095
- ctx.bezierCurveTo(
1096
- pTop.x - dirX / lenTop * k,
1097
- pTop.y - dirYTop / lenTop * k,
1098
- topStart.x - k,
1099
- 0,
1100
- topStart.x,
1101
- 0
1102
- );
1103
- ctx.closePath();
1152
+ requestAnimationFrame(() => {
1153
+ void drainQueue();
1154
+ });
1104
1155
  };
1105
-
1106
- // src/render/shapes/draw-shape.ts
1107
- var ATOMIC = /* @__PURE__ */ new Set(["rect", "ellipse", "diamond", "tag", "thought-cloud"]);
1108
- var COMPOSITE = /* @__PURE__ */ new Set([
1109
- "capsule",
1110
- "layered-rect",
1111
- "layered-ellipse",
1112
- "layered-diamond",
1113
- "soft-diamond"
1114
- ]);
1115
- var isCompositePrimitive = (type) => COMPOSITE.has(type);
1116
- var isDrawablePrimitive = (type) => ATOMIC.has(type) || COMPOSITE.has(type);
1117
- var PLAIN_RECT_CORNER_THRESHOLD_PX = 1.5;
1118
- var LAYERED_OFFSET = 12;
1119
- var drawShape = (ctx, node, scale, theme, opts) => {
1120
- if (!isDrawablePrimitive(node.type)) return;
1121
- if (node.hidden) return;
1122
- if (node.w <= 0 || node.h <= 0) return;
1123
- if (COMPOSITE.has(node.type)) {
1124
- drawComposite(ctx, node, scale, theme, opts);
1156
+ var drainQueue = async () => {
1157
+ compileScheduled = false;
1158
+ if (compileQueue.length === 0) return;
1159
+ const mj = getMathJax();
1160
+ if (!mj) {
1161
+ onMathJaxReady(() => scheduleCompile());
1125
1162
  return;
1126
1163
  }
1127
- drawAtomic(ctx, node.type, node.w, node.h, node.style, scale, theme, opts);
1164
+ const FRAME_BUDGET_MS = 4;
1165
+ const start = performance.now();
1166
+ let didResolve = false;
1167
+ while (compileQueue.length > 0 && performance.now() - start < FRAME_BUDGET_MS) {
1168
+ const item = compileQueue.shift();
1169
+ if (cache2.get(item.key)?.state !== "pending") continue;
1170
+ try {
1171
+ const bitmap = await compileOne(mj, item.source, item.color, item.sizePx);
1172
+ cache2.set(item.key, { state: "ready", bitmap });
1173
+ didResolve = true;
1174
+ } catch (err) {
1175
+ cache2.set(item.key, { state: "error", err });
1176
+ console.warn(`[math] failed to compile "${item.source}":`, err);
1177
+ }
1178
+ }
1179
+ if (didResolve) bumpMathEpoch();
1180
+ if (compileQueue.length > 0) scheduleCompile();
1128
1181
  };
1129
- var drawAtomic = (ctx, type, w, h, style, scale, theme, opts) => {
1130
- if (w <= 0 || h <= 0) return;
1131
- const strokeWidth = resolveStrokeWidth(style, theme);
1132
- const opacity = resolveOpacity(style, theme);
1133
- const fill = resolveColor(style, "backgroundColor", DEFAULT_STYLE.backgroundColor, theme);
1134
- const stroke = resolveColor(style, "strokeColor", DEFAULT_STYLE.strokeColor, theme);
1135
- const fillVisible = !isFullyTransparent(fill);
1136
- const strokeVisible = strokeWidth > 0 && !isFullyTransparent(stroke);
1137
- if (!fillVisible && !strokeVisible) return;
1138
- const cornerRadius = (style?.roundness ?? DEFAULT_STYLE.roundness) * 4;
1139
- switch (type) {
1140
- case "rect": {
1141
- if (cornerRadius * scale < PLAIN_RECT_CORNER_THRESHOLD_PX) {
1142
- ctx.beginPath();
1143
- ctx.rect(0, 0, w, h);
1144
- } else {
1145
- buildRectPath(ctx, w, h, cornerRadius);
1146
- }
1147
- break;
1148
- }
1149
- case "ellipse":
1150
- buildEllipsePath(ctx, w, h);
1151
- break;
1152
- case "diamond":
1153
- buildDiamondPath(ctx, w, h, cornerRadius);
1154
- break;
1155
- case "tag":
1156
- buildTagPath(ctx, w, h, cornerRadius);
1157
- break;
1158
- case "thought-cloud":
1159
- buildThoughtCloudPath(ctx, w, h, cornerRadius);
1160
- break;
1161
- }
1162
- const needsScope = opacity !== 1;
1163
- if (needsScope) {
1164
- ctx.save();
1165
- ctx.globalAlpha = opacity;
1166
- }
1167
- if (fillVisible) {
1168
- ctx.fillStyle = fill;
1169
- ctx.fill();
1170
- }
1171
- if (strokeVisible && !opts?.skipStroke) {
1172
- ctx.strokeStyle = stroke;
1173
- ctx.lineWidth = Math.max(strokeWidth, 1 / scale);
1174
- ctx.setLineDash(dashPatternFor(style?.strokeStyle, strokeWidth));
1175
- ctx.stroke();
1176
- }
1177
- if (needsScope) ctx.restore();
1178
- };
1179
- var drawComposite = (ctx, node, scale, theme, opts) => {
1180
- const subs = compositeLayout(node);
1181
- for (const s of subs) {
1182
- ctx.save();
1183
- ctx.translate(s.x, s.y);
1184
- drawAtomic(ctx, s.atomic, s.w, s.h, s.style ?? node.style, scale, theme, opts);
1185
- ctx.restore();
1182
+ var compileOne = async (mj, source, color, sizePx) => {
1183
+ const svgElement = await mj.tex2svgPromise(source, { display: false, em: sizePx, ex: sizePx / 2 });
1184
+ let markup = mj.startup.adaptor.serializeXML ? mj.startup.adaptor.serializeXML(svgElement) : mj.startup.adaptor.outerHTML(svgElement);
1185
+ const svgMatch = /<svg[\s\S]*?<\/svg>/.exec(markup);
1186
+ if (svgMatch) markup = svgMatch[0];
1187
+ 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, "");
1188
+ if (!markup.includes('xmlns="http://www.w3.org/2000/svg"')) {
1189
+ markup = markup.replace(/^<svg\b/, '<svg xmlns="http://www.w3.org/2000/svg"');
1186
1190
  }
1187
- };
1188
- var compositeLayout = (node) => {
1189
- const { w, h } = node;
1190
- switch (node.type) {
1191
- case "capsule": {
1192
- const circ = Math.min(h * 0.55, w * 0.28, 56);
1193
- const overlap = circ * 0.15;
1194
- const rectX = circ - overlap;
1195
- const rectW = Math.max(0, w - rectX);
1196
- const circY = (h - circ) / 2;
1197
- return [
1198
- { atomic: "ellipse", x: 0, y: circY, w: circ, h: circ },
1199
- { atomic: "rect", x: rectX, y: 0, w: rectW, h }
1200
- ];
1201
- }
1202
- case "layered-rect":
1203
- case "layered-ellipse":
1204
- case "layered-diamond": {
1205
- const atomic = node.type === "layered-rect" ? "rect" : node.type === "layered-ellipse" ? "ellipse" : "diamond";
1206
- const off = Math.min(LAYERED_OFFSET, w * 0.15, h * 0.15);
1207
- const back = {
1208
- atomic,
1209
- x: off,
1210
- y: off,
1211
- w,
1212
- h,
1213
- style: darkenedStyle(node.style)
1214
- };
1215
- const front = { atomic, x: 0, y: 0, w, h };
1216
- return [back, front];
1217
- }
1218
- case "soft-diamond": {
1219
- const backScale = 1.08;
1220
- const frontScale = 0.96;
1221
- const bw = w * backScale;
1222
- const bh = h * backScale;
1223
- const fw = w * frontScale;
1224
- const fh = h * frontScale;
1225
- const back = {
1226
- atomic: "diamond",
1227
- x: (w - bw) / 2,
1228
- y: (h - bh) / 2,
1229
- w: bw,
1230
- h: bh,
1231
- style: darkenedStyle(node.style)
1232
- };
1233
- const front = {
1234
- atomic: "diamond",
1235
- x: (w - fw) / 2,
1236
- y: (h - fh) / 2,
1237
- w: fw,
1238
- h: fh
1239
- };
1240
- return [back, front];
1191
+ markup = markup.replace(/currentColor/gi, color);
1192
+ const dims = parseSvgDims(markup, sizePx);
1193
+ const blob = new Blob([markup], { type: "image/svg+xml" });
1194
+ const url = URL.createObjectURL(blob);
1195
+ try {
1196
+ let img;
1197
+ try {
1198
+ img = await loadImage(url);
1199
+ } catch (e) {
1200
+ console.warn(`[math] SVG failed to load for "${source}":
1201
+ ${markup}`);
1202
+ throw e;
1241
1203
  }
1204
+ const rasterW = Math.max(1, Math.ceil(dims.width * 2));
1205
+ const rasterH = Math.max(1, Math.ceil(dims.height * 2));
1206
+ const bitmap = await createImageBitmap(img, {
1207
+ resizeWidth: rasterW,
1208
+ resizeHeight: rasterH,
1209
+ resizeQuality: "high"
1210
+ });
1211
+ return {
1212
+ bitmap,
1213
+ width: dims.width,
1214
+ height: dims.height,
1215
+ baselineOffset: dims.baselineOffset
1216
+ };
1217
+ } finally {
1218
+ URL.revokeObjectURL(url);
1242
1219
  }
1243
- return [];
1244
1220
  };
1245
- var DARKENED_NO_STYLE = {};
1246
- var darkenedStyleCache = /* @__PURE__ */ new WeakMap();
1247
- var darkenedStyle = (style) => {
1248
- if (!style) return DARKENED_NO_STYLE;
1249
- const hit = darkenedStyleCache.get(style);
1250
- if (hit) return hit;
1251
- const fill = style.backgroundColor;
1252
- const stroke = style.strokeColor;
1253
- const next = {
1254
- ...style,
1255
- ...fill ? { backgroundColor: darkenHex(fill) } : {},
1256
- ...stroke ? { strokeColor: darkenHex(stroke) } : {}
1257
- };
1258
- darkenedStyleCache.set(style, next);
1259
- return next;
1221
+ var loadImage = (src) => new Promise((resolve, reject) => {
1222
+ const img = new Image();
1223
+ img.onload = () => resolve(img);
1224
+ img.onerror = (e) => reject(e);
1225
+ img.src = src;
1226
+ });
1227
+ var parseSvgDims = (markup, sizePx) => {
1228
+ const exToPx = sizePx / 2;
1229
+ const widthMatch = /<svg[^>]*\bwidth="([0-9.]+)ex"/.exec(markup);
1230
+ const heightMatch = /<svg[^>]*\bheight="([0-9.]+)ex"/.exec(markup);
1231
+ const vAlignMatch = /vertical-align:\s*(-?[0-9.]+)ex/.exec(markup);
1232
+ const widthEx = widthMatch ? Number.parseFloat(widthMatch[1]) : 2;
1233
+ const heightEx = heightMatch ? Number.parseFloat(heightMatch[1]) : 2;
1234
+ const vAlignEx = vAlignMatch ? Number.parseFloat(vAlignMatch[1]) : 0;
1235
+ const width = widthEx * exToPx;
1236
+ const height = heightEx * exToPx;
1237
+ const descent = Math.abs(vAlignEx) * exToPx;
1238
+ const baselineOffset = height - descent;
1239
+ return { width, height, baselineOffset };
1240
+ };
1241
+ var clearMathCache = () => {
1242
+ cache2.clear();
1243
+ compileQueue.length = 0;
1244
+ compileScheduled = false;
1260
1245
  };
1246
+ var getMathCacheSize = () => cache2.size;
1261
1247
 
1262
- // src/render/rough/cache.ts
1263
- var cache = /* @__PURE__ */ new Map();
1264
- var MAX_ENTRIES = 1e3;
1265
- var getOrBuildDrawable = (key, build) => {
1266
- const hit = cache.get(key);
1267
- if (hit !== void 0) {
1268
- cache.delete(key);
1269
- cache.set(key, hit);
1270
- return hit;
1248
+ // src/text/measure.ts
1249
+ var MAX_WIDTH_CACHE_SIZE = 5e3;
1250
+ var measureCanvas = typeof document !== "undefined" ? document.createElement("canvas") : null;
1251
+ var measureCtx = measureCanvas?.getContext("2d") ?? null;
1252
+ var widthCache = /* @__PURE__ */ new Map();
1253
+ var getCanvasFont = (opts) => {
1254
+ const weight = opts.type === "bold" || opts.textStyle === "bold" ? "700" : "400";
1255
+ const italic = opts.type === "italic" || opts.textStyle === "italic" ? "italic" : "normal";
1256
+ const family = opts.type === "code" ? FONT_FAMILY_MAP.monospace : FONT_FAMILY_MAP[opts.fontFamily];
1257
+ return `${italic} ${weight} ${FONT_SIZE_MAP[opts.fontSize]}px ${family}`;
1258
+ };
1259
+ var measureText = (opts) => {
1260
+ if (!opts.text) return 0;
1261
+ if (opts.type === "math") {
1262
+ const fontSizePx = FONT_SIZE_MAP[opts.fontSize];
1263
+ const bitmap = getMathBitmap(opts.text, DEFAULT_TEXT_COLOR, fontSizePx);
1264
+ if (bitmap) return bitmap.width;
1265
+ return Math.max(8, opts.text.length * fontSizePx * 0.55 + fontSizePx);
1271
1266
  }
1272
- const drawable = build();
1273
- if (cache.size >= MAX_ENTRIES) {
1274
- const first = cache.keys().next().value;
1275
- if (first !== void 0) cache.delete(first);
1267
+ const font = getCanvasFont(opts);
1268
+ const key = `${font}|${opts.text}`;
1269
+ const cached2 = widthCache.get(key);
1270
+ if (cached2 !== void 0) return cached2;
1271
+ if (!measureCtx) {
1272
+ return opts.text.length * FONT_SIZE_MAP[opts.fontSize] * 0.55;
1276
1273
  }
1277
- cache.set(key, drawable);
1278
- return drawable;
1279
- };
1280
- var seedFromId = (id) => {
1281
- let hash = 2166136261;
1282
- for (let i = 0; i < id.length; i += 1) {
1283
- hash ^= id.charCodeAt(i);
1284
- hash = Math.imul(hash, 16777619);
1274
+ measureCtx.font = font;
1275
+ const width = measureCtx.measureText(opts.text).width;
1276
+ widthCache.set(key, width);
1277
+ if (widthCache.size > MAX_WIDTH_CACHE_SIZE) {
1278
+ const oldestKey = widthCache.keys().next().value;
1279
+ if (oldestKey !== void 0) widthCache.delete(oldestKey);
1285
1280
  }
1286
- return hash >>> 0;
1281
+ return width;
1282
+ };
1283
+ var clearMeasureCache = () => {
1284
+ widthCache.clear();
1287
1285
  };
1288
1286
 
1289
- // src/render/rough/loader.ts
1290
- var cachedCtor = null;
1291
- var loadPromise = null;
1292
- var readyCallbacks = /* @__PURE__ */ new Set();
1293
- var getRoughCanvasCtor = () => {
1294
- if (cachedCtor) return cachedCtor;
1295
- if (!loadPromise) {
1296
- loadPromise = import('roughjs/bin/canvas').then((mod) => {
1297
- cachedCtor = mod.RoughCanvas;
1298
- for (const cb of readyCallbacks) cb();
1299
- readyCallbacks.clear();
1300
- return cachedCtor;
1301
- }).catch((err) => {
1302
- console.warn("[rough] failed to load roughjs:", err);
1303
- return null;
1304
- });
1305
- }
1306
- return null;
1287
+ // src/text/layout.ts
1288
+ var splitChunks = (text) => text.split(/(\s+)/g).filter(Boolean);
1289
+ var wrapCodeLine = (line, opts, maxWidth) => {
1290
+ const normalized = line.replace(/\t/g, " ");
1291
+ if (!normalized) return [""];
1292
+ const wrapped = [];
1293
+ let part = "";
1294
+ for (const ch of normalized) {
1295
+ const next = part + ch;
1296
+ const nextWidth = measureText({
1297
+ text: next,
1298
+ type: "code",
1299
+ fontFamily: opts.fontFamily,
1300
+ fontSize: opts.fontSize,
1301
+ textStyle: "normal"
1302
+ });
1303
+ if (part && nextWidth > maxWidth) {
1304
+ wrapped.push(part);
1305
+ part = ch;
1306
+ } else {
1307
+ part = next;
1308
+ }
1309
+ }
1310
+ if (part) wrapped.push(part);
1311
+ return wrapped;
1307
1312
  };
1308
- var onRoughReady = (cb) => {
1309
- if (cachedCtor) return;
1310
- readyCallbacks.add(cb);
1313
+ var layoutTokens = (tokens, opts) => {
1314
+ const maxWidth = Math.max(40, opts.width);
1315
+ const lines = [];
1316
+ let currentRuns = [];
1317
+ let cursorX = 0;
1318
+ const pushLine = () => {
1319
+ lines.push({ kind: "text", runs: currentRuns });
1320
+ currentRuns = [];
1321
+ cursorX = 0;
1322
+ };
1323
+ const pushRule = (double) => {
1324
+ if (currentRuns.length > 0) pushLine();
1325
+ lines.push({ kind: "rule", double });
1326
+ };
1327
+ const pushCodeBlock = (content) => {
1328
+ if (currentRuns.length > 0) pushLine();
1329
+ const rawLines = content.split("\n");
1330
+ const visualRuns = [];
1331
+ const codeMaxWidth = Math.max(20, maxWidth - CODE_BLOCK_PADDING_X * 2);
1332
+ for (const raw of rawLines) {
1333
+ for (const part of wrapCodeLine(raw, opts, codeMaxWidth)) {
1334
+ visualRuns.push([{ text: part, type: "code" }]);
1335
+ }
1336
+ }
1337
+ if (visualRuns.length === 0) {
1338
+ lines.push({ kind: "code-block", runs: [], isFirst: true, isLast: true });
1339
+ return;
1340
+ }
1341
+ for (let index = 0; index < visualRuns.length; index++) {
1342
+ const runs = visualRuns[index];
1343
+ lines.push({
1344
+ kind: "code-block",
1345
+ runs,
1346
+ isFirst: index === 0,
1347
+ isLast: index === visualRuns.length - 1
1348
+ });
1349
+ }
1350
+ };
1351
+ const pushChunk = (chunk, type) => {
1352
+ if (!chunk) return;
1353
+ const chunkWidth = measureText({
1354
+ text: chunk,
1355
+ type,
1356
+ fontFamily: opts.fontFamily,
1357
+ fontSize: opts.fontSize,
1358
+ textStyle: opts.textStyle
1359
+ });
1360
+ if (!chunk.trim()) {
1361
+ if (cursorX === 0) return;
1362
+ currentRuns.push({ text: chunk, type });
1363
+ cursorX += chunkWidth;
1364
+ return;
1365
+ }
1366
+ if (cursorX > 0 && cursorX + chunkWidth > maxWidth) {
1367
+ pushLine();
1368
+ }
1369
+ if (chunkWidth > maxWidth && chunk.length > 1) {
1370
+ let part = "";
1371
+ for (const ch of chunk) {
1372
+ const next = part + ch;
1373
+ const nextWidth = measureText({
1374
+ text: next,
1375
+ type,
1376
+ fontFamily: opts.fontFamily,
1377
+ fontSize: opts.fontSize,
1378
+ textStyle: opts.textStyle
1379
+ });
1380
+ if (cursorX > 0 && nextWidth > maxWidth) {
1381
+ if (part) {
1382
+ currentRuns.push({ text: part, type });
1383
+ cursorX += measureText({
1384
+ text: part,
1385
+ type,
1386
+ fontFamily: opts.fontFamily,
1387
+ fontSize: opts.fontSize,
1388
+ textStyle: opts.textStyle
1389
+ });
1390
+ }
1391
+ pushLine();
1392
+ part = ch;
1393
+ } else {
1394
+ part = next;
1395
+ }
1396
+ }
1397
+ if (part) {
1398
+ currentRuns.push({ text: part, type });
1399
+ cursorX += measureText({
1400
+ text: part,
1401
+ type,
1402
+ fontFamily: opts.fontFamily,
1403
+ fontSize: opts.fontSize,
1404
+ textStyle: opts.textStyle
1405
+ });
1406
+ }
1407
+ return;
1408
+ }
1409
+ currentRuns.push({ text: chunk, type });
1410
+ cursorX += chunkWidth;
1411
+ };
1412
+ const fontSizePx = FONT_SIZE_MAP[opts.fontSize];
1413
+ const mathColor = opts.textColor || DEFAULT_TEXT_COLOR;
1414
+ for (const token of tokens) {
1415
+ if (token.type === "code-block") {
1416
+ pushCodeBlock(token.content);
1417
+ continue;
1418
+ }
1419
+ if (token.type === "br") {
1420
+ pushLine();
1421
+ continue;
1422
+ }
1423
+ if (token.type === "hr") {
1424
+ pushRule(false);
1425
+ continue;
1426
+ }
1427
+ if (token.type === "hr-double") {
1428
+ pushRule(true);
1429
+ continue;
1430
+ }
1431
+ if (token.type === "math") {
1432
+ const bitmap = getMathBitmap(token.content, mathColor, fontSizePx);
1433
+ const width = bitmap ? bitmap.width : (
1434
+ // Placeholder: roughly proportional to source length so wrap
1435
+ // doesn't dramatically shift on resolve. Capped at maxWidth.
1436
+ Math.min(maxWidth, token.content.length * fontSizePx * 0.55 + fontSizePx)
1437
+ );
1438
+ if (cursorX > 0 && cursorX + width > maxWidth) pushLine();
1439
+ currentRuns.push({ text: token.content, type: "math" });
1440
+ cursorX += width;
1441
+ continue;
1442
+ }
1443
+ for (const chunk of splitChunks(token.content)) pushChunk(chunk, token.type);
1444
+ }
1445
+ if (currentRuns.length > 0 || lines.length === 0) {
1446
+ lines.push({ kind: "text", runs: currentRuns });
1447
+ }
1448
+ return lines;
1311
1449
  };
1312
1450
 
1313
- // src/render/rough/paths.ts
1314
- var rectPath = (x, y, w, h) => {
1315
- return `M${x} ${y} L${x + w} ${y} L${x + w} ${y + h} L${x} ${y + h} Z`;
1451
+ // src/text/font-epoch.ts
1452
+ var fontEpochListeners = /* @__PURE__ */ new Set();
1453
+ var fontEpoch = 0;
1454
+ var fontTrackingInitialized = false;
1455
+ var emitFontEpoch = () => {
1456
+ for (const listener of fontEpochListeners) listener(fontEpoch);
1316
1457
  };
1317
- var excalidrawRoundedRectPath = (x, y, w, h, radius) => {
1318
- const r = Math.max(0, Math.min(radius, w / 2, h / 2));
1319
- if (r === 0) return rectPath(x, y, w, h);
1320
- const x2 = x + w;
1321
- const y2 = y + h;
1322
- return [
1323
- `M${x + r} ${y}`,
1324
- `L${x2 - r} ${y}`,
1325
- `Q${x2} ${y}, ${x2} ${y + r}`,
1326
- `L${x2} ${y2 - r}`,
1327
- `Q${x2} ${y2}, ${x2 - r} ${y2}`,
1328
- `L${x + r} ${y2}`,
1329
- `Q${x} ${y2}, ${x} ${y2 - r}`,
1330
- `L${x} ${y + r}`,
1331
- `Q${x} ${y}, ${x + r} ${y}`,
1332
- "Z"
1333
- ].join(" ");
1458
+ var bumpFontEpoch = () => {
1459
+ fontEpoch += 1;
1460
+ clearMeasureCache();
1461
+ emitFontEpoch();
1334
1462
  };
1335
- var diamondPath = (x, y, w, h, radius = 0) => {
1336
- const cx = x + w / 2;
1337
- const cy = y + h / 2;
1338
- if (radius <= 0) {
1339
- return `M${cx} ${y} L${x + w} ${cy} L${cx} ${y + h} L${x} ${cy} Z`;
1340
- }
1341
- const T = { x: cx, y };
1342
- const R = { x: x + w, y: cy };
1343
- const B = { x: cx, y: y + h };
1344
- const L = { x, y: cy };
1345
- const edgeLen = Math.hypot(R.x - T.x, R.y - T.y);
1346
- const sMax = Math.max(0, edgeLen / 2 - 0.01);
1347
- const s = Math.min(radius * Math.SQRT2, sMax);
1348
- if (s <= 1e-4) {
1349
- return `M${T.x} ${T.y} L${R.x} ${R.y} L${B.x} ${B.y} L${L.x} ${L.y} Z`;
1350
- }
1351
- const along = (a, b, d) => {
1352
- const dx = b.x - a.x;
1353
- const dy = b.y - a.y;
1354
- const len = Math.hypot(dx, dy) || 1;
1355
- const t = d / len;
1356
- return { x: a.x + dx * t, y: a.y + dy * t };
1357
- };
1358
- const TR = along(T, R, s);
1359
- const RT = along(R, T, s);
1360
- const RB = along(R, B, s);
1361
- const BR = along(B, R, s);
1362
- const BL = along(B, L, s);
1363
- const LB = along(L, B, s);
1364
- const LT = along(L, T, s);
1365
- const TL = along(T, L, s);
1366
- return [
1367
- `M${TR.x} ${TR.y}`,
1368
- `L${RT.x} ${RT.y}`,
1369
- `Q${R.x} ${R.y}, ${RB.x} ${RB.y}`,
1370
- `L${BR.x} ${BR.y}`,
1371
- `Q${B.x} ${B.y}, ${BL.x} ${BL.y}`,
1372
- `L${LB.x} ${LB.y}`,
1373
- `Q${L.x} ${L.y}, ${LT.x} ${LT.y}`,
1374
- `L${TL.x} ${TL.y}`,
1375
- `Q${T.x} ${T.y}, ${TR.x} ${TR.y}`,
1376
- "Z"
1377
- ].join(" ");
1463
+ var initFontTracking = () => {
1464
+ if (fontTrackingInitialized) return;
1465
+ fontTrackingInitialized = true;
1466
+ if (typeof document === "undefined" || !("fonts" in document)) return;
1467
+ const fontSet = document.fonts;
1468
+ let didSettleInitialFonts = false;
1469
+ fontSet.ready.then(() => {
1470
+ if (didSettleInitialFonts) return;
1471
+ didSettleInitialFonts = true;
1472
+ bumpFontEpoch();
1473
+ }).catch(() => {
1474
+ });
1475
+ fontSet.addEventListener?.("loadingdone", () => {
1476
+ if (!didSettleInitialFonts) didSettleInitialFonts = true;
1477
+ bumpFontEpoch();
1478
+ });
1378
1479
  };
1379
- var ellipsePath = (x, y, w, h) => {
1380
- const cx = x + w / 2;
1381
- const rx = w / 2;
1382
- const ry = h / 2;
1383
- return [
1384
- `M${cx} ${y}`,
1385
- `A${rx} ${ry} 0 1 0 ${cx} ${y + h}`,
1386
- `A${rx} ${ry} 0 1 0 ${cx} ${y}`,
1387
- "Z"
1388
- ].join(" ");
1480
+ var subscribeFontEpoch = (listener) => {
1481
+ initFontTracking();
1482
+ fontEpochListeners.add(listener);
1483
+ return () => {
1484
+ fontEpochListeners.delete(listener);
1485
+ };
1389
1486
  };
1390
- var thoughtCloudPath = (x, y, w, h, radius) => {
1391
- const domeW = Math.min(w * 0.4, h * 1.2);
1392
- const domeH = Math.min(h * 0.45, domeW);
1393
- const domeAnchorX = w * 0.3;
1394
- const domeX = Math.max(0, Math.min(w - domeW, domeAnchorX - domeW / 2));
1395
- const cx = x + domeX + domeW / 2;
1396
- const cy = y + domeH / 2;
1397
- const rx = domeW / 2;
1398
- const ry = domeH / 2;
1399
- const bodyY = y + domeH * 0.55;
1400
- const bodyH = y + h - bodyY;
1401
- const r = Math.max(0, Math.min(radius, bodyH / 2, w / 2));
1402
- const t = ry > 0 ? (bodyY - cy) / ry : 0;
1403
- let xL = x + domeX;
1404
- let xR = x + domeX + domeW;
1405
- if (Math.abs(t) < 1) {
1406
- const xOffset = rx * Math.sqrt(1 - t * t);
1407
- xL = cx - xOffset;
1408
- xR = cx + xOffset;
1409
- }
1410
- xL = Math.max(x + r, xL);
1411
- xR = Math.min(x + w - r, xR);
1412
- return [
1413
- `M${x + r} ${bodyY}`,
1414
- `L${xL} ${bodyY}`,
1415
- `A${rx} ${ry} 0 1 1 ${xR} ${bodyY}`,
1416
- `L${x + w - r} ${bodyY}`,
1417
- `Q${x + w} ${bodyY}, ${x + w} ${bodyY + r}`,
1418
- `L${x + w} ${y + h - r}`,
1419
- `Q${x + w} ${y + h}, ${x + w - r} ${y + h}`,
1420
- `L${x + r} ${y + h}`,
1421
- `Q${x} ${y + h}, ${x} ${y + h - r}`,
1422
- `L${x} ${bodyY + r}`,
1423
- `Q${x} ${bodyY}, ${x + r} ${bodyY}`,
1424
- "Z"
1425
- ].join(" ");
1487
+ var getFontEpoch = () => fontEpoch;
1488
+
1489
+ // src/text/render-scale.ts
1490
+ var MIN_RENDER_SCALE = 0.15;
1491
+ var MAX_RENDER_SCALE = 1.5;
1492
+ var MAX_RENDER_WIDTH = 2e3;
1493
+ var MAX_RENDER_HEIGHT = 1200;
1494
+ var quantizeZoom = (value) => {
1495
+ if (!Number.isFinite(value)) return 1;
1496
+ return Math.max(0.1, Math.round(value * 10) / 10);
1426
1497
  };
1427
- var tagPath = (x, y, w, h, radius = 8) => {
1428
- const notch = Math.min(h * 0.5, w * 0.3);
1429
- const tipRadius = 6;
1430
- const tipX = x;
1431
- const tipY = y + h / 2;
1432
- const bodyLeft = x + Math.max(0, Math.min(notch, w));
1433
- const right = x + w;
1434
- const bottom = y + h;
1435
- const rBody = Math.min(radius, h / 2, (right - bodyLeft) / 2);
1436
- const rJoin = Math.min(radius, h * 0.45, (bodyLeft - x) * 0.8);
1437
- if (bodyLeft - x <= 1e-3) {
1438
- return excalidrawRoundedRectPath(x, y, w, h, Math.min(radius, h / 2, w / 2));
1498
+ var quantizeDpr = (value) => {
1499
+ if (!Number.isFinite(value)) return 1;
1500
+ const clamped = Math.max(1, Math.min(3, value));
1501
+ return Math.round(clamped * 4) / 4;
1502
+ };
1503
+ var resolveRenderScale = (baseScale, zoom, isMoving2) => {
1504
+ const clampedBase = Math.max(MIN_RENDER_SCALE, Math.min(MAX_RENDER_SCALE, baseScale));
1505
+ let idleScale = clampedBase;
1506
+ if (zoom <= 0.4) {
1507
+ idleScale = 0.45;
1508
+ } else if (zoom <= 0.7) {
1509
+ idleScale = 0.85;
1510
+ } else if (zoom <= 1) {
1511
+ idleScale = 1.15;
1512
+ } else if (zoom <= 1.8) {
1513
+ idleScale = 1.35;
1514
+ } else {
1515
+ idleScale = 1 + (zoom - 1.8) * 0.2;
1439
1516
  }
1440
- const pTop = { x: bodyLeft, y: y + rJoin };
1441
- const pBot = { x: bodyLeft, y: bottom - rJoin };
1442
- const dirX = tipX - bodyLeft;
1443
- const dirYTop = tipY - pTop.y;
1444
- const dirYBot = tipY - pBot.y;
1445
- const lenTop = Math.hypot(dirX, dirYTop) || 1;
1446
- const lenBot = Math.hypot(dirX, dirYBot) || 1;
1447
- const maxTipRound = Math.min(lenTop, lenBot) * 0.49;
1448
- const t = Math.max(0, Math.min(tipRadius, maxTipRound));
1449
- const tipEnter = { x: tipX - dirX / lenBot * t, y: tipY - dirYBot / lenBot * t };
1450
- const tipExit = { x: tipX - dirX / lenTop * t, y: tipY - dirYTop / lenTop * t };
1451
- const k = rJoin * 0.65;
1452
- const topStart = { x: bodyLeft + rBody, y };
1453
- const botEnd = { x: bodyLeft + rBody, y: bottom };
1454
- const parts = [
1455
- `M${topStart.x} ${topStart.y}`,
1456
- `L${right - rBody} ${y}`,
1457
- `Q${right} ${y}, ${right} ${y + rBody}`,
1458
- `L${right} ${bottom - rBody}`,
1459
- `Q${right} ${bottom}, ${right - rBody} ${bottom}`,
1460
- `L${botEnd.x} ${botEnd.y}`,
1461
- `C${botEnd.x - k} ${bottom}, ${pBot.x - dirX / lenBot * k} ${pBot.y - dirYBot / lenBot * k}, ${pBot.x} ${pBot.y}`,
1462
- `L${t > 0 ? tipEnter.x : tipX} ${t > 0 ? tipEnter.y : tipY}`
1463
- ];
1464
- if (t > 0) parts.push(`Q${tipX} ${tipY}, ${tipExit.x} ${tipExit.y}`);
1465
- parts.push(
1466
- `L${pTop.x} ${pTop.y}`,
1467
- `C${pTop.x - dirX / lenTop * k} ${pTop.y - dirYTop / lenTop * k}, ${topStart.x - k} ${y}, ${topStart.x} ${topStart.y}`,
1468
- "Z"
1517
+ idleScale = Math.max(MIN_RENDER_SCALE, Math.min(MAX_RENDER_SCALE, idleScale));
1518
+ if (isMoving2) {
1519
+ let movingScale = idleScale * (zoom >= 0.4 ? 0.72 : 0.6);
1520
+ if (zoom < 0.4) {
1521
+ movingScale = Math.min(movingScale, 0.22);
1522
+ } else if (zoom <= 0.7) {
1523
+ movingScale = Math.min(movingScale, 0.4);
1524
+ }
1525
+ return Math.max(MIN_RENDER_SCALE, Math.min(0.65, movingScale));
1526
+ }
1527
+ return idleScale;
1528
+ };
1529
+ var clampEffectiveScale = (baseScale, width, height) => {
1530
+ const limiter = Math.min(
1531
+ 1,
1532
+ MAX_RENDER_WIDTH / Math.max(1, width * baseScale),
1533
+ MAX_RENDER_HEIGHT / Math.max(1, height * baseScale)
1469
1534
  );
1470
- return parts.join(" ");
1535
+ return baseScale * limiter;
1471
1536
  };
1472
1537
 
1473
- // src/render/rough/tone-down.ts
1474
- var TONE_BLEND2 = 0.2;
1475
- var cache2 = /* @__PURE__ */ new Map();
1476
- var deriveRoughStrokeColor = (stroke, fill, isDark) => {
1477
- if (!isFullyTransparent(stroke)) return stroke;
1478
- if (isFullyTransparent(fill)) return stroke;
1479
- const key = `${fill}|${isDark ? "d" : "l"}`;
1480
- const hit = cache2.get(key);
1481
- if (hit) return hit;
1482
- const next = mixHex(fill, isDark ? "#000000" : "#ffffff", TONE_BLEND2);
1483
- cache2.set(key, next);
1484
- return next;
1538
+ // src/text/estimate-height.ts
1539
+ var getLineAdvance = (line, lineHeight) => {
1540
+ if (line.kind !== "code-block") return lineHeight;
1541
+ let advance = lineHeight;
1542
+ if (line.isFirst) advance += CODE_BLOCK_MARGIN_Y;
1543
+ if (line.isLast) advance += CODE_BLOCK_MARGIN_Y;
1544
+ return advance;
1545
+ };
1546
+ var getContentHeight = (lines, lineHeight) => Math.max(
1547
+ lineHeight,
1548
+ lines.reduce((sum, line) => sum + getLineAdvance(line, lineHeight), 0)
1549
+ );
1550
+ var estimateMarkdownContentHeight = ({
1551
+ text,
1552
+ width,
1553
+ fontFamily = "handwriting",
1554
+ fontSize = "M",
1555
+ textStyle = "normal"
1556
+ }) => {
1557
+ const normalizedText = text.trim();
1558
+ if (!normalizedText) return 0;
1559
+ const resolvedWidth = Math.max(40, Math.ceil(width));
1560
+ const lines = layoutTokens(tokenize(text), {
1561
+ width: resolvedWidth,
1562
+ fontFamily,
1563
+ fontSize,
1564
+ textStyle
1565
+ });
1566
+ const lineHeight = LINE_HEIGHT_MAP[fontSize];
1567
+ return getContentHeight(lines, lineHeight) + CONTENT_HEIGHT_BUFFER;
1485
1568
  };
1569
+ var getMarkdownLineHeightPx = (fontSize) => LINE_HEIGHT_MAP[fontSize];
1486
1570
 
1487
- // src/render/rough/draw.ts
1488
- var apparentDetail = (maxSide, zoom) => {
1489
- const apparent = maxSide * Math.min(1, zoom);
1490
- if (apparent >= 800) return { curveStepCount: 3, maxRandomnessOffset: 0.9 };
1491
- if (apparent >= 400) return { curveStepCount: 4, maxRandomnessOffset: 1.1 };
1492
- return { curveStepCount: 5, maxRandomnessOffset: 1.3 };
1571
+ // src/text/paint-canvas.ts
1572
+ var getLineAdvance2 = (line, lineHeight) => {
1573
+ if (line.kind !== "code-block") return lineHeight;
1574
+ let advance = lineHeight;
1575
+ if (line.isFirst) advance += CODE_BLOCK_MARGIN_Y;
1576
+ if (line.isLast) advance += CODE_BLOCK_MARGIN_Y;
1577
+ return advance;
1493
1578
  };
1494
- var drawRoughShape = (ctx, node, scale, theme) => {
1495
- const Ctor = getRoughCanvasCtor();
1496
- if (!Ctor) return false;
1497
- const rc = ensureRoughCanvas(ctx, Ctor);
1498
- if (!rc) return false;
1499
- const seed = node.id ? seedFromId(node.id) % 2147483646 + 1 : 1337;
1500
- paintAtomicRough(
1501
- rc,
1502
- ctx,
1503
- node.type,
1504
- node.w,
1505
- node.h,
1506
- node.style,
1507
- scale,
1508
- theme,
1509
- seed
1510
- );
1511
- return true;
1579
+ var getTextX = (opts, lineWidth) => {
1580
+ if (opts.align === "center") return Math.floor((opts.width - lineWidth) / 2);
1581
+ if (opts.align === "right") return Math.max(0, opts.width - lineWidth);
1582
+ return 0;
1512
1583
  };
1513
- var drawCompositeRough = (ctx, node, scale, theme) => {
1514
- const Ctor = getRoughCanvasCtor();
1515
- if (!Ctor) return false;
1516
- const rc = ensureRoughCanvas(ctx, Ctor);
1517
- if (!rc) return false;
1518
- const subs = compositeLayout(node);
1519
- const baseSeed = node.id ? seedFromId(node.id) % 2147483646 + 1 : 1337;
1520
- for (let i = 0; i < subs.length; i++) {
1521
- const s = subs[i];
1522
- const subStyle = s.style ?? node.style;
1523
- ctx.save();
1524
- ctx.translate(s.x, s.y);
1525
- ctx.translate(ROUGH_FILL_MISREGISTER_X, ROUGH_FILL_MISREGISTER_Y);
1526
- drawAtomic(ctx, s.atomic, s.w, s.h, subStyle, scale, theme, { skipStroke: true });
1527
- ctx.translate(-ROUGH_FILL_MISREGISTER_X, -ROUGH_FILL_MISREGISTER_Y);
1528
- paintAtomicRough(
1529
- rc,
1530
- ctx,
1531
- s.atomic,
1532
- s.w,
1533
- s.h,
1534
- subStyle,
1535
- scale,
1536
- theme,
1537
- (baseSeed + i * 7919) % 2147483646 + 1
1538
- );
1539
- ctx.restore();
1540
- }
1541
- return true;
1542
- };
1543
- var paintAtomicRough = (rc, ctx, type, w, h, style, scale, theme, seed) => {
1544
- if (w <= 0 || h <= 0) return;
1545
- const rawStroke = resolveColor(style, "strokeColor", "#1f2937", theme);
1546
- const isDark = theme?.("mode") === "dark";
1547
- const fill = resolveColor(style, "backgroundColor", DEFAULT_STYLE.backgroundColor, theme);
1548
- const strokeColor = deriveRoughStrokeColor(rawStroke, fill, isDark);
1549
- const rawStrokeWidth = resolveStrokeWidth(style, theme);
1550
- if (rawStrokeWidth <= 0) return;
1551
- const roughness = style?.roughness ?? 0;
1552
- if (roughness <= 0) return;
1553
- const isNoBorderIntent = isFullyTransparent(rawStroke);
1554
- const effectiveStrokeStyle = isNoBorderIntent ? "solid" : style?.strokeStyle ?? "solid";
1555
- const strokeWidth = isNoBorderIntent ? DEFAULT_STYLE.strokeWidth : rawStrokeWidth;
1556
- const cornerRadius = (style?.roundness ?? DEFAULT_STYLE.roundness) * 4;
1557
- const radius = Math.max(0, Math.min(cornerRadius, w / 2, h / 2));
1558
- const dash = dashPatternFor(effectiveStrokeStyle, strokeWidth);
1559
- const detail = apparentDetail(Math.max(w, h), scale);
1560
- const cacheKey = [
1561
- type,
1562
- w.toFixed(1),
1563
- h.toFixed(1),
1564
- radius.toFixed(1),
1565
- strokeColor,
1566
- strokeWidth.toFixed(2),
1567
- effectiveStrokeStyle,
1568
- roughness.toFixed(2),
1569
- seed,
1570
- detail.curveStepCount,
1571
- detail.maxRandomnessOffset.toFixed(2)
1572
- ].join("|");
1573
- const drawable = getOrBuildDrawable(cacheKey, () => {
1574
- const pathData = buildPath(type, 0, 0, w, h, radius);
1575
- return rc.generator.path(pathData, {
1576
- ...ROUGH_DEFAULTS,
1577
- stroke: strokeColor,
1578
- strokeWidth,
1579
- roughness,
1580
- seed,
1581
- strokeLineDash: dash.length > 0 ? dash : void 0,
1582
- curveStepCount: detail.curveStepCount,
1583
- maxRandomnessOffset: detail.maxRandomnessOffset
1584
- });
1585
- });
1586
- ctx.save();
1587
- ctx.lineJoin = "round";
1588
- rc.draw(drawable);
1589
- ctx.restore();
1590
- };
1591
- var drawRoughEdge = (ctx, edge, samples, scale, theme) => {
1592
- const Ctor = getRoughCanvasCtor();
1593
- if (!Ctor) return false;
1594
- if (samples.length < 2) return true;
1595
- const style = edge.style;
1596
- const strokeColor = resolveColor(style, "strokeColor", "#475569", theme);
1597
- const strokeWidth = resolveStrokeWidth(style, theme);
1598
- if (strokeWidth <= 0) return true;
1599
- const roughness = style?.roughness ?? 0;
1600
- if (roughness <= 0) return true;
1601
- const seed = edge.id ? seedFromId(edge.id) % 2147483646 + 1 : 1337;
1602
- const dash = dashPatternFor(style?.strokeStyle, strokeWidth);
1603
- let minX = samples[0].x;
1604
- let maxX = samples[0].x;
1605
- let minY = samples[0].y;
1606
- let maxY = samples[0].y;
1607
- for (const p of samples) {
1608
- if (p.x < minX) minX = p.x;
1609
- if (p.x > maxX) maxX = p.x;
1610
- if (p.y < minY) minY = p.y;
1611
- if (p.y > maxY) maxY = p.y;
1612
- }
1613
- const detail = apparentDetail(Math.max(maxX - minX, maxY - minY), scale);
1614
- const cacheKey = [
1615
- "edge",
1616
- edge.id,
1617
- samples.length,
1618
- Math.round(minX),
1619
- Math.round(minY),
1620
- Math.round(maxX),
1621
- Math.round(maxY),
1622
- strokeColor,
1623
- strokeWidth.toFixed(2),
1624
- style?.strokeStyle ?? "solid",
1625
- roughness.toFixed(2),
1626
- seed,
1627
- detail.curveStepCount,
1628
- detail.maxRandomnessOffset.toFixed(2)
1629
- ].join("|");
1630
- const rc = ensureRoughCanvas(ctx, Ctor);
1631
- if (!rc) return false;
1632
- const drawable = getOrBuildDrawable(cacheKey, () => {
1633
- const points = samples.map((s) => [s.x, s.y]);
1634
- return rc.generator.linearPath(points, {
1635
- ...ROUGH_DEFAULTS,
1636
- stroke: strokeColor,
1637
- strokeWidth,
1638
- roughness,
1639
- seed,
1640
- strokeLineDash: dash.length > 0 ? dash : void 0,
1641
- curveStepCount: detail.curveStepCount,
1642
- maxRandomnessOffset: detail.maxRandomnessOffset
1643
- });
1644
- });
1645
- ctx.save();
1646
- ctx.lineJoin = "round";
1647
- rc.draw(drawable);
1648
- ctx.restore();
1649
- return true;
1650
- };
1651
- var ROUGH_CANVAS_KEY = "__roughCanvas";
1652
- var ensureRoughCanvas = (ctx, Ctor) => {
1653
- const ctxWithCache = ctx;
1654
- if (ctxWithCache[ROUGH_CANVAS_KEY]) return ctxWithCache[ROUGH_CANVAS_KEY];
1655
- const rc = new Ctor(ctx.canvas);
1656
- ctxWithCache[ROUGH_CANVAS_KEY] = rc;
1657
- return rc;
1658
- };
1659
- var buildPath = (type, x, y, w, h, radius) => {
1660
- switch (type) {
1661
- case "rect":
1662
- return radius > 0 ? excalidrawRoundedRectPath(x, y, w, h, radius) : rectPath(x, y, w, h);
1663
- case "ellipse":
1664
- return ellipsePath(x, y, w, h);
1665
- case "diamond":
1666
- return diamondPath(x, y, w, h, radius);
1667
- case "tag":
1668
- return tagPath(x, y, w, h, radius);
1669
- case "thought-cloud":
1670
- return thoughtCloudPath(x, y, w, h, radius);
1671
- }
1672
- };
1673
-
1674
- // src/text/tokens.ts
1675
- var INLINE_PATTERN = /(\$\$[^\n]+?\$\$|\*\*[^*]+\*\*|==[^=\s](?:[^=]*?[^=\s])?==|`[^`]+`|\*[^*]+\*|__[^_]+__|~~[^~]+~~|_[^_]+_|\[[^\]]+\]\([^)]+\))/g;
1676
- var HR_LINE_PATTERN = /^[ \t]*---[ \t]*$/;
1677
- var DOUBLE_HR_LINE_PATTERN = /^[ \t]*===[ \t]*$/;
1678
- var transformSymbols = (value) => value.replace(/<=>|<->|<-|->|\[\]|\[[vx]\]/gi, (match) => {
1679
- const normalized = match.toLowerCase();
1680
- if (normalized === "->") return "\u2192";
1681
- if (normalized === "<-") return "\u2190";
1682
- if (normalized === "<->") return "\u2194";
1683
- if (normalized === "<=>") return "\u21D4";
1684
- if (normalized === "[]") return "\u2610";
1685
- if (normalized === "[v]") return "\u2705";
1686
- if (normalized === "[x]") return "\u274E";
1687
- return match;
1688
- });
1689
- var tokenizeInline = (segment) => {
1690
- if (!segment) return [];
1691
- const tokens = [];
1692
- let lastIndex = 0;
1693
- segment.replace(INLINE_PATTERN, (match, _group, offset) => {
1694
- const idx = offset;
1695
- if (idx > lastIndex) {
1696
- tokens.push({ type: "text", content: transformSymbols(segment.slice(lastIndex, idx)) });
1697
- }
1698
- if (match.startsWith("**") && match.endsWith("**") || match.startsWith("__") && match.endsWith("__")) {
1699
- tokens.push({ type: "bold", content: transformSymbols(match.slice(2, -2)) });
1700
- } else if (match.startsWith("*") && match.endsWith("*")) {
1701
- tokens.push({ type: "italic", content: transformSymbols(match.slice(1, -1)) });
1702
- } else if (match.startsWith("~~") && match.endsWith("~~")) {
1703
- tokens.push({ type: "strike", content: transformSymbols(match.slice(2, -2)) });
1704
- } else if (match.startsWith("==") && match.endsWith("==")) {
1705
- tokens.push({ type: "highlight", content: transformSymbols(match.slice(2, -2)) });
1706
- } else if (match.startsWith("_") && match.endsWith("_")) {
1707
- tokens.push({ type: "underline", content: transformSymbols(match.slice(1, -1)) });
1708
- } else if (match.startsWith("[") && match.includes("](") && match.endsWith(")")) {
1709
- const splitIndex = match.indexOf("](");
1710
- tokens.push({ type: "link", content: transformSymbols(match.slice(1, splitIndex)) });
1711
- } else if (match.startsWith("`") && match.endsWith("`")) {
1712
- tokens.push({ type: "code", content: match.slice(1, -1) });
1713
- } else if (match.startsWith("$$") && match.endsWith("$$")) {
1714
- tokens.push({ type: "math", content: match.slice(2, -2) });
1715
- } else {
1716
- tokens.push({ type: "text", content: transformSymbols(match) });
1717
- }
1718
- lastIndex = idx + match.length;
1719
- return match;
1720
- });
1721
- if (lastIndex < segment.length) {
1722
- tokens.push({ type: "text", content: transformSymbols(segment.slice(lastIndex)) });
1723
- }
1724
- return tokens;
1725
- };
1726
- var tokenizeLine = (line) => {
1727
- if (DOUBLE_HR_LINE_PATTERN.test(line)) return [{ type: "hr-double" }];
1728
- if (HR_LINE_PATTERN.test(line)) return [{ type: "hr" }];
1729
- return tokenizeInline(line);
1730
- };
1731
- var tokenizeTextBlock = (block) => {
1732
- if (!block) return [];
1733
- const tokens = [];
1734
- const lines = block.split("\n");
1735
- lines.forEach((line, index) => {
1736
- const lineTokens = tokenizeLine(line);
1737
- tokens.push(...lineTokens);
1738
- const isRuleLine = lineTokens.length === 1 && (lineTokens[0]?.type === "hr" || lineTokens[0]?.type === "hr-double");
1739
- if (index < lines.length - 1 && !isRuleLine) tokens.push({ type: "br" });
1740
- });
1741
- return tokens;
1742
- };
1743
- var tokenize = (input) => {
1744
- if (!input) return [];
1745
- const tokens = [];
1746
- let cursor = 0;
1747
- while (cursor < input.length) {
1748
- const fenceStart = input.indexOf("```", cursor);
1749
- if (fenceStart === -1) {
1750
- tokens.push(...tokenizeTextBlock(input.slice(cursor)));
1751
- break;
1752
- }
1753
- if (fenceStart > cursor) {
1754
- tokens.push(...tokenizeTextBlock(input.slice(cursor, fenceStart)));
1755
- }
1756
- const fenceEnd = input.indexOf("```", fenceStart + 3);
1757
- if (fenceEnd === -1) {
1758
- tokens.push(...tokenizeTextBlock(input.slice(fenceStart)));
1759
- break;
1760
- }
1761
- const fenceContent = input.slice(fenceStart + 3, fenceEnd);
1762
- const delimiterIndex = fenceContent.search(/[\r\n]/);
1763
- let codeContent = fenceContent;
1764
- if (delimiterIndex >= 0) {
1765
- codeContent = fenceContent.slice(delimiterIndex).replace(/^\r?\n/, "");
1766
- }
1767
- tokens.push({ type: "code-block", content: codeContent.replace(/\r\n/g, "\n") });
1768
- cursor = fenceEnd + 3;
1769
- }
1770
- return tokens;
1771
- };
1772
-
1773
- // src/text/defaults.ts
1774
- var FONT_FAMILY_MAP = {
1775
- handwriting: '"Architects Daughter", cursive',
1776
- "sans-serif": '"Atkinson Hyperlegible Next", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", ui-sans-serif, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"',
1777
- serif: '"Lora", "Source Serif 4", ui-serif, Georgia, Cambria, "Times New Roman", Times, serif',
1778
- monospace: '"Inconsolata", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
1779
- informal: '"Shantell Sans", ui-handwriting, cursive'
1780
- };
1781
- var FONT_SIZE_MAP = {
1782
- S: 14,
1783
- M: 16,
1784
- L: 24,
1785
- XL: 36
1786
- };
1787
- var LINE_HEIGHT_MAP = {
1788
- S: 20,
1789
- M: 24,
1790
- L: 32,
1791
- XL: 40
1792
- };
1793
- var CODE_BLOCK_PADDING_X = 6;
1794
- var CODE_BLOCK_MARGIN_Y = 4;
1795
- var CONTENT_HEIGHT_BUFFER = 4;
1796
- var CONTENT_PADDING = 6;
1797
- var DEFAULT_TEXT_COLOR = "#1f2937";
1798
- var DEFAULT_HIGHLIGHT_COLOR = "#fde047";
1799
- var DEFAULT_HIGHLIGHT_COLOR_DARK = "#6b5a23";
1800
- var LINK_COLOR = "#2563eb";
1801
- var CODE_BG_COLOR = "rgba(148, 163, 184, 0.18)";
1802
-
1803
- // src/text/math/loader.ts
1804
- var cached = null;
1805
- var loadPromise2 = null;
1806
- var loadFailed = false;
1807
- var readyCallbacks2 = /* @__PURE__ */ new Set();
1808
- var getMathJax = () => {
1809
- if (cached) return cached;
1810
- if (loadFailed) return null;
1811
- if (!loadPromise2) {
1812
- loadPromise2 = loadMathJax().then((instance) => {
1813
- if (instance) {
1814
- cached = instance;
1815
- for (const cb of readyCallbacks2) cb();
1816
- } else {
1817
- loadFailed = true;
1818
- }
1819
- readyCallbacks2.clear();
1820
- return cached;
1821
- }).catch((err) => {
1822
- console.warn("[math] failed to load MathJax:", err);
1823
- loadFailed = true;
1824
- readyCallbacks2.clear();
1825
- return null;
1826
- });
1827
- }
1828
- return null;
1829
- };
1830
- var onMathJaxReady = (cb) => {
1831
- if (cached) return;
1832
- if (loadFailed) return;
1833
- readyCallbacks2.add(cb);
1834
- };
1835
- var loadMathJax = async () => {
1836
- if (typeof window === "undefined") return null;
1837
- const winAny = window;
1838
- winAny.MathJax = {
1839
- ...winAny.MathJax ?? {},
1840
- startup: { typeset: false },
1841
- options: {
1842
- enableMenu: false,
1843
- enableEnrichment: false,
1844
- enableSpeech: false,
1845
- enableComplexity: false,
1846
- sre: { speech: "none" }
1847
- },
1848
- // `fontCache: 'none'` inlines every glyph as a raw <path>
1849
- // (slightly bigger SVG, no <use> references). Required for SVGs
1850
- // we extract to a Blob URL and rasterize via <img> — `<use>`
1851
- // refs to <defs> elsewhere in the page wouldn't resolve.
1852
- // `linebreaks: { inline: false }` keeps the whole formula in one
1853
- // <svg> element (v4 defaults to true for long inline math).
1854
- svg: {
1855
- scale: 1,
1856
- fontCache: "none",
1857
- linebreaks: { inline: false }
1858
- }
1859
- };
1860
- const VENDOR_URL = "https://cdn.jsdelivr.net/npm/mathjax@4/tex-svg.js";
1861
- await new Promise((resolve, reject) => {
1862
- const existing = document.querySelector(
1863
- `script[src="${VENDOR_URL}"]`
1864
- );
1865
- if (existing) {
1866
- existing.addEventListener("load", () => resolve(), { once: true });
1867
- existing.addEventListener("error", () => reject(new Error("MathJax CDN load failed")), {
1868
- once: true
1869
- });
1870
- return;
1871
- }
1872
- const script = document.createElement("script");
1873
- script.src = VENDOR_URL;
1874
- script.async = true;
1875
- script.onload = () => resolve();
1876
- script.onerror = () => reject(new Error("MathJax CDN load failed"));
1877
- document.head.appendChild(script);
1878
- });
1879
- const mj = winAny.MathJax;
1880
- if (!mj) throw new Error("MathJax did not install on window after import");
1881
- if (typeof mj.tex2svgPromise !== "function") {
1882
- throw new Error("MathJax loaded but tex2svgPromise is missing \u2014 wrong bundle?");
1883
- }
1884
- await mj.startup?.promise;
1885
- return mj;
1886
- };
1887
-
1888
- // src/text/math/cache.ts
1889
- var normalizeSize = (px) => Math.max(8, Math.round(px));
1890
- var cache3 = /* @__PURE__ */ new Map();
1891
- var compileQueue = [];
1892
- var compileScheduled = false;
1893
- var mathEpoch = 0;
1894
- var epochSubscribers = /* @__PURE__ */ new Set();
1895
- var getMathEpoch = () => mathEpoch;
1896
- var subscribeMathEpoch = (cb) => {
1897
- epochSubscribers.add(cb);
1898
- return () => {
1899
- epochSubscribers.delete(cb);
1900
- };
1901
- };
1902
- var bumpMathEpoch = () => {
1903
- mathEpoch += 1;
1904
- for (const cb of epochSubscribers) cb();
1905
- };
1906
- var getMathBitmap = (source, color, sizePx) => {
1907
- const size = normalizeSize(sizePx);
1908
- const key = `${size}:${color}:${source}`;
1909
- const existing = cache3.get(key);
1910
- if (existing) {
1911
- if (existing.state === "ready") return existing.bitmap;
1912
- return null;
1913
- }
1914
- cache3.set(key, { state: "pending" });
1915
- compileQueue.push({ key, source, color, sizePx: size });
1916
- scheduleCompile();
1917
- return null;
1918
- };
1919
- var scheduleCompile = () => {
1920
- if (compileScheduled) return;
1921
- compileScheduled = true;
1922
- if (typeof window === "undefined" || typeof requestAnimationFrame === "undefined") {
1923
- void drainQueue();
1924
- return;
1925
- }
1926
- requestAnimationFrame(() => {
1927
- void drainQueue();
1928
- });
1929
- };
1930
- var drainQueue = async () => {
1931
- compileScheduled = false;
1932
- if (compileQueue.length === 0) return;
1933
- const mj = getMathJax();
1934
- if (!mj) {
1935
- onMathJaxReady(() => scheduleCompile());
1936
- return;
1937
- }
1938
- const FRAME_BUDGET_MS = 4;
1939
- const start = performance.now();
1940
- let didResolve = false;
1941
- while (compileQueue.length > 0 && performance.now() - start < FRAME_BUDGET_MS) {
1942
- const item = compileQueue.shift();
1943
- if (cache3.get(item.key)?.state !== "pending") continue;
1944
- try {
1945
- const bitmap = await compileOne(mj, item.source, item.color, item.sizePx);
1946
- cache3.set(item.key, { state: "ready", bitmap });
1947
- didResolve = true;
1948
- } catch (err) {
1949
- cache3.set(item.key, { state: "error", err });
1950
- console.warn(`[math] failed to compile "${item.source}":`, err);
1951
- }
1952
- }
1953
- if (didResolve) bumpMathEpoch();
1954
- if (compileQueue.length > 0) scheduleCompile();
1955
- };
1956
- var compileOne = async (mj, source, color, sizePx) => {
1957
- const svgElement = await mj.tex2svgPromise(source, { display: false, em: sizePx, ex: sizePx / 2 });
1958
- let markup = mj.startup.adaptor.serializeXML ? mj.startup.adaptor.serializeXML(svgElement) : mj.startup.adaptor.outerHTML(svgElement);
1959
- const svgMatch = /<svg[\s\S]*?<\/svg>/.exec(markup);
1960
- if (svgMatch) markup = svgMatch[0];
1961
- 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, "");
1962
- if (!markup.includes('xmlns="http://www.w3.org/2000/svg"')) {
1963
- markup = markup.replace(/^<svg\b/, '<svg xmlns="http://www.w3.org/2000/svg"');
1964
- }
1965
- markup = markup.replace(/currentColor/gi, color);
1966
- const dims = parseSvgDims(markup, sizePx);
1967
- const blob = new Blob([markup], { type: "image/svg+xml" });
1968
- const url = URL.createObjectURL(blob);
1969
- try {
1970
- let img;
1971
- try {
1972
- img = await loadImage(url);
1973
- } catch (e) {
1974
- console.warn(`[math] SVG failed to load for "${source}":
1975
- ${markup}`);
1976
- throw e;
1977
- }
1978
- const rasterW = Math.max(1, Math.ceil(dims.width * 2));
1979
- const rasterH = Math.max(1, Math.ceil(dims.height * 2));
1980
- const bitmap = await createImageBitmap(img, {
1981
- resizeWidth: rasterW,
1982
- resizeHeight: rasterH,
1983
- resizeQuality: "high"
1984
- });
1985
- return {
1986
- bitmap,
1987
- width: dims.width,
1988
- height: dims.height,
1989
- baselineOffset: dims.baselineOffset
1990
- };
1991
- } finally {
1992
- URL.revokeObjectURL(url);
1993
- }
1994
- };
1995
- var loadImage = (src) => new Promise((resolve, reject) => {
1996
- const img = new Image();
1997
- img.onload = () => resolve(img);
1998
- img.onerror = (e) => reject(e);
1999
- img.src = src;
2000
- });
2001
- var parseSvgDims = (markup, sizePx) => {
2002
- const exToPx = sizePx / 2;
2003
- const widthMatch = /<svg[^>]*\bwidth="([0-9.]+)ex"/.exec(markup);
2004
- const heightMatch = /<svg[^>]*\bheight="([0-9.]+)ex"/.exec(markup);
2005
- const vAlignMatch = /vertical-align:\s*(-?[0-9.]+)ex/.exec(markup);
2006
- const widthEx = widthMatch ? Number.parseFloat(widthMatch[1]) : 2;
2007
- const heightEx = heightMatch ? Number.parseFloat(heightMatch[1]) : 2;
2008
- const vAlignEx = vAlignMatch ? Number.parseFloat(vAlignMatch[1]) : 0;
2009
- const width = widthEx * exToPx;
2010
- const height = heightEx * exToPx;
2011
- const descent = Math.abs(vAlignEx) * exToPx;
2012
- const baselineOffset = height - descent;
2013
- return { width, height, baselineOffset };
2014
- };
2015
- var clearMathCache = () => {
2016
- cache3.clear();
2017
- compileQueue.length = 0;
2018
- compileScheduled = false;
2019
- };
2020
- var getMathCacheSize = () => cache3.size;
2021
-
2022
- // src/text/measure.ts
2023
- var MAX_WIDTH_CACHE_SIZE = 5e3;
2024
- var measureCanvas = typeof document !== "undefined" ? document.createElement("canvas") : null;
2025
- var measureCtx = measureCanvas?.getContext("2d") ?? null;
2026
- var widthCache = /* @__PURE__ */ new Map();
2027
- var getCanvasFont = (opts) => {
2028
- const weight = opts.type === "bold" || opts.textStyle === "bold" ? "700" : "400";
2029
- const italic = opts.type === "italic" || opts.textStyle === "italic" ? "italic" : "normal";
2030
- const family = opts.type === "code" ? FONT_FAMILY_MAP.monospace : FONT_FAMILY_MAP[opts.fontFamily];
2031
- return `${italic} ${weight} ${FONT_SIZE_MAP[opts.fontSize]}px ${family}`;
2032
- };
2033
- var measureText = (opts) => {
2034
- if (!opts.text) return 0;
2035
- if (opts.type === "math") {
2036
- const fontSizePx = FONT_SIZE_MAP[opts.fontSize];
2037
- const bitmap = getMathBitmap(opts.text, DEFAULT_TEXT_COLOR, fontSizePx);
2038
- if (bitmap) return bitmap.width;
2039
- return Math.max(8, opts.text.length * fontSizePx * 0.55 + fontSizePx);
2040
- }
2041
- const font = getCanvasFont(opts);
2042
- const key = `${font}|${opts.text}`;
2043
- const cached2 = widthCache.get(key);
2044
- if (cached2 !== void 0) return cached2;
2045
- if (!measureCtx) {
2046
- return opts.text.length * FONT_SIZE_MAP[opts.fontSize] * 0.55;
2047
- }
2048
- measureCtx.font = font;
2049
- const width = measureCtx.measureText(opts.text).width;
2050
- widthCache.set(key, width);
2051
- if (widthCache.size > MAX_WIDTH_CACHE_SIZE) {
2052
- const oldestKey = widthCache.keys().next().value;
2053
- if (oldestKey !== void 0) widthCache.delete(oldestKey);
2054
- }
2055
- return width;
2056
- };
2057
- var clearMeasureCache = () => {
2058
- widthCache.clear();
2059
- };
2060
-
2061
- // src/text/layout.ts
2062
- var splitChunks = (text) => text.split(/(\s+)/g).filter(Boolean);
2063
- var wrapCodeLine = (line, opts, maxWidth) => {
2064
- const normalized = line.replace(/\t/g, " ");
2065
- if (!normalized) return [""];
2066
- const wrapped = [];
2067
- let part = "";
2068
- for (const ch of normalized) {
2069
- const next = part + ch;
2070
- const nextWidth = measureText({
2071
- text: next,
2072
- type: "code",
2073
- fontFamily: opts.fontFamily,
2074
- fontSize: opts.fontSize,
2075
- textStyle: "normal"
2076
- });
2077
- if (part && nextWidth > maxWidth) {
2078
- wrapped.push(part);
2079
- part = ch;
2080
- } else {
2081
- part = next;
2082
- }
2083
- }
2084
- if (part) wrapped.push(part);
2085
- return wrapped;
2086
- };
2087
- var layoutTokens = (tokens, opts) => {
2088
- const maxWidth = Math.max(40, opts.width);
2089
- const lines = [];
2090
- let currentRuns = [];
2091
- let cursorX = 0;
2092
- const pushLine = () => {
2093
- lines.push({ kind: "text", runs: currentRuns });
2094
- currentRuns = [];
2095
- cursorX = 0;
2096
- };
2097
- const pushRule = (double) => {
2098
- if (currentRuns.length > 0) pushLine();
2099
- lines.push({ kind: "rule", double });
2100
- };
2101
- const pushCodeBlock = (content) => {
2102
- if (currentRuns.length > 0) pushLine();
2103
- const rawLines = content.split("\n");
2104
- const visualRuns = [];
2105
- const codeMaxWidth = Math.max(20, maxWidth - CODE_BLOCK_PADDING_X * 2);
2106
- for (const raw of rawLines) {
2107
- for (const part of wrapCodeLine(raw, opts, codeMaxWidth)) {
2108
- visualRuns.push([{ text: part, type: "code" }]);
2109
- }
2110
- }
2111
- if (visualRuns.length === 0) {
2112
- lines.push({ kind: "code-block", runs: [], isFirst: true, isLast: true });
2113
- return;
2114
- }
2115
- for (let index = 0; index < visualRuns.length; index++) {
2116
- const runs = visualRuns[index];
2117
- lines.push({
2118
- kind: "code-block",
2119
- runs,
2120
- isFirst: index === 0,
2121
- isLast: index === visualRuns.length - 1
2122
- });
2123
- }
2124
- };
2125
- const pushChunk = (chunk, type) => {
2126
- if (!chunk) return;
2127
- const chunkWidth = measureText({
2128
- text: chunk,
2129
- type,
2130
- fontFamily: opts.fontFamily,
2131
- fontSize: opts.fontSize,
2132
- textStyle: opts.textStyle
2133
- });
2134
- if (!chunk.trim()) {
2135
- if (cursorX === 0) return;
2136
- currentRuns.push({ text: chunk, type });
2137
- cursorX += chunkWidth;
2138
- return;
2139
- }
2140
- if (cursorX > 0 && cursorX + chunkWidth > maxWidth) {
2141
- pushLine();
2142
- }
2143
- if (chunkWidth > maxWidth && chunk.length > 1) {
2144
- let part = "";
2145
- for (const ch of chunk) {
2146
- const next = part + ch;
2147
- const nextWidth = measureText({
2148
- text: next,
2149
- type,
2150
- fontFamily: opts.fontFamily,
2151
- fontSize: opts.fontSize,
2152
- textStyle: opts.textStyle
2153
- });
2154
- if (cursorX > 0 && nextWidth > maxWidth) {
2155
- if (part) {
2156
- currentRuns.push({ text: part, type });
2157
- cursorX += measureText({
2158
- text: part,
2159
- type,
2160
- fontFamily: opts.fontFamily,
2161
- fontSize: opts.fontSize,
2162
- textStyle: opts.textStyle
2163
- });
2164
- }
2165
- pushLine();
2166
- part = ch;
2167
- } else {
2168
- part = next;
2169
- }
2170
- }
2171
- if (part) {
2172
- currentRuns.push({ text: part, type });
2173
- cursorX += measureText({
2174
- text: part,
2175
- type,
2176
- fontFamily: opts.fontFamily,
2177
- fontSize: opts.fontSize,
2178
- textStyle: opts.textStyle
2179
- });
2180
- }
2181
- return;
2182
- }
2183
- currentRuns.push({ text: chunk, type });
2184
- cursorX += chunkWidth;
2185
- };
2186
- const fontSizePx = FONT_SIZE_MAP[opts.fontSize];
2187
- const mathColor = opts.textColor || DEFAULT_TEXT_COLOR;
2188
- for (const token of tokens) {
2189
- if (token.type === "code-block") {
2190
- pushCodeBlock(token.content);
2191
- continue;
2192
- }
2193
- if (token.type === "br") {
2194
- pushLine();
2195
- continue;
2196
- }
2197
- if (token.type === "hr") {
2198
- pushRule(false);
2199
- continue;
2200
- }
2201
- if (token.type === "hr-double") {
2202
- pushRule(true);
2203
- continue;
2204
- }
2205
- if (token.type === "math") {
2206
- const bitmap = getMathBitmap(token.content, mathColor, fontSizePx);
2207
- const width = bitmap ? bitmap.width : (
2208
- // Placeholder: roughly proportional to source length so wrap
2209
- // doesn't dramatically shift on resolve. Capped at maxWidth.
2210
- Math.min(maxWidth, token.content.length * fontSizePx * 0.55 + fontSizePx)
2211
- );
2212
- if (cursorX > 0 && cursorX + width > maxWidth) pushLine();
2213
- currentRuns.push({ text: token.content, type: "math" });
2214
- cursorX += width;
2215
- continue;
2216
- }
2217
- for (const chunk of splitChunks(token.content)) pushChunk(chunk, token.type);
2218
- }
2219
- if (currentRuns.length > 0 || lines.length === 0) {
2220
- lines.push({ kind: "text", runs: currentRuns });
2221
- }
2222
- return lines;
2223
- };
2224
-
2225
- // src/text/font-epoch.ts
2226
- var fontEpochListeners = /* @__PURE__ */ new Set();
2227
- var fontEpoch = 0;
2228
- var fontTrackingInitialized = false;
2229
- var emitFontEpoch = () => {
2230
- for (const listener of fontEpochListeners) listener(fontEpoch);
2231
- };
2232
- var bumpFontEpoch = () => {
2233
- fontEpoch += 1;
2234
- clearMeasureCache();
2235
- emitFontEpoch();
2236
- };
2237
- var initFontTracking = () => {
2238
- if (fontTrackingInitialized) return;
2239
- fontTrackingInitialized = true;
2240
- if (typeof document === "undefined" || !("fonts" in document)) return;
2241
- const fontSet = document.fonts;
2242
- let didSettleInitialFonts = false;
2243
- fontSet.ready.then(() => {
2244
- if (didSettleInitialFonts) return;
2245
- didSettleInitialFonts = true;
2246
- bumpFontEpoch();
2247
- }).catch(() => {
2248
- });
2249
- fontSet.addEventListener?.("loadingdone", () => {
2250
- if (!didSettleInitialFonts) didSettleInitialFonts = true;
2251
- bumpFontEpoch();
2252
- });
2253
- };
2254
- var subscribeFontEpoch = (listener) => {
2255
- initFontTracking();
2256
- fontEpochListeners.add(listener);
2257
- return () => {
2258
- fontEpochListeners.delete(listener);
2259
- };
2260
- };
2261
- var getFontEpoch = () => fontEpoch;
2262
-
2263
- // src/text/render-scale.ts
2264
- var MIN_RENDER_SCALE = 0.15;
2265
- var MAX_RENDER_SCALE = 1.5;
2266
- var MAX_RENDER_WIDTH = 2e3;
2267
- var MAX_RENDER_HEIGHT = 1200;
2268
- var quantizeZoom = (value) => {
2269
- if (!Number.isFinite(value)) return 1;
2270
- return Math.max(0.1, Math.round(value * 10) / 10);
2271
- };
2272
- var quantizeDpr = (value) => {
2273
- if (!Number.isFinite(value)) return 1;
2274
- const clamped = Math.max(1, Math.min(3, value));
2275
- return Math.round(clamped * 4) / 4;
2276
- };
2277
- var resolveRenderScale = (baseScale, zoom, isMoving2) => {
2278
- const clampedBase = Math.max(MIN_RENDER_SCALE, Math.min(MAX_RENDER_SCALE, baseScale));
2279
- let idleScale = clampedBase;
2280
- if (zoom <= 0.4) {
2281
- idleScale = 0.45;
2282
- } else if (zoom <= 0.7) {
2283
- idleScale = 0.85;
2284
- } else if (zoom <= 1) {
2285
- idleScale = 1.15;
2286
- } else if (zoom <= 1.8) {
2287
- idleScale = 1.35;
2288
- } else {
2289
- idleScale = 1 + (zoom - 1.8) * 0.2;
2290
- }
2291
- idleScale = Math.max(MIN_RENDER_SCALE, Math.min(MAX_RENDER_SCALE, idleScale));
2292
- if (isMoving2) {
2293
- let movingScale = idleScale * (zoom >= 0.4 ? 0.72 : 0.6);
2294
- if (zoom < 0.4) {
2295
- movingScale = Math.min(movingScale, 0.22);
2296
- } else if (zoom <= 0.7) {
2297
- movingScale = Math.min(movingScale, 0.4);
2298
- }
2299
- return Math.max(MIN_RENDER_SCALE, Math.min(0.65, movingScale));
2300
- }
2301
- return idleScale;
2302
- };
2303
- var clampEffectiveScale = (baseScale, width, height) => {
2304
- const limiter = Math.min(
2305
- 1,
2306
- MAX_RENDER_WIDTH / Math.max(1, width * baseScale),
2307
- MAX_RENDER_HEIGHT / Math.max(1, height * baseScale)
2308
- );
2309
- return baseScale * limiter;
2310
- };
2311
-
2312
- // src/text/estimate-height.ts
2313
- var getLineAdvance = (line, lineHeight) => {
2314
- if (line.kind !== "code-block") return lineHeight;
2315
- let advance = lineHeight;
2316
- if (line.isFirst) advance += CODE_BLOCK_MARGIN_Y;
2317
- if (line.isLast) advance += CODE_BLOCK_MARGIN_Y;
2318
- return advance;
2319
- };
2320
- var getContentHeight = (lines, lineHeight) => Math.max(
2321
- lineHeight,
2322
- lines.reduce((sum, line) => sum + getLineAdvance(line, lineHeight), 0)
2323
- );
2324
- var estimateMarkdownContentHeight = ({
2325
- text,
2326
- width,
2327
- fontFamily = "handwriting",
2328
- fontSize = "M",
2329
- textStyle = "normal"
2330
- }) => {
2331
- const normalizedText = text.trim();
2332
- if (!normalizedText) return 0;
2333
- const resolvedWidth = Math.max(40, Math.ceil(width));
2334
- const lines = layoutTokens(tokenize(text), {
2335
- width: resolvedWidth,
2336
- fontFamily,
2337
- fontSize,
2338
- textStyle
2339
- });
2340
- const lineHeight = LINE_HEIGHT_MAP[fontSize];
2341
- return getContentHeight(lines, lineHeight) + CONTENT_HEIGHT_BUFFER;
2342
- };
2343
- var getMarkdownLineHeightPx = (fontSize) => LINE_HEIGHT_MAP[fontSize];
2344
-
2345
- // src/text/paint-canvas.ts
2346
- var getLineAdvance2 = (line, lineHeight) => {
2347
- if (line.kind !== "code-block") return lineHeight;
2348
- let advance = lineHeight;
2349
- if (line.isFirst) advance += CODE_BLOCK_MARGIN_Y;
2350
- if (line.isLast) advance += CODE_BLOCK_MARGIN_Y;
2351
- return advance;
2352
- };
2353
- var getTextX = (opts, lineWidth) => {
2354
- if (opts.align === "center") return Math.floor((opts.width - lineWidth) / 2);
2355
- if (opts.align === "right") return Math.max(0, opts.width - lineWidth);
2356
- return 0;
2357
- };
2358
- var drawRunDecoration = (ctx, x, y, width, type, fontSize) => {
2359
- if (type === "underline" || type === "link") {
2360
- const lineY = y + 2;
2361
- ctx.beginPath();
2362
- ctx.moveTo(x, lineY);
2363
- ctx.lineTo(x + width, lineY);
2364
- ctx.lineWidth = 1;
2365
- ctx.stroke();
1584
+ var drawRunDecoration = (ctx, x, y, width, type, fontSize) => {
1585
+ if (type === "underline" || type === "link") {
1586
+ const lineY = y + 2;
1587
+ ctx.beginPath();
1588
+ ctx.moveTo(x, lineY);
1589
+ ctx.lineTo(x + width, lineY);
1590
+ ctx.lineWidth = 1;
1591
+ ctx.stroke();
2366
1592
  }
2367
1593
  if (type === "strike") {
2368
1594
  const lineY = y - Math.floor(fontSize * 0.35);
@@ -2624,12 +1850,12 @@ var clearTextBitmapCache = () => {
2624
1850
  };
2625
1851
  var getTextBitmapCacheSize = () => renderCache.size;
2626
1852
  var FREEHAND_CACHE_MAX = 500;
2627
- var cache4 = /* @__PURE__ */ new Map();
1853
+ var cache3 = /* @__PURE__ */ new Map();
2628
1854
  var remember = (key, path) => {
2629
- cache4.set(key, path);
2630
- if (cache4.size > FREEHAND_CACHE_MAX) {
2631
- const oldest = cache4.keys().next().value;
2632
- if (oldest !== void 0) cache4.delete(oldest);
1855
+ cache3.set(key, path);
1856
+ if (cache3.size > FREEHAND_CACHE_MAX) {
1857
+ const oldest = cache3.keys().next().value;
1858
+ if (oldest !== void 0) cache3.delete(oldest);
2633
1859
  }
2634
1860
  };
2635
1861
  var signaturePoints = (samples) => {
@@ -2681,10 +1907,10 @@ var outlineToPath2D = (ring) => {
2681
1907
  var getOrBuildFreehandPath = (samples, strokeWidth, seed) => {
2682
1908
  if (samples.length < 2) return null;
2683
1909
  const cacheKey = `${seed}|${strokeWidth.toFixed(2)}|${signaturePoints(samples)}`;
2684
- const hit = cache4.get(cacheKey);
1910
+ const hit = cache3.get(cacheKey);
2685
1911
  if (hit) {
2686
- cache4.delete(cacheKey);
2687
- cache4.set(cacheKey, hit);
1912
+ cache3.delete(cacheKey);
1913
+ cache3.set(cacheKey, hit);
2688
1914
  return hit;
2689
1915
  }
2690
1916
  const pts = buildPressurePoints(samples);
@@ -2741,62 +1967,36 @@ var drawEdge = (ctx, edge, geom, sourceNode, targetNode, scale, theme, opts) =>
2741
1967
  const drawTargetArrow = targetArrowhead !== "none" && headEndWorld * scale >= ARROWHEAD_VISIBILITY_THRESHOLD_PX;
2742
1968
  const lineStart = drawSourceArrow ? retreatFromPoint(samples, clip.startIndex, clip.startPoint, headStartWorld, 1) : clip.startPoint;
2743
1969
  const lineEnd = drawTargetArrow ? retreatFromPoint(samples, clip.endIndex, clip.endPoint, headEndWorld, -1) : clip.endPoint;
2744
- const useRough = (opts?.roughEnabled ?? false) && (style?.roughness ?? 0) > 0;
1970
+ const isSolidStroke = (style?.strokeStyle ?? "solid") === "solid";
1971
+ const useRough = isSolidStroke && (opts?.roughEnabled ?? false) && (style?.roughness ?? 0) > 0;
2745
1972
  if (useRough) {
2746
1973
  const clipped = [lineStart];
2747
1974
  for (let i = clip.startIndex + 1; i < clip.endIndex; i++) clipped.push(samples[i]);
2748
1975
  clipped.push(lineEnd);
2749
- const isSolid = (style?.strokeStyle ?? "solid") === "solid";
2750
- if (isSolid) {
2751
- const seed = edge.id ? seedFromId(edge.id) % 2147483646 + 1 : 1337;
2752
- const path = getOrBuildFreehandPath(clipped, strokeWidth, seed);
2753
- if (path) {
2754
- ctx.save();
2755
- ctx.fillStyle = strokeColor;
2756
- ctx.fill(path);
2757
- ctx.restore();
2758
- if (drawSourceArrow) {
2759
- const tipDir = directionTowardTip(samples, clip.startIndex, clip.startPoint, 1);
2760
- drawArrowhead(
2761
- ctx,
2762
- sourceArrowhead,
2763
- clip.startPoint,
2764
- negateVec(tipDir),
2765
- strokeColor,
2766
- strokeWidth
2767
- );
2768
- }
2769
- if (drawTargetArrow) {
2770
- const tipDir = directionTowardTip(samples, clip.endIndex, clip.endPoint, -1);
2771
- drawArrowhead(ctx, targetArrowhead, clip.endPoint, tipDir, strokeColor, strokeWidth);
2772
- }
2773
- if (edge.content?.trim()) drawEdgeLabel(ctx, edge, geom, scale, theme, opts);
2774
- return;
1976
+ const seed = edge.id ? seedFromId(edge.id) % 2147483646 + 1 : 1337;
1977
+ const path = getOrBuildFreehandPath(clipped, strokeWidth, seed);
1978
+ if (path) {
1979
+ ctx.save();
1980
+ ctx.fillStyle = strokeColor;
1981
+ ctx.fill(path);
1982
+ ctx.restore();
1983
+ if (drawSourceArrow) {
1984
+ const tipDir = directionTowardTip(samples, clip.startIndex, clip.startPoint, 1);
1985
+ drawArrowhead(
1986
+ ctx,
1987
+ sourceArrowhead,
1988
+ clip.startPoint,
1989
+ negateVec(tipDir),
1990
+ strokeColor,
1991
+ strokeWidth
1992
+ );
2775
1993
  }
2776
- } else {
2777
- const ok = drawRoughEdge(ctx, edge, clipped, scale, theme);
2778
- if (!ok) {
2779
- onRoughReady(() => {
2780
- });
2781
- } else {
2782
- if (drawSourceArrow) {
2783
- const tipDir = directionTowardTip(samples, clip.startIndex, clip.startPoint, 1);
2784
- drawArrowhead(
2785
- ctx,
2786
- sourceArrowhead,
2787
- clip.startPoint,
2788
- negateVec(tipDir),
2789
- strokeColor,
2790
- strokeWidth
2791
- );
2792
- }
2793
- if (drawTargetArrow) {
2794
- const tipDir = directionTowardTip(samples, clip.endIndex, clip.endPoint, -1);
2795
- drawArrowhead(ctx, targetArrowhead, clip.endPoint, tipDir, strokeColor, strokeWidth);
2796
- }
2797
- if (edge.content?.trim()) drawEdgeLabel(ctx, edge, geom, scale, theme, opts);
2798
- return;
1994
+ if (drawTargetArrow) {
1995
+ const tipDir = directionTowardTip(samples, clip.endIndex, clip.endPoint, -1);
1996
+ drawArrowhead(ctx, targetArrowhead, clip.endPoint, tipDir, strokeColor, strokeWidth);
2799
1997
  }
1998
+ if (edge.content?.trim()) drawEdgeLabel(ctx, edge, geom, scale, theme, opts);
1999
+ return;
2800
2000
  }
2801
2001
  }
2802
2002
  ctx.save();
@@ -3429,6 +2629,7 @@ var detectConflicts = (batch, getNode, getEdge) => {
3429
2629
  };
3430
2630
  var sameValue = (a, b) => {
3431
2631
  if (a === b) return true;
2632
+ if (a == null && b == null) return true;
3432
2633
  if (a == null || b == null) return false;
3433
2634
  if (typeof a !== typeof b) return false;
3434
2635
  if (typeof a === "object") return JSON.stringify(a) === JSON.stringify(b);
@@ -3771,12 +2972,20 @@ var createCanvasStore = (opts = {}) => {
3771
2972
  applyOpInternal(op);
3772
2973
  }
3773
2974
  };
2975
+ const normalizeUndefinedToNull = (obj) => {
2976
+ const out = {};
2977
+ for (const key of Object.keys(obj)) {
2978
+ const v = obj[key];
2979
+ out[key] = v === void 0 ? null : v;
2980
+ }
2981
+ return out;
2982
+ };
3774
2983
  const slicePrev = (current, patch) => {
3775
2984
  const prev = {};
3776
2985
  for (const key of Object.keys(patch)) {
3777
2986
  prev[key] = current[key];
3778
2987
  }
3779
- return prev;
2988
+ return normalizeUndefinedToNull(prev);
3780
2989
  };
3781
2990
  const populateInitial = (scene) => {
3782
2991
  const seededFrameOrder = [];
@@ -3832,7 +3041,10 @@ var createCanvasStore = (opts = {}) => {
3832
3041
  enqueueOp({
3833
3042
  type: "node.update",
3834
3043
  id,
3835
- patch: resolvedPatch,
3044
+ // Normalize both halves so explicit-clear patches (e.g.
3045
+ // `updateNode(id, { content: undefined })`) and first-time-set
3046
+ // prev slices both survive a JSON-serialized sync transport.
3047
+ patch: normalizeUndefinedToNull(resolvedPatch),
3836
3048
  prev: slicePrev(current, resolvedPatch)
3837
3049
  });
3838
3050
  },
@@ -3910,982 +3122,1701 @@ var createCanvasStore = (opts = {}) => {
3910
3122
  enqueueOp({ type: "edge.add", edge: withZ });
3911
3123
  return withZ.id;
3912
3124
  },
3913
- updateEdge(id, patch) {
3914
- const current = edgeAtoms.get(id)?.value;
3915
- if (!current) return;
3916
- enqueueOp({ type: "edge.update", id, patch, prev: slicePrev(current, patch) });
3125
+ updateEdge(id, patch) {
3126
+ const current = edgeAtoms.get(id)?.value;
3127
+ if (!current) return;
3128
+ enqueueOp({
3129
+ type: "edge.update",
3130
+ id,
3131
+ patch: normalizeUndefinedToNull(patch),
3132
+ prev: slicePrev(current, patch)
3133
+ });
3134
+ },
3135
+ removeEdge(id) {
3136
+ const edge = edgeAtoms.get(id)?.value;
3137
+ if (!edge) return;
3138
+ enqueueOp({ type: "edge.remove", edge });
3139
+ },
3140
+ bringToFront(ids) {
3141
+ this.batch(() => {
3142
+ for (const id of ids) {
3143
+ if (nodeAtoms.has(id)) this.updateNode(id, { z: ++topZ });
3144
+ else if (edgeAtoms.has(id)) this.updateEdge(id, { z: ++topZ });
3145
+ }
3146
+ });
3147
+ },
3148
+ sendToBack(ids) {
3149
+ this.batch(() => {
3150
+ for (const id of ids) {
3151
+ if (nodeAtoms.has(id)) this.updateNode(id, { z: --bottomZ });
3152
+ else if (edgeAtoms.has(id)) this.updateEdge(id, { z: --bottomZ });
3153
+ }
3154
+ });
3155
+ },
3156
+ bringForward(ids) {
3157
+ const targets = new Set(ids);
3158
+ const allZ = [];
3159
+ for (const a of nodeAtoms.values()) if (!targets.has(a.value.id)) allZ.push(a.value.z);
3160
+ for (const a of edgeAtoms.values()) if (!targets.has(a.value.id)) allZ.push(a.value.z);
3161
+ allZ.sort((a, b) => a - b);
3162
+ this.batch(() => {
3163
+ for (const id of ids) {
3164
+ const node = nodeAtoms.get(id)?.value;
3165
+ const edge = node ? null : edgeAtoms.get(id)?.value;
3166
+ const currentZ = node?.z ?? edge?.z;
3167
+ if (currentZ === void 0) continue;
3168
+ const idx = binaryFirstGreater(allZ, currentZ);
3169
+ const nextZ = idx >= 0 ? allZ[idx] + 1 : currentZ + 1;
3170
+ if (node) this.updateNode(id, { z: nextZ });
3171
+ else this.updateEdge(id, { z: nextZ });
3172
+ }
3173
+ });
3174
+ },
3175
+ sendBackward(ids) {
3176
+ const targets = new Set(ids);
3177
+ const allZ = [];
3178
+ for (const a of nodeAtoms.values()) if (!targets.has(a.value.id)) allZ.push(a.value.z);
3179
+ for (const a of edgeAtoms.values()) if (!targets.has(a.value.id)) allZ.push(a.value.z);
3180
+ allZ.sort((a, b) => a - b);
3181
+ this.batch(() => {
3182
+ for (const id of ids) {
3183
+ const node = nodeAtoms.get(id)?.value;
3184
+ const edge = node ? null : edgeAtoms.get(id)?.value;
3185
+ const currentZ = node?.z ?? edge?.z;
3186
+ if (currentZ === void 0) continue;
3187
+ const idx = binaryLastLess(allZ, currentZ);
3188
+ const nextZ = idx >= 0 ? allZ[idx] - 1 : currentZ - 1;
3189
+ if (node) this.updateNode(id, { z: nextZ });
3190
+ else this.updateEdge(id, { z: nextZ });
3191
+ }
3192
+ });
3193
+ },
3194
+ upsertGroup(group) {
3195
+ const prev = groupAtoms.get(group.id)?.value;
3196
+ enqueueOp({ type: "group.upsert", group, prev });
3197
+ },
3198
+ removeGroup(id) {
3199
+ const group = groupAtoms.get(id)?.value;
3200
+ if (!group) return;
3201
+ enqueueOp({ type: "group.remove", group });
3202
+ },
3203
+ batch(fn) {
3204
+ signia.transact(() => {
3205
+ startBatch();
3206
+ try {
3207
+ fn();
3208
+ } finally {
3209
+ const batch = endBatch();
3210
+ if (batch) emitChange(batch);
3211
+ }
3212
+ });
3213
+ },
3214
+ applyOp(op, applyOpts) {
3215
+ const origin = applyOpts?.origin ?? "local";
3216
+ if (origin !== "local") {
3217
+ applyOpInternal(op);
3218
+ emitChange({
3219
+ id: asBatchId(idGenerator()),
3220
+ clientId,
3221
+ ts: Date.now(),
3222
+ origin,
3223
+ ops: [op]
3224
+ });
3225
+ return;
3226
+ }
3227
+ enqueueOp(op);
3228
+ },
3229
+ applyBatch(b) {
3230
+ signia.transact(() => {
3231
+ if (b.origin === "remote") {
3232
+ const conflicts = detectConflicts(
3233
+ b,
3234
+ (id) => nodeAtoms.get(id)?.value,
3235
+ (id) => edgeAtoms.get(id)?.value
3236
+ );
3237
+ if (conflicts.length > 0) emit("conflict", { batch: b, conflicts });
3238
+ }
3239
+ for (const op of b.ops) applyOpInternal(op);
3240
+ emitChange(b);
3241
+ });
3242
+ },
3243
+ canUndo: () => undoStack.length > 0,
3244
+ canRedo: () => redoStack.length > 0,
3245
+ undo() {
3246
+ const batch = undoStack.pop();
3247
+ if (!batch) return false;
3248
+ const ops = inverseBatch(batch);
3249
+ const inverseB = {
3250
+ id: asBatchId(idGenerator()),
3251
+ clientId,
3252
+ ts: Date.now(),
3253
+ origin: "history",
3254
+ ops
3255
+ };
3256
+ signia.transact(() => {
3257
+ for (const op of ops) applyOpInternal(op);
3258
+ emit("change", inverseB);
3259
+ });
3260
+ redoStack.push(batch);
3261
+ return true;
3262
+ },
3263
+ redo() {
3264
+ const batch = redoStack.pop();
3265
+ if (!batch) return false;
3266
+ const redoB = { ...batch, origin: "history" };
3267
+ signia.transact(() => {
3268
+ for (const op of redoB.ops) applyOpInternal(op);
3269
+ emit("change", redoB);
3270
+ });
3271
+ undoStack.push(batch);
3272
+ return true;
3273
+ },
3274
+ clearHistory() {
3275
+ undoStack.length = 0;
3276
+ redoStack.length = 0;
3277
+ },
3278
+ // reads
3279
+ getNode: (id) => nodeAtoms.get(id)?.value,
3280
+ getEdge: (id) => edgeAtoms.get(id)?.value,
3281
+ getGroup: (id) => groupAtoms.get(id)?.value,
3282
+ getAllNodes: () => nodeIdsAtom.value.map((id) => nodeAtoms.get(id).value),
3283
+ getAllEdges: () => edgeIdsAtom.value.map((id) => edgeAtoms.get(id).value),
3284
+ getAllGroups: () => groupIdsAtom.value.map((id) => groupAtoms.get(id).value),
3285
+ getNodeCount: () => nodeIdsAtom.value.length,
3286
+ getEdgeCount: () => edgeIdsAtom.value.length,
3287
+ getGroupCount: () => groupIdsAtom.value.length,
3288
+ getFrames: () => {
3289
+ const out = [];
3290
+ for (const id of frameOrderAtom.value) {
3291
+ const n = nodeAtoms.get(id)?.value;
3292
+ if (n && n.type === "frame") out.push(n);
3293
+ }
3294
+ return out;
3295
+ },
3296
+ setFrameOrder(ids) {
3297
+ const valid = /* @__PURE__ */ new Set();
3298
+ for (const a of nodeAtoms.values()) {
3299
+ if (a.value.type === "frame") valid.add(a.value.id);
3300
+ }
3301
+ const filtered = [];
3302
+ const seen = /* @__PURE__ */ new Set();
3303
+ for (const id of ids) {
3304
+ if (valid.has(id) && !seen.has(id)) {
3305
+ filtered.push(id);
3306
+ seen.add(id);
3307
+ }
3308
+ }
3309
+ for (const id of valid) {
3310
+ if (!seen.has(id)) filtered.push(id);
3311
+ }
3312
+ const prev = [...frameOrderAtom.value];
3313
+ if (filtered.length === prev.length && filtered.every((id, i) => id === prev[i])) {
3314
+ return;
3315
+ }
3316
+ enqueueOp({ type: "frame.reorder", ids: filtered, prev });
3317
+ },
3318
+ getNodesInFrame(id) {
3319
+ const frame = nodeAtoms.get(id)?.value;
3320
+ if (!frame || frame.type !== "frame") return [];
3321
+ const frameAabb = nodeAABB(frame);
3322
+ const candidates = nodeIndex.queryRect(frameAabb);
3323
+ const out = [];
3324
+ for (const cid of candidates) {
3325
+ if (cid === id) continue;
3326
+ const node = nodeAtoms.get(cid)?.value;
3327
+ if (!node || node.type === "frame") continue;
3328
+ const a = nodeAABB(node);
3329
+ if (a.x >= frameAabb.x && a.y >= frameAabb.y && a.x + a.w <= frameAabb.x + frameAabb.w && a.y + a.h <= frameAabb.y + frameAabb.h) {
3330
+ out.push(node);
3331
+ }
3332
+ }
3333
+ return out;
3917
3334
  },
3918
- removeEdge(id) {
3335
+ getEdgeGeometry(id) {
3919
3336
  const edge = edgeAtoms.get(id)?.value;
3920
- if (!edge) return;
3921
- enqueueOp({ type: "edge.remove", edge });
3337
+ if (!edge) return void 0;
3338
+ const version = edgeVersions.get(id) ?? 0;
3339
+ return edgeGeoCache.get(edge, version, getNodeForGeo) ?? void 0;
3922
3340
  },
3923
- bringToFront(ids) {
3924
- this.batch(() => {
3925
- for (const id of ids) {
3926
- if (nodeAtoms.has(id)) this.updateNode(id, { z: ++topZ });
3927
- else if (edgeAtoms.has(id)) this.updateEdge(id, { z: ++topZ });
3928
- }
3929
- });
3341
+ getIncidentEdges(id) {
3342
+ const set = incidentEdges.get(id);
3343
+ return set ? [...set] : [];
3930
3344
  },
3931
- sendToBack(ids) {
3932
- this.batch(() => {
3933
- for (const id of ids) {
3934
- if (nodeAtoms.has(id)) this.updateNode(id, { z: --bottomZ });
3935
- else if (edgeAtoms.has(id)) this.updateEdge(id, { z: --bottomZ });
3936
- }
3937
- });
3345
+ getNodeTypeDef(type) {
3346
+ return nodeTypeRegistry.get(type);
3938
3347
  },
3939
- bringForward(ids) {
3940
- const targets = new Set(ids);
3941
- const allZ = [];
3942
- for (const a of nodeAtoms.values()) if (!targets.has(a.value.id)) allZ.push(a.value.z);
3943
- for (const a of edgeAtoms.values()) if (!targets.has(a.value.id)) allZ.push(a.value.z);
3944
- allZ.sort((a, b) => a - b);
3945
- this.batch(() => {
3946
- for (const id of ids) {
3947
- const node = nodeAtoms.get(id)?.value;
3948
- const edge = node ? null : edgeAtoms.get(id)?.value;
3949
- const currentZ = node?.z ?? edge?.z;
3950
- if (currentZ === void 0) continue;
3951
- const idx = binaryFirstGreater(allZ, currentZ);
3952
- const nextZ = idx >= 0 ? allZ[idx] + 1 : currentZ + 1;
3953
- if (node) this.updateNode(id, { z: nextZ });
3954
- else this.updateEdge(id, { z: nextZ });
3955
- }
3956
- });
3348
+ querySpatial(q) {
3349
+ const rect = q.rect ?? (q.point ? { x: q.point.x, y: q.point.y, w: 0, h: 0 } : null);
3350
+ if (!rect) return { nodes: [], edges: [] };
3351
+ return {
3352
+ nodes: nodeIndex.queryRect(rect),
3353
+ edges: edgeIndex.queryRect(rect)
3354
+ };
3957
3355
  },
3958
- sendBackward(ids) {
3959
- const targets = new Set(ids);
3960
- const allZ = [];
3961
- for (const a of nodeAtoms.values()) if (!targets.has(a.value.id)) allZ.push(a.value.z);
3962
- for (const a of edgeAtoms.values()) if (!targets.has(a.value.id)) allZ.push(a.value.z);
3963
- allZ.sort((a, b) => a - b);
3964
- this.batch(() => {
3965
- for (const id of ids) {
3966
- const node = nodeAtoms.get(id)?.value;
3967
- const edge = node ? null : edgeAtoms.get(id)?.value;
3968
- const currentZ = node?.z ?? edge?.z;
3969
- if (currentZ === void 0) continue;
3970
- const idx = binaryLastLess(allZ, currentZ);
3971
- const nextZ = idx >= 0 ? allZ[idx] - 1 : currentZ - 1;
3972
- if (node) this.updateNode(id, { z: nextZ });
3973
- else this.updateEdge(id, { z: nextZ });
3974
- }
3975
- });
3356
+ getCamera: () => cameraAtom.value,
3357
+ setCamera(patch) {
3358
+ const next = { ...cameraAtom.value, ...patch };
3359
+ cameraAtom.set(next);
3360
+ emit("camera", next);
3976
3361
  },
3977
- upsertGroup(group) {
3978
- const prev = groupAtoms.get(group.id)?.value;
3979
- enqueueOp({ type: "group.upsert", group, prev });
3362
+ getSelection: () => selectionAtom.value,
3363
+ setSelection(ids) {
3364
+ selectionAtom.set(ids);
3365
+ emit("selection", ids);
3980
3366
  },
3981
- removeGroup(id) {
3982
- const group = groupAtoms.get(id)?.value;
3983
- if (!group) return;
3984
- enqueueOp({ type: "group.remove", group });
3367
+ getInteractionState: () => interactionAtom.value,
3368
+ setInteractionState(patch) {
3369
+ const next = { ...interactionAtom.value, ...patch };
3370
+ interactionAtom.set(next);
3371
+ emit("interaction", next);
3985
3372
  },
3986
- batch(fn) {
3987
- signia.transact(() => {
3988
- startBatch();
3989
- try {
3990
- fn();
3991
- } finally {
3992
- const batch = endBatch();
3993
- if (batch) emitChange(batch);
3373
+ resetInteractionState() {
3374
+ const next = idleInteractionState();
3375
+ interactionAtom.set(next);
3376
+ emit("interaction", next);
3377
+ },
3378
+ beginEdit(id) {
3379
+ let target = null;
3380
+ if (nodeAtoms.has(id)) target = { kind: "node", id };
3381
+ else if (edgeAtoms.has(id)) target = { kind: "edge", id };
3382
+ if (!target) return;
3383
+ const next = {
3384
+ ...interactionAtom.value,
3385
+ mode: "editing",
3386
+ editingTarget: target
3387
+ };
3388
+ interactionAtom.set(next);
3389
+ emit("interaction", next);
3390
+ },
3391
+ commitEdit(content) {
3392
+ const state = interactionAtom.value;
3393
+ if (state.mode !== "editing" || !state.editingTarget) return;
3394
+ const target = state.editingTarget;
3395
+ if (target.kind === "node") this.updateNode(target.id, { content });
3396
+ else this.updateEdge(target.id, { content });
3397
+ const idleState = { ...interactionAtom.value, mode: "idle", editingTarget: null };
3398
+ interactionAtom.set(idleState);
3399
+ emit("interaction", idleState);
3400
+ },
3401
+ cancelEdit() {
3402
+ const state = interactionAtom.value;
3403
+ if (state.mode !== "editing") return;
3404
+ const idleState = { ...state, mode: "idle", editingTarget: null };
3405
+ interactionAtom.set(idleState);
3406
+ emit("interaction", idleState);
3407
+ },
3408
+ presence: {
3409
+ setLocal(patch) {
3410
+ const next = { ...localPresenceAtom.value, ...patch };
3411
+ localPresenceAtom.set(next);
3412
+ emit("presence", { state: next });
3413
+ },
3414
+ getLocal: () => localPresenceAtom.value,
3415
+ get: (id) => remotePresence.get(id),
3416
+ getAll: () => remotePresence,
3417
+ applyRemote(id, state) {
3418
+ if (state === null) {
3419
+ if (remotePresence.delete(id)) emit("presence", { clientId: id, removed: true });
3420
+ return;
3994
3421
  }
3995
- });
3422
+ remotePresence.set(id, state);
3423
+ emit("presence", { state });
3424
+ }
3425
+ },
3426
+ subscribe(event, cb) {
3427
+ subscribers[event].add(cb);
3428
+ return () => {
3429
+ subscribers[event].delete(cb);
3430
+ };
3431
+ }
3432
+ };
3433
+ return store;
3434
+ };
3435
+
3436
+ // src/store/sync.ts
3437
+ var attachSync = (store, adapter) => {
3438
+ if (!adapter.capabilities.causalOrdering && !adapter.capabilities.crdt) {
3439
+ throw new Error(
3440
+ "SyncAdapter must advertise capabilities.causalOrdering or capabilities.crdt. See ARCHITECTURE.md \xA710.6."
3441
+ );
3442
+ }
3443
+ const unsubChange = store.subscribe("change", (batch) => {
3444
+ if (batch.origin !== "remote") adapter.sendBatch(batch);
3445
+ });
3446
+ const unsubPresence = store.subscribe("presence", (e) => {
3447
+ if ("removed" in e && e.removed) return;
3448
+ if (e.state.clientId !== store.clientId) return;
3449
+ const { clientId: _id, ...patch } = e.state;
3450
+ adapter.sendPresence(patch);
3451
+ });
3452
+ const unsubRemoteBatch = adapter.onBatch((batch) => {
3453
+ store.applyBatch({ ...batch, origin: "remote" });
3454
+ });
3455
+ const unsubRemotePresence = adapter.onPresence((clientId, state) => {
3456
+ store.presence.applyRemote(clientId, state);
3457
+ });
3458
+ return () => {
3459
+ unsubChange();
3460
+ unsubPresence();
3461
+ unsubRemoteBatch();
3462
+ unsubRemotePresence();
3463
+ adapter.destroy?.();
3464
+ };
3465
+ };
3466
+
3467
+ // src/store/palm-rejection.ts
3468
+ var PALM_REJECTION_GRACE_MS = 300;
3469
+ var createPalmRejectionState = () => ({
3470
+ penActive: false,
3471
+ lastPenUpAt: 0
3472
+ });
3473
+ var notePenActive = (state) => {
3474
+ state.penActive = true;
3475
+ };
3476
+ var notePenInactive = (state, now) => {
3477
+ state.penActive = false;
3478
+ state.lastPenUpAt = now;
3479
+ };
3480
+ var shouldRejectTouch = (state, now) => {
3481
+ if (state.penActive) return true;
3482
+ return now - state.lastPenUpAt < PALM_REJECTION_GRACE_MS;
3483
+ };
3484
+
3485
+ // src/codec/index.ts
3486
+ var migrators = /* @__PURE__ */ new Map();
3487
+ var registerMigrator = (fromVersion, fn) => {
3488
+ migrators.set(fromVersion, fn);
3489
+ };
3490
+ var toSerialized = (scene) => ({
3491
+ schemaVersion: scene.schemaVersion,
3492
+ nodes: Object.values(scene.nodes),
3493
+ edges: Object.values(scene.edges),
3494
+ groups: Object.values(scene.groups),
3495
+ camera: scene.camera,
3496
+ selection: scene.selection,
3497
+ ...scene.frameOrder && scene.frameOrder.length > 0 ? { frameOrder: scene.frameOrder } : {}
3498
+ });
3499
+ var fromSerialized = (raw) => {
3500
+ let working = raw;
3501
+ let version = working.schemaVersion ?? 0;
3502
+ while (version < SCHEMA_VERSION) {
3503
+ const fn = migrators.get(version);
3504
+ if (!fn) {
3505
+ throw new Error(
3506
+ `Cannot migrate scene from schemaVersion ${version} to ${SCHEMA_VERSION}; no migrator registered`
3507
+ );
3508
+ }
3509
+ working = fn(working);
3510
+ version++;
3511
+ }
3512
+ const ser = working;
3513
+ return {
3514
+ schemaVersion: SCHEMA_VERSION,
3515
+ nodes: Object.fromEntries(ser.nodes.map((n) => [asNodeId(n.id), n])),
3516
+ edges: Object.fromEntries(ser.edges.map((e) => [asEdgeId(e.id), e])),
3517
+ groups: Object.fromEntries(ser.groups.map((g) => [asGroupId(g.id), g])),
3518
+ camera: ser.camera,
3519
+ selection: ser.selection,
3520
+ ...ser.frameOrder ? { frameOrder: ser.frameOrder } : {}
3521
+ };
3522
+ };
3523
+ var storeToJSON = (store) => ({
3524
+ schemaVersion: SCHEMA_VERSION,
3525
+ nodes: store.getAllNodes(),
3526
+ edges: store.getAllEdges(),
3527
+ groups: store.getAllGroups(),
3528
+ camera: store.getCamera(),
3529
+ selection: store.getSelection()
3530
+ });
3531
+
3532
+ // src/render/canvas-setup.ts
3533
+ var HARD_MAX_DPR = 3;
3534
+ var defaultMaxDprForSize = (cssW, cssH) => {
3535
+ const cssPx = cssW * cssH;
3536
+ if (cssPx >= 25e5) return 1;
3537
+ if (cssPx >= 15e5) return 1.5;
3538
+ return 2;
3539
+ };
3540
+ var getDpr = (maxDpr, cssW = 0, cssH = 0) => {
3541
+ if (typeof window === "undefined") return 1;
3542
+ const raw = window.devicePixelRatio || 1;
3543
+ const resolvedMax = maxDpr === void 0 && cssW > 0 && cssH > 0 ? defaultMaxDprForSize(cssW, cssH) : maxDpr ?? 1;
3544
+ const cap = Math.max(1, Math.min(HARD_MAX_DPR, resolvedMax));
3545
+ return Math.max(1, Math.min(cap, raw));
3546
+ };
3547
+ var setupSurface = (canvas, _maxDpr) => {
3548
+ const ctx = canvas.getContext("2d");
3549
+ if (!ctx) throw new Error("Canvas 2d context unavailable");
3550
+ return {
3551
+ canvas,
3552
+ ctx,
3553
+ cssWidth: 0,
3554
+ cssHeight: 0,
3555
+ dpr: 1
3556
+ // placeholder; `sizeSurface` writes the real value
3557
+ };
3558
+ };
3559
+ var sizeSurface = (surface, cssW, cssH, maxDpr) => {
3560
+ const dpr = getDpr(maxDpr, cssW, cssH);
3561
+ if (surface.cssWidth === cssW && surface.cssHeight === cssH && surface.dpr === dpr) {
3562
+ return false;
3563
+ }
3564
+ surface.cssWidth = cssW;
3565
+ surface.cssHeight = cssH;
3566
+ surface.dpr = dpr;
3567
+ surface.canvas.width = Math.max(1, Math.round(cssW * dpr));
3568
+ surface.canvas.height = Math.max(1, Math.round(cssH * dpr));
3569
+ surface.canvas.style.width = `${cssW}px`;
3570
+ surface.canvas.style.height = `${cssH}px`;
3571
+ return true;
3572
+ };
3573
+ var clearSurface = (surface) => {
3574
+ surface.ctx.setTransform(1, 0, 0, 1, 0, 0);
3575
+ surface.ctx.clearRect(0, 0, surface.canvas.width, surface.canvas.height);
3576
+ };
3577
+
3578
+ // src/render/frame-loop.ts
3579
+ var createFrameLoop = ({ draw, historySize = 60 }) => {
3580
+ let running = false;
3581
+ let scheduled = false;
3582
+ let frameId = 0;
3583
+ const history = [];
3584
+ let frames = 0;
3585
+ let lastMs = 0;
3586
+ let avgMs = 0;
3587
+ const fpsWindow = [];
3588
+ let fps = 0;
3589
+ const tick = () => {
3590
+ frameId = 0;
3591
+ scheduled = false;
3592
+ if (!running) return;
3593
+ const t0 = performance.now();
3594
+ draw();
3595
+ const dur = performance.now() - t0;
3596
+ history.push(dur);
3597
+ if (history.length > historySize) history.shift();
3598
+ let sum = 0;
3599
+ for (const v of history) sum += v;
3600
+ avgMs = sum / history.length;
3601
+ lastMs = dur;
3602
+ frames++;
3603
+ fpsWindow.push(t0);
3604
+ const cutoff = t0 - 1e3;
3605
+ while (fpsWindow.length > 0 && fpsWindow[0] < cutoff) fpsWindow.shift();
3606
+ fps = fpsWindow.length;
3607
+ };
3608
+ const schedule = () => {
3609
+ if (scheduled || !running) return;
3610
+ scheduled = true;
3611
+ frameId = requestAnimationFrame(tick);
3612
+ };
3613
+ return {
3614
+ start() {
3615
+ if (running) return;
3616
+ running = true;
3617
+ schedule();
3996
3618
  },
3997
- applyOp(op, applyOpts) {
3998
- const origin = applyOpts?.origin ?? "local";
3999
- if (origin !== "local") {
4000
- applyOpInternal(op);
4001
- emitChange({
4002
- id: asBatchId(idGenerator()),
4003
- clientId,
4004
- ts: Date.now(),
4005
- origin,
4006
- ops: [op]
4007
- });
4008
- return;
3619
+ stop() {
3620
+ running = false;
3621
+ if (frameId !== 0) {
3622
+ cancelAnimationFrame(frameId);
3623
+ frameId = 0;
4009
3624
  }
4010
- enqueueOp(op);
4011
- },
4012
- applyBatch(b) {
4013
- signia.transact(() => {
4014
- if (b.origin === "remote") {
4015
- const conflicts = detectConflicts(
4016
- b,
4017
- (id) => nodeAtoms.get(id)?.value,
4018
- (id) => edgeAtoms.get(id)?.value
4019
- );
4020
- if (conflicts.length > 0) emit("conflict", { batch: b, conflicts });
4021
- }
4022
- for (const op of b.ops) applyOpInternal(op);
4023
- emitChange(b);
4024
- });
4025
- },
4026
- canUndo: () => undoStack.length > 0,
4027
- canRedo: () => redoStack.length > 0,
4028
- undo() {
4029
- const batch = undoStack.pop();
4030
- if (!batch) return false;
4031
- const ops = inverseBatch(batch);
4032
- const inverseB = {
4033
- id: asBatchId(idGenerator()),
4034
- clientId,
4035
- ts: Date.now(),
4036
- origin: "history",
4037
- ops
4038
- };
4039
- signia.transact(() => {
4040
- for (const op of ops) applyOpInternal(op);
4041
- emit("change", inverseB);
4042
- });
4043
- redoStack.push(batch);
4044
- return true;
4045
- },
4046
- redo() {
4047
- const batch = redoStack.pop();
4048
- if (!batch) return false;
4049
- const redoB = { ...batch, origin: "history" };
4050
- signia.transact(() => {
4051
- for (const op of redoB.ops) applyOpInternal(op);
4052
- emit("change", redoB);
4053
- });
4054
- undoStack.push(batch);
4055
- return true;
4056
- },
4057
- clearHistory() {
4058
- undoStack.length = 0;
4059
- redoStack.length = 0;
3625
+ scheduled = false;
4060
3626
  },
4061
- // reads
4062
- getNode: (id) => nodeAtoms.get(id)?.value,
4063
- getEdge: (id) => edgeAtoms.get(id)?.value,
4064
- getGroup: (id) => groupAtoms.get(id)?.value,
4065
- getAllNodes: () => nodeIdsAtom.value.map((id) => nodeAtoms.get(id).value),
4066
- getAllEdges: () => edgeIdsAtom.value.map((id) => edgeAtoms.get(id).value),
4067
- getAllGroups: () => groupIdsAtom.value.map((id) => groupAtoms.get(id).value),
4068
- getNodeCount: () => nodeIdsAtom.value.length,
4069
- getEdgeCount: () => edgeIdsAtom.value.length,
4070
- getGroupCount: () => groupIdsAtom.value.length,
4071
- getFrames: () => {
4072
- const out = [];
4073
- for (const id of frameOrderAtom.value) {
4074
- const n = nodeAtoms.get(id)?.value;
4075
- if (n && n.type === "frame") out.push(n);
4076
- }
4077
- return out;
3627
+ requestFrame() {
3628
+ schedule();
4078
3629
  },
4079
- setFrameOrder(ids) {
4080
- const valid = /* @__PURE__ */ new Set();
4081
- for (const a of nodeAtoms.values()) {
4082
- if (a.value.type === "frame") valid.add(a.value.id);
3630
+ stats: () => ({ lastMs, avgMs, frames, fps })
3631
+ };
3632
+ };
3633
+
3634
+ // src/render/assets/cache.ts
3635
+ var MAX_ENTRIES2 = 256;
3636
+ var bucketSize = (px) => {
3637
+ if (px <= 32) return 32;
3638
+ if (px <= 64) return 64;
3639
+ if (px <= 128) return 128;
3640
+ if (px <= 256) return 256;
3641
+ if (px <= 512) return 512;
3642
+ return Math.ceil(px / 256) * 256;
3643
+ };
3644
+ var createAssetCache = (opts = {}) => {
3645
+ const entries = /* @__PURE__ */ new Map();
3646
+ let disposed = false;
3647
+ const notify = () => {
3648
+ if (disposed) return;
3649
+ opts.onReady?.();
3650
+ };
3651
+ const touch = (key, entry) => {
3652
+ entries.delete(key);
3653
+ entries.set(key, entry);
3654
+ if (entries.size > MAX_ENTRIES2) {
3655
+ const oldestKey = entries.keys().next().value;
3656
+ if (oldestKey !== void 0) {
3657
+ const evicted = entries.get(oldestKey);
3658
+ if (evicted?.kind === "icon" && evicted.bitmap) evicted.bitmap.close?.();
3659
+ entries.delete(oldestKey);
4083
3660
  }
4084
- const filtered = [];
4085
- const seen = /* @__PURE__ */ new Set();
4086
- for (const id of ids) {
4087
- if (valid.has(id) && !seen.has(id)) {
4088
- filtered.push(id);
4089
- seen.add(id);
3661
+ }
3662
+ };
3663
+ const startImageDecode = (key, src) => {
3664
+ const entry = { kind: "image", state: "pending", bitmap: null };
3665
+ touch(key, entry);
3666
+ const img = new Image();
3667
+ img.onload = () => {
3668
+ if (disposed) return;
3669
+ entry.state = "ready";
3670
+ entry.bitmap = img;
3671
+ notify();
3672
+ };
3673
+ img.onerror = (e) => {
3674
+ if (disposed) return;
3675
+ entry.state = "error";
3676
+ entry.err = e;
3677
+ notify();
3678
+ };
3679
+ img.src = src;
3680
+ };
3681
+ const startIconRaster = (key, markup, color, sizePx) => {
3682
+ const entry = { kind: "icon", state: "pending", bitmap: null };
3683
+ touch(key, entry);
3684
+ const colored = color ? applySvgColor(markup, color) : markup;
3685
+ const blob = new Blob([colored], { type: "image/svg+xml" });
3686
+ const url = URL.createObjectURL(blob);
3687
+ const img = new Image();
3688
+ img.onload = async () => {
3689
+ URL.revokeObjectURL(url);
3690
+ if (disposed) return;
3691
+ try {
3692
+ const bitmap = await createImageBitmap(img, {
3693
+ resizeWidth: sizePx,
3694
+ resizeHeight: sizePx,
3695
+ resizeQuality: "high"
3696
+ });
3697
+ if (disposed) {
3698
+ bitmap.close?.();
3699
+ return;
4090
3700
  }
3701
+ entry.state = "ready";
3702
+ entry.bitmap = bitmap;
3703
+ notify();
3704
+ } catch (e) {
3705
+ entry.state = "error";
3706
+ entry.err = e;
3707
+ notify();
4091
3708
  }
4092
- for (const id of valid) {
4093
- if (!seen.has(id)) filtered.push(id);
4094
- }
4095
- const prev = [...frameOrderAtom.value];
4096
- if (filtered.length === prev.length && filtered.every((id, i) => id === prev[i])) {
4097
- return;
4098
- }
4099
- enqueueOp({ type: "frame.reorder", ids: filtered, prev });
4100
- },
4101
- getNodesInFrame(id) {
4102
- const frame = nodeAtoms.get(id)?.value;
4103
- if (!frame || frame.type !== "frame") return [];
4104
- const frameAabb = nodeAABB(frame);
4105
- const candidates = nodeIndex.queryRect(frameAabb);
4106
- const out = [];
4107
- for (const cid of candidates) {
4108
- if (cid === id) continue;
4109
- const node = nodeAtoms.get(cid)?.value;
4110
- if (!node || node.type === "frame") continue;
4111
- const a = nodeAABB(node);
4112
- if (a.x >= frameAabb.x && a.y >= frameAabb.y && a.x + a.w <= frameAabb.x + frameAabb.w && a.y + a.h <= frameAabb.y + frameAabb.h) {
4113
- out.push(node);
3709
+ };
3710
+ img.onerror = (e) => {
3711
+ URL.revokeObjectURL(url);
3712
+ if (disposed) return;
3713
+ entry.state = "error";
3714
+ entry.err = e;
3715
+ notify();
3716
+ };
3717
+ img.src = url;
3718
+ };
3719
+ return {
3720
+ getImage(src) {
3721
+ const key = `img:${src}`;
3722
+ const existing = entries.get(key);
3723
+ if (existing && existing.kind === "image") {
3724
+ if (existing.state === "ready") {
3725
+ touch(key, existing);
3726
+ return existing.bitmap;
4114
3727
  }
3728
+ return null;
4115
3729
  }
4116
- return out;
4117
- },
4118
- getEdgeGeometry(id) {
4119
- const edge = edgeAtoms.get(id)?.value;
4120
- if (!edge) return void 0;
4121
- const version = edgeVersions.get(id) ?? 0;
4122
- return edgeGeoCache.get(edge, version, getNodeForGeo) ?? void 0;
4123
- },
4124
- getIncidentEdges(id) {
4125
- const set = incidentEdges.get(id);
4126
- return set ? [...set] : [];
4127
- },
4128
- getNodeTypeDef(type) {
4129
- return nodeTypeRegistry.get(type);
4130
- },
4131
- querySpatial(q) {
4132
- const rect = q.rect ?? (q.point ? { x: q.point.x, y: q.point.y, w: 0, h: 0 } : null);
4133
- if (!rect) return { nodes: [], edges: [] };
4134
- return {
4135
- nodes: nodeIndex.queryRect(rect),
4136
- edges: edgeIndex.queryRect(rect)
4137
- };
4138
- },
4139
- getCamera: () => cameraAtom.value,
4140
- setCamera(patch) {
4141
- const next = { ...cameraAtom.value, ...patch };
4142
- cameraAtom.set(next);
4143
- emit("camera", next);
4144
- },
4145
- getSelection: () => selectionAtom.value,
4146
- setSelection(ids) {
4147
- selectionAtom.set(ids);
4148
- emit("selection", ids);
4149
- },
4150
- getInteractionState: () => interactionAtom.value,
4151
- setInteractionState(patch) {
4152
- const next = { ...interactionAtom.value, ...patch };
4153
- interactionAtom.set(next);
4154
- emit("interaction", next);
4155
- },
4156
- resetInteractionState() {
4157
- const next = idleInteractionState();
4158
- interactionAtom.set(next);
4159
- emit("interaction", next);
4160
- },
4161
- beginEdit(id) {
4162
- let target = null;
4163
- if (nodeAtoms.has(id)) target = { kind: "node", id };
4164
- else if (edgeAtoms.has(id)) target = { kind: "edge", id };
4165
- if (!target) return;
4166
- const next = {
4167
- ...interactionAtom.value,
4168
- mode: "editing",
4169
- editingTarget: target
4170
- };
4171
- interactionAtom.set(next);
4172
- emit("interaction", next);
4173
- },
4174
- commitEdit(content) {
4175
- const state = interactionAtom.value;
4176
- if (state.mode !== "editing" || !state.editingTarget) return;
4177
- const target = state.editingTarget;
4178
- if (target.kind === "node") this.updateNode(target.id, { content });
4179
- else this.updateEdge(target.id, { content });
4180
- const idleState = { ...interactionAtom.value, mode: "idle", editingTarget: null };
4181
- interactionAtom.set(idleState);
4182
- emit("interaction", idleState);
4183
- },
4184
- cancelEdit() {
4185
- const state = interactionAtom.value;
4186
- if (state.mode !== "editing") return;
4187
- const idleState = { ...state, mode: "idle", editingTarget: null };
4188
- interactionAtom.set(idleState);
4189
- emit("interaction", idleState);
3730
+ startImageDecode(key, src);
3731
+ return null;
4190
3732
  },
4191
- presence: {
4192
- setLocal(patch) {
4193
- const next = { ...localPresenceAtom.value, ...patch };
4194
- localPresenceAtom.set(next);
4195
- emit("presence", { state: next });
4196
- },
4197
- getLocal: () => localPresenceAtom.value,
4198
- get: (id) => remotePresence.get(id),
4199
- getAll: () => remotePresence,
4200
- applyRemote(id, state) {
4201
- if (state === null) {
4202
- if (remotePresence.delete(id)) emit("presence", { clientId: id, removed: true });
4203
- return;
3733
+ getIcon(markup, color, devicePixelSize) {
3734
+ const size = bucketSize(Math.max(1, Math.ceil(devicePixelSize)));
3735
+ const key = `icon:${size}:${color ?? ""}:${markup}`;
3736
+ const existing = entries.get(key);
3737
+ if (existing && existing.kind === "icon") {
3738
+ if (existing.state === "ready") {
3739
+ touch(key, existing);
3740
+ return existing.bitmap;
4204
3741
  }
4205
- remotePresence.set(id, state);
4206
- emit("presence", { state });
3742
+ return null;
4207
3743
  }
3744
+ startIconRaster(key, markup, color, size);
3745
+ return null;
4208
3746
  },
4209
- subscribe(event, cb) {
4210
- subscribers[event].add(cb);
4211
- return () => {
4212
- subscribers[event].delete(cb);
4213
- };
3747
+ dispose() {
3748
+ disposed = true;
3749
+ for (const entry of entries.values()) {
3750
+ if (entry.kind === "icon" && entry.bitmap) entry.bitmap.close?.();
3751
+ }
3752
+ entries.clear();
3753
+ }
3754
+ };
3755
+ };
3756
+
3757
+ // src/render/assets/paint.ts
3758
+ var PLACEHOLDER_FILL = "#e5e7eb";
3759
+ var PLACEHOLDER_TEXT_FILL = "#94a3b8";
3760
+ var paintPlaceholder = (ctx, w, h, label) => {
3761
+ ctx.fillStyle = PLACEHOLDER_FILL;
3762
+ ctx.fillRect(0, 0, w, h);
3763
+ if (w >= 32 && h >= 16) {
3764
+ ctx.fillStyle = PLACEHOLDER_TEXT_FILL;
3765
+ ctx.font = "11px system-ui, sans-serif";
3766
+ ctx.textAlign = "center";
3767
+ ctx.textBaseline = "middle";
3768
+ ctx.fillText(label, w / 2, h / 2);
3769
+ }
3770
+ };
3771
+ var paintImageNode = (ctx, node, cache5, theme) => {
3772
+ if (node.w <= 0 || node.h <= 0) return;
3773
+ const data = node.data;
3774
+ if (!data?.src) return;
3775
+ const bitmap = cache5.getImage(data.src);
3776
+ const opacity = resolveOpacity(node.style, theme);
3777
+ const needsScope = opacity !== 1;
3778
+ if (needsScope) {
3779
+ ctx.save();
3780
+ ctx.globalAlpha = opacity;
3781
+ }
3782
+ if (bitmap?.complete) {
3783
+ ctx.drawImage(bitmap, 0, 0, node.w, node.h);
3784
+ } else {
3785
+ paintPlaceholder(ctx, node.w, node.h, "loading\u2026");
3786
+ }
3787
+ if (needsScope) ctx.restore();
3788
+ };
3789
+ var paintIconNode = (ctx, node, cache5, scale, theme) => {
3790
+ if (node.w <= 0 || node.h <= 0) return;
3791
+ const data = node.data;
3792
+ if (!data?.src) return;
3793
+ const sizePx = Math.max(node.w, node.h) * scale;
3794
+ const color = node.style?.iconColor;
3795
+ const bitmap = cache5.getIcon(data.src, color, sizePx);
3796
+ const opacity = resolveOpacity(node.style, theme);
3797
+ const needsScope = opacity !== 1;
3798
+ if (needsScope) {
3799
+ ctx.save();
3800
+ ctx.globalAlpha = opacity;
3801
+ }
3802
+ if (bitmap) {
3803
+ ctx.drawImage(bitmap, 0, 0, node.w, node.h);
3804
+ } else {
3805
+ paintPlaceholder(ctx, node.w, node.h, "svg\u2026");
3806
+ }
3807
+ if (needsScope) ctx.restore();
3808
+ };
3809
+
3810
+ // src/render/background.ts
3811
+ var MIN_PATTERN_SCREEN_PX = 8;
3812
+ var MIN_VISIBLE_PATTERN_PX = 2;
3813
+ var paintBackground = (ctx, opts) => {
3814
+ const bg = { ...DEFAULT_BACKGROUND, ...opts.background };
3815
+ ctx.save();
3816
+ ctx.fillStyle = bg.color;
3817
+ ctx.fillRect(opts.viewport.x, opts.viewport.y, opts.viewport.w, opts.viewport.h);
3818
+ ctx.restore();
3819
+ if (bg.pattern === "none") return;
3820
+ if (opts.zoom < bg.minZoom) return;
3821
+ if (opts.zoom > bg.maxZoom) return;
3822
+ let effectiveGap = bg.gap;
3823
+ while (effectiveGap * opts.zoom < MIN_PATTERN_SCREEN_PX) {
3824
+ effectiveGap *= 2;
3825
+ if (effectiveGap > 1e6) return;
3826
+ }
3827
+ if (effectiveGap * opts.zoom < MIN_VISIBLE_PATTERN_PX) return;
3828
+ const minX = Math.floor(opts.viewport.x / effectiveGap) * effectiveGap;
3829
+ const minY = Math.floor(opts.viewport.y / effectiveGap) * effectiveGap;
3830
+ const maxX = opts.viewport.x + opts.viewport.w;
3831
+ const maxY = opts.viewport.y + opts.viewport.h;
3832
+ if (bg.pattern === "dots") {
3833
+ paintDots(ctx, minX, minY, maxX, maxY, effectiveGap, bg.patternColor, opts.zoom);
3834
+ } else if (bg.pattern === "grid") {
3835
+ paintGrid(ctx, minX, minY, maxX, maxY, effectiveGap, bg.patternColor, opts.zoom);
3836
+ }
3837
+ };
3838
+ var paintDots = (ctx, minX, minY, maxX, maxY, gap, color, zoom) => {
3839
+ const sizeWorld = Math.max(1, 1.6 / zoom);
3840
+ const half = sizeWorld / 2;
3841
+ ctx.save();
3842
+ ctx.fillStyle = color;
3843
+ for (let y = minY; y <= maxY; y += gap) {
3844
+ for (let x = minX; x <= maxX; x += gap) {
3845
+ ctx.fillRect(x - half, y - half, sizeWorld, sizeWorld);
4214
3846
  }
4215
- };
4216
- return store;
3847
+ }
3848
+ ctx.restore();
3849
+ };
3850
+ var paintGrid = (ctx, minX, minY, maxX, maxY, gap, color, zoom) => {
3851
+ const lineWidth = 1 / zoom;
3852
+ ctx.save();
3853
+ ctx.strokeStyle = color;
3854
+ ctx.lineWidth = lineWidth;
3855
+ ctx.beginPath();
3856
+ for (let x = minX; x <= maxX; x += gap) {
3857
+ ctx.moveTo(x, minY);
3858
+ ctx.lineTo(x, maxY);
3859
+ }
3860
+ for (let y = minY; y <= maxY; y += gap) {
3861
+ ctx.moveTo(minX, y);
3862
+ ctx.lineTo(maxX, y);
3863
+ }
3864
+ ctx.stroke();
3865
+ ctx.restore();
4217
3866
  };
4218
3867
 
4219
- // src/store/sync.ts
4220
- var attachSync = (store, adapter) => {
4221
- if (!adapter.capabilities.causalOrdering && !adapter.capabilities.crdt) {
4222
- throw new Error(
4223
- "SyncAdapter must advertise capabilities.causalOrdering or capabilities.crdt. See ARCHITECTURE.md \xA710.6."
4224
- );
3868
+ // src/hit-test/handle.ts
3869
+ var RESIZE_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
3870
+ var RESIZE_HANDLE_SIZE_PX = 14;
3871
+ var ROTATE_HANDLE_OFFSET_PX = 24;
3872
+ var ROTATE_HANDLE_RADIUS_PX = 9;
3873
+ var handleWorldPositions = (node) => {
3874
+ const localCenters = {
3875
+ nw: { x: 0, y: 0 },
3876
+ n: { x: node.w / 2, y: 0 },
3877
+ ne: { x: node.w, y: 0 },
3878
+ e: { x: node.w, y: node.h / 2 },
3879
+ se: { x: node.w, y: node.h },
3880
+ s: { x: node.w / 2, y: node.h },
3881
+ sw: { x: 0, y: node.h },
3882
+ w: { x: 0, y: node.h / 2 }
3883
+ };
3884
+ if (node.angle === 0) {
3885
+ const offsetX = node.x;
3886
+ const offsetY = node.y;
3887
+ return {
3888
+ nw: { x: offsetX + localCenters.nw.x, y: offsetY + localCenters.nw.y },
3889
+ n: { x: offsetX + localCenters.n.x, y: offsetY + localCenters.n.y },
3890
+ ne: { x: offsetX + localCenters.ne.x, y: offsetY + localCenters.ne.y },
3891
+ e: { x: offsetX + localCenters.e.x, y: offsetY + localCenters.e.y },
3892
+ se: { x: offsetX + localCenters.se.x, y: offsetY + localCenters.se.y },
3893
+ s: { x: offsetX + localCenters.s.x, y: offsetY + localCenters.s.y },
3894
+ sw: { x: offsetX + localCenters.sw.x, y: offsetY + localCenters.sw.y },
3895
+ w: { x: offsetX + localCenters.w.x, y: offsetY + localCenters.w.y }
3896
+ };
4225
3897
  }
4226
- const unsubChange = store.subscribe("change", (batch) => {
4227
- if (batch.origin !== "remote") adapter.sendBatch(batch);
4228
- });
4229
- const unsubPresence = store.subscribe("presence", (e) => {
4230
- if ("removed" in e && e.removed) return;
4231
- if (e.state.clientId !== store.clientId) return;
4232
- const { clientId: _id, ...patch } = e.state;
4233
- adapter.sendPresence(patch);
4234
- });
4235
- const unsubRemoteBatch = adapter.onBatch((batch) => {
4236
- store.applyBatch({ ...batch, origin: "remote" });
4237
- });
4238
- const unsubRemotePresence = adapter.onPresence((clientId, state) => {
4239
- store.presence.applyRemote(clientId, state);
4240
- });
4241
- return () => {
4242
- unsubChange();
4243
- unsubPresence();
4244
- unsubRemoteBatch();
4245
- unsubRemotePresence();
4246
- adapter.destroy?.();
3898
+ const cx = node.x + node.w / 2;
3899
+ const cy = node.y + node.h / 2;
3900
+ const cos = Math.cos(node.angle);
3901
+ const sin = Math.sin(node.angle);
3902
+ const rotate = (p) => {
3903
+ const dx = p.x - node.w / 2;
3904
+ const dy = p.y - node.h / 2;
3905
+ return { x: cx + dx * cos - dy * sin, y: cy + dx * sin + dy * cos };
3906
+ };
3907
+ return {
3908
+ nw: rotate(localCenters.nw),
3909
+ n: rotate(localCenters.n),
3910
+ ne: rotate(localCenters.ne),
3911
+ e: rotate(localCenters.e),
3912
+ se: rotate(localCenters.se),
3913
+ s: rotate(localCenters.s),
3914
+ sw: rotate(localCenters.sw),
3915
+ w: rotate(localCenters.w)
3916
+ };
3917
+ };
3918
+ var hitTestHandles = (node, worldPoint, cameraZ) => {
3919
+ const halfWorld = RESIZE_HANDLE_SIZE_PX / 2 / cameraZ;
3920
+ const positions = handleWorldPositions(node);
3921
+ for (const h of RESIZE_HANDLES) {
3922
+ const center = positions[h];
3923
+ if (Math.abs(worldPoint.x - center.x) <= halfWorld && Math.abs(worldPoint.y - center.y) <= halfWorld) {
3924
+ return h;
3925
+ }
3926
+ }
3927
+ return null;
3928
+ };
3929
+ var rotateHandleWorldPosition = (node, cameraZ) => {
3930
+ const offsetWorld = ROTATE_HANDLE_OFFSET_PX / cameraZ;
3931
+ const cx = node.x + node.w / 2;
3932
+ const cy = node.y + node.h / 2;
3933
+ const localX = 0;
3934
+ const localY = -node.h / 2 - offsetWorld;
3935
+ const cos = Math.cos(node.angle);
3936
+ const sin = Math.sin(node.angle);
3937
+ return {
3938
+ x: cx + localX * cos - localY * sin,
3939
+ y: cy + localX * sin + localY * cos
4247
3940
  };
4248
3941
  };
3942
+ var hitTestRotateHandle = (node, worldPoint, cameraZ) => {
3943
+ const center = rotateHandleWorldPosition(node, cameraZ);
3944
+ const rWorld = ROTATE_HANDLE_RADIUS_PX / cameraZ;
3945
+ const dx = worldPoint.x - center.x;
3946
+ const dy = worldPoint.y - center.y;
3947
+ return dx * dx + dy * dy <= rWorld * rWorld;
3948
+ };
4249
3949
 
4250
- // src/store/palm-rejection.ts
4251
- var PALM_REJECTION_GRACE_MS = 300;
4252
- var createPalmRejectionState = () => ({
4253
- penActive: false,
4254
- lastPenUpAt: 0
4255
- });
4256
- var notePenActive = (state) => {
4257
- state.penActive = true;
3950
+ // src/render/overlay.ts
3951
+ var DEFAULT_SELECTION_COLOR = "#3b82f6";
3952
+ var SELECTION_OUTLINE_PX = 1.5;
3953
+ var MARQUEE_FILL_ALPHA = 0.08;
3954
+ var MARQUEE_STROKE_PX = 1;
3955
+ var drawSelectionOutline = (ctx, node, scale, color) => {
3956
+ if (node.angle === 0) {
3957
+ ctx.save();
3958
+ ctx.strokeStyle = color;
3959
+ ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
3960
+ ctx.beginPath();
3961
+ ctx.rect(node.x, node.y, node.w, node.h);
3962
+ ctx.stroke();
3963
+ ctx.restore();
3964
+ return;
3965
+ }
3966
+ const cx = node.x + node.w / 2;
3967
+ const cy = node.y + node.h / 2;
3968
+ const cos = Math.cos(node.angle);
3969
+ const sin = Math.sin(node.angle);
3970
+ const corners = [
3971
+ { x: -node.w / 2, y: -node.h / 2 },
3972
+ { x: node.w / 2, y: -node.h / 2 },
3973
+ { x: node.w / 2, y: node.h / 2 },
3974
+ { x: -node.w / 2, y: node.h / 2 }
3975
+ ].map((p) => ({ x: cx + p.x * cos - p.y * sin, y: cy + p.x * sin + p.y * cos }));
3976
+ ctx.save();
3977
+ ctx.strokeStyle = color;
3978
+ ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
3979
+ ctx.beginPath();
3980
+ const first = corners[0];
3981
+ ctx.moveTo(first.x, first.y);
3982
+ for (let i = 1; i < corners.length; i++) {
3983
+ const c = corners[i];
3984
+ ctx.lineTo(c.x, c.y);
3985
+ }
3986
+ ctx.closePath();
3987
+ ctx.stroke();
3988
+ ctx.restore();
4258
3989
  };
4259
- var notePenInactive = (state, now) => {
4260
- state.penActive = false;
4261
- state.lastPenUpAt = now;
3990
+ var drawResizeHandles = (ctx, node, scale, color) => {
3991
+ const halfPx = RESIZE_HANDLE_SIZE_PX / 2;
3992
+ const halfWorld = halfPx / scale;
3993
+ const positions = handleWorldPositions(node);
3994
+ ctx.save();
3995
+ ctx.fillStyle = "#fff";
3996
+ ctx.strokeStyle = color;
3997
+ ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
3998
+ for (const key of Object.keys(positions)) {
3999
+ const p = positions[key];
4000
+ ctx.beginPath();
4001
+ ctx.rect(p.x - halfWorld, p.y - halfWorld, halfWorld * 2, halfWorld * 2);
4002
+ ctx.fill();
4003
+ ctx.stroke();
4004
+ }
4005
+ ctx.restore();
4006
+ };
4007
+ var drawRotateHandle = (ctx, node, scale, cameraZ, color) => {
4008
+ const center = rotateHandleWorldPosition(node, cameraZ);
4009
+ const radiusWorld = ROTATE_HANDLE_RADIUS_PX / scale;
4010
+ const cx = node.x + node.w / 2;
4011
+ const cy = node.y + node.h / 2;
4012
+ const cos = Math.cos(node.angle);
4013
+ const sin = Math.sin(node.angle);
4014
+ const topMidLocalY = -node.h / 2;
4015
+ const topMidWorld = {
4016
+ x: cx + 0 * cos - topMidLocalY * sin,
4017
+ y: cy + 0 * sin + topMidLocalY * cos
4018
+ };
4019
+ ctx.save();
4020
+ ctx.strokeStyle = color;
4021
+ ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
4022
+ ctx.beginPath();
4023
+ ctx.moveTo(topMidWorld.x, topMidWorld.y);
4024
+ ctx.lineTo(center.x, center.y);
4025
+ ctx.stroke();
4026
+ ctx.fillStyle = "#fff";
4027
+ ctx.beginPath();
4028
+ ctx.arc(center.x, center.y, radiusWorld, 0, Math.PI * 2);
4029
+ ctx.fill();
4030
+ ctx.stroke();
4031
+ ctx.restore();
4032
+ };
4033
+ var drawEdgeMidpointHandle = (ctx, midpoint, scale, color) => {
4034
+ const radiusPx = 5;
4035
+ const radiusWorld = radiusPx / scale;
4036
+ ctx.save();
4037
+ ctx.fillStyle = "#fff";
4038
+ ctx.strokeStyle = color;
4039
+ ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
4040
+ ctx.beginPath();
4041
+ ctx.arc(midpoint.x, midpoint.y, radiusWorld, 0, Math.PI * 2);
4042
+ ctx.fill();
4043
+ ctx.stroke();
4044
+ ctx.restore();
4045
+ };
4046
+ var drawEdgeEndpointHandles = (ctx, source, target, scale, color) => {
4047
+ const radiusPx = 5;
4048
+ const radiusWorld = radiusPx / scale;
4049
+ ctx.save();
4050
+ ctx.fillStyle = "#fff";
4051
+ ctx.strokeStyle = color;
4052
+ ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
4053
+ for (const p of [source, target]) {
4054
+ ctx.beginPath();
4055
+ ctx.arc(p.x, p.y, radiusWorld, 0, Math.PI * 2);
4056
+ ctx.fill();
4057
+ ctx.stroke();
4058
+ }
4059
+ ctx.restore();
4262
4060
  };
4263
- var shouldRejectTouch = (state, now) => {
4264
- if (state.penActive) return true;
4265
- return now - state.lastPenUpAt < PALM_REJECTION_GRACE_MS;
4061
+ var drawMarquee = (ctx, rect, scale, color) => {
4062
+ ctx.save();
4063
+ ctx.globalAlpha = MARQUEE_FILL_ALPHA;
4064
+ ctx.fillStyle = color;
4065
+ ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
4066
+ ctx.globalAlpha = 1;
4067
+ ctx.strokeStyle = color;
4068
+ ctx.lineWidth = MARQUEE_STROKE_PX / scale;
4069
+ ctx.setLineDash([4 / scale, 3 / scale]);
4070
+ ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
4071
+ ctx.restore();
4266
4072
  };
4267
4073
 
4268
- // src/codec/index.ts
4269
- var migrators = /* @__PURE__ */ new Map();
4270
- var registerMigrator = (fromVersion, fn) => {
4271
- migrators.set(fromVersion, fn);
4272
- };
4273
- var toSerialized = (scene) => ({
4274
- schemaVersion: scene.schemaVersion,
4275
- nodes: Object.values(scene.nodes),
4276
- edges: Object.values(scene.edges),
4277
- groups: Object.values(scene.groups),
4278
- camera: scene.camera,
4279
- selection: scene.selection,
4280
- ...scene.frameOrder && scene.frameOrder.length > 0 ? { frameOrder: scene.frameOrder } : {}
4281
- });
4282
- var fromSerialized = (raw) => {
4283
- let working = raw;
4284
- let version = working.schemaVersion ?? 0;
4285
- while (version < SCHEMA_VERSION) {
4286
- const fn = migrators.get(version);
4287
- if (!fn) {
4288
- throw new Error(
4289
- `Cannot migrate scene from schemaVersion ${version} to ${SCHEMA_VERSION}; no migrator registered`
4290
- );
4291
- }
4292
- working = fn(working);
4293
- version++;
4074
+ // src/render/paint-frame.ts
4075
+ var FRAME_BORDER_PX = 1.5;
4076
+ var FRAME_BORDER_COLOR_DEFAULT = "#94a3b8";
4077
+ var FRAME_FILL_DEFAULT = "rgba(148, 163, 184, 0.06)";
4078
+ var FRAME_LABEL_FONT_PX = 12;
4079
+ var FRAME_LABEL_GAP_PX = 6;
4080
+ var FRAME_LABEL_COLOR = "#64748b";
4081
+ var paintFrameNode = (ctx, node, scale, theme) => {
4082
+ if (node.w <= 0 || node.h <= 0) return;
4083
+ const opacity = resolveOpacity(node.style, theme);
4084
+ const needsScope = opacity !== 1;
4085
+ if (needsScope) {
4086
+ ctx.save();
4087
+ ctx.globalAlpha = opacity;
4294
4088
  }
4295
- const ser = working;
4296
- return {
4297
- schemaVersion: SCHEMA_VERSION,
4298
- nodes: Object.fromEntries(ser.nodes.map((n) => [asNodeId(n.id), n])),
4299
- edges: Object.fromEntries(ser.edges.map((e) => [asEdgeId(e.id), e])),
4300
- groups: Object.fromEntries(ser.groups.map((g) => [asGroupId(g.id), g])),
4301
- camera: ser.camera,
4302
- selection: ser.selection,
4303
- ...ser.frameOrder ? { frameOrder: ser.frameOrder } : {}
4304
- };
4089
+ const fill = node.style?.backgroundColor ?? (theme ? theme("frame.background") : void 0) ?? FRAME_FILL_DEFAULT;
4090
+ ctx.fillStyle = fill;
4091
+ ctx.fillRect(0, 0, node.w, node.h);
4092
+ const stroke = resolveColor(node.style, "strokeColor", FRAME_BORDER_COLOR_DEFAULT, theme);
4093
+ ctx.strokeStyle = stroke;
4094
+ ctx.lineWidth = FRAME_BORDER_PX / scale;
4095
+ ctx.setLineDash([]);
4096
+ ctx.strokeRect(0, 0, node.w, node.h);
4097
+ const labelPx = FRAME_LABEL_FONT_PX / scale;
4098
+ const gapPx = FRAME_LABEL_GAP_PX / scale;
4099
+ const label = node.content?.trim() || "Frame";
4100
+ ctx.fillStyle = FRAME_LABEL_COLOR;
4101
+ ctx.textBaseline = "bottom";
4102
+ ctx.textAlign = "left";
4103
+ ctx.font = `500 ${labelPx}px system-ui, -apple-system, sans-serif`;
4104
+ ctx.fillText(label, 0, -gapPx);
4105
+ if (needsScope) ctx.restore();
4305
4106
  };
4306
- var storeToJSON = (store) => ({
4307
- schemaVersion: SCHEMA_VERSION,
4308
- nodes: store.getAllNodes(),
4309
- edges: store.getAllEdges(),
4310
- groups: store.getAllGroups(),
4311
- camera: store.getCamera(),
4312
- selection: store.getSelection()
4313
- });
4314
4107
 
4315
- // src/render/canvas-setup.ts
4316
- var HARD_MAX_DPR = 3;
4317
- var defaultMaxDprForSize = (cssW, cssH) => {
4318
- const cssPx = cssW * cssH;
4319
- if (cssPx >= 25e5) return 1;
4320
- if (cssPx >= 15e5) return 1.5;
4321
- return 2;
4108
+ // src/render/rough/constants.ts
4109
+ var ROUGH_MIN_ZOOM = 0.4;
4110
+ var ROUGH_MAX_NODES = 800;
4111
+ var ROUGH_MAX_MOVING_NODES = 5;
4112
+ var ROUGH_DEFAULTS = {
4113
+ bowing: 2,
4114
+ disableMultiStroke: true,
4115
+ preserveVertices: true
4322
4116
  };
4323
- var getDpr = (maxDpr, cssW = 0, cssH = 0) => {
4324
- if (typeof window === "undefined") return 1;
4325
- const raw = window.devicePixelRatio || 1;
4326
- const resolvedMax = maxDpr === void 0 && cssW > 0 && cssH > 0 ? defaultMaxDprForSize(cssW, cssH) : maxDpr ?? 1;
4327
- const cap = Math.max(1, Math.min(HARD_MAX_DPR, resolvedMax));
4328
- return Math.max(1, Math.min(cap, raw));
4117
+ var ROUGH_FILL_MISREGISTER_X = -3;
4118
+ var ROUGH_FILL_MISREGISTER_Y = -2;
4119
+
4120
+ // src/render/color.ts
4121
+ var TONE_BLEND = 0.2;
4122
+ var parseHex = (hex) => {
4123
+ if (!hex.startsWith("#")) return null;
4124
+ const h = hex.slice(1);
4125
+ if (h.length === 3) {
4126
+ return [
4127
+ Number.parseInt(h[0] + h[0], 16),
4128
+ Number.parseInt(h[1] + h[1], 16),
4129
+ Number.parseInt(h[2] + h[2], 16)
4130
+ ];
4131
+ }
4132
+ if (h.length === 6 || h.length === 8) {
4133
+ return [
4134
+ Number.parseInt(h.slice(0, 2), 16),
4135
+ Number.parseInt(h.slice(2, 4), 16),
4136
+ Number.parseInt(h.slice(4, 6), 16)
4137
+ ];
4138
+ }
4139
+ return null;
4329
4140
  };
4330
- var setupSurface = (canvas, _maxDpr) => {
4331
- const ctx = canvas.getContext("2d");
4332
- if (!ctx) throw new Error("Canvas 2d context unavailable");
4333
- return {
4334
- canvas,
4335
- ctx,
4336
- cssWidth: 0,
4337
- cssHeight: 0,
4338
- dpr: 1
4339
- // placeholder; `sizeSurface` writes the real value
4340
- };
4141
+ var toHexPair = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
4142
+ var mixHex = (a, b, t) => {
4143
+ const A = parseHex(a);
4144
+ const B = parseHex(b);
4145
+ if (!A || !B) return a;
4146
+ const p = Math.max(0, Math.min(1, t));
4147
+ return `#${toHexPair(A[0] * (1 - p) + B[0] * p)}${toHexPair(A[1] * (1 - p) + B[1] * p)}${toHexPair(A[2] * (1 - p) + B[2] * p)}`;
4341
4148
  };
4342
- var sizeSurface = (surface, cssW, cssH, maxDpr) => {
4343
- const dpr = getDpr(maxDpr, cssW, cssH);
4344
- if (surface.cssWidth === cssW && surface.cssHeight === cssH && surface.dpr === dpr) {
4345
- return false;
4149
+ var darkenCache = /* @__PURE__ */ new Map();
4150
+ var darkenHex = (hex) => {
4151
+ const cached2 = darkenCache.get(hex);
4152
+ if (cached2 !== void 0) return cached2;
4153
+ const result = mixHex(hex, "#000000", TONE_BLEND);
4154
+ darkenCache.set(hex, result);
4155
+ return result;
4156
+ };
4157
+
4158
+ // src/render/shapes/path-helpers.ts
4159
+ var buildRectPath = (ctx, w, h, radius) => {
4160
+ ctx.beginPath();
4161
+ if (radius <= 0) {
4162
+ ctx.rect(0, 0, w, h);
4163
+ return;
4346
4164
  }
4347
- surface.cssWidth = cssW;
4348
- surface.cssHeight = cssH;
4349
- surface.dpr = dpr;
4350
- surface.canvas.width = Math.max(1, Math.round(cssW * dpr));
4351
- surface.canvas.height = Math.max(1, Math.round(cssH * dpr));
4352
- surface.canvas.style.width = `${cssW}px`;
4353
- surface.canvas.style.height = `${cssH}px`;
4354
- return true;
4165
+ ctx.roundRect(0, 0, w, h, radius);
4355
4166
  };
4356
- var clearSurface = (surface) => {
4357
- surface.ctx.setTransform(1, 0, 0, 1, 0, 0);
4358
- surface.ctx.clearRect(0, 0, surface.canvas.width, surface.canvas.height);
4167
+ var buildEllipsePath = (ctx, w, h) => {
4168
+ const rx = w / 2;
4169
+ const ry = h / 2;
4170
+ ctx.beginPath();
4171
+ ctx.ellipse(rx, ry, rx, ry, 0, 0, Math.PI * 2);
4359
4172
  };
4360
-
4361
- // src/render/frame-loop.ts
4362
- var createFrameLoop = ({ draw, historySize = 60 }) => {
4363
- let running = false;
4364
- let scheduled = false;
4365
- let frameId = 0;
4366
- const history = [];
4367
- let frames = 0;
4368
- let lastMs = 0;
4369
- let avgMs = 0;
4370
- const fpsWindow = [];
4371
- let fps = 0;
4372
- const tick = () => {
4373
- frameId = 0;
4374
- scheduled = false;
4375
- if (!running) return;
4376
- const t0 = performance.now();
4377
- draw();
4378
- const dur = performance.now() - t0;
4379
- history.push(dur);
4380
- if (history.length > historySize) history.shift();
4381
- let sum = 0;
4382
- for (const v of history) sum += v;
4383
- avgMs = sum / history.length;
4384
- lastMs = dur;
4385
- frames++;
4386
- fpsWindow.push(t0);
4387
- const cutoff = t0 - 1e3;
4388
- while (fpsWindow.length > 0 && fpsWindow[0] < cutoff) fpsWindow.shift();
4389
- fps = fpsWindow.length;
4390
- };
4391
- const schedule = () => {
4392
- if (scheduled || !running) return;
4393
- scheduled = true;
4394
- frameId = requestAnimationFrame(tick);
4173
+ var buildDiamondPath = (ctx, w, h, radius = 0) => {
4174
+ ctx.beginPath();
4175
+ if (radius <= 0) {
4176
+ ctx.moveTo(w / 2, 0);
4177
+ ctx.lineTo(w, h / 2);
4178
+ ctx.lineTo(w / 2, h);
4179
+ ctx.lineTo(0, h / 2);
4180
+ ctx.closePath();
4181
+ return;
4182
+ }
4183
+ const cx = w / 2;
4184
+ const cy = h / 2;
4185
+ const T = { x: cx, y: 0 };
4186
+ const R = { x: w, y: cy };
4187
+ const B = { x: cx, y: h };
4188
+ const L = { x: 0, y: cy };
4189
+ const edgeLen = Math.hypot(R.x - T.x, R.y - T.y);
4190
+ const sMax = Math.max(0, edgeLen / 2 - 0.01);
4191
+ const s = Math.min(radius * Math.SQRT2, sMax);
4192
+ if (s <= 1e-4) {
4193
+ ctx.moveTo(T.x, T.y);
4194
+ ctx.lineTo(R.x, R.y);
4195
+ ctx.lineTo(B.x, B.y);
4196
+ ctx.lineTo(L.x, L.y);
4197
+ ctx.closePath();
4198
+ return;
4199
+ }
4200
+ const along = (a, b, d) => {
4201
+ const dx = b.x - a.x;
4202
+ const dy = b.y - a.y;
4203
+ const len = Math.hypot(dx, dy) || 1;
4204
+ const t = d / len;
4205
+ return { x: a.x + dx * t, y: a.y + dy * t };
4395
4206
  };
4207
+ const TR = along(T, R, s);
4208
+ const RT = along(R, T, s);
4209
+ const RB = along(R, B, s);
4210
+ const BR = along(B, R, s);
4211
+ const BL = along(B, L, s);
4212
+ const LB = along(L, B, s);
4213
+ const LT = along(L, T, s);
4214
+ const TL = along(T, L, s);
4215
+ ctx.moveTo(TR.x, TR.y);
4216
+ ctx.lineTo(RT.x, RT.y);
4217
+ ctx.quadraticCurveTo(R.x, R.y, RB.x, RB.y);
4218
+ ctx.lineTo(BR.x, BR.y);
4219
+ ctx.quadraticCurveTo(B.x, B.y, BL.x, BL.y);
4220
+ ctx.lineTo(LB.x, LB.y);
4221
+ ctx.quadraticCurveTo(L.x, L.y, LT.x, LT.y);
4222
+ ctx.lineTo(TL.x, TL.y);
4223
+ ctx.quadraticCurveTo(T.x, T.y, TR.x, TR.y);
4224
+ ctx.closePath();
4225
+ };
4226
+ var thoughtCloudGeometry = (w, h) => {
4227
+ const domeW = Math.min(w * 0.4, h * 1.2);
4228
+ const domeH = Math.min(h * 0.45, domeW);
4229
+ const domeAnchorX = w * 0.3;
4230
+ const domeX = Math.max(0, Math.min(w - domeW, domeAnchorX - domeW / 2));
4396
4231
  return {
4397
- start() {
4398
- if (running) return;
4399
- running = true;
4400
- schedule();
4401
- },
4402
- stop() {
4403
- running = false;
4404
- if (frameId !== 0) {
4405
- cancelAnimationFrame(frameId);
4406
- frameId = 0;
4407
- }
4408
- scheduled = false;
4409
- },
4410
- requestFrame() {
4411
- schedule();
4412
- },
4413
- stats: () => ({ lastMs, avgMs, frames, fps })
4232
+ domeX,
4233
+ domeW,
4234
+ domeH,
4235
+ cx: domeX + domeW / 2,
4236
+ cy: domeH / 2,
4237
+ rx: domeW / 2,
4238
+ ry: domeH / 2,
4239
+ bodyY: domeH * 0.55
4414
4240
  };
4415
4241
  };
4416
-
4417
- // src/render/assets/cache.ts
4418
- var MAX_ENTRIES2 = 256;
4419
- var bucketSize = (px) => {
4420
- if (px <= 32) return 32;
4421
- if (px <= 64) return 64;
4422
- if (px <= 128) return 128;
4423
- if (px <= 256) return 256;
4424
- if (px <= 512) return 512;
4425
- return Math.ceil(px / 256) * 256;
4242
+ var buildThoughtCloudPath = (ctx, w, h, radius) => {
4243
+ const g = thoughtCloudGeometry(w, h);
4244
+ const bodyH = h - g.bodyY;
4245
+ const r = Math.max(0, Math.min(radius, bodyH / 2, w / 2));
4246
+ const t = g.ry > 0 ? (g.bodyY - g.cy) / g.ry : 0;
4247
+ const inRange = Math.abs(t) < 1;
4248
+ let xL = g.domeX;
4249
+ let xR = g.domeX + g.domeW;
4250
+ if (inRange) {
4251
+ const xOffset = g.rx * Math.sqrt(1 - t * t);
4252
+ xL = g.cx - xOffset;
4253
+ xR = g.cx + xOffset;
4254
+ }
4255
+ xL = Math.max(r, xL);
4256
+ xR = Math.min(w - r, xR);
4257
+ ctx.beginPath();
4258
+ ctx.moveTo(r, g.bodyY);
4259
+ ctx.lineTo(xL, g.bodyY);
4260
+ const startAngle = Math.atan2((g.bodyY - g.cy) / g.ry, (xL - g.cx) / g.rx);
4261
+ let endAngle = Math.atan2((g.bodyY - g.cy) / g.ry, (xR - g.cx) / g.rx);
4262
+ if (endAngle <= startAngle) endAngle += 2 * Math.PI;
4263
+ ctx.ellipse(g.cx, g.cy, g.rx, g.ry, 0, startAngle, endAngle, false);
4264
+ ctx.lineTo(w - r, g.bodyY);
4265
+ ctx.quadraticCurveTo(w, g.bodyY, w, g.bodyY + r);
4266
+ ctx.lineTo(w, h - r);
4267
+ ctx.quadraticCurveTo(w, h, w - r, h);
4268
+ ctx.lineTo(r, h);
4269
+ ctx.quadraticCurveTo(0, h, 0, h - r);
4270
+ ctx.lineTo(0, g.bodyY + r);
4271
+ ctx.quadraticCurveTo(0, g.bodyY, r, g.bodyY);
4272
+ ctx.closePath();
4426
4273
  };
4427
- var createAssetCache = (opts = {}) => {
4428
- const entries = /* @__PURE__ */ new Map();
4429
- let disposed = false;
4430
- const notify = () => {
4431
- if (disposed) return;
4432
- opts.onReady?.();
4433
- };
4434
- const touch = (key, entry) => {
4435
- entries.delete(key);
4436
- entries.set(key, entry);
4437
- if (entries.size > MAX_ENTRIES2) {
4438
- const oldestKey = entries.keys().next().value;
4439
- if (oldestKey !== void 0) {
4440
- const evicted = entries.get(oldestKey);
4441
- if (evicted?.kind === "icon" && evicted.bitmap) evicted.bitmap.close?.();
4442
- entries.delete(oldestKey);
4443
- }
4444
- }
4445
- };
4446
- const startImageDecode = (key, src) => {
4447
- const entry = { kind: "image", state: "pending", bitmap: null };
4448
- touch(key, entry);
4449
- const img = new Image();
4450
- img.onload = () => {
4451
- if (disposed) return;
4452
- entry.state = "ready";
4453
- entry.bitmap = img;
4454
- notify();
4455
- };
4456
- img.onerror = (e) => {
4457
- if (disposed) return;
4458
- entry.state = "error";
4459
- entry.err = e;
4460
- notify();
4461
- };
4462
- img.src = src;
4463
- };
4464
- const startIconRaster = (key, markup, color, sizePx) => {
4465
- const entry = { kind: "icon", state: "pending", bitmap: null };
4466
- touch(key, entry);
4467
- const colored = color ? applySvgColor(markup, color) : markup;
4468
- const blob = new Blob([colored], { type: "image/svg+xml" });
4469
- const url = URL.createObjectURL(blob);
4470
- const img = new Image();
4471
- img.onload = async () => {
4472
- URL.revokeObjectURL(url);
4473
- if (disposed) return;
4474
- try {
4475
- const bitmap = await createImageBitmap(img, {
4476
- resizeWidth: sizePx,
4477
- resizeHeight: sizePx,
4478
- resizeQuality: "high"
4479
- });
4480
- if (disposed) {
4481
- bitmap.close?.();
4482
- return;
4483
- }
4484
- entry.state = "ready";
4485
- entry.bitmap = bitmap;
4486
- notify();
4487
- } catch (e) {
4488
- entry.state = "error";
4489
- entry.err = e;
4490
- notify();
4491
- }
4492
- };
4493
- img.onerror = (e) => {
4494
- URL.revokeObjectURL(url);
4495
- if (disposed) return;
4496
- entry.state = "error";
4497
- entry.err = e;
4498
- notify();
4499
- };
4500
- img.src = url;
4501
- };
4502
- return {
4503
- getImage(src) {
4504
- const key = `img:${src}`;
4505
- const existing = entries.get(key);
4506
- if (existing && existing.kind === "image") {
4507
- if (existing.state === "ready") {
4508
- touch(key, existing);
4509
- return existing.bitmap;
4510
- }
4511
- return null;
4512
- }
4513
- startImageDecode(key, src);
4514
- return null;
4515
- },
4516
- getIcon(markup, color, devicePixelSize) {
4517
- const size = bucketSize(Math.max(1, Math.ceil(devicePixelSize)));
4518
- const key = `icon:${size}:${color ?? ""}:${markup}`;
4519
- const existing = entries.get(key);
4520
- if (existing && existing.kind === "icon") {
4521
- if (existing.state === "ready") {
4522
- touch(key, existing);
4523
- return existing.bitmap;
4524
- }
4525
- return null;
4526
- }
4527
- startIconRaster(key, markup, color, size);
4528
- return null;
4529
- },
4530
- dispose() {
4531
- disposed = true;
4532
- for (const entry of entries.values()) {
4533
- if (entry.kind === "icon" && entry.bitmap) entry.bitmap.close?.();
4534
- }
4535
- entries.clear();
4536
- }
4537
- };
4274
+ var buildTagPath = (ctx, w, h, radius = 8) => {
4275
+ const notch = Math.min(h * 0.5, w * 0.3);
4276
+ const tipRadius = 6;
4277
+ const tipX = 0;
4278
+ const tipY = h / 2;
4279
+ const bodyLeft = Math.max(0, Math.min(notch, w));
4280
+ const right = w;
4281
+ const bottom = h;
4282
+ const rBody = Math.min(radius, h / 2, (right - bodyLeft) / 2);
4283
+ const rJoin = Math.min(radius, h * 0.45, bodyLeft * 0.8);
4284
+ ctx.beginPath();
4285
+ if (bodyLeft <= 1e-3) {
4286
+ const r = Math.min(radius, h / 2, w / 2);
4287
+ ctx.moveTo(r, 0);
4288
+ ctx.lineTo(w - r, 0);
4289
+ ctx.quadraticCurveTo(w, 0, w, r);
4290
+ ctx.lineTo(w, h - r);
4291
+ ctx.quadraticCurveTo(w, h, w - r, h);
4292
+ ctx.lineTo(r, h);
4293
+ ctx.quadraticCurveTo(0, h, 0, h - r);
4294
+ ctx.lineTo(0, r);
4295
+ ctx.quadraticCurveTo(0, 0, r, 0);
4296
+ ctx.closePath();
4297
+ return;
4298
+ }
4299
+ const pTop = { x: bodyLeft, y: rJoin };
4300
+ const pBot = { x: bodyLeft, y: bottom - rJoin };
4301
+ const dirX = tipX - bodyLeft;
4302
+ const dirYTop = tipY - rJoin;
4303
+ const dirYBot = tipY - (bottom - rJoin);
4304
+ const lenTop = Math.hypot(dirX, dirYTop) || 1;
4305
+ const lenBot = Math.hypot(dirX, dirYBot) || 1;
4306
+ const maxTipRound = Math.min(lenTop, lenBot) * 0.49;
4307
+ const t = Math.max(0, Math.min(tipRadius, maxTipRound));
4308
+ const tipEnter = { x: tipX - dirX / lenBot * t, y: tipY - dirYBot / lenBot * t };
4309
+ const tipExit = { x: tipX - dirX / lenTop * t, y: tipY - dirYTop / lenTop * t };
4310
+ const k = rJoin * 0.65;
4311
+ const topStart = { x: bodyLeft + rBody, y: 0 };
4312
+ const botEnd = { x: bodyLeft + rBody, y: bottom };
4313
+ ctx.moveTo(topStart.x, topStart.y);
4314
+ ctx.lineTo(right - rBody, 0);
4315
+ ctx.quadraticCurveTo(right, 0, right, rBody);
4316
+ ctx.lineTo(right, bottom - rBody);
4317
+ ctx.quadraticCurveTo(right, bottom, right - rBody, bottom);
4318
+ ctx.lineTo(botEnd.x, botEnd.y);
4319
+ ctx.bezierCurveTo(
4320
+ botEnd.x - k,
4321
+ bottom,
4322
+ pBot.x - dirX / lenBot * k,
4323
+ pBot.y - dirYBot / lenBot * k,
4324
+ pBot.x,
4325
+ pBot.y
4326
+ );
4327
+ ctx.lineTo(t > 0 ? tipEnter.x : tipX, t > 0 ? tipEnter.y : tipY);
4328
+ if (t > 0) ctx.quadraticCurveTo(tipX, tipY, tipExit.x, tipExit.y);
4329
+ ctx.lineTo(pTop.x, pTop.y);
4330
+ ctx.bezierCurveTo(
4331
+ pTop.x - dirX / lenTop * k,
4332
+ pTop.y - dirYTop / lenTop * k,
4333
+ topStart.x - k,
4334
+ 0,
4335
+ topStart.x,
4336
+ 0
4337
+ );
4338
+ ctx.closePath();
4538
4339
  };
4539
4340
 
4540
- // src/render/assets/paint.ts
4541
- var PLACEHOLDER_FILL = "#e5e7eb";
4542
- var PLACEHOLDER_TEXT_FILL = "#94a3b8";
4543
- var paintPlaceholder = (ctx, w, h, label) => {
4544
- ctx.fillStyle = PLACEHOLDER_FILL;
4545
- ctx.fillRect(0, 0, w, h);
4546
- if (w >= 32 && h >= 16) {
4547
- ctx.fillStyle = PLACEHOLDER_TEXT_FILL;
4548
- ctx.font = "11px system-ui, sans-serif";
4549
- ctx.textAlign = "center";
4550
- ctx.textBaseline = "middle";
4551
- ctx.fillText(label, w / 2, h / 2);
4552
- }
4553
- };
4554
- var paintImageNode = (ctx, node, cache5, theme) => {
4341
+ // src/render/shapes/draw-shape.ts
4342
+ var ATOMIC = /* @__PURE__ */ new Set(["rect", "ellipse", "diamond", "tag", "thought-cloud"]);
4343
+ var COMPOSITE = /* @__PURE__ */ new Set([
4344
+ "capsule",
4345
+ "layered-rect",
4346
+ "layered-ellipse",
4347
+ "layered-diamond",
4348
+ "soft-diamond"
4349
+ ]);
4350
+ var isCompositePrimitive = (type) => COMPOSITE.has(type);
4351
+ var isDrawablePrimitive = (type) => ATOMIC.has(type) || COMPOSITE.has(type);
4352
+ var PLAIN_RECT_CORNER_THRESHOLD_PX = 1.5;
4353
+ var LAYERED_OFFSET = 12;
4354
+ var drawShape = (ctx, node, scale, theme, opts) => {
4355
+ if (!isDrawablePrimitive(node.type)) return;
4356
+ if (node.hidden) return;
4555
4357
  if (node.w <= 0 || node.h <= 0) return;
4556
- const data = node.data;
4557
- if (!data?.src) return;
4558
- const bitmap = cache5.getImage(data.src);
4559
- const opacity = resolveOpacity(node.style, theme);
4560
- const needsScope = opacity !== 1;
4561
- if (needsScope) {
4562
- ctx.save();
4563
- ctx.globalAlpha = opacity;
4564
- }
4565
- if (bitmap?.complete) {
4566
- ctx.drawImage(bitmap, 0, 0, node.w, node.h);
4567
- } else {
4568
- paintPlaceholder(ctx, node.w, node.h, "loading\u2026");
4358
+ if (COMPOSITE.has(node.type)) {
4359
+ drawComposite(ctx, node, scale, theme, opts);
4360
+ return;
4569
4361
  }
4570
- if (needsScope) ctx.restore();
4362
+ drawAtomic(ctx, node.type, node.w, node.h, node.style, scale, theme, opts);
4571
4363
  };
4572
- var paintIconNode = (ctx, node, cache5, scale, theme) => {
4573
- if (node.w <= 0 || node.h <= 0) return;
4574
- const data = node.data;
4575
- if (!data?.src) return;
4576
- const sizePx = Math.max(node.w, node.h) * scale;
4577
- const color = node.style?.iconColor;
4578
- const bitmap = cache5.getIcon(data.src, color, sizePx);
4579
- const opacity = resolveOpacity(node.style, theme);
4364
+ var drawAtomic = (ctx, type, w, h, style, scale, theme, opts) => {
4365
+ if (w <= 0 || h <= 0) return;
4366
+ const strokeWidth = resolveStrokeWidth(style, theme);
4367
+ const opacity = resolveOpacity(style, theme);
4368
+ const fill = resolveColor(style, "backgroundColor", DEFAULT_STYLE.backgroundColor, theme);
4369
+ const stroke = resolveColor(style, "strokeColor", DEFAULT_STYLE.strokeColor, theme);
4370
+ const fillVisible = !isFullyTransparent(fill);
4371
+ const strokeVisible = strokeWidth > 0 && !isFullyTransparent(stroke);
4372
+ if (!fillVisible && !strokeVisible) return;
4373
+ const cornerRadius = (style?.roundness ?? DEFAULT_STYLE.roundness) * 4;
4374
+ switch (type) {
4375
+ case "rect": {
4376
+ if (cornerRadius * scale < PLAIN_RECT_CORNER_THRESHOLD_PX) {
4377
+ ctx.beginPath();
4378
+ ctx.rect(0, 0, w, h);
4379
+ } else {
4380
+ buildRectPath(ctx, w, h, cornerRadius);
4381
+ }
4382
+ break;
4383
+ }
4384
+ case "ellipse":
4385
+ buildEllipsePath(ctx, w, h);
4386
+ break;
4387
+ case "diamond":
4388
+ buildDiamondPath(ctx, w, h, cornerRadius);
4389
+ break;
4390
+ case "tag":
4391
+ buildTagPath(ctx, w, h, cornerRadius);
4392
+ break;
4393
+ case "thought-cloud":
4394
+ buildThoughtCloudPath(ctx, w, h, cornerRadius);
4395
+ break;
4396
+ }
4580
4397
  const needsScope = opacity !== 1;
4581
4398
  if (needsScope) {
4582
4399
  ctx.save();
4583
4400
  ctx.globalAlpha = opacity;
4584
4401
  }
4585
- if (bitmap) {
4586
- ctx.drawImage(bitmap, 0, 0, node.w, node.h);
4587
- } else {
4588
- paintPlaceholder(ctx, node.w, node.h, "svg\u2026");
4402
+ if (fillVisible) {
4403
+ ctx.fillStyle = fill;
4404
+ ctx.fill();
4405
+ }
4406
+ if (strokeVisible && !opts?.skipStroke) {
4407
+ ctx.strokeStyle = stroke;
4408
+ ctx.lineWidth = Math.max(strokeWidth, 1 / scale);
4409
+ ctx.setLineDash(dashPatternFor(style?.strokeStyle, strokeWidth));
4410
+ ctx.stroke();
4589
4411
  }
4590
4412
  if (needsScope) ctx.restore();
4591
4413
  };
4592
-
4593
- // src/render/background.ts
4594
- var MIN_PATTERN_SCREEN_PX = 8;
4595
- var MIN_VISIBLE_PATTERN_PX = 2;
4596
- var paintBackground = (ctx, opts) => {
4597
- const bg = { ...DEFAULT_BACKGROUND, ...opts.background };
4598
- ctx.save();
4599
- ctx.fillStyle = bg.color;
4600
- ctx.fillRect(opts.viewport.x, opts.viewport.y, opts.viewport.w, opts.viewport.h);
4601
- ctx.restore();
4602
- if (bg.pattern === "none") return;
4603
- if (opts.zoom < bg.minZoom) return;
4604
- if (opts.zoom > bg.maxZoom) return;
4605
- let effectiveGap = bg.gap;
4606
- while (effectiveGap * opts.zoom < MIN_PATTERN_SCREEN_PX) {
4607
- effectiveGap *= 2;
4608
- if (effectiveGap > 1e6) return;
4609
- }
4610
- if (effectiveGap * opts.zoom < MIN_VISIBLE_PATTERN_PX) return;
4611
- const minX = Math.floor(opts.viewport.x / effectiveGap) * effectiveGap;
4612
- const minY = Math.floor(opts.viewport.y / effectiveGap) * effectiveGap;
4613
- const maxX = opts.viewport.x + opts.viewport.w;
4614
- const maxY = opts.viewport.y + opts.viewport.h;
4615
- if (bg.pattern === "dots") {
4616
- paintDots(ctx, minX, minY, maxX, maxY, effectiveGap, bg.patternColor, opts.zoom);
4617
- } else if (bg.pattern === "grid") {
4618
- paintGrid(ctx, minX, minY, maxX, maxY, effectiveGap, bg.patternColor, opts.zoom);
4414
+ var drawComposite = (ctx, node, scale, theme, opts) => {
4415
+ const subs = compositeLayout(node);
4416
+ for (const s of subs) {
4417
+ ctx.save();
4418
+ ctx.translate(s.x, s.y);
4419
+ drawAtomic(ctx, s.atomic, s.w, s.h, s.style ?? node.style, scale, theme, opts);
4420
+ ctx.restore();
4619
4421
  }
4620
4422
  };
4621
- var paintDots = (ctx, minX, minY, maxX, maxY, gap, color, zoom) => {
4622
- const sizeWorld = Math.max(1, 1.6 / zoom);
4623
- const half = sizeWorld / 2;
4624
- ctx.save();
4625
- ctx.fillStyle = color;
4626
- for (let y = minY; y <= maxY; y += gap) {
4627
- for (let x = minX; x <= maxX; x += gap) {
4628
- ctx.fillRect(x - half, y - half, sizeWorld, sizeWorld);
4423
+ var compositeLayout = (node) => {
4424
+ const { w, h } = node;
4425
+ switch (node.type) {
4426
+ case "capsule": {
4427
+ const circ = Math.min(h * 0.55, w * 0.28, 56);
4428
+ const overlap = circ * 0.15;
4429
+ const rectX = circ - overlap;
4430
+ const rectW = Math.max(0, w - rectX);
4431
+ const circY = (h - circ) / 2;
4432
+ return [
4433
+ { atomic: "ellipse", x: 0, y: circY, w: circ, h: circ },
4434
+ { atomic: "rect", x: rectX, y: 0, w: rectW, h }
4435
+ ];
4436
+ }
4437
+ case "layered-rect":
4438
+ case "layered-ellipse":
4439
+ case "layered-diamond": {
4440
+ const atomic = node.type === "layered-rect" ? "rect" : node.type === "layered-ellipse" ? "ellipse" : "diamond";
4441
+ const off = Math.min(LAYERED_OFFSET, w * 0.15, h * 0.15);
4442
+ const back = {
4443
+ atomic,
4444
+ x: off,
4445
+ y: off,
4446
+ w,
4447
+ h,
4448
+ style: darkenedStyle(node.style)
4449
+ };
4450
+ const front = { atomic, x: 0, y: 0, w, h };
4451
+ return [back, front];
4452
+ }
4453
+ case "soft-diamond": {
4454
+ const backScale = 1.08;
4455
+ const frontScale = 0.96;
4456
+ const bw = w * backScale;
4457
+ const bh = h * backScale;
4458
+ const fw = w * frontScale;
4459
+ const fh = h * frontScale;
4460
+ const back = {
4461
+ atomic: "diamond",
4462
+ x: (w - bw) / 2,
4463
+ y: (h - bh) / 2,
4464
+ w: bw,
4465
+ h: bh,
4466
+ style: darkenedStyle(node.style)
4467
+ };
4468
+ const front = {
4469
+ atomic: "diamond",
4470
+ x: (w - fw) / 2,
4471
+ y: (h - fh) / 2,
4472
+ w: fw,
4473
+ h: fh
4474
+ };
4475
+ return [back, front];
4629
4476
  }
4630
4477
  }
4631
- ctx.restore();
4632
- };
4633
- var paintGrid = (ctx, minX, minY, maxX, maxY, gap, color, zoom) => {
4634
- const lineWidth = 1 / zoom;
4635
- ctx.save();
4636
- ctx.strokeStyle = color;
4637
- ctx.lineWidth = lineWidth;
4638
- ctx.beginPath();
4639
- for (let x = minX; x <= maxX; x += gap) {
4640
- ctx.moveTo(x, minY);
4641
- ctx.lineTo(x, maxY);
4642
- }
4643
- for (let y = minY; y <= maxY; y += gap) {
4644
- ctx.moveTo(minX, y);
4645
- ctx.lineTo(maxX, y);
4646
- }
4647
- ctx.stroke();
4648
- ctx.restore();
4649
- };
4650
-
4651
- // src/hit-test/handle.ts
4652
- var RESIZE_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w"];
4653
- var RESIZE_HANDLE_SIZE_PX = 14;
4654
- var ROTATE_HANDLE_OFFSET_PX = 24;
4655
- var ROTATE_HANDLE_RADIUS_PX = 9;
4656
- var handleWorldPositions = (node) => {
4657
- const localCenters = {
4658
- nw: { x: 0, y: 0 },
4659
- n: { x: node.w / 2, y: 0 },
4660
- ne: { x: node.w, y: 0 },
4661
- e: { x: node.w, y: node.h / 2 },
4662
- se: { x: node.w, y: node.h },
4663
- s: { x: node.w / 2, y: node.h },
4664
- sw: { x: 0, y: node.h },
4665
- w: { x: 0, y: node.h / 2 }
4666
- };
4667
- if (node.angle === 0) {
4668
- const offsetX = node.x;
4669
- const offsetY = node.y;
4670
- return {
4671
- nw: { x: offsetX + localCenters.nw.x, y: offsetY + localCenters.nw.y },
4672
- n: { x: offsetX + localCenters.n.x, y: offsetY + localCenters.n.y },
4673
- ne: { x: offsetX + localCenters.ne.x, y: offsetY + localCenters.ne.y },
4674
- e: { x: offsetX + localCenters.e.x, y: offsetY + localCenters.e.y },
4675
- se: { x: offsetX + localCenters.se.x, y: offsetY + localCenters.se.y },
4676
- s: { x: offsetX + localCenters.s.x, y: offsetY + localCenters.s.y },
4677
- sw: { x: offsetX + localCenters.sw.x, y: offsetY + localCenters.sw.y },
4678
- w: { x: offsetX + localCenters.w.x, y: offsetY + localCenters.w.y }
4679
- };
4680
- }
4681
- const cx = node.x + node.w / 2;
4682
- const cy = node.y + node.h / 2;
4683
- const cos = Math.cos(node.angle);
4684
- const sin = Math.sin(node.angle);
4685
- const rotate = (p) => {
4686
- const dx = p.x - node.w / 2;
4687
- const dy = p.y - node.h / 2;
4688
- return { x: cx + dx * cos - dy * sin, y: cy + dx * sin + dy * cos };
4689
- };
4690
- return {
4691
- nw: rotate(localCenters.nw),
4692
- n: rotate(localCenters.n),
4693
- ne: rotate(localCenters.ne),
4694
- e: rotate(localCenters.e),
4695
- se: rotate(localCenters.se),
4696
- s: rotate(localCenters.s),
4697
- sw: rotate(localCenters.sw),
4698
- w: rotate(localCenters.w)
4478
+ return [];
4479
+ };
4480
+ var DARKENED_NO_STYLE = {};
4481
+ var darkenedStyleCache = /* @__PURE__ */ new WeakMap();
4482
+ var darkenedStyle = (style) => {
4483
+ if (!style) return DARKENED_NO_STYLE;
4484
+ const hit = darkenedStyleCache.get(style);
4485
+ if (hit) return hit;
4486
+ const fill = style.backgroundColor;
4487
+ const stroke = style.strokeColor;
4488
+ const next = {
4489
+ ...style,
4490
+ ...fill ? { backgroundColor: darkenHex(fill) } : {},
4491
+ ...stroke ? { strokeColor: darkenHex(stroke) } : {}
4699
4492
  };
4493
+ darkenedStyleCache.set(style, next);
4494
+ return next;
4700
4495
  };
4701
- var hitTestHandles = (node, worldPoint, cameraZ) => {
4702
- const halfWorld = RESIZE_HANDLE_SIZE_PX / 2 / cameraZ;
4703
- const positions = handleWorldPositions(node);
4704
- for (const h of RESIZE_HANDLES) {
4705
- const center = positions[h];
4706
- if (Math.abs(worldPoint.x - center.x) <= halfWorld && Math.abs(worldPoint.y - center.y) <= halfWorld) {
4707
- return h;
4708
- }
4496
+
4497
+ // src/render/rough/loader.ts
4498
+ var cachedCtor = null;
4499
+ var loadPromise2 = null;
4500
+ var readyCallbacks2 = /* @__PURE__ */ new Set();
4501
+ var getRoughCanvasCtor = () => {
4502
+ if (cachedCtor) return cachedCtor;
4503
+ if (!loadPromise2) {
4504
+ loadPromise2 = import('roughjs/bin/canvas').then((mod) => {
4505
+ cachedCtor = mod.RoughCanvas;
4506
+ for (const cb of readyCallbacks2) cb();
4507
+ readyCallbacks2.clear();
4508
+ return cachedCtor;
4509
+ }).catch((err) => {
4510
+ console.warn("[rough] failed to load roughjs:", err);
4511
+ return null;
4512
+ });
4709
4513
  }
4710
4514
  return null;
4711
4515
  };
4712
- var rotateHandleWorldPosition = (node, cameraZ) => {
4713
- const offsetWorld = ROTATE_HANDLE_OFFSET_PX / cameraZ;
4714
- const cx = node.x + node.w / 2;
4715
- const cy = node.y + node.h / 2;
4716
- const localX = 0;
4717
- const localY = -node.h / 2 - offsetWorld;
4718
- const cos = Math.cos(node.angle);
4719
- const sin = Math.sin(node.angle);
4720
- return {
4721
- x: cx + localX * cos - localY * sin,
4722
- y: cy + localX * sin + localY * cos
4723
- };
4724
- };
4725
- var hitTestRotateHandle = (node, worldPoint, cameraZ) => {
4726
- const center = rotateHandleWorldPosition(node, cameraZ);
4727
- const rWorld = ROTATE_HANDLE_RADIUS_PX / cameraZ;
4728
- const dx = worldPoint.x - center.x;
4729
- const dy = worldPoint.y - center.y;
4730
- return dx * dx + dy * dy <= rWorld * rWorld;
4516
+ var onRoughReady = (cb) => {
4517
+ if (cachedCtor) return;
4518
+ readyCallbacks2.add(cb);
4731
4519
  };
4732
4520
 
4733
- // src/render/overlay.ts
4734
- var DEFAULT_SELECTION_COLOR = "#3b82f6";
4735
- var SELECTION_OUTLINE_PX = 1.5;
4736
- var MARQUEE_FILL_ALPHA = 0.08;
4737
- var MARQUEE_STROKE_PX = 1;
4738
- var drawSelectionOutline = (ctx, node, scale, color) => {
4739
- if (node.angle === 0) {
4740
- ctx.save();
4741
- ctx.strokeStyle = color;
4742
- ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
4743
- ctx.beginPath();
4744
- ctx.rect(node.x, node.y, node.w, node.h);
4745
- ctx.stroke();
4746
- ctx.restore();
4747
- return;
4521
+ // src/render/rough/paths.ts
4522
+ var rectPath = (x, y, w, h) => {
4523
+ return `M${x} ${y} L${x + w} ${y} L${x + w} ${y + h} L${x} ${y + h} Z`;
4524
+ };
4525
+ var excalidrawRoundedRectPath = (x, y, w, h, radius) => {
4526
+ const r = Math.max(0, Math.min(radius, w / 2, h / 2));
4527
+ if (r === 0) return rectPath(x, y, w, h);
4528
+ const x2 = x + w;
4529
+ const y2 = y + h;
4530
+ return [
4531
+ `M${x + r} ${y}`,
4532
+ `L${x2 - r} ${y}`,
4533
+ `Q${x2} ${y}, ${x2} ${y + r}`,
4534
+ `L${x2} ${y2 - r}`,
4535
+ `Q${x2} ${y2}, ${x2 - r} ${y2}`,
4536
+ `L${x + r} ${y2}`,
4537
+ `Q${x} ${y2}, ${x} ${y2 - r}`,
4538
+ `L${x} ${y + r}`,
4539
+ `Q${x} ${y}, ${x + r} ${y}`,
4540
+ "Z"
4541
+ ].join(" ");
4542
+ };
4543
+ var diamondPath = (x, y, w, h, radius = 0) => {
4544
+ const cx = x + w / 2;
4545
+ const cy = y + h / 2;
4546
+ if (radius <= 0) {
4547
+ return `M${cx} ${y} L${x + w} ${cy} L${cx} ${y + h} L${x} ${cy} Z`;
4748
4548
  }
4749
- const cx = node.x + node.w / 2;
4750
- const cy = node.y + node.h / 2;
4751
- const cos = Math.cos(node.angle);
4752
- const sin = Math.sin(node.angle);
4753
- const corners = [
4754
- { x: -node.w / 2, y: -node.h / 2 },
4755
- { x: node.w / 2, y: -node.h / 2 },
4756
- { x: node.w / 2, y: node.h / 2 },
4757
- { x: -node.w / 2, y: node.h / 2 }
4758
- ].map((p) => ({ x: cx + p.x * cos - p.y * sin, y: cy + p.x * sin + p.y * cos }));
4759
- ctx.save();
4760
- ctx.strokeStyle = color;
4761
- ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
4762
- ctx.beginPath();
4763
- const first = corners[0];
4764
- ctx.moveTo(first.x, first.y);
4765
- for (let i = 1; i < corners.length; i++) {
4766
- const c = corners[i];
4767
- ctx.lineTo(c.x, c.y);
4549
+ const T = { x: cx, y };
4550
+ const R = { x: x + w, y: cy };
4551
+ const B = { x: cx, y: y + h };
4552
+ const L = { x, y: cy };
4553
+ const edgeLen = Math.hypot(R.x - T.x, R.y - T.y);
4554
+ const sMax = Math.max(0, edgeLen / 2 - 0.01);
4555
+ const s = Math.min(radius * Math.SQRT2, sMax);
4556
+ if (s <= 1e-4) {
4557
+ return `M${T.x} ${T.y} L${R.x} ${R.y} L${B.x} ${B.y} L${L.x} ${L.y} Z`;
4768
4558
  }
4769
- ctx.closePath();
4770
- ctx.stroke();
4771
- ctx.restore();
4559
+ const along = (a, b, d) => {
4560
+ const dx = b.x - a.x;
4561
+ const dy = b.y - a.y;
4562
+ const len = Math.hypot(dx, dy) || 1;
4563
+ const t = d / len;
4564
+ return { x: a.x + dx * t, y: a.y + dy * t };
4565
+ };
4566
+ const TR = along(T, R, s);
4567
+ const RT = along(R, T, s);
4568
+ const RB = along(R, B, s);
4569
+ const BR = along(B, R, s);
4570
+ const BL = along(B, L, s);
4571
+ const LB = along(L, B, s);
4572
+ const LT = along(L, T, s);
4573
+ const TL = along(T, L, s);
4574
+ return [
4575
+ `M${TR.x} ${TR.y}`,
4576
+ `L${RT.x} ${RT.y}`,
4577
+ `Q${R.x} ${R.y}, ${RB.x} ${RB.y}`,
4578
+ `L${BR.x} ${BR.y}`,
4579
+ `Q${B.x} ${B.y}, ${BL.x} ${BL.y}`,
4580
+ `L${LB.x} ${LB.y}`,
4581
+ `Q${L.x} ${L.y}, ${LT.x} ${LT.y}`,
4582
+ `L${TL.x} ${TL.y}`,
4583
+ `Q${T.x} ${T.y}, ${TR.x} ${TR.y}`,
4584
+ "Z"
4585
+ ].join(" ");
4772
4586
  };
4773
- var drawResizeHandles = (ctx, node, scale, color) => {
4774
- const halfPx = RESIZE_HANDLE_SIZE_PX / 2;
4775
- const halfWorld = halfPx / scale;
4776
- const positions = handleWorldPositions(node);
4777
- ctx.save();
4778
- ctx.fillStyle = "#fff";
4779
- ctx.strokeStyle = color;
4780
- ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
4781
- for (const key of Object.keys(positions)) {
4782
- const p = positions[key];
4783
- ctx.beginPath();
4784
- ctx.rect(p.x - halfWorld, p.y - halfWorld, halfWorld * 2, halfWorld * 2);
4785
- ctx.fill();
4786
- ctx.stroke();
4587
+ var ellipsePath = (x, y, w, h) => {
4588
+ const cx = x + w / 2;
4589
+ const rx = w / 2;
4590
+ const ry = h / 2;
4591
+ return [
4592
+ `M${cx} ${y}`,
4593
+ `A${rx} ${ry} 0 1 0 ${cx} ${y + h}`,
4594
+ `A${rx} ${ry} 0 1 0 ${cx} ${y}`,
4595
+ "Z"
4596
+ ].join(" ");
4597
+ };
4598
+ var thoughtCloudPath = (x, y, w, h, radius) => {
4599
+ const domeW = Math.min(w * 0.4, h * 1.2);
4600
+ const domeH = Math.min(h * 0.45, domeW);
4601
+ const domeAnchorX = w * 0.3;
4602
+ const domeX = Math.max(0, Math.min(w - domeW, domeAnchorX - domeW / 2));
4603
+ const cx = x + domeX + domeW / 2;
4604
+ const cy = y + domeH / 2;
4605
+ const rx = domeW / 2;
4606
+ const ry = domeH / 2;
4607
+ const bodyY = y + domeH * 0.55;
4608
+ const bodyH = y + h - bodyY;
4609
+ const r = Math.max(0, Math.min(radius, bodyH / 2, w / 2));
4610
+ const t = ry > 0 ? (bodyY - cy) / ry : 0;
4611
+ let xL = x + domeX;
4612
+ let xR = x + domeX + domeW;
4613
+ if (Math.abs(t) < 1) {
4614
+ const xOffset = rx * Math.sqrt(1 - t * t);
4615
+ xL = cx - xOffset;
4616
+ xR = cx + xOffset;
4617
+ }
4618
+ xL = Math.max(x + r, xL);
4619
+ xR = Math.min(x + w - r, xR);
4620
+ return [
4621
+ `M${x + r} ${bodyY}`,
4622
+ `L${xL} ${bodyY}`,
4623
+ `A${rx} ${ry} 0 1 1 ${xR} ${bodyY}`,
4624
+ `L${x + w - r} ${bodyY}`,
4625
+ `Q${x + w} ${bodyY}, ${x + w} ${bodyY + r}`,
4626
+ `L${x + w} ${y + h - r}`,
4627
+ `Q${x + w} ${y + h}, ${x + w - r} ${y + h}`,
4628
+ `L${x + r} ${y + h}`,
4629
+ `Q${x} ${y + h}, ${x} ${y + h - r}`,
4630
+ `L${x} ${bodyY + r}`,
4631
+ `Q${x} ${bodyY}, ${x + r} ${bodyY}`,
4632
+ "Z"
4633
+ ].join(" ");
4634
+ };
4635
+ var tagPath = (x, y, w, h, radius = 8) => {
4636
+ const notch = Math.min(h * 0.5, w * 0.3);
4637
+ const tipRadius = 6;
4638
+ const tipX = x;
4639
+ const tipY = y + h / 2;
4640
+ const bodyLeft = x + Math.max(0, Math.min(notch, w));
4641
+ const right = x + w;
4642
+ const bottom = y + h;
4643
+ const rBody = Math.min(radius, h / 2, (right - bodyLeft) / 2);
4644
+ const rJoin = Math.min(radius, h * 0.45, (bodyLeft - x) * 0.8);
4645
+ if (bodyLeft - x <= 1e-3) {
4646
+ return excalidrawRoundedRectPath(x, y, w, h, Math.min(radius, h / 2, w / 2));
4787
4647
  }
4788
- ctx.restore();
4648
+ const pTop = { x: bodyLeft, y: y + rJoin };
4649
+ const pBot = { x: bodyLeft, y: bottom - rJoin };
4650
+ const dirX = tipX - bodyLeft;
4651
+ const dirYTop = tipY - pTop.y;
4652
+ const dirYBot = tipY - pBot.y;
4653
+ const lenTop = Math.hypot(dirX, dirYTop) || 1;
4654
+ const lenBot = Math.hypot(dirX, dirYBot) || 1;
4655
+ const maxTipRound = Math.min(lenTop, lenBot) * 0.49;
4656
+ const t = Math.max(0, Math.min(tipRadius, maxTipRound));
4657
+ const tipEnter = { x: tipX - dirX / lenBot * t, y: tipY - dirYBot / lenBot * t };
4658
+ const tipExit = { x: tipX - dirX / lenTop * t, y: tipY - dirYTop / lenTop * t };
4659
+ const k = rJoin * 0.65;
4660
+ const topStart = { x: bodyLeft + rBody, y };
4661
+ const botEnd = { x: bodyLeft + rBody, y: bottom };
4662
+ const parts = [
4663
+ `M${topStart.x} ${topStart.y}`,
4664
+ `L${right - rBody} ${y}`,
4665
+ `Q${right} ${y}, ${right} ${y + rBody}`,
4666
+ `L${right} ${bottom - rBody}`,
4667
+ `Q${right} ${bottom}, ${right - rBody} ${bottom}`,
4668
+ `L${botEnd.x} ${botEnd.y}`,
4669
+ `C${botEnd.x - k} ${bottom}, ${pBot.x - dirX / lenBot * k} ${pBot.y - dirYBot / lenBot * k}, ${pBot.x} ${pBot.y}`,
4670
+ `L${t > 0 ? tipEnter.x : tipX} ${t > 0 ? tipEnter.y : tipY}`
4671
+ ];
4672
+ if (t > 0) parts.push(`Q${tipX} ${tipY}, ${tipExit.x} ${tipExit.y}`);
4673
+ parts.push(
4674
+ `L${pTop.x} ${pTop.y}`,
4675
+ `C${pTop.x - dirX / lenTop * k} ${pTop.y - dirYTop / lenTop * k}, ${topStart.x - k} ${y}, ${topStart.x} ${topStart.y}`,
4676
+ "Z"
4677
+ );
4678
+ return parts.join(" ");
4789
4679
  };
4790
- var drawRotateHandle = (ctx, node, scale, cameraZ, color) => {
4791
- const center = rotateHandleWorldPosition(node, cameraZ);
4792
- const radiusWorld = ROTATE_HANDLE_RADIUS_PX / scale;
4793
- const cx = node.x + node.w / 2;
4794
- const cy = node.y + node.h / 2;
4795
- const cos = Math.cos(node.angle);
4796
- const sin = Math.sin(node.angle);
4797
- const topMidLocalY = -node.h / 2;
4798
- const topMidWorld = {
4799
- x: cx + 0 * cos - topMidLocalY * sin,
4800
- y: cy + 0 * sin + topMidLocalY * cos
4801
- };
4802
- ctx.save();
4803
- ctx.strokeStyle = color;
4804
- ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
4805
- ctx.beginPath();
4806
- ctx.moveTo(topMidWorld.x, topMidWorld.y);
4807
- ctx.lineTo(center.x, center.y);
4808
- ctx.stroke();
4809
- ctx.fillStyle = "#fff";
4810
- ctx.beginPath();
4811
- ctx.arc(center.x, center.y, radiusWorld, 0, Math.PI * 2);
4812
- ctx.fill();
4813
- ctx.stroke();
4814
- ctx.restore();
4680
+
4681
+ // src/render/rough/tone-down.ts
4682
+ var TONE_BLEND2 = 0.2;
4683
+ var cache4 = /* @__PURE__ */ new Map();
4684
+ var deriveRoughStrokeColor = (stroke, fill, isDark) => {
4685
+ if (!isFullyTransparent(stroke)) return stroke;
4686
+ if (isFullyTransparent(fill)) return stroke;
4687
+ const key = `${fill}|${isDark ? "d" : "l"}`;
4688
+ const hit = cache4.get(key);
4689
+ if (hit) return hit;
4690
+ const next = mixHex(fill, isDark ? "#000000" : "#ffffff", TONE_BLEND2);
4691
+ cache4.set(key, next);
4692
+ return next;
4815
4693
  };
4816
- var drawEdgeMidpointHandle = (ctx, midpoint, scale, color) => {
4817
- const radiusPx = 5;
4818
- const radiusWorld = radiusPx / scale;
4819
- ctx.save();
4820
- ctx.fillStyle = "#fff";
4821
- ctx.strokeStyle = color;
4822
- ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
4823
- ctx.beginPath();
4824
- ctx.arc(midpoint.x, midpoint.y, radiusWorld, 0, Math.PI * 2);
4825
- ctx.fill();
4826
- ctx.stroke();
4827
- ctx.restore();
4694
+
4695
+ // src/render/rough/draw.ts
4696
+ var apparentDetail = (maxSide, zoom) => {
4697
+ const apparent = maxSide * Math.min(1, zoom);
4698
+ if (apparent >= 800) return { curveStepCount: 3, maxRandomnessOffset: 0.9 };
4699
+ if (apparent >= 400) return { curveStepCount: 4, maxRandomnessOffset: 1.1 };
4700
+ return { curveStepCount: 5, maxRandomnessOffset: 1.3 };
4828
4701
  };
4829
- var drawEdgeEndpointHandles = (ctx, source, target, scale, color) => {
4830
- const radiusPx = 5;
4831
- const radiusWorld = radiusPx / scale;
4832
- ctx.save();
4833
- ctx.fillStyle = "#fff";
4834
- ctx.strokeStyle = color;
4835
- ctx.lineWidth = SELECTION_OUTLINE_PX / scale;
4836
- for (const p of [source, target]) {
4837
- ctx.beginPath();
4838
- ctx.arc(p.x, p.y, radiusWorld, 0, Math.PI * 2);
4839
- ctx.fill();
4840
- ctx.stroke();
4702
+ var drawRoughShape = (ctx, node, scale, theme) => {
4703
+ const Ctor = getRoughCanvasCtor();
4704
+ if (!Ctor) return false;
4705
+ const rc = ensureRoughCanvas(ctx, Ctor);
4706
+ if (!rc) return false;
4707
+ const seed = node.id ? seedFromId(node.id) % 2147483646 + 1 : 1337;
4708
+ paintAtomicRough(
4709
+ rc,
4710
+ ctx,
4711
+ node.type,
4712
+ node.w,
4713
+ node.h,
4714
+ node.style,
4715
+ scale,
4716
+ theme,
4717
+ seed
4718
+ );
4719
+ return true;
4720
+ };
4721
+ var drawCompositeRough = (ctx, node, scale, theme) => {
4722
+ const Ctor = getRoughCanvasCtor();
4723
+ if (!Ctor) return false;
4724
+ const rc = ensureRoughCanvas(ctx, Ctor);
4725
+ if (!rc) return false;
4726
+ const subs = compositeLayout(node);
4727
+ const baseSeed = node.id ? seedFromId(node.id) % 2147483646 + 1 : 1337;
4728
+ for (let i = 0; i < subs.length; i++) {
4729
+ const s = subs[i];
4730
+ const subStyle = s.style ?? node.style;
4731
+ ctx.save();
4732
+ ctx.translate(s.x, s.y);
4733
+ ctx.translate(ROUGH_FILL_MISREGISTER_X, ROUGH_FILL_MISREGISTER_Y);
4734
+ drawAtomic(ctx, s.atomic, s.w, s.h, subStyle, scale, theme, { skipStroke: true });
4735
+ ctx.translate(-ROUGH_FILL_MISREGISTER_X, -ROUGH_FILL_MISREGISTER_Y);
4736
+ paintAtomicRough(
4737
+ rc,
4738
+ ctx,
4739
+ s.atomic,
4740
+ s.w,
4741
+ s.h,
4742
+ subStyle,
4743
+ scale,
4744
+ theme,
4745
+ (baseSeed + i * 7919) % 2147483646 + 1
4746
+ );
4747
+ ctx.restore();
4841
4748
  }
4842
- ctx.restore();
4749
+ return true;
4843
4750
  };
4844
- var drawMarquee = (ctx, rect, scale, color) => {
4751
+ var paintAtomicRough = (rc, ctx, type, w, h, style, scale, theme, seed) => {
4752
+ if (w <= 0 || h <= 0) return;
4753
+ const rawStroke = resolveColor(style, "strokeColor", "#1f2937", theme);
4754
+ const isDark = theme?.("mode") === "dark";
4755
+ const fill = resolveColor(style, "backgroundColor", DEFAULT_STYLE.backgroundColor, theme);
4756
+ const strokeColor = deriveRoughStrokeColor(rawStroke, fill, isDark);
4757
+ const rawStrokeWidth = resolveStrokeWidth(style, theme);
4758
+ if (rawStrokeWidth <= 0) return;
4759
+ const roughness = style?.roughness ?? 0;
4760
+ if (roughness <= 0) return;
4761
+ const isNoBorderIntent = isFullyTransparent(rawStroke);
4762
+ const effectiveStrokeStyle = isNoBorderIntent ? "solid" : style?.strokeStyle ?? "solid";
4763
+ const strokeWidth = isNoBorderIntent ? DEFAULT_STYLE.strokeWidth : rawStrokeWidth;
4764
+ const cornerRadius = (style?.roundness ?? DEFAULT_STYLE.roundness) * 4;
4765
+ const radius = Math.max(0, Math.min(cornerRadius, w / 2, h / 2));
4766
+ const dash = dashPatternFor(effectiveStrokeStyle, strokeWidth);
4767
+ const detail = apparentDetail(Math.max(w, h), scale);
4768
+ const cacheKey = [
4769
+ type,
4770
+ w.toFixed(1),
4771
+ h.toFixed(1),
4772
+ radius.toFixed(1),
4773
+ strokeColor,
4774
+ strokeWidth.toFixed(2),
4775
+ effectiveStrokeStyle,
4776
+ roughness.toFixed(2),
4777
+ seed,
4778
+ detail.curveStepCount,
4779
+ detail.maxRandomnessOffset.toFixed(2)
4780
+ ].join("|");
4781
+ const drawable = getOrBuildDrawable(cacheKey, () => {
4782
+ const pathData = buildPath(type, 0, 0, w, h, radius);
4783
+ return rc.generator.path(pathData, {
4784
+ ...ROUGH_DEFAULTS,
4785
+ stroke: strokeColor,
4786
+ strokeWidth,
4787
+ roughness,
4788
+ seed,
4789
+ strokeLineDash: dash.length > 0 ? dash : void 0,
4790
+ curveStepCount: detail.curveStepCount,
4791
+ maxRandomnessOffset: detail.maxRandomnessOffset
4792
+ });
4793
+ });
4845
4794
  ctx.save();
4846
- ctx.globalAlpha = MARQUEE_FILL_ALPHA;
4847
- ctx.fillStyle = color;
4848
- ctx.fillRect(rect.x, rect.y, rect.w, rect.h);
4849
- ctx.globalAlpha = 1;
4850
- ctx.strokeStyle = color;
4851
- ctx.lineWidth = MARQUEE_STROKE_PX / scale;
4852
- ctx.setLineDash([4 / scale, 3 / scale]);
4853
- ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
4795
+ ctx.lineJoin = "round";
4796
+ rc.draw(drawable);
4854
4797
  ctx.restore();
4855
4798
  };
4856
-
4857
- // src/render/paint-frame.ts
4858
- var FRAME_BORDER_PX = 1.5;
4859
- var FRAME_BORDER_COLOR_DEFAULT = "#94a3b8";
4860
- var FRAME_FILL_DEFAULT = "rgba(148, 163, 184, 0.06)";
4861
- var FRAME_LABEL_FONT_PX = 12;
4862
- var FRAME_LABEL_GAP_PX = 6;
4863
- var FRAME_LABEL_COLOR = "#64748b";
4864
- var paintFrameNode = (ctx, node, scale, theme) => {
4865
- if (node.w <= 0 || node.h <= 0) return;
4866
- const opacity = resolveOpacity(node.style, theme);
4867
- const needsScope = opacity !== 1;
4868
- if (needsScope) {
4869
- ctx.save();
4870
- ctx.globalAlpha = opacity;
4799
+ var ROUGH_CANVAS_KEY = "__roughCanvas";
4800
+ var ensureRoughCanvas = (ctx, Ctor) => {
4801
+ const ctxWithCache = ctx;
4802
+ if (ctxWithCache[ROUGH_CANVAS_KEY]) return ctxWithCache[ROUGH_CANVAS_KEY];
4803
+ const rc = new Ctor(ctx.canvas);
4804
+ ctxWithCache[ROUGH_CANVAS_KEY] = rc;
4805
+ return rc;
4806
+ };
4807
+ var buildPath = (type, x, y, w, h, radius) => {
4808
+ switch (type) {
4809
+ case "rect":
4810
+ return radius > 0 ? excalidrawRoundedRectPath(x, y, w, h, radius) : rectPath(x, y, w, h);
4811
+ case "ellipse":
4812
+ return ellipsePath(x, y, w, h);
4813
+ case "diamond":
4814
+ return diamondPath(x, y, w, h, radius);
4815
+ case "tag":
4816
+ return tagPath(x, y, w, h, radius);
4817
+ case "thought-cloud":
4818
+ return thoughtCloudPath(x, y, w, h, radius);
4871
4819
  }
4872
- const fill = node.style?.backgroundColor ?? (theme ? theme("frame.background") : void 0) ?? FRAME_FILL_DEFAULT;
4873
- ctx.fillStyle = fill;
4874
- ctx.fillRect(0, 0, node.w, node.h);
4875
- const stroke = resolveColor(node.style, "strokeColor", FRAME_BORDER_COLOR_DEFAULT, theme);
4876
- ctx.strokeStyle = stroke;
4877
- ctx.lineWidth = FRAME_BORDER_PX / scale;
4878
- ctx.setLineDash([]);
4879
- ctx.strokeRect(0, 0, node.w, node.h);
4880
- const labelPx = FRAME_LABEL_FONT_PX / scale;
4881
- const gapPx = FRAME_LABEL_GAP_PX / scale;
4882
- const label = node.content?.trim() || "Frame";
4883
- ctx.fillStyle = FRAME_LABEL_COLOR;
4884
- ctx.textBaseline = "bottom";
4885
- ctx.textAlign = "left";
4886
- ctx.font = `500 ${labelPx}px system-ui, -apple-system, sans-serif`;
4887
- ctx.fillText(label, 0, -gapPx);
4888
- if (needsScope) ctx.restore();
4889
4820
  };
4890
4821
 
4891
4822
  // src/render/shapes/content-bounds.ts