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