@xom11/whiteboard 0.11.0 → 0.24.1

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.
Files changed (112) hide show
  1. package/README.md +67 -0
  2. package/dist/{ExcalidrawWithMenus-EAVPOPJZ.mjs → ExcalidrawWithMenus-WENZRYYE.mjs} +2 -3
  3. package/dist/ExcalidrawWithMenus-WENZRYYE.mjs.map +1 -0
  4. package/dist/catalog.json +57 -0
  5. package/dist/chunk-4D5CSIJO.mjs +1167 -0
  6. package/dist/chunk-4D5CSIJO.mjs.map +1 -0
  7. package/dist/chunk-5UTGXHLJ.mjs +57 -0
  8. package/dist/chunk-5UTGXHLJ.mjs.map +1 -0
  9. package/dist/chunk-6V4SH4JJ.mjs +1801 -0
  10. package/dist/chunk-6V4SH4JJ.mjs.map +1 -0
  11. package/dist/chunk-AZIARTGX.mjs +23 -0
  12. package/dist/chunk-AZIARTGX.mjs.map +1 -0
  13. package/dist/chunk-BKSXPNPQ.mjs +348 -0
  14. package/dist/chunk-BKSXPNPQ.mjs.map +1 -0
  15. package/dist/{chunk-YVJP7NRG.mjs → chunk-CRAPWQKJ.mjs} +7 -9
  16. package/dist/chunk-CRAPWQKJ.mjs.map +1 -0
  17. package/dist/chunk-CSCF3YFZ.mjs +388 -0
  18. package/dist/chunk-CSCF3YFZ.mjs.map +1 -0
  19. package/dist/chunk-HNQLZIEP.mjs +78 -0
  20. package/dist/chunk-HNQLZIEP.mjs.map +1 -0
  21. package/dist/chunk-IBTRMWD6.mjs +28 -0
  22. package/dist/chunk-IBTRMWD6.mjs.map +1 -0
  23. package/dist/chunk-ICR4CVOE.mjs +57 -0
  24. package/dist/chunk-ICR4CVOE.mjs.map +1 -0
  25. package/dist/chunk-LVNCYP4J.mjs +57 -0
  26. package/dist/chunk-LVNCYP4J.mjs.map +1 -0
  27. package/dist/chunk-MFOGFFIL.mjs +95 -0
  28. package/dist/chunk-MFOGFFIL.mjs.map +1 -0
  29. package/dist/chunk-NVJ7K3DK.mjs +29 -0
  30. package/dist/chunk-NVJ7K3DK.mjs.map +1 -0
  31. package/dist/chunk-O4WIZFRQ.mjs +11 -0
  32. package/dist/chunk-O4WIZFRQ.mjs.map +1 -0
  33. package/dist/{chunk-C6SCVOMC.mjs → chunk-QGNU34T7.mjs} +5 -41
  34. package/dist/chunk-QGNU34T7.mjs.map +1 -0
  35. package/dist/chunk-R5FL6S7L.mjs +22 -0
  36. package/dist/chunk-R5FL6S7L.mjs.map +1 -0
  37. package/dist/{chunk-7P7SQFOW.mjs → chunk-SGFJLHHG.mjs} +3 -3
  38. package/dist/chunk-SGFJLHHG.mjs.map +1 -0
  39. package/dist/{chunk-PWIMZIB6.mjs → chunk-WWMQ2VHZ.mjs} +7 -8
  40. package/dist/chunk-WWMQ2VHZ.mjs.map +1 -0
  41. package/dist/chunk-YIPI3WUL.mjs +61 -0
  42. package/dist/chunk-YIPI3WUL.mjs.map +1 -0
  43. package/dist/chunk-ZBJBQKJ2.mjs +330 -0
  44. package/dist/chunk-ZBJBQKJ2.mjs.map +1 -0
  45. package/dist/geometry-2d.d.mts +3 -6
  46. package/dist/geometry-2d.d.ts +3 -6
  47. package/dist/geometry-2d.js +7007 -2633
  48. package/dist/geometry-2d.js.map +1 -1
  49. package/dist/geometry-2d.mjs +8 -4
  50. package/dist/geometry-3d.d.mts +4 -7
  51. package/dist/geometry-3d.d.ts +4 -7
  52. package/dist/geometry-3d.js +5446 -2507
  53. package/dist/geometry-3d.js.map +1 -1
  54. package/dist/geometry-3d.mjs +7 -4
  55. package/dist/graph-2d.d.mts +4 -7
  56. package/dist/graph-2d.d.ts +4 -7
  57. package/dist/graph-2d.js +5300 -1677
  58. package/dist/graph-2d.js.map +1 -1
  59. package/dist/graph-2d.mjs +10 -3
  60. package/dist/host-DOAYVL35.mjs +3199 -0
  61. package/dist/host-DOAYVL35.mjs.map +1 -0
  62. package/dist/host-GKNQBBUE.mjs +1142 -0
  63. package/dist/host-GKNQBBUE.mjs.map +1 -0
  64. package/dist/{host-Z3TEJKZA.mjs → host-QS2EOTRJ.mjs} +4 -4
  65. package/dist/{host-Z3TEJKZA.mjs.map → host-QS2EOTRJ.mjs.map} +1 -1
  66. package/dist/host-TLIXN4CF.mjs +2374 -0
  67. package/dist/host-TLIXN4CF.mjs.map +1 -0
  68. package/dist/index.css +4 -1
  69. package/dist/index.css.map +1 -1
  70. package/dist/index.d.mts +659 -19
  71. package/dist/index.d.ts +659 -19
  72. package/dist/index.js +13736 -9491
  73. package/dist/index.js.map +1 -1
  74. package/dist/index.mjs +1465 -342
  75. package/dist/index.mjs.map +1 -1
  76. package/dist/latex.d.mts +3 -4
  77. package/dist/latex.d.ts +3 -4
  78. package/dist/latex.js +33 -18
  79. package/dist/latex.js.map +1 -1
  80. package/dist/latex.mjs +2 -3
  81. package/dist/render-SA4JTOW3.mjs +8 -0
  82. package/dist/render-SA4JTOW3.mjs.map +1 -0
  83. package/dist/serialize-3NZS6A6Q.mjs +6 -0
  84. package/dist/serialize-3NZS6A6Q.mjs.map +1 -0
  85. package/dist/{types-CinstD7T.d.mts → types-rA4slL08.d.mts} +69 -4
  86. package/dist/{types-CinstD7T.d.ts → types-rA4slL08.d.ts} +69 -4
  87. package/package.json +34 -6
  88. package/dist/ExcalidrawWithMenus-EAVPOPJZ.mjs.map +0 -1
  89. package/dist/chunk-74VEEZBV.mjs +0 -619
  90. package/dist/chunk-74VEEZBV.mjs.map +0 -1
  91. package/dist/chunk-7P7SQFOW.mjs.map +0 -1
  92. package/dist/chunk-BJTO5JO5.mjs +0 -11
  93. package/dist/chunk-BJTO5JO5.mjs.map +0 -1
  94. package/dist/chunk-C6SCVOMC.mjs.map +0 -1
  95. package/dist/chunk-D257NCQW.mjs +0 -58
  96. package/dist/chunk-D257NCQW.mjs.map +0 -1
  97. package/dist/chunk-G7FR3AIV.mjs +0 -193
  98. package/dist/chunk-G7FR3AIV.mjs.map +0 -1
  99. package/dist/chunk-HTBLO5JO.mjs +0 -41
  100. package/dist/chunk-HTBLO5JO.mjs.map +0 -1
  101. package/dist/chunk-PWIMZIB6.mjs.map +0 -1
  102. package/dist/chunk-SBDMF4NQ.mjs +0 -212
  103. package/dist/chunk-SBDMF4NQ.mjs.map +0 -1
  104. package/dist/chunk-WQOABS6N.mjs +0 -197
  105. package/dist/chunk-WQOABS6N.mjs.map +0 -1
  106. package/dist/chunk-YVJP7NRG.mjs.map +0 -1
  107. package/dist/host-N6ACNJKI.mjs +0 -3226
  108. package/dist/host-N6ACNJKI.mjs.map +0 -1
  109. package/dist/host-NKGV6RF2.mjs +0 -1134
  110. package/dist/host-NKGV6RF2.mjs.map +0 -1
  111. package/dist/host-XVK7UCRE.mjs +0 -2908
  112. package/dist/host-XVK7UCRE.mjs.map +0 -1
package/dist/index.mjs CHANGED
@@ -1,35 +1,72 @@
1
1
  "use client";
2
2
  import './index.css';
3
- import { geometryStamp } from './chunk-YVJP7NRG.mjs';
4
- export { geometryStamp } from './chunk-YVJP7NRG.mjs';
5
- import { geometry3dStamp } from './chunk-PWIMZIB6.mjs';
6
- export { geometry3dStamp } from './chunk-PWIMZIB6.mjs';
7
- import { latexStamp } from './chunk-7P7SQFOW.mjs';
8
- export { latexStamp } from './chunk-7P7SQFOW.mjs';
9
- import { graph2dStamp } from './chunk-D257NCQW.mjs';
10
- export { graph2dStamp } from './chunk-D257NCQW.mjs';
11
- export { isGraph2DCustomData } from './chunk-74VEEZBV.mjs';
12
- export { isGeometryCustomData } from './chunk-G7FR3AIV.mjs';
13
- export { isLatexCustomData } from './chunk-X5R72SSJ.mjs';
14
- export { isGeometry3DCustomData } from './chunk-WQOABS6N.mjs';
15
- import './chunk-HTBLO5JO.mjs';
16
- import './chunk-C6SCVOMC.mjs';
17
- import './chunk-BJTO5JO5.mjs';
18
- import { lazy, useState, useRef, useMemo, useCallback, useEffect, Suspense } from 'react';
3
+ import { geometryStamp } from './chunk-CRAPWQKJ.mjs';
4
+ export { geometryStamp } from './chunk-CRAPWQKJ.mjs';
5
+ import { geometry3dStamp } from './chunk-WWMQ2VHZ.mjs';
6
+ export { geometry3dStamp } from './chunk-WWMQ2VHZ.mjs';
7
+ import { latexStamp } from './chunk-SGFJLHHG.mjs';
8
+ export { latexStamp } from './chunk-SGFJLHHG.mjs';
9
+ import { graph2dStamp } from './chunk-YIPI3WUL.mjs';
10
+ export { graph2dStamp } from './chunk-YIPI3WUL.mjs';
11
+ import './chunk-LVNCYP4J.mjs';
12
+ import './chunk-O4WIZFRQ.mjs';
13
+ import './chunk-AZIARTGX.mjs';
14
+ import './chunk-IBTRMWD6.mjs';
15
+ import './chunk-MFOGFFIL.mjs';
16
+ import './chunk-BKSXPNPQ.mjs';
17
+ import './chunk-X5R72SSJ.mjs';
18
+ import './chunk-CSCF3YFZ.mjs';
19
+ import './chunk-R5FL6S7L.mjs';
20
+ import './chunk-ICR4CVOE.mjs';
21
+ import { createEmptyState } from './chunk-6V4SH4JJ.mjs';
22
+ import './chunk-ZBJBQKJ2.mjs';
23
+ import './chunk-QGNU34T7.mjs';
24
+ import './chunk-5UTGXHLJ.mjs';
25
+ import { lazy, useRef, useCallback, useEffect, Suspense, useState, useMemo, useLayoutEffect } from 'react';
19
26
  import { createPortal } from 'react-dom';
20
27
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
21
28
  import '@excalidraw/excalidraw/index.css';
29
+ import { z } from 'zod';
30
+ import Anthropic from '@anthropic-ai/sdk';
31
+ import { zodToJsonSchema } from 'zod-to-json-schema';
22
32
 
23
- // src/serialize.ts
24
- function pickSyncableAppState(s) {
25
- return {
26
- viewBackgroundColor: s.viewBackgroundColor,
27
- zoom: s.zoom,
28
- scrollX: s.scrollX,
29
- scrollY: s.scrollY,
30
- gridSize: s.gridSize ?? null,
31
- theme: s.theme
32
- };
33
+ // src/stamps/shared/catalog.ts
34
+ var STAMP_CATALOG = Object.freeze([
35
+ {
36
+ id: "geometry",
37
+ title: "H\xECnh h\u1ECDc 2D (JSXGraph)",
38
+ version: 1,
39
+ experimental: false,
40
+ runtimeDeps: ["jsxgraph"],
41
+ bundleSize: { js: 60.33, css: 0 }
42
+ },
43
+ {
44
+ id: "latex",
45
+ title: "C\xF4ng th\u1EE9c LaTeX (KaTeX)",
46
+ version: 1,
47
+ experimental: false,
48
+ runtimeDeps: ["katex"],
49
+ bundleSize: { js: 8.93, css: 0 }
50
+ },
51
+ {
52
+ id: "geometry3d",
53
+ title: "H\xECnh h\u1ECDc 3D (JSXGraph view3d)",
54
+ version: 2,
55
+ experimental: true,
56
+ runtimeDeps: ["jsxgraph"],
57
+ bundleSize: { js: 50.73, css: 0 }
58
+ },
59
+ {
60
+ id: "graph2d",
61
+ title: "\u0110\u1ED3 th\u1ECB h\xE0m s\u1ED1 2D (JSXGraph)",
62
+ version: 2,
63
+ experimental: true,
64
+ runtimeDeps: ["jsxgraph"],
65
+ bundleSize: { js: 42.91, css: 0 }
66
+ }
67
+ ]);
68
+ function findCatalogEntry(id) {
69
+ return STAMP_CATALOG.find((entry) => entry.id === id) ?? null;
33
70
  }
34
71
 
35
72
  // src/stamps/shared/registry.ts
@@ -922,17 +959,14 @@ function ThumbnailItem({ pageNum, thumb, selected, onToggle }) {
922
959
  alignItems: "center",
923
960
  justifyContent: "center"
924
961
  },
925
- children: thumb ? (
926
- // eslint-disable-next-line @next/next/no-img-element
927
- /* @__PURE__ */ jsx(
928
- "img",
929
- {
930
- src: thumb.dataURL,
931
- alt: "",
932
- style: { width: "100%", height: "100%", display: "block", objectFit: "contain" },
933
- draggable: false
934
- }
935
- )
962
+ children: thumb ? /* @__PURE__ */ jsx(
963
+ "img",
964
+ {
965
+ src: thumb.dataURL,
966
+ alt: "",
967
+ style: { width: "100%", height: "100%", display: "block", objectFit: "contain" },
968
+ draggable: false
969
+ }
936
970
  ) : /* @__PURE__ */ jsx("div", { style: { fontSize: 11, opacity: 0.5 }, children: "\u2026" })
937
971
  }
938
972
  ),
@@ -980,6 +1014,173 @@ function ThumbnailItem({ pageNum, thumb, selected, onToggle }) {
980
1014
  }
981
1015
  );
982
1016
  }
1017
+ var DOUBLE_CLICK_MS = 400;
1018
+ function useStampDoubleClick({ enabled, stamps, onOpen }) {
1019
+ const lastClickRef = useRef({
1020
+ time: 0,
1021
+ elementId: null
1022
+ });
1023
+ return useCallback(
1024
+ (_activeTool, pointerDownState) => {
1025
+ if (!enabled) return;
1026
+ const hitElement = pointerDownState?.hit?.element;
1027
+ if (!hitElement || hitElement.type !== "image") return;
1028
+ const stamp = findStampForCustomData(hitElement.customData, stamps);
1029
+ if (!stamp) return;
1030
+ const now = Date.now();
1031
+ const isDouble = lastClickRef.current.elementId === hitElement.id && now - lastClickRef.current.time < DOUBLE_CLICK_MS;
1032
+ lastClickRef.current = { time: now, elementId: hitElement.id };
1033
+ if (!isDouble) return;
1034
+ onOpen(stamp.kind, {
1035
+ id: hitElement.id,
1036
+ customData: hitElement.customData
1037
+ });
1038
+ },
1039
+ [enabled, stamps, onOpen]
1040
+ );
1041
+ }
1042
+ var ALLOWED_KEYS = /* @__PURE__ */ new Set([
1043
+ "Tab",
1044
+ "ArrowUp",
1045
+ "ArrowDown",
1046
+ "ArrowLeft",
1047
+ "ArrowRight",
1048
+ "Shift",
1049
+ "Control",
1050
+ "Alt",
1051
+ "Meta",
1052
+ "CapsLock",
1053
+ "Home",
1054
+ "End",
1055
+ "PageUp",
1056
+ "PageDown"
1057
+ ]);
1058
+ function isEditable(el) {
1059
+ if (!(el instanceof HTMLElement)) return false;
1060
+ if (el.isContentEditable) return true;
1061
+ const tag = el.tagName;
1062
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
1063
+ }
1064
+ function useStampShortcutBlocker({ activeStamp, stamps }) {
1065
+ const shortcutKeys = useMemo(
1066
+ () => new Set(stamps.map((s) => s.shortcutKey.toLowerCase())),
1067
+ [stamps]
1068
+ );
1069
+ useEffect(() => {
1070
+ if (!activeStamp) return;
1071
+ const blocker = (e) => {
1072
+ if (isEditable(e.target)) return;
1073
+ if (e.ctrlKey || e.metaKey) return;
1074
+ if (ALLOWED_KEYS.has(e.key)) return;
1075
+ if (e.key === "Escape") return;
1076
+ if (shortcutKeys.has(e.key.toLowerCase())) return;
1077
+ e.preventDefault();
1078
+ e.stopPropagation();
1079
+ };
1080
+ window.addEventListener("keydown", blocker, { capture: true });
1081
+ return () => window.removeEventListener("keydown", blocker, { capture: true });
1082
+ }, [activeStamp, shortcutKeys]);
1083
+ }
1084
+ function useStampClickOutside({ activeStamp, hostRef, onClose }) {
1085
+ useEffect(() => {
1086
+ if (!activeStamp) return;
1087
+ let lastFire = 0;
1088
+ const handler = (e) => {
1089
+ const target = e.target;
1090
+ if (!target) return;
1091
+ if (target.closest('[data-stamp-area="true"]')) return;
1092
+ const now = Date.now();
1093
+ if (now - lastFire < 50) return;
1094
+ lastFire = now;
1095
+ hostRef.current?.tryInsert();
1096
+ onClose();
1097
+ };
1098
+ window.addEventListener("pointerdown", handler, { capture: true });
1099
+ window.addEventListener("mousedown", handler, { capture: true });
1100
+ return () => {
1101
+ window.removeEventListener("pointerdown", handler, { capture: true });
1102
+ window.removeEventListener("mousedown", handler, { capture: true });
1103
+ };
1104
+ }, [activeStamp, hostRef, onClose]);
1105
+ }
1106
+ function useExcalidrawApi(opts = {}) {
1107
+ const { onApi } = opts;
1108
+ const [api, setApi] = useState(null);
1109
+ const apiRef = useRef(null);
1110
+ const [isDark, setIsDark] = useState(false);
1111
+ const isDarkRef = useRef(false);
1112
+ const onApiRef = useRef(onApi);
1113
+ onApiRef.current = onApi;
1114
+ const setApiFromExcalidraw = useCallback((a) => {
1115
+ if (apiRef.current === a) return;
1116
+ apiRef.current = a;
1117
+ queueMicrotask(() => {
1118
+ setApi(a);
1119
+ onApiRef.current?.(a);
1120
+ });
1121
+ }, []);
1122
+ const syncThemeFromAppState = useCallback(
1123
+ (appState) => {
1124
+ const next = appState?.theme === "dark";
1125
+ if (isDarkRef.current !== next) {
1126
+ isDarkRef.current = next;
1127
+ queueMicrotask(() => setIsDark(next));
1128
+ }
1129
+ },
1130
+ []
1131
+ );
1132
+ return { api, apiRef, isDark, isDarkRef, setApiFromExcalidraw, syncThemeFromAppState };
1133
+ }
1134
+ function useActiveStamp(opts) {
1135
+ const { readOnly, stamps } = opts;
1136
+ const [activeStamp, setActiveStamp] = useState(null);
1137
+ const [editingElement, setEditingElement] = useState(null);
1138
+ const stampByKind = useMemo(() => {
1139
+ const m = /* @__PURE__ */ new Map();
1140
+ for (const s of stamps) m.set(s.kind, s);
1141
+ return m;
1142
+ }, [stamps]);
1143
+ const activeStampDef = activeStamp ? stampByKind.get(activeStamp) ?? null : null;
1144
+ const HostComponent = activeStampDef?.Host ?? null;
1145
+ const openStamp = useCallback(
1146
+ (kind, element = null) => {
1147
+ if (readOnly) return;
1148
+ if (!stampByKind.has(kind)) return;
1149
+ setEditingElement(element);
1150
+ setActiveStamp(kind);
1151
+ },
1152
+ [readOnly, stampByKind]
1153
+ );
1154
+ const closeStamp = useCallback(() => {
1155
+ setActiveStamp(null);
1156
+ setEditingElement(null);
1157
+ }, []);
1158
+ const toggleStampByKind = useCallback(
1159
+ (kind) => {
1160
+ setActiveStamp((cur) => {
1161
+ if (cur === kind) {
1162
+ setEditingElement(null);
1163
+ return null;
1164
+ }
1165
+ if (readOnly) return cur;
1166
+ if (!stampByKind.has(kind)) return cur;
1167
+ setEditingElement(null);
1168
+ return kind;
1169
+ });
1170
+ },
1171
+ [readOnly, stampByKind]
1172
+ );
1173
+ return {
1174
+ activeStamp,
1175
+ editingElement,
1176
+ stampByKind,
1177
+ activeStampDef,
1178
+ HostComponent,
1179
+ openStamp,
1180
+ closeStamp,
1181
+ toggleStampByKind
1182
+ };
1183
+ }
983
1184
 
984
1185
  // src/pdf/insertPdfPages.ts
985
1186
  var PAGE_GAP = 24;
@@ -1089,95 +1290,103 @@ async function insertPdfPages(api, source, options = {}) {
1089
1290
  });
1090
1291
  return { insertedElementIds, pages: rendered };
1091
1292
  }
1092
- var DOUBLE_CLICK_MS = 400;
1093
- function useStampDoubleClick({ enabled, stamps, onOpen }) {
1094
- const lastClickRef = useRef({
1095
- time: 0,
1096
- elementId: null
1097
- });
1098
- return useCallback(
1099
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1100
- (_activeTool, pointerDownState) => {
1101
- if (!enabled) return;
1102
- const hitElement = pointerDownState?.hit?.element;
1103
- if (!hitElement || hitElement.type !== "image") return;
1104
- const stamp = findStampForCustomData(hitElement.customData, stamps);
1105
- if (!stamp) return;
1106
- const now = Date.now();
1107
- const isDouble = lastClickRef.current.elementId === hitElement.id && now - lastClickRef.current.time < DOUBLE_CLICK_MS;
1108
- lastClickRef.current = { time: now, elementId: hitElement.id };
1109
- if (!isDouble) return;
1110
- onOpen(stamp.kind, {
1111
- id: hitElement.id,
1112
- customData: hitElement.customData
1113
- });
1293
+
1294
+ // src/hooks/usePdfImporter.ts
1295
+ function usePdfImporter(opts) {
1296
+ const { readOnly, api } = opts;
1297
+ const [pdfPending, setPdfPending] = useState(null);
1298
+ const [pdfBusy, setPdfBusy] = useState(false);
1299
+ const handlePdfPick = useCallback(
1300
+ async (file) => {
1301
+ if (readOnly || pdfBusy) return;
1302
+ setPdfBusy(true);
1303
+ try {
1304
+ const doc = await loadPdfDocument(file);
1305
+ setPdfPending({ doc, fileName: file.name, totalPages: doc.numPages });
1306
+ } catch (err) {
1307
+ console.warn("[whiteboard] \u0110\u1ECDc PDF th\u1EA5t b\u1EA1i:", err);
1308
+ window.alert("Kh\xF4ng \u0111\u1ECDc \u0111\u01B0\u1EE3c PDF. File c\xF3 th\u1EC3 \u0111\xE3 h\u1ECFng ho\u1EB7c b\u1ECB m\u1EADt kh\u1EA9u b\u1EA3o v\u1EC7.");
1309
+ } finally {
1310
+ setPdfBusy(false);
1311
+ }
1114
1312
  },
1115
- [enabled, stamps, onOpen]
1313
+ [readOnly, pdfBusy]
1116
1314
  );
1117
- }
1118
- var ALLOWED_KEYS = /* @__PURE__ */ new Set([
1119
- "Tab",
1120
- "ArrowUp",
1121
- "ArrowDown",
1122
- "ArrowLeft",
1123
- "ArrowRight",
1124
- "Shift",
1125
- "Control",
1126
- "Alt",
1127
- "Meta",
1128
- "CapsLock",
1129
- "Home",
1130
- "End",
1131
- "PageUp",
1132
- "PageDown"
1133
- ]);
1134
- function isEditable(el) {
1135
- if (!(el instanceof HTMLElement)) return false;
1136
- if (el.isContentEditable) return true;
1137
- const tag = el.tagName;
1138
- return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
1139
- }
1140
- function useStampShortcutBlocker({ activeStamp, stamps }) {
1141
- const shortcutKeys = useMemo(
1142
- () => new Set(stamps.map((s) => s.shortcutKey.toLowerCase())),
1143
- [stamps]
1315
+ const handlePdfPickRef = useRef(handlePdfPick);
1316
+ useLayoutEffect(() => {
1317
+ handlePdfPickRef.current = handlePdfPick;
1318
+ });
1319
+ const handlePdfConfirm = useCallback(
1320
+ async (pages) => {
1321
+ if (!pdfPending || !api) return;
1322
+ const { doc } = pdfPending;
1323
+ setPdfPending(null);
1324
+ setPdfBusy(true);
1325
+ const scale = 2;
1326
+ try {
1327
+ const rendered = await rasterizePdf(doc, { pages, scale });
1328
+ await closePdfDocument(doc);
1329
+ insertRasterizedPagesIntoScene(api, rendered, { scale });
1330
+ } catch (err) {
1331
+ console.warn("[whiteboard] Ch\xE8n PDF th\u1EA5t b\u1EA1i:", err);
1332
+ window.alert("Ch\xE8n PDF th\u1EA5t b\u1EA1i. Xem console \u0111\u1EC3 bi\u1EBFt chi ti\u1EBFt.");
1333
+ } finally {
1334
+ setPdfBusy(false);
1335
+ }
1336
+ },
1337
+ [pdfPending, api]
1144
1338
  );
1339
+ const handlePdfCancel = useCallback(() => {
1340
+ if (pdfPending) {
1341
+ void closePdfDocument(pdfPending.doc);
1342
+ }
1343
+ setPdfPending(null);
1344
+ }, [pdfPending]);
1145
1345
  useEffect(() => {
1146
- if (!activeStamp) return;
1147
- const blocker = (e) => {
1148
- if (isEditable(e.target)) return;
1149
- if (e.ctrlKey || e.metaKey) return;
1150
- if (ALLOWED_KEYS.has(e.key)) return;
1151
- if (e.key === "Escape") return;
1152
- if (shortcutKeys.has(e.key.toLowerCase())) return;
1153
- e.preventDefault();
1154
- e.stopPropagation();
1155
- };
1156
- window.addEventListener("keydown", blocker, { capture: true });
1157
- return () => window.removeEventListener("keydown", blocker, { capture: true });
1158
- }, [activeStamp, shortcutKeys]);
1159
- }
1160
- function useStampClickOutside({ activeStamp, hostRef, onClose }) {
1161
- useEffect(() => {
1162
- if (!activeStamp) return;
1163
- let lastFire = 0;
1164
- const handler = (e) => {
1165
- const target = e.target;
1166
- if (!target) return;
1167
- if (target.closest('[data-stamp-area="true"]')) return;
1168
- const now = Date.now();
1169
- if (now - lastFire < 50) return;
1170
- lastFire = now;
1171
- hostRef.current?.tryInsert();
1172
- onClose();
1346
+ if (readOnly) return;
1347
+ const root = document.querySelector(".excalidraw");
1348
+ if (!root) return;
1349
+ const onDragOver = (e) => {
1350
+ const items = e.dataTransfer?.items;
1351
+ if (!items) return;
1352
+ for (let i = 0; i < items.length; i++) {
1353
+ if (items[i].kind === "file" && items[i].type === "application/pdf") {
1354
+ e.preventDefault();
1355
+ e.stopPropagation();
1356
+ if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
1357
+ return;
1358
+ }
1359
+ }
1173
1360
  };
1174
- window.addEventListener("pointerdown", handler, { capture: true });
1175
- window.addEventListener("mousedown", handler, { capture: true });
1361
+ const onDrop = (e) => {
1362
+ const files = e.dataTransfer?.files;
1363
+ if (!files || files.length === 0) return;
1364
+ const pdf = Array.from(files).find((f) => f.type === "application/pdf");
1365
+ if (!pdf) return;
1366
+ e.preventDefault();
1367
+ e.stopPropagation();
1368
+ void handlePdfPickRef.current(pdf);
1369
+ };
1370
+ root.addEventListener("dragover", onDragOver, { capture: true });
1371
+ root.addEventListener("drop", onDrop, { capture: true });
1176
1372
  return () => {
1177
- window.removeEventListener("pointerdown", handler, { capture: true });
1178
- window.removeEventListener("mousedown", handler, { capture: true });
1373
+ root.removeEventListener("dragover", onDragOver, { capture: true });
1374
+ root.removeEventListener("drop", onDrop, { capture: true });
1179
1375
  };
1180
- }, [activeStamp, hostRef, onClose]);
1376
+ }, [readOnly, api]);
1377
+ return { pdfPending, pdfBusy, handlePdfPick, handlePdfConfirm, handlePdfCancel };
1378
+ }
1379
+
1380
+ // src/serialize.ts
1381
+ function pickSyncableAppState(s) {
1382
+ return {
1383
+ viewBackgroundColor: s.viewBackgroundColor,
1384
+ zoom: s.zoom,
1385
+ scrollX: s.scrollX,
1386
+ scrollY: s.scrollY,
1387
+ gridSize: s.gridSize ?? null,
1388
+ theme: s.theme
1389
+ };
1181
1390
  }
1182
1391
 
1183
1392
  // src/stamps/shared/restoreStampFiles.ts
@@ -1187,6 +1396,7 @@ function svgToDataURL(svg) {
1187
1396
  }
1188
1397
  async function buildFileForStamp(fileId, customData, stamp) {
1189
1398
  try {
1399
+ if (!stamp.matchesCustomData(customData)) return null;
1190
1400
  const svg = await stamp.renderSvgFromCustomData(customData);
1191
1401
  return { id: fileId, dataURL: svgToDataURL(svg), mimeType: "image/svg+xml", created: Date.now() };
1192
1402
  } catch (err) {
@@ -1507,26 +1717,25 @@ async function pruneFiles(storageKey, keepIds) {
1507
1717
  console.warn("[whiteboard] pruneFiles failed:", err);
1508
1718
  }
1509
1719
  }
1510
- var Excalidraw = lazy(
1511
- () => import('./ExcalidrawWithMenus-EAVPOPJZ.mjs').then((m) => ({ default: m.ExcalidrawWithMenus }))
1512
- );
1513
- var ExcalidrawLoadingFallback = () => /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center text-sm text-gray-500", children: "\u0110ang t\u1EA3i b\u1EA3ng\u2026" });
1720
+
1721
+ // src/hooks/useScenePersist.ts
1514
1722
  var SYNC_THROTTLE_MS = 200;
1515
- function Whiteboard({
1516
- storageKey = "default",
1517
- readOnly = false,
1518
- onSceneChange,
1519
- onFilesChange,
1520
- onApi,
1521
- langCode = "vi-VN",
1522
- stamps = DEFAULT_STAMPS,
1523
- initialScene,
1524
- initialFiles
1525
- }) {
1526
- const [api, setApi] = useState(null);
1527
- const apiRef = useRef(null);
1528
- const [isDarkTheme, setIsDarkTheme] = useState(false);
1529
- const isDarkThemeRef = useRef(false);
1723
+ var FILE_THROTTLE_MS = 1e3;
1724
+ var PRUNE_THROTTLE_MS = 2e3;
1725
+ var RESTORE_PASS_DELAY_MS = 400;
1726
+ function useScenePersist(opts) {
1727
+ const {
1728
+ storageKey,
1729
+ initialScene,
1730
+ initialFiles,
1731
+ readOnly,
1732
+ onSceneChange,
1733
+ onFilesChange,
1734
+ api,
1735
+ apiRef,
1736
+ stamps
1737
+ } = opts;
1738
+ const persistEnabled = typeof storageKey === "string" && storageKey.length > 0;
1530
1739
  const knownFileIdsRef = useRef(/* @__PURE__ */ new Set());
1531
1740
  const lastSceneHashRef = useRef("");
1532
1741
  const sceneThrottleRef = useRef(null);
@@ -1537,7 +1746,6 @@ function Whiteboard({
1537
1746
  const hashElementsVersionRef = useRef(null);
1538
1747
  const stampsRef = useRef(stamps);
1539
1748
  stampsRef.current = stamps;
1540
- const persistEnabled = typeof storageKey === "string" && storageKey.length > 0;
1541
1749
  const persistKeyRef = useRef(storageKey);
1542
1750
  persistKeyRef.current = storageKey;
1543
1751
  const onSceneChangeRef = useRef(onSceneChange);
@@ -1554,117 +1762,6 @@ function Whiteboard({
1554
1762
  elements: persistedInitial.elements,
1555
1763
  appState: persistedInitial.appState
1556
1764
  } : null;
1557
- const [activeStamp, setActiveStamp] = useState(null);
1558
- const activeStampRef = useRef(activeStamp);
1559
- activeStampRef.current = activeStamp;
1560
- const [editingElement, setEditingElement] = useState(null);
1561
- const hostRef = useRef(null);
1562
- const [pdfPending, setPdfPending] = useState(null);
1563
- const [pdfBusy, setPdfBusy] = useState(false);
1564
- const handledCropIdRef = useRef(null);
1565
- const prevExcalidrawToolRef = useRef("selection");
1566
- const stampByKind = useMemo(() => {
1567
- const m = /* @__PURE__ */ new Map();
1568
- for (const s of stamps) m.set(s.kind, s);
1569
- return m;
1570
- }, [stamps]);
1571
- const activeStampDef = activeStamp ? stampByKind.get(activeStamp) ?? null : null;
1572
- const HostComponent = activeStampDef?.Host ?? null;
1573
- const openStamp = useCallback(
1574
- (kind, element = null) => {
1575
- if (readOnly) return;
1576
- if (!stampByKind.has(kind)) return;
1577
- setEditingElement(element);
1578
- setActiveStamp(kind);
1579
- },
1580
- [readOnly, stampByKind]
1581
- );
1582
- const closeStamp = useCallback(() => {
1583
- setActiveStamp(null);
1584
- setEditingElement(null);
1585
- }, []);
1586
- const toggleStampByKind = useCallback(
1587
- (kind) => {
1588
- if (activeStamp === kind) closeStamp();
1589
- else openStamp(kind);
1590
- },
1591
- [activeStamp, openStamp, closeStamp]
1592
- );
1593
- const handleChange = useCallback(
1594
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1595
- (elements, appState, files) => {
1596
- const nextDark = appState?.theme === "dark";
1597
- if (isDarkThemeRef.current !== nextDark) {
1598
- isDarkThemeRef.current = nextDark;
1599
- queueMicrotask(() => setIsDarkTheme(nextDark));
1600
- }
1601
- if (readOnly) return;
1602
- latestSceneRef.current = { elements, appState };
1603
- const cropId = appState?.croppingElementId;
1604
- if (cropId && cropId !== handledCropIdRef.current && api) {
1605
- const el = elements.find((e) => e.id === cropId);
1606
- if (el) {
1607
- const stamp = findStampForCustomData(el.customData, stamps);
1608
- if (stamp) {
1609
- handledCropIdRef.current = cropId;
1610
- const elId = el.id;
1611
- const elCustom = el.customData;
1612
- const stampKind = stamp.kind;
1613
- queueMicrotask(() => {
1614
- try {
1615
- api.updateScene({
1616
- appState: { croppingElementId: null, selectedElementIds: {} }
1617
- });
1618
- } catch {
1619
- }
1620
- openStamp(stampKind, { id: elId, customData: elCustom });
1621
- });
1622
- return;
1623
- }
1624
- }
1625
- }
1626
- if (!cropId) {
1627
- handledCropIdRef.current = null;
1628
- }
1629
- const fileIds = Object.keys(files);
1630
- const newIds = fileIds.filter((id) => !knownFileIdsRef.current.has(id));
1631
- if (newIds.length > 0) {
1632
- newIds.forEach((id) => knownFileIdsRef.current.add(id));
1633
- onFilesChange?.(files, newIds);
1634
- }
1635
- if (!sceneThrottleRef.current) {
1636
- sceneThrottleRef.current = setTimeout(async () => {
1637
- sceneThrottleRef.current = null;
1638
- try {
1639
- const mod = await import('@excalidraw/excalidraw');
1640
- hashElementsVersionRef.current = mod.hashElementsVersion;
1641
- } catch (err) {
1642
- console.warn("[whiteboard] import excalidraw \u0111\u1EC3 flush scene th\u1EA5t b\u1EA1i:", err);
1643
- return;
1644
- }
1645
- flushSceneRef.current();
1646
- }, SYNC_THROTTLE_MS);
1647
- }
1648
- if (persistEnabled && newIds.length > 0) {
1649
- for (const id of newIds) {
1650
- if (files[id]) pendingFilesRef.current[id] = files[id];
1651
- }
1652
- if (!fileThrottleRef.current) {
1653
- fileThrottleRef.current = setTimeout(() => {
1654
- fileThrottleRef.current = null;
1655
- flushFilesRef.current();
1656
- }, 1e3);
1657
- }
1658
- }
1659
- if (persistEnabled && !pruneThrottleRef.current) {
1660
- pruneThrottleRef.current = setTimeout(() => {
1661
- pruneThrottleRef.current = null;
1662
- flushPruneRef.current();
1663
- }, 2e3);
1664
- }
1665
- },
1666
- [readOnly, api, onSceneChange, onFilesChange, persistEnabled, storageKey, stamps, openStamp]
1667
- );
1668
1765
  const flushSceneRef = useRef(() => void 0);
1669
1766
  flushSceneRef.current = () => {
1670
1767
  try {
@@ -1725,6 +1822,46 @@ function Whiteboard({
1725
1822
  console.warn("[whiteboard] flushPrune th\u1EA5t b\u1EA1i:", err);
1726
1823
  }
1727
1824
  };
1825
+ const onSceneTick = (elements, appState, files) => {
1826
+ if (readOnly) return;
1827
+ latestSceneRef.current = { elements, appState };
1828
+ const fileIds = Object.keys(files);
1829
+ const newIds = fileIds.filter((id) => !knownFileIdsRef.current.has(id));
1830
+ if (newIds.length > 0) {
1831
+ newIds.forEach((id) => knownFileIdsRef.current.add(id));
1832
+ onFilesChangeRef.current?.(files, newIds);
1833
+ }
1834
+ if (!sceneThrottleRef.current) {
1835
+ sceneThrottleRef.current = setTimeout(async () => {
1836
+ sceneThrottleRef.current = null;
1837
+ try {
1838
+ const mod = await import('@excalidraw/excalidraw');
1839
+ hashElementsVersionRef.current = mod.hashElementsVersion;
1840
+ } catch (err) {
1841
+ console.warn("[whiteboard] import excalidraw \u0111\u1EC3 flush scene th\u1EA5t b\u1EA1i:", err);
1842
+ return;
1843
+ }
1844
+ flushSceneRef.current();
1845
+ }, SYNC_THROTTLE_MS);
1846
+ }
1847
+ if (persistEnabled && newIds.length > 0) {
1848
+ for (const id of newIds) {
1849
+ if (files[id]) pendingFilesRef.current[id] = files[id];
1850
+ }
1851
+ if (!fileThrottleRef.current) {
1852
+ fileThrottleRef.current = setTimeout(() => {
1853
+ fileThrottleRef.current = null;
1854
+ flushFilesRef.current();
1855
+ }, FILE_THROTTLE_MS);
1856
+ }
1857
+ }
1858
+ if (persistEnabled && !pruneThrottleRef.current) {
1859
+ pruneThrottleRef.current = setTimeout(() => {
1860
+ pruneThrottleRef.current = null;
1861
+ flushPruneRef.current();
1862
+ }, PRUNE_THROTTLE_MS);
1863
+ }
1864
+ };
1728
1865
  const initialFilesAddedRef = useRef(false);
1729
1866
  useEffect(() => {
1730
1867
  if (!api || initialFilesAddedRef.current) return;
@@ -1736,11 +1873,8 @@ function Whiteboard({
1736
1873
  api.addFiles(
1737
1874
  entries.map(([id, f]) => ({
1738
1875
  id,
1739
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1740
1876
  dataURL: f.dataURL,
1741
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1742
1877
  mimeType: f.mimeType,
1743
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1744
1878
  created: f.created ?? Date.now()
1745
1879
  }))
1746
1880
  );
@@ -1762,11 +1896,8 @@ function Whiteboard({
1762
1896
  api.addFiles(
1763
1897
  entries.map(([id, f]) => ({
1764
1898
  id,
1765
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1766
1899
  dataURL: f.dataURL,
1767
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1768
1900
  mimeType: f.mimeType,
1769
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1770
1901
  created: f.created ?? Date.now()
1771
1902
  }))
1772
1903
  );
@@ -1804,7 +1935,7 @@ function Whiteboard({
1804
1935
  void run();
1805
1936
  const t = setTimeout(() => {
1806
1937
  void run();
1807
- }, 400);
1938
+ }, RESTORE_PASS_DELAY_MS);
1808
1939
  return () => {
1809
1940
  cancelled = true;
1810
1941
  clearTimeout(t);
@@ -1830,6 +1961,88 @@ function Whiteboard({
1830
1961
  },
1831
1962
  []
1832
1963
  );
1964
+ return { effectiveInitialScene, onSceneTick };
1965
+ }
1966
+ var Excalidraw = lazy(
1967
+ () => import('./ExcalidrawWithMenus-WENZRYYE.mjs').then((m) => ({ default: m.ExcalidrawWithMenus }))
1968
+ );
1969
+ var ExcalidrawLoadingFallback = () => /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center text-sm text-gray-500", children: "\u0110ang t\u1EA3i b\u1EA3ng\u2026" });
1970
+ function Whiteboard({
1971
+ storageKey = "default",
1972
+ readOnly = false,
1973
+ onSceneChange,
1974
+ onFilesChange,
1975
+ onApi,
1976
+ langCode = "vi-VN",
1977
+ stamps = DEFAULT_STAMPS,
1978
+ initialScene,
1979
+ initialFiles,
1980
+ generateGeometryFigure
1981
+ }) {
1982
+ const { api, apiRef, isDark, setApiFromExcalidraw, syncThemeFromAppState } = useExcalidrawApi({ onApi });
1983
+ const {
1984
+ activeStamp,
1985
+ editingElement,
1986
+ HostComponent,
1987
+ openStamp,
1988
+ closeStamp,
1989
+ toggleStampByKind
1990
+ } = useActiveStamp({ readOnly, stamps });
1991
+ const {
1992
+ pdfPending,
1993
+ pdfBusy,
1994
+ handlePdfPick,
1995
+ handlePdfConfirm,
1996
+ handlePdfCancel
1997
+ } = usePdfImporter({ readOnly, api });
1998
+ const { effectiveInitialScene, onSceneTick } = useScenePersist({
1999
+ storageKey,
2000
+ initialScene,
2001
+ initialFiles,
2002
+ readOnly,
2003
+ onSceneChange,
2004
+ onFilesChange,
2005
+ api,
2006
+ apiRef,
2007
+ stamps
2008
+ });
2009
+ const hostRef = useRef(null);
2010
+ const handledCropIdRef = useRef(null);
2011
+ const prevExcalidrawToolRef = useRef("selection");
2012
+ const handleChange = useCallback(
2013
+ (elements, appState, files) => {
2014
+ syncThemeFromAppState(appState);
2015
+ if (readOnly) return;
2016
+ const cropId = appState?.croppingElementId;
2017
+ if (cropId && cropId !== handledCropIdRef.current && api) {
2018
+ const el = elements.find((e) => e.id === cropId);
2019
+ if (el) {
2020
+ const stamp = findStampForCustomData(el.customData, stamps);
2021
+ if (stamp) {
2022
+ handledCropIdRef.current = cropId;
2023
+ const elId = el.id;
2024
+ const elCustom = el.customData;
2025
+ const stampKind = stamp.kind;
2026
+ queueMicrotask(() => {
2027
+ try {
2028
+ api.updateScene({
2029
+ appState: { croppingElementId: null, selectedElementIds: {} }
2030
+ });
2031
+ } catch {
2032
+ }
2033
+ openStamp(stampKind, { id: elId, customData: elCustom });
2034
+ });
2035
+ return;
2036
+ }
2037
+ }
2038
+ }
2039
+ if (!cropId) {
2040
+ handledCropIdRef.current = null;
2041
+ }
2042
+ onSceneTick(elements, appState, files);
2043
+ },
2044
+ [readOnly, api, stamps, openStamp, syncThemeFromAppState, onSceneTick]
2045
+ );
1833
2046
  const handlePointerDown = useStampDoubleClick({
1834
2047
  enabled: !readOnly,
1835
2048
  stamps,
@@ -1873,92 +2086,11 @@ function Whiteboard({
1873
2086
  return () => window.removeEventListener("keydown", onKey, { capture: true });
1874
2087
  }, [activeStamp, closeStamp]);
1875
2088
  useStampClickOutside({ activeStamp, hostRef, onClose: closeStamp });
1876
- const handlePdfPick = useCallback(
1877
- async (file) => {
1878
- if (readOnly || pdfBusy) return;
1879
- setPdfBusy(true);
1880
- try {
1881
- const doc = await loadPdfDocument(file);
1882
- setPdfPending({ doc, fileName: file.name, totalPages: doc.numPages });
1883
- } catch (err) {
1884
- console.warn("[whiteboard] \u0110\u1ECDc PDF th\u1EA5t b\u1EA1i:", err);
1885
- window.alert("Kh\xF4ng \u0111\u1ECDc \u0111\u01B0\u1EE3c PDF. File c\xF3 th\u1EC3 \u0111\xE3 h\u1ECFng ho\u1EB7c b\u1ECB m\u1EADt kh\u1EA9u b\u1EA3o v\u1EC7.");
1886
- } finally {
1887
- setPdfBusy(false);
1888
- }
1889
- },
1890
- [readOnly, pdfBusy]
1891
- );
1892
- const handlePdfConfirm = useCallback(
1893
- async (pages) => {
1894
- if (!pdfPending || !api) return;
1895
- const { doc } = pdfPending;
1896
- setPdfPending(null);
1897
- setPdfBusy(true);
1898
- const scale = 2;
1899
- try {
1900
- const rendered = await rasterizePdf(doc, { pages, scale });
1901
- await closePdfDocument(doc);
1902
- insertRasterizedPagesIntoScene(api, rendered, { scale });
1903
- } catch (err) {
1904
- console.warn("[whiteboard] Ch\xE8n PDF th\u1EA5t b\u1EA1i:", err);
1905
- window.alert("Ch\xE8n PDF th\u1EA5t b\u1EA1i. Xem console \u0111\u1EC3 bi\u1EBFt chi ti\u1EBFt.");
1906
- } finally {
1907
- setPdfBusy(false);
1908
- }
1909
- },
1910
- [pdfPending, api]
1911
- );
1912
- const handlePdfCancel = useCallback(() => {
1913
- if (pdfPending) {
1914
- void closePdfDocument(pdfPending.doc);
1915
- }
1916
- setPdfPending(null);
1917
- }, [pdfPending]);
1918
- useEffect(() => {
1919
- if (readOnly) return;
1920
- const root = document.querySelector(".excalidraw");
1921
- if (!root) return;
1922
- const onDragOver = (e) => {
1923
- const items = e.dataTransfer?.items;
1924
- if (!items) return;
1925
- for (let i = 0; i < items.length; i++) {
1926
- if (items[i].kind === "file" && items[i].type === "application/pdf") {
1927
- e.preventDefault();
1928
- e.stopPropagation();
1929
- if (e.dataTransfer) e.dataTransfer.dropEffect = "copy";
1930
- return;
1931
- }
1932
- }
1933
- };
1934
- const onDrop = (e) => {
1935
- const files = e.dataTransfer?.files;
1936
- if (!files || files.length === 0) return;
1937
- const pdf = Array.from(files).find((f) => f.type === "application/pdf");
1938
- if (!pdf) return;
1939
- e.preventDefault();
1940
- e.stopPropagation();
1941
- void handlePdfPick(pdf);
1942
- };
1943
- root.addEventListener("dragover", onDragOver, { capture: true });
1944
- root.addEventListener("drop", onDrop, { capture: true });
1945
- return () => {
1946
- root.removeEventListener("dragover", onDragOver, { capture: true });
1947
- root.removeEventListener("drop", onDrop, { capture: true });
1948
- };
1949
- }, [readOnly, handlePdfPick, api]);
1950
- return /* @__PURE__ */ jsxs("div", { className: `relative h-full w-full${isDarkTheme ? " theme--dark" : ""}`, children: [
2089
+ return /* @__PURE__ */ jsxs("div", { className: `relative h-full w-full${isDark ? " theme--dark" : ""}`, children: [
1951
2090
  /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx(ExcalidrawLoadingFallback, {}), children: /* @__PURE__ */ jsx(
1952
2091
  Excalidraw,
1953
2092
  {
1954
- excalidrawAPI: (a) => {
1955
- if (apiRef.current === a) return;
1956
- apiRef.current = a;
1957
- queueMicrotask(() => {
1958
- setApi(a);
1959
- onApi?.(a);
1960
- });
1961
- },
2093
+ excalidrawAPI: setApiFromExcalidraw,
1962
2094
  langCode,
1963
2095
  viewModeEnabled: readOnly,
1964
2096
  initialData: effectiveInitialScene ? {
@@ -2017,12 +2149,1003 @@ function Whiteboard({
2017
2149
  api,
2018
2150
  editingElement,
2019
2151
  onClose: closeStamp,
2020
- isDark: isDarkTheme
2152
+ isDark,
2153
+ generateGeometryFigure
2021
2154
  }
2022
2155
  )
2023
2156
  ] });
2024
2157
  }
2158
+ var NameZ = z.string().regex(/^[A-Za-z][A-Za-z0-9_'₀-₉]{0,11}$/);
2159
+ var DslPoint = z.discriminatedUnion("kind", [
2160
+ z.object({
2161
+ name: NameZ,
2162
+ kind: z.literal("free"),
2163
+ x: z.number().finite(),
2164
+ y: z.number().finite()
2165
+ }),
2166
+ z.object({
2167
+ name: NameZ,
2168
+ kind: z.literal("midpoint"),
2169
+ p1: NameZ,
2170
+ p2: NameZ
2171
+ }),
2172
+ z.object({
2173
+ name: NameZ,
2174
+ kind: z.literal("onSegment"),
2175
+ segmentId: NameZ,
2176
+ t: z.number().min(0).max(1)
2177
+ }),
2178
+ z.object({
2179
+ name: NameZ,
2180
+ kind: z.literal("onLine"),
2181
+ lineId: NameZ,
2182
+ t: z.number().finite()
2183
+ }),
2184
+ z.object({
2185
+ name: NameZ,
2186
+ kind: z.literal("onCircle"),
2187
+ circleId: NameZ,
2188
+ theta: z.number().finite()
2189
+ }),
2190
+ z.object({
2191
+ name: NameZ,
2192
+ kind: z.literal("perpFoot"),
2193
+ from: NameZ,
2194
+ onLine: NameZ
2195
+ }),
2196
+ z.object({
2197
+ name: NameZ,
2198
+ kind: z.literal("circumcenter"),
2199
+ vertices: z.tuple([NameZ, NameZ, NameZ])
2200
+ }),
2201
+ z.object({
2202
+ name: NameZ,
2203
+ kind: z.literal("incenter"),
2204
+ vertices: z.tuple([NameZ, NameZ, NameZ])
2205
+ }),
2206
+ z.object({
2207
+ name: NameZ,
2208
+ kind: z.literal("centroid"),
2209
+ vertices: z.tuple([NameZ, NameZ, NameZ])
2210
+ }),
2211
+ z.object({
2212
+ name: NameZ,
2213
+ kind: z.literal("orthocenter"),
2214
+ vertices: z.tuple([NameZ, NameZ, NameZ])
2215
+ }),
2216
+ z.object({
2217
+ name: NameZ,
2218
+ kind: z.literal("intersection"),
2219
+ ref1: NameZ,
2220
+ ref2: NameZ,
2221
+ branch: z.union([z.literal(0), z.literal(1)]).optional()
2222
+ })
2223
+ ]);
2224
+ var DslShape = z.discriminatedUnion("kind", [
2225
+ z.object({
2226
+ name: NameZ,
2227
+ kind: z.literal("segment"),
2228
+ p1: NameZ,
2229
+ p2: NameZ
2230
+ }),
2231
+ z.object({
2232
+ name: NameZ,
2233
+ kind: z.literal("line"),
2234
+ p1: NameZ,
2235
+ p2: NameZ
2236
+ }),
2237
+ z.object({
2238
+ name: NameZ,
2239
+ kind: z.literal("ray"),
2240
+ origin: NameZ,
2241
+ through: NameZ
2242
+ }),
2243
+ z.object({
2244
+ name: NameZ,
2245
+ kind: z.literal("polygon"),
2246
+ vertices: z.array(NameZ).min(3)
2247
+ }),
2248
+ // Line constructions
2249
+ z.object({
2250
+ name: NameZ,
2251
+ kind: z.literal("perpendicular"),
2252
+ throughPoint: NameZ,
2253
+ toLine: NameZ
2254
+ }),
2255
+ z.object({
2256
+ name: NameZ,
2257
+ kind: z.literal("parallel"),
2258
+ throughPoint: NameZ,
2259
+ toLine: NameZ
2260
+ }),
2261
+ z.object({
2262
+ name: NameZ,
2263
+ kind: z.literal("perpBisector"),
2264
+ p1: NameZ,
2265
+ p2: NameZ
2266
+ }),
2267
+ z.object({
2268
+ name: NameZ,
2269
+ kind: z.literal("angleBisector"),
2270
+ p1: NameZ,
2271
+ vertex: NameZ,
2272
+ p2: NameZ
2273
+ }),
2274
+ z.object({
2275
+ name: NameZ,
2276
+ kind: z.literal("tangent"),
2277
+ throughPoint: NameZ,
2278
+ toCircle: NameZ,
2279
+ branch: z.union([z.literal(0), z.literal(1), z.literal("on")]).optional()
2280
+ }),
2281
+ // Circle constructions
2282
+ z.object({
2283
+ name: NameZ,
2284
+ kind: z.literal("circleCP"),
2285
+ center: NameZ,
2286
+ surfacePoint: NameZ
2287
+ }),
2288
+ z.object({
2289
+ name: NameZ,
2290
+ kind: z.literal("circle3"),
2291
+ p1: NameZ,
2292
+ p2: NameZ,
2293
+ p3: NameZ
2294
+ })
2295
+ ]);
2296
+ var DslInput = z.object({
2297
+ version: z.literal(1),
2298
+ points: z.array(DslPoint),
2299
+ shapes: z.array(DslShape).default([])
2300
+ });
2301
+
2302
+ // src/stamps/geometry-2d/dsl/transpile/errors.ts
2303
+ function mkError(code, message, opts) {
2304
+ return { code, message, path: opts?.path, hint: opts?.hint };
2305
+ }
2306
+
2307
+ // src/stamps/geometry-2d/dsl/transpile/symbols.ts
2308
+ function buildSymbols(dsl) {
2309
+ const symbols = /* @__PURE__ */ new Map();
2310
+ const errors = [];
2311
+ let counter = 0;
2312
+ for (const p of dsl.points) {
2313
+ if (symbols.has(p.name)) {
2314
+ errors.push(mkError("DUPLICATE_NAME", `T\xEAn tr\xF9ng: "${p.name}"`, { path: [p.name] }));
2315
+ continue;
2316
+ }
2317
+ symbols.set(p.name, { name: p.name, role: "point", entity: p, index: counter++ });
2318
+ }
2319
+ for (const s of dsl.shapes) {
2320
+ if (symbols.has(s.name)) {
2321
+ errors.push(mkError("DUPLICATE_NAME", `T\xEAn tr\xF9ng: "${s.name}"`, { path: [s.name] }));
2322
+ continue;
2323
+ }
2324
+ symbols.set(s.name, { name: s.name, role: "shape", entity: s, index: counter++ });
2325
+ }
2326
+ return { symbols, errors };
2327
+ }
2328
+
2329
+ // src/stamps/geometry-2d/dsl/transpile/refs.ts
2330
+ function isPointLike(sym) {
2331
+ return !!sym && sym.role === "point";
2332
+ }
2333
+ var LINE_LIKE_SHAPE_KINDS = /* @__PURE__ */ new Set([
2334
+ "line",
2335
+ "segment",
2336
+ "ray",
2337
+ "perpendicular",
2338
+ "parallel",
2339
+ "perpBisector",
2340
+ "angleBisector",
2341
+ "tangent"
2342
+ ]);
2343
+ var CIRCLE_KINDS = /* @__PURE__ */ new Set(["circleCP", "circle3"]);
2344
+ function isLineLike(sym) {
2345
+ if (!sym || sym.role !== "shape") return false;
2346
+ return LINE_LIKE_SHAPE_KINDS.has(sym.entity.kind);
2347
+ }
2348
+ function isCircleLike(sym) {
2349
+ if (!sym || sym.role !== "shape") return false;
2350
+ return CIRCLE_KINDS.has(sym.entity.kind);
2351
+ }
2352
+ function isSegmentExact(sym) {
2353
+ return !!sym && sym.role === "shape" && sym.entity.kind === "segment";
2354
+ }
2355
+ function validateRefs(dsl, symbols) {
2356
+ const errors = [];
2357
+ const check = (owner, field, refName, predicate, expected) => {
2358
+ const sym = symbols.get(refName);
2359
+ if (!sym) {
2360
+ errors.push(mkError(
2361
+ "UNKNOWN_REF",
2362
+ `${owner}.${field} tham chi\u1EBFu "${refName}" kh\xF4ng t\u1ED3n t\u1EA1i`,
2363
+ { path: [owner, field] }
2364
+ ));
2365
+ return;
2366
+ }
2367
+ if (!predicate(sym)) {
2368
+ errors.push(mkError(
2369
+ "KIND_MISMATCH",
2370
+ `${owner}.${field}="${refName}" sai ki\u1EC3u (c\u1EA7n ${expected}, g\u1EB7p ${sym.role === "point" ? "point" : sym.entity.kind})`,
2371
+ { path: [owner, field] }
2372
+ ));
2373
+ }
2374
+ };
2375
+ for (const p of dsl.points) {
2376
+ switch (p.kind) {
2377
+ case "free":
2378
+ break;
2379
+ case "midpoint":
2380
+ check(p.name, "p1", p.p1, isPointLike, "point");
2381
+ check(p.name, "p2", p.p2, isPointLike, "point");
2382
+ break;
2383
+ case "onSegment":
2384
+ check(p.name, "segmentId", p.segmentId, isSegmentExact, "segment");
2385
+ break;
2386
+ case "onLine":
2387
+ check(p.name, "lineId", p.lineId, isLineLike, "line-like");
2388
+ break;
2389
+ case "onCircle":
2390
+ check(p.name, "circleId", p.circleId, isCircleLike, "circle");
2391
+ break;
2392
+ case "perpFoot":
2393
+ check(p.name, "from", p.from, isPointLike, "point");
2394
+ check(p.name, "onLine", p.onLine, isLineLike, "line-like");
2395
+ break;
2396
+ case "circumcenter":
2397
+ case "incenter":
2398
+ case "centroid":
2399
+ case "orthocenter":
2400
+ for (let i = 0; i < 3; i++) {
2401
+ check(p.name, `vertices[${i}]`, p.vertices[i], isPointLike, "point");
2402
+ }
2403
+ break;
2404
+ case "intersection": {
2405
+ const refPredicate = (s) => isLineLike(s) || isCircleLike(s);
2406
+ check(p.name, "ref1", p.ref1, refPredicate, "line-like ho\u1EB7c circle");
2407
+ check(p.name, "ref2", p.ref2, refPredicate, "line-like ho\u1EB7c circle");
2408
+ break;
2409
+ }
2410
+ }
2411
+ }
2412
+ for (const s of dsl.shapes) {
2413
+ switch (s.kind) {
2414
+ case "segment":
2415
+ case "line":
2416
+ check(s.name, "p1", s.p1, isPointLike, "point");
2417
+ check(s.name, "p2", s.p2, isPointLike, "point");
2418
+ break;
2419
+ case "ray":
2420
+ check(s.name, "origin", s.origin, isPointLike, "point");
2421
+ check(s.name, "through", s.through, isPointLike, "point");
2422
+ break;
2423
+ case "polygon":
2424
+ s.vertices.forEach((v, i) => check(s.name, `vertices[${i}]`, v, isPointLike, "point"));
2425
+ break;
2426
+ case "perpendicular":
2427
+ case "parallel":
2428
+ check(s.name, "throughPoint", s.throughPoint, isPointLike, "point");
2429
+ check(s.name, "toLine", s.toLine, isLineLike, "line-like");
2430
+ break;
2431
+ case "perpBisector":
2432
+ check(s.name, "p1", s.p1, isPointLike, "point");
2433
+ check(s.name, "p2", s.p2, isPointLike, "point");
2434
+ break;
2435
+ case "angleBisector":
2436
+ check(s.name, "p1", s.p1, isPointLike, "point");
2437
+ check(s.name, "vertex", s.vertex, isPointLike, "point");
2438
+ check(s.name, "p2", s.p2, isPointLike, "point");
2439
+ break;
2440
+ case "tangent":
2441
+ check(s.name, "throughPoint", s.throughPoint, isPointLike, "point");
2442
+ check(s.name, "toCircle", s.toCircle, isCircleLike, "circle");
2443
+ break;
2444
+ case "circleCP":
2445
+ check(s.name, "center", s.center, isPointLike, "point");
2446
+ check(s.name, "surfacePoint", s.surfacePoint, isPointLike, "point");
2447
+ break;
2448
+ case "circle3":
2449
+ check(s.name, "p1", s.p1, isPointLike, "point");
2450
+ check(s.name, "p2", s.p2, isPointLike, "point");
2451
+ check(s.name, "p3", s.p3, isPointLike, "point");
2452
+ break;
2453
+ }
2454
+ }
2455
+ return { errors };
2456
+ }
2457
+ function collectRefs(entity) {
2458
+ if ("kind" in entity) {
2459
+ switch (entity.kind) {
2460
+ case "free":
2461
+ return [];
2462
+ case "midpoint":
2463
+ return [entity.p1, entity.p2];
2464
+ case "onSegment":
2465
+ return [entity.segmentId];
2466
+ case "onLine":
2467
+ return [entity.lineId];
2468
+ case "onCircle":
2469
+ return [entity.circleId];
2470
+ case "perpFoot":
2471
+ return [entity.from, entity.onLine];
2472
+ case "circumcenter":
2473
+ case "incenter":
2474
+ case "centroid":
2475
+ case "orthocenter":
2476
+ return [...entity.vertices];
2477
+ case "intersection":
2478
+ return [entity.ref1, entity.ref2];
2479
+ case "segment":
2480
+ case "line":
2481
+ return [entity.p1, entity.p2];
2482
+ case "ray":
2483
+ return [entity.origin, entity.through];
2484
+ case "polygon":
2485
+ return [...entity.vertices];
2486
+ case "perpendicular":
2487
+ case "parallel":
2488
+ return [entity.throughPoint, entity.toLine];
2489
+ case "perpBisector":
2490
+ return [entity.p1, entity.p2];
2491
+ case "angleBisector":
2492
+ return [entity.p1, entity.vertex, entity.p2];
2493
+ case "tangent":
2494
+ return [entity.throughPoint, entity.toCircle];
2495
+ case "circleCP":
2496
+ return [entity.center, entity.surfacePoint];
2497
+ case "circle3":
2498
+ return [entity.p1, entity.p2, entity.p3];
2499
+ }
2500
+ }
2501
+ return [];
2502
+ }
2503
+
2504
+ // src/stamps/geometry-2d/dsl/transpile/cycles.ts
2505
+ function detectCycles(symbols) {
2506
+ const color = /* @__PURE__ */ new Map();
2507
+ const parent = /* @__PURE__ */ new Map();
2508
+ const errors = [];
2509
+ const reportedCycles = /* @__PURE__ */ new Set();
2510
+ for (const name of symbols.keys()) color.set(name, "white");
2511
+ function reportCycle(start, hit) {
2512
+ const chain = [start];
2513
+ let cur = parent.get(start);
2514
+ while (cur && cur !== hit && chain.length < symbols.size + 2) {
2515
+ chain.push(cur);
2516
+ cur = parent.get(cur);
2517
+ }
2518
+ chain.push(hit);
2519
+ const minIdx = chain.indexOf(chain.reduce((a, b) => a < b ? a : b));
2520
+ const rotated = [...chain.slice(minIdx), ...chain.slice(0, minIdx)];
2521
+ const key = rotated.join("\u2192");
2522
+ if (reportedCycles.has(key)) return;
2523
+ reportedCycles.add(key);
2524
+ errors.push(mkError(
2525
+ "CYCLE",
2526
+ `Ph\u1EE5 thu\u1ED9c v\xF2ng: ${chain.reverse().join(" \u2192 ")}`,
2527
+ { path: [...chain], hint: "Ki\u1EC3m tra l\u1EA1i quan h\u1EC7 midpoint/perpFoot/intersection." }
2528
+ ));
2529
+ }
2530
+ function dfs(name) {
2531
+ color.set(name, "gray");
2532
+ const sym = symbols.get(name);
2533
+ if (sym) {
2534
+ for (const ref of collectRefs(sym.entity)) {
2535
+ if (!symbols.has(ref)) continue;
2536
+ const c = color.get(ref);
2537
+ if (c === "gray") {
2538
+ reportCycle(name, ref);
2539
+ continue;
2540
+ }
2541
+ if (c === "white") {
2542
+ parent.set(ref, name);
2543
+ dfs(ref);
2544
+ }
2545
+ }
2546
+ }
2547
+ color.set(name, "black");
2548
+ }
2549
+ for (const name of symbols.keys()) {
2550
+ if (color.get(name) === "white") {
2551
+ parent.set(name, null);
2552
+ dfs(name);
2553
+ }
2554
+ }
2555
+ return { errors };
2556
+ }
2557
+
2558
+ // src/stamps/geometry-2d/dsl/transpile/ids.ts
2559
+ function prefixFor(sym) {
2560
+ if (sym.role === "point") {
2561
+ const p = sym.entity;
2562
+ return p.kind === "intersection" ? "i" : "p";
2563
+ }
2564
+ const s = sym.entity;
2565
+ switch (s.kind) {
2566
+ case "segment":
2567
+ return "s";
2568
+ case "ray":
2569
+ return "r";
2570
+ case "polygon":
2571
+ return "poly";
2572
+ case "circleCP":
2573
+ case "circle3":
2574
+ return "c";
2575
+ // line + 5 line-constructions all share 'l'
2576
+ case "line":
2577
+ case "perpendicular":
2578
+ case "parallel":
2579
+ case "perpBisector":
2580
+ case "angleBisector":
2581
+ case "tangent":
2582
+ return "l";
2583
+ }
2584
+ }
2585
+ function assignIds(symbols) {
2586
+ const counters = { p: 0, i: 0, s: 0, l: 0, r: 0, poly: 0, c: 0 };
2587
+ const ids = /* @__PURE__ */ new Map();
2588
+ for (const [name, sym] of symbols.entries()) {
2589
+ const prefix = prefixFor(sym);
2590
+ counters[prefix] += 1;
2591
+ ids.set(name, `${prefix}${counters[prefix]}`);
2592
+ }
2593
+ return ids;
2594
+ }
2595
+
2596
+ // src/stamps/geometry-2d/dsl/transpile/emitPoint.ts
2597
+ function resolveId(ids, name) {
2598
+ const id = ids.get(name);
2599
+ if (!id) throw new Error(`emitPoint: id not assigned for "${name}"`);
2600
+ return id;
2601
+ }
2602
+ function emitPoint(p, ids, kindHints) {
2603
+ const baseId = resolveId(ids, p.name);
2604
+ const baseFields = {
2605
+ label: p.name,
2606
+ visible: true,
2607
+ locked: false,
2608
+ layer: "default",
2609
+ schemaVersion: 1
2610
+ };
2611
+ if (p.kind === "intersection") {
2612
+ const r1Hint = kindHints.get(p.ref1);
2613
+ const r2Hint = kindHints.get(p.ref2);
2614
+ const r1IsCircle = r1Hint === "circle";
2615
+ const r2IsCircle = r2Hint === "circle";
2616
+ let intersectKind;
2617
+ if (r1IsCircle && r2IsCircle) intersectKind = "circleCircle";
2618
+ else if (r1IsCircle || r2IsCircle) intersectKind = "lineCircle";
2619
+ else intersectKind = "lineLine";
2620
+ const attrs = {
2621
+ kind: intersectKind,
2622
+ ref1: resolveId(ids, p.ref1),
2623
+ ref2: resolveId(ids, p.ref2)
2624
+ };
2625
+ if (intersectKind !== "lineLine") {
2626
+ attrs.branch = p.branch ?? 0;
2627
+ }
2628
+ return {
2629
+ id: baseId,
2630
+ kind: "intersection",
2631
+ ...baseFields,
2632
+ attrs
2633
+ };
2634
+ }
2635
+ let constraint;
2636
+ switch (p.kind) {
2637
+ case "free":
2638
+ constraint = { kind: "free", x: p.x, y: p.y };
2639
+ break;
2640
+ case "midpoint":
2641
+ constraint = { kind: "midpoint", p1: resolveId(ids, p.p1), p2: resolveId(ids, p.p2) };
2642
+ break;
2643
+ case "onSegment":
2644
+ constraint = { kind: "onSegment", segmentId: resolveId(ids, p.segmentId), t: p.t };
2645
+ break;
2646
+ case "onLine":
2647
+ constraint = { kind: "onLine", lineId: resolveId(ids, p.lineId), t: p.t };
2648
+ break;
2649
+ case "onCircle":
2650
+ constraint = { kind: "onCircle", circleId: resolveId(ids, p.circleId), theta: p.theta };
2651
+ break;
2652
+ case "perpFoot":
2653
+ constraint = { kind: "perpFoot", from: resolveId(ids, p.from), onLine: resolveId(ids, p.onLine) };
2654
+ break;
2655
+ case "circumcenter":
2656
+ case "incenter":
2657
+ case "centroid":
2658
+ case "orthocenter":
2659
+ constraint = {
2660
+ kind: p.kind,
2661
+ vertices: [resolveId(ids, p.vertices[0]), resolveId(ids, p.vertices[1]), resolveId(ids, p.vertices[2])]
2662
+ };
2663
+ break;
2664
+ }
2665
+ return {
2666
+ id: baseId,
2667
+ kind: "point",
2668
+ ...baseFields,
2669
+ attrs: { constraint }
2670
+ };
2671
+ }
2672
+
2673
+ // src/stamps/geometry-2d/dsl/transpile/emitShape.ts
2674
+ function r(ids, name) {
2675
+ const id = ids.get(name);
2676
+ if (!id) throw new Error(`emitShape: id not assigned for "${name}"`);
2677
+ return id;
2678
+ }
2679
+ function emitShape(s, ids) {
2680
+ const id = r(ids, s.name);
2681
+ const base = {
2682
+ label: s.name,
2683
+ visible: true,
2684
+ locked: false,
2685
+ layer: "default",
2686
+ schemaVersion: 1
2687
+ };
2688
+ switch (s.kind) {
2689
+ case "segment":
2690
+ return { id, kind: "segment", ...base, attrs: { p1: r(ids, s.p1), p2: r(ids, s.p2) } };
2691
+ case "line":
2692
+ return { id, kind: "line", ...base, attrs: { p1: r(ids, s.p1), p2: r(ids, s.p2) } };
2693
+ case "ray":
2694
+ return { id, kind: "ray", ...base, attrs: { origin: r(ids, s.origin), through: r(ids, s.through) } };
2695
+ case "polygon":
2696
+ return { id, kind: "polygon", ...base, attrs: { vertices: s.vertices.map((v) => r(ids, v)) } };
2697
+ case "perpendicular":
2698
+ case "parallel":
2699
+ return {
2700
+ id,
2701
+ kind: "line",
2702
+ ...base,
2703
+ attrs: { construction: { kind: s.kind, throughPoint: r(ids, s.throughPoint), toLine: r(ids, s.toLine) } }
2704
+ };
2705
+ case "perpBisector":
2706
+ return {
2707
+ id,
2708
+ kind: "line",
2709
+ ...base,
2710
+ attrs: { construction: { kind: "perpBisector", p1: r(ids, s.p1), p2: r(ids, s.p2) } }
2711
+ };
2712
+ case "angleBisector":
2713
+ return {
2714
+ id,
2715
+ kind: "line",
2716
+ ...base,
2717
+ attrs: { construction: { kind: "angleBisector", p1: r(ids, s.p1), vertex: r(ids, s.vertex), p2: r(ids, s.p2) } }
2718
+ };
2719
+ case "tangent": {
2720
+ const construction = {
2721
+ kind: "tangent",
2722
+ throughPoint: r(ids, s.throughPoint),
2723
+ toCircle: r(ids, s.toCircle)
2724
+ };
2725
+ if (s.branch !== void 0) construction.branch = s.branch;
2726
+ return { id, kind: "line", ...base, attrs: { construction } };
2727
+ }
2728
+ case "circleCP":
2729
+ return {
2730
+ id,
2731
+ kind: "circle",
2732
+ ...base,
2733
+ attrs: { center: r(ids, s.center), surfacePoint: r(ids, s.surfacePoint) }
2734
+ };
2735
+ case "circle3":
2736
+ return {
2737
+ id,
2738
+ kind: "circle",
2739
+ ...base,
2740
+ attrs: { construction: { kind: "circumscribed", p1: r(ids, s.p1), p2: r(ids, s.p2), p3: r(ids, s.p3) } }
2741
+ };
2742
+ }
2743
+ }
2744
+
2745
+ // src/stamps/geometry-2d/dsl/transpile.ts
2746
+ function hintOf(entity) {
2747
+ if ("kind" in entity) {
2748
+ switch (entity.kind) {
2749
+ case "free":
2750
+ case "midpoint":
2751
+ case "onSegment":
2752
+ case "onLine":
2753
+ case "onCircle":
2754
+ case "perpFoot":
2755
+ case "circumcenter":
2756
+ case "incenter":
2757
+ case "centroid":
2758
+ case "orthocenter":
2759
+ case "intersection":
2760
+ return "point";
2761
+ case "segment":
2762
+ return "segment";
2763
+ case "line":
2764
+ return "line";
2765
+ case "ray":
2766
+ return "ray";
2767
+ case "polygon":
2768
+ return "point";
2769
+ // not used as ref target in MVP
2770
+ case "perpendicular":
2771
+ case "parallel":
2772
+ case "perpBisector":
2773
+ case "angleBisector":
2774
+ case "tangent":
2775
+ return "lineConstruction";
2776
+ case "circleCP":
2777
+ case "circle3":
2778
+ return "circle";
2779
+ }
2780
+ }
2781
+ return "point";
2782
+ }
2783
+ function transpile(dslRaw) {
2784
+ const parsed = DslInput.safeParse(dslRaw);
2785
+ if (!parsed.success) {
2786
+ const errors = parsed.error.issues.map(
2787
+ (iss) => mkError("SCHEMA", iss.message, { path: iss.path.map(String) })
2788
+ );
2789
+ return { ok: false, errors };
2790
+ }
2791
+ const dsl = parsed.data;
2792
+ const { symbols, errors: dupErrors } = buildSymbols(dsl);
2793
+ const { errors: refErrors } = validateRefs(dsl, symbols);
2794
+ const { errors: cycleErrors } = detectCycles(symbols);
2795
+ const allErrors = [...dupErrors, ...refErrors, ...cycleErrors];
2796
+ if (allErrors.length > 0) return { ok: false, errors: allErrors };
2797
+ const ids = assignIds(symbols);
2798
+ const kindHints = /* @__PURE__ */ new Map();
2799
+ for (const [name, sym] of symbols.entries()) {
2800
+ kindHints.set(name, hintOf(sym.entity));
2801
+ }
2802
+ const objects = {};
2803
+ const order = [];
2804
+ for (const p of dsl.points) {
2805
+ const obj = emitPoint(p, ids, kindHints);
2806
+ objects[obj.id] = obj;
2807
+ order.push(obj.id);
2808
+ }
2809
+ for (const s of dsl.shapes) {
2810
+ const obj = emitShape(s, ids);
2811
+ objects[obj.id] = obj;
2812
+ order.push(obj.id);
2813
+ }
2814
+ const empty = createEmptyState("2d");
2815
+ const state = {
2816
+ objects,
2817
+ order,
2818
+ counter: order.length,
2819
+ meta: empty.meta
2820
+ };
2821
+ return { ok: true, state };
2822
+ }
2823
+ async function callProvider(args) {
2824
+ const client = new Anthropic({ apiKey: args.apiKey });
2825
+ const resp = await client.messages.create(
2826
+ {
2827
+ model: args.model,
2828
+ max_tokens: args.maxTokens,
2829
+ system: args.system,
2830
+ tools: args.tools,
2831
+ tool_choice: args.toolChoice,
2832
+ messages: args.messages
2833
+ },
2834
+ args.signal ? { signal: args.signal } : void 0
2835
+ );
2836
+ return resp;
2837
+ }
2838
+
2839
+ // src/stamps/geometry-2d/dsl/fixtures/triangle-equilateral.ts
2840
+ var fixture = {
2841
+ problem: "Cho tam gi\xE1c \u0111\u1EC1u ABC c\u1EA1nh 4.",
2842
+ dsl: {
2843
+ version: 1,
2844
+ points: [
2845
+ { name: "A", kind: "free", x: 0, y: 0 },
2846
+ { name: "B", kind: "free", x: 4, y: 0 },
2847
+ { name: "C", kind: "free", x: 2, y: 3.464 }
2848
+ ],
2849
+ shapes: [
2850
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] }
2851
+ ]
2852
+ }
2853
+ };
2854
+
2855
+ // src/stamps/geometry-2d/dsl/fixtures/triangle-median.ts
2856
+ var fixture2 = {
2857
+ problem: "Tam gi\xE1c ABC, M l\xE0 trung \u0111i\u1EC3m BC. V\u1EBD AM.",
2858
+ dsl: {
2859
+ version: 1,
2860
+ points: [
2861
+ { name: "A", kind: "free", x: 0, y: 3 },
2862
+ { name: "B", kind: "free", x: -2, y: 0 },
2863
+ { name: "C", kind: "free", x: 3, y: 0 },
2864
+ { name: "M", kind: "midpoint", p1: "B", p2: "C" }
2865
+ ],
2866
+ shapes: [
2867
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] },
2868
+ { name: "AM", kind: "segment", p1: "A", p2: "M" }
2869
+ ]
2870
+ }
2871
+ };
2872
+
2873
+ // src/stamps/geometry-2d/dsl/fixtures/triangle-altitude.ts
2874
+ var fixture3 = {
2875
+ problem: "Tam gi\xE1c ABC, AH l\xE0 \u0111\u01B0\u1EDDng cao xu\u1ED1ng BC.",
2876
+ dsl: {
2877
+ version: 1,
2878
+ points: [
2879
+ { name: "A", kind: "free", x: 1, y: 3 },
2880
+ { name: "B", kind: "free", x: -2, y: 0 },
2881
+ { name: "C", kind: "free", x: 3, y: 0 },
2882
+ { name: "H", kind: "perpFoot", from: "A", onLine: "BC" }
2883
+ ],
2884
+ shapes: [
2885
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] },
2886
+ { name: "BC", kind: "segment", p1: "B", p2: "C" },
2887
+ { name: "AH", kind: "segment", p1: "A", p2: "H" }
2888
+ ]
2889
+ }
2890
+ };
2891
+
2892
+ // src/stamps/geometry-2d/dsl/fixtures/triangle-centroid.ts
2893
+ var fixture4 = {
2894
+ problem: "Tam gi\xE1c ABC, G l\xE0 tr\u1ECDng t\xE2m.",
2895
+ dsl: {
2896
+ version: 1,
2897
+ points: [
2898
+ { name: "A", kind: "free", x: 0, y: 3 },
2899
+ { name: "B", kind: "free", x: -2, y: 0 },
2900
+ { name: "C", kind: "free", x: 3, y: 0 },
2901
+ { name: "G", kind: "centroid", vertices: ["A", "B", "C"] }
2902
+ ],
2903
+ shapes: [
2904
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] }
2905
+ ]
2906
+ }
2907
+ };
2908
+
2909
+ // src/stamps/geometry-2d/dsl/fixtures/triangle-orthocenter.ts
2910
+ var fixture5 = {
2911
+ problem: "Tam gi\xE1c ABC, H l\xE0 tr\u1EF1c t\xE2m.",
2912
+ dsl: {
2913
+ version: 1,
2914
+ points: [
2915
+ { name: "A", kind: "free", x: 0, y: 3 },
2916
+ { name: "B", kind: "free", x: -2, y: 0 },
2917
+ { name: "C", kind: "free", x: 3, y: 0 },
2918
+ { name: "H", kind: "orthocenter", vertices: ["A", "B", "C"] }
2919
+ ],
2920
+ shapes: [
2921
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] }
2922
+ ]
2923
+ }
2924
+ };
2925
+
2926
+ // src/stamps/geometry-2d/dsl/fixtures/triangle-circumcircle.ts
2927
+ var fixture6 = {
2928
+ problem: "Tam gi\xE1c ABC n\u1ED9i ti\u1EBFp \u0111\u01B0\u1EDDng tr\xF2n t\xE2m O.",
2929
+ dsl: {
2930
+ version: 1,
2931
+ points: [
2932
+ { name: "A", kind: "free", x: 0, y: 3 },
2933
+ { name: "B", kind: "free", x: -2, y: 0 },
2934
+ { name: "C", kind: "free", x: 3, y: 0 },
2935
+ { name: "O", kind: "circumcenter", vertices: ["A", "B", "C"] }
2936
+ ],
2937
+ shapes: [
2938
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] },
2939
+ { name: "k", kind: "circle3", p1: "A", p2: "B", p3: "C" }
2940
+ ]
2941
+ }
2942
+ };
2943
+
2944
+ // src/stamps/geometry-2d/dsl/fixtures/triangle-incircle.ts
2945
+ var fixture7 = {
2946
+ problem: "Tam gi\xE1c ABC, I l\xE0 t\xE2m n\u1ED9i ti\u1EBFp, \u0111\u01B0\u1EDDng tr\xF2n (I) ti\u1EBFp x\xFAc BC t\u1EA1i D.",
2947
+ dsl: {
2948
+ version: 1,
2949
+ points: [
2950
+ { name: "A", kind: "free", x: 0, y: 3 },
2951
+ { name: "B", kind: "free", x: -2, y: 0 },
2952
+ { name: "C", kind: "free", x: 3, y: 0 },
2953
+ { name: "I", kind: "incenter", vertices: ["A", "B", "C"] },
2954
+ { name: "D", kind: "perpFoot", from: "I", onLine: "BC" }
2955
+ ],
2956
+ shapes: [
2957
+ { name: "ABC", kind: "polygon", vertices: ["A", "B", "C"] },
2958
+ { name: "BC", kind: "segment", p1: "B", p2: "C" },
2959
+ { name: "incircle", kind: "circleCP", center: "I", surfacePoint: "D" }
2960
+ ]
2961
+ }
2962
+ };
2963
+
2964
+ // src/stamps/geometry-2d/dsl/fixtures/parallelogram.ts
2965
+ var fixture8 = {
2966
+ problem: "H\xECnh b\xECnh h\xE0nh ABCD, hai \u0111\u01B0\u1EDDng ch\xE9o AC, BD c\u1EAFt nhau t\u1EA1i O.",
2967
+ dsl: {
2968
+ version: 1,
2969
+ points: [
2970
+ { name: "A", kind: "free", x: 0, y: 0 },
2971
+ { name: "B", kind: "free", x: 4, y: 0 },
2972
+ { name: "C", kind: "free", x: 5, y: 2 },
2973
+ { name: "D", kind: "free", x: 1, y: 2 },
2974
+ { name: "O", kind: "intersection", ref1: "AC", ref2: "BD" }
2975
+ ],
2976
+ shapes: [
2977
+ { name: "ABCD", kind: "polygon", vertices: ["A", "B", "C", "D"] },
2978
+ { name: "AC", kind: "segment", p1: "A", p2: "C" },
2979
+ { name: "BD", kind: "segment", p1: "B", p2: "D" }
2980
+ ]
2981
+ }
2982
+ };
2983
+
2984
+ // src/stamps/geometry-2d/dsl/fixtures/two-circles-intersect.ts
2985
+ var fixture9 = {
2986
+ problem: "Hai \u0111\u01B0\u1EDDng tr\xF2n (O\u2081), (O\u2082) c\u1EAFt nhau t\u1EA1i P, Q.",
2987
+ dsl: {
2988
+ version: 1,
2989
+ points: [
2990
+ { name: "O1", kind: "free", x: 0, y: 0 },
2991
+ { name: "A1", kind: "free", x: 2, y: 0 },
2992
+ { name: "O2", kind: "free", x: 3, y: 0 },
2993
+ { name: "A2", kind: "free", x: 5, y: 0 },
2994
+ { name: "P", kind: "intersection", ref1: "k1", ref2: "k2", branch: 0 },
2995
+ { name: "Q", kind: "intersection", ref1: "k1", ref2: "k2", branch: 1 }
2996
+ ],
2997
+ shapes: [
2998
+ { name: "k1", kind: "circleCP", center: "O1", surfacePoint: "A1" },
2999
+ { name: "k2", kind: "circleCP", center: "O2", surfacePoint: "A2" }
3000
+ ]
3001
+ }
3002
+ };
3003
+
3004
+ // src/stamps/geometry-2d/ai/prompt.ts
3005
+ var FIXTURES = [fixture, fixture2, fixture3, fixture4, fixture5, fixture6, fixture7, fixture8, fixture9];
3006
+ function buildSystemPrompt() {
3007
+ const examples = FIXTURES.map(
3008
+ (f, i) => `### V\xED d\u1EE5 ${i + 1}
3009
+ **\u0110\u1EC1:** ${f.problem}
3010
+ **DSL:**
3011
+ \`\`\`json
3012
+ ${JSON.stringify(f.dsl, null, 2)}
3013
+ \`\`\``
3014
+ ).join("\n\n");
3015
+ return `B\u1EA1n l\xE0 tr\u1EE3 l\xFD v\u1EBD h\xECnh h\u1ECDc 2D cho h\u1ECDc sinh THCS v\xE0 l\u1EDBp 10 Vi\u1EC7t Nam.
3016
+
3017
+ ## Nhi\u1EC7m v\u1EE5
3018
+ \u0110\u1ECDc \u0111\u1EC1 b\xE0i ti\u1EBFng Vi\u1EC7t \u2192 emit DSL JSON m\xF4 t\u1EA3 h\xECnh. H\u1EC7 th\u1ED1ng s\u1EBD render h\xECnh t\u1EEB DSL.
3019
+
3020
+ ## Quy t\u1EAFc
3021
+ 1. D\xF9ng tool \`build_figure\` khi v\u1EBD \u0111\u01B0\u1EE3c. D\xF9ng tool \`refuse\` khi kh\xF4ng v\u1EBD \u0111\u01B0\u1EE3c ho\u1EB7c \u0111\u1EC1 ngo\xE0i ph\u1EA1m vi (3D, l\u01B0\u1EE3ng gi\xE1c, ph\xE9p bi\u1EBFn h\xECnh l\u1EDBp 11+, \u0111\u1EA1i s\u1ED1).
3022
+ 2. \u01AFu ti\xEAn derived points (midpoint, perpFoot, circumcenter, ...) thay v\xEC t\u1EF1 compute to\u1EA1 \u0111\u1ED9.
3023
+ 3. Anchor (free) ch\u1EC9 d\xF9ng cho \u0111i\u1EC3m g\u1ED1c (th\u01B0\u1EDDng A, B, C c\u1EE7a tam gi\xE1c). \u0110\u1EB7t coord h\u1EE3p l\xFD quanh g\u1ED1c (-5..5).
3024
+ 4. M\u1ECDi \u0111i\u1EC3m + h\xECnh ph\u1EA3i c\xF3 \`name\` (label "A", "M", "O\u2081", ...). Tham chi\u1EBFu b\u1EB1ng name, kh\xF4ng ph\u1EA3i id.
3025
+ 5. Tam gi\xE1c: emit c\u1EA3 \`polygon\` (v\u1EBD vi\u1EC1n) + segment/\u0111\u01B0\u1EDDng ph\u1EE5 ri\xEAng n\u1EBFu \u0111\u1EC1 y\xEAu c\u1EA7u (\u0111\u01B0\u1EDDng cao, trung tuy\u1EBFn).
3026
+ 6. \u0110\u01B0\u1EDDng tr\xF2n (O; R) cho tr\u01B0\u1EDBc b\xE1n k\xEDnh s\u1ED1: emit anchor helper tr\xEAn \u0111\u01B0\u1EDDng tr\xF2n r\u1ED3i d\xF9ng \`circleCP\` (DSL kh\xF4ng h\u1ED7 tr\u1EE3 radius numeric tr\u1EF1c ti\u1EBFp).
3027
+ 7. N\u1EBFu \u0111\u1EC1 m\u01A1 h\u1ED3: ch\u1ECDn case ph\u1ED5 bi\u1EBFn nh\u1EA5t, kh\xF4ng h\u1ECFi l\u1EA1i.
3028
+
3029
+ ## Primitives s\u1EB5n c\xF3
3030
+ **Points:** free, midpoint, onSegment, onLine, onCircle, perpFoot, circumcenter, incenter, centroid, orthocenter, intersection
3031
+ **Shapes:** segment, line, ray, polygon, perpendicular, parallel, perpBisector, angleBisector, tangent, circleCP, circle3
3032
+
3033
+ ## 9 v\xED d\u1EE5
3034
+ ${examples}
3035
+
3036
+ ## Khi kh\xF4ng v\u1EBD \u0111\u01B0\u1EE3c
3037
+ G\u1ECDi \`refuse\` v\u1EDBi \`reason\` ti\u1EBFng Vi\u1EC7t gi\u1EA3i th\xEDch c\u1EE5 th\u1EC3 (vd: "\u0110\u1EC1 thu\u1ED9c l\u1EDBp 11, ngo\xE0i ph\u1EA1m vi MVP" ho\u1EB7c "\u0110\u1EC1 kh\xF4ng r\xF5 v\u1ECB tr\xED \u0111i\u1EC3m M").`;
3038
+ }
3039
+ var BUILD_FIGURE_TOOL = {
3040
+ name: "build_figure",
3041
+ description: "V\u1EBD h\xECnh h\u1ECDc 2D theo \u0111\u1EC1 b\xE0i. Emit DSL JSON m\xF4 t\u1EA3 c\xE1c \u0111i\u1EC3m v\xE0 h\xECnh.",
3042
+ input_schema: zodToJsonSchema(DslInput, {
3043
+ target: "jsonSchema7",
3044
+ $refStrategy: "none"
3045
+ })
3046
+ };
3047
+ var RefuseInputZ = z.object({
3048
+ reason: z.string().min(1).describe("L\xFD do kh\xF4ng v\u1EBD \u0111\u01B0\u1EE3c (ti\u1EBFng Vi\u1EC7t)")
3049
+ });
3050
+ var REFUSE_TOOL = {
3051
+ name: "refuse",
3052
+ description: "T\u1EEB ch\u1ED1i khi kh\xF4ng v\u1EBD \u0111\u01B0\u1EE3c ho\u1EB7c \u0111\u1EC1 ngo\xE0i ph\u1EA1m vi (3D, l\u01B0\u1EE3ng gi\xE1c, l\u1EDBp 11+).",
3053
+ input_schema: zodToJsonSchema(RefuseInputZ, { target: "jsonSchema7" })
3054
+ };
3055
+ var TOOLS = [BUILD_FIGURE_TOOL, REFUSE_TOOL];
3056
+
3057
+ // src/stamps/geometry-2d/ai/buildFigure.ts
3058
+ var DEFAULT_MODEL = "claude-opus-4-7";
3059
+ var DEFAULT_MAX_TOKENS = 8192;
3060
+ function toUsage(u) {
3061
+ return {
3062
+ inputTokens: u.input_tokens,
3063
+ outputTokens: u.output_tokens,
3064
+ cacheReadTokens: u.cache_read_input_tokens ?? 0,
3065
+ cacheCreationTokens: u.cache_creation_input_tokens ?? 0
3066
+ };
3067
+ }
3068
+ async function generateFigure(problem, opts) {
3069
+ if (!opts.apiKey) {
3070
+ return { ok: false, reason: "api_error", message: "apiKey b\u1EAFt bu\u1ED9c" };
3071
+ }
3072
+ if (!problem || !problem.trim()) {
3073
+ return { ok: false, reason: "api_error", message: "\u0110\u1EC1 b\xE0i r\u1ED7ng" };
3074
+ }
3075
+ const systemText = buildSystemPrompt();
3076
+ const enableCaching = opts.enableCaching !== false;
3077
+ const systemBlock = enableCaching ? { type: "text", text: systemText, cache_control: { type: "ephemeral" } } : { type: "text", text: systemText };
3078
+ let response;
3079
+ try {
3080
+ response = await callProvider({
3081
+ apiKey: opts.apiKey,
3082
+ model: opts.model ?? DEFAULT_MODEL,
3083
+ maxTokens: opts.maxTokens ?? DEFAULT_MAX_TOKENS,
3084
+ system: [systemBlock],
3085
+ tools: TOOLS,
3086
+ toolChoice: { type: "any" },
3087
+ messages: [{ role: "user", content: problem }],
3088
+ signal: opts.signal
3089
+ });
3090
+ } catch (e) {
3091
+ const err = e;
3092
+ return {
3093
+ ok: false,
3094
+ reason: "api_error",
3095
+ message: err.message ?? "L\u1ED7i g\u1ECDi Claude API",
3096
+ ...err.status !== void 0 ? { status: err.status } : {}
3097
+ };
3098
+ }
3099
+ const usage = toUsage(response.usage);
3100
+ const toolUse = response.content.find((c) => c.type === "tool_use");
3101
+ if (!toolUse || toolUse.type !== "tool_use") {
3102
+ const text = response.content.find((c) => c.type === "text");
3103
+ const textStr = text?.type === "text" ? text.text : "(empty)";
3104
+ return {
3105
+ ok: false,
3106
+ reason: "parse_error",
3107
+ message: "AI kh\xF4ng g\u1ECDi tool n\xE0o. Response: " + textStr,
3108
+ raw: response.content,
3109
+ usage
3110
+ };
3111
+ }
3112
+ if (toolUse.name === "refuse") {
3113
+ const input = toolUse.input;
3114
+ return {
3115
+ ok: false,
3116
+ reason: "refused",
3117
+ message: input.reason ?? "AI t\u1EEB ch\u1ED1i kh\xF4ng n\xEAu l\xFD do",
3118
+ usage
3119
+ };
3120
+ }
3121
+ if (toolUse.name !== "build_figure") {
3122
+ return {
3123
+ ok: false,
3124
+ reason: "parse_error",
3125
+ message: `Tool kh\xF4ng x\xE1c \u0111\u1ECBnh: "${toolUse.name}"`,
3126
+ raw: toolUse,
3127
+ usage
3128
+ };
3129
+ }
3130
+ const tResult = transpile(toolUse.input);
3131
+ if (!tResult.ok) {
3132
+ return {
3133
+ ok: false,
3134
+ reason: "transpile_error",
3135
+ message: "DSL t\u1EEB AI kh\xF4ng h\u1EE3p l\u1EC7",
3136
+ errors: tResult.errors,
3137
+ dsl: toolUse.input,
3138
+ usage
3139
+ };
3140
+ }
3141
+ return {
3142
+ ok: true,
3143
+ state: tResult.state,
3144
+ dsl: toolUse.input,
3145
+ usage
3146
+ };
3147
+ }
2025
3148
 
2026
- export { ALL_STAMPS, DEFAULT_STAMPS, EXPERIMENTAL_STAMPS, STABLE_STAMPS, Whiteboard, closePdfDocument, configurePdfWorker, findStampForCustomData, insertPdfPages, insertRasterizedPagesIntoScene, isStampElement, loadPdfDocument, parsePageRange, pickSyncableAppState, rasterizePdf, restoreMissingStampFiles };
3149
+ export { ALL_STAMPS, DEFAULT_STAMPS, EXPERIMENTAL_STAMPS, STABLE_STAMPS, STAMP_CATALOG, Whiteboard, closePdfDocument, configurePdfWorker, findCatalogEntry, findStampForCustomData, generateFigure, insertPdfPages, insertRasterizedPagesIntoScene, isStampElement, loadPdfDocument, parsePageRange, pickSyncableAppState, rasterizePdf, restoreMissingStampFiles };
2027
3150
  //# sourceMappingURL=index.mjs.map
2028
3151
  //# sourceMappingURL=index.mjs.map