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