@canvas-harness/core 0.1.7 → 0.1.8

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