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