@xom11/whiteboard 0.6.5 → 0.9.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 (66) hide show
  1. package/README.md +87 -1
  2. package/dist/chunk-74VEEZBV.mjs +619 -0
  3. package/dist/chunk-74VEEZBV.mjs.map +1 -0
  4. package/dist/chunk-7P7SQFOW.mjs +39 -0
  5. package/dist/chunk-7P7SQFOW.mjs.map +1 -0
  6. package/dist/chunk-C6SCVOMC.mjs +111 -0
  7. package/dist/chunk-C6SCVOMC.mjs.map +1 -0
  8. package/dist/chunk-DU2NFHRR.mjs +103 -0
  9. package/dist/chunk-DU2NFHRR.mjs.map +1 -0
  10. package/dist/chunk-DU3RHKT5.mjs +44 -0
  11. package/dist/chunk-DU3RHKT5.mjs.map +1 -0
  12. package/dist/chunk-HTBLO5JO.mjs +41 -0
  13. package/dist/chunk-HTBLO5JO.mjs.map +1 -0
  14. package/dist/chunk-IUVV52HO.mjs +144 -0
  15. package/dist/chunk-IUVV52HO.mjs.map +1 -0
  16. package/dist/chunk-KEYZ5EZT.mjs +154 -0
  17. package/dist/chunk-KEYZ5EZT.mjs.map +1 -0
  18. package/dist/chunk-P2AOIF7S.mjs +40 -0
  19. package/dist/chunk-P2AOIF7S.mjs.map +1 -0
  20. package/dist/chunk-SBDMF4NQ.mjs +212 -0
  21. package/dist/chunk-SBDMF4NQ.mjs.map +1 -0
  22. package/dist/chunk-X5R72SSJ.mjs +52 -0
  23. package/dist/chunk-X5R72SSJ.mjs.map +1 -0
  24. package/dist/chunk-ZVN356JZ.mjs +58 -0
  25. package/dist/chunk-ZVN356JZ.mjs.map +1 -0
  26. package/dist/geometry-2d.d.mts +16 -0
  27. package/dist/geometry-2d.d.ts +16 -0
  28. package/dist/geometry-2d.js +3581 -0
  29. package/dist/geometry-2d.js.map +1 -0
  30. package/dist/geometry-2d.mjs +7 -0
  31. package/dist/geometry-2d.mjs.map +1 -0
  32. package/dist/geometry-3d.d.mts +16 -0
  33. package/dist/geometry-3d.d.ts +16 -0
  34. package/dist/geometry-3d.js +4105 -0
  35. package/dist/geometry-3d.js.map +1 -0
  36. package/dist/geometry-3d.mjs +7 -0
  37. package/dist/geometry-3d.mjs.map +1 -0
  38. package/dist/graph-2d.d.mts +16 -0
  39. package/dist/graph-2d.d.ts +16 -0
  40. package/dist/graph-2d.js +2019 -0
  41. package/dist/graph-2d.js.map +1 -0
  42. package/dist/graph-2d.mjs +6 -0
  43. package/dist/graph-2d.mjs.map +1 -0
  44. package/dist/host-LZH2FZ2N.mjs +1066 -0
  45. package/dist/host-LZH2FZ2N.mjs.map +1 -0
  46. package/dist/host-PIIDSMVE.mjs +3187 -0
  47. package/dist/host-PIIDSMVE.mjs.map +1 -0
  48. package/dist/host-VDNAJMLC.mjs +2864 -0
  49. package/dist/host-VDNAJMLC.mjs.map +1 -0
  50. package/dist/host-Z3TEJKZA.mjs +466 -0
  51. package/dist/host-Z3TEJKZA.mjs.map +1 -0
  52. package/dist/index.d.mts +30 -148
  53. package/dist/index.d.ts +30 -148
  54. package/dist/index.js +8370 -5614
  55. package/dist/index.js.map +1 -1
  56. package/dist/index.mjs +395 -7294
  57. package/dist/index.mjs.map +1 -1
  58. package/dist/latex.d.mts +15 -0
  59. package/dist/latex.d.ts +15 -0
  60. package/dist/latex.js +750 -0
  61. package/dist/latex.js.map +1 -0
  62. package/dist/latex.mjs +6 -0
  63. package/dist/latex.mjs.map +1 -0
  64. package/dist/types-CinstD7T.d.mts +110 -0
  65. package/dist/types-CinstD7T.d.ts +110 -0
  66. package/package.json +26 -7
@@ -0,0 +1,2864 @@
1
+ "use client";
2
+ import { serializeBoard, renderGeometrySvgFromState, isGeometryCustomData, safeJsx } from './chunk-KEYZ5EZT.mjs';
3
+ import { useChordShortcut, MobileToolDrawer } from './chunk-SBDMF4NQ.mjs';
4
+ import { resolveAttrColors, paletteFor, themeLabel, themeAxis, themeGrid } from './chunk-HTBLO5JO.mjs';
5
+ import { useIsMobile } from './chunk-P2AOIF7S.mjs';
6
+ import { insertStampImage } from './chunk-C6SCVOMC.mjs';
7
+ import './chunk-BJTO5JO5.mjs';
8
+ import { forwardRef, useRef, useState, useEffect, useCallback, useImperativeHandle, useMemo, useId, useLayoutEffect } from 'react';
9
+ import { createPortal } from 'react-dom';
10
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
11
+
12
+ // src/stamps/geometry-2d/editor/transforms.ts
13
+ var LINE_LIKE = /* @__PURE__ */ new Set(["line", "segment", "arrow"]);
14
+ function copyVisAttrs(obj) {
15
+ const v = obj?.visProp ?? {};
16
+ const pick = (k) => v?.[k];
17
+ const out = {};
18
+ const mapping = [
19
+ ["strokecolor", "strokeColor"],
20
+ ["strokewidth", "strokeWidth"],
21
+ ["strokeopacity", "strokeOpacity"],
22
+ ["dash", "dash"],
23
+ ["fillcolor", "fillColor"],
24
+ ["fillopacity", "fillOpacity"]
25
+ ];
26
+ for (const [from, to] of mapping) {
27
+ const val = pick(from);
28
+ if (val !== void 0) out[to] = val;
29
+ }
30
+ return out;
31
+ }
32
+ function getDefiningPoints(obj) {
33
+ if (!obj) return null;
34
+ const e = (obj.elType ?? obj.type ?? "").toString().toLowerCase();
35
+ if (e === "point" || e === "glider" || e === "midpoint") {
36
+ return { kind: "point", points: [obj], attrs: copyVisAttrs(obj) };
37
+ }
38
+ if (LINE_LIKE.has(e) && obj.point1 && obj.point2) {
39
+ const kind = e === "segment" ? "segment" : e === "arrow" ? "arrow" : "line";
40
+ return { kind, points: [obj.point1, obj.point2], attrs: copyVisAttrs(obj) };
41
+ }
42
+ if (e === "circle" && obj.center && obj.point2) {
43
+ return { kind: "circleCenter", points: [obj.center, obj.point2], attrs: copyVisAttrs(obj) };
44
+ }
45
+ if (e === "circumcircle" && obj.point1 && obj.point2 && obj.point3) {
46
+ return {
47
+ kind: "circle3",
48
+ points: [obj.point1, obj.point2, obj.point3],
49
+ attrs: copyVisAttrs(obj)
50
+ };
51
+ }
52
+ return null;
53
+ }
54
+ function buildTransformSpec(input) {
55
+ switch (input.kind) {
56
+ case "translate": {
57
+ const [a, b] = input.vectorPoints;
58
+ const dx = b.X() - a.X();
59
+ const dy = b.Y() - a.Y();
60
+ return { params: [dx, dy], attrs: { type: "translate" } };
61
+ }
62
+ case "rotate":
63
+ return {
64
+ params: [input.angleDeg * Math.PI / 180, input.center],
65
+ attrs: { type: "rotate" }
66
+ };
67
+ case "reflectLine":
68
+ return { params: [input.line], attrs: { type: "reflect" } };
69
+ case "reflectPoint":
70
+ return { params: [Math.PI, input.center], attrs: { type: "rotate" } };
71
+ case "dilate":
72
+ return {
73
+ params: [],
74
+ attrs: { type: "scale" },
75
+ chain: [
76
+ { params: [-input.center.X(), -input.center.Y()], attrs: { type: "translate" } },
77
+ { params: [input.k, input.k], attrs: { type: "scale" } },
78
+ { params: [input.center.X(), input.center.Y()], attrs: { type: "translate" } }
79
+ ]
80
+ };
81
+ }
82
+ }
83
+ var Icon = {
84
+ cursor: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "M4 4 L20 12 L13 13 L11 20 Z" }) }),
85
+ select: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
86
+ /* @__PURE__ */ jsx("path", { d: "M4 4 L20 12 L13 13 L11 20 Z", fill: "none" }),
87
+ /* @__PURE__ */ jsx("rect", { x: "2.5", y: "2.5", width: "19", height: "19", strokeDasharray: "3 2", fill: "none" })
88
+ ] }),
89
+ point: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "4", fill: "currentColor" }) }),
90
+ midpoint: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
91
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
92
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "2.5", fill: "currentColor", stroke: "none" }),
93
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "12", r: "1.6", fill: "currentColor", stroke: "none" }),
94
+ /* @__PURE__ */ jsx("circle", { cx: "20", cy: "12", r: "1.6", fill: "currentColor", stroke: "none" })
95
+ ] }),
96
+ segment: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
97
+ /* @__PURE__ */ jsx("line", { x1: "5", y1: "18", x2: "19", y2: "6" }),
98
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "18", r: "1.7", fill: "currentColor" }),
99
+ /* @__PURE__ */ jsx("circle", { cx: "19", cy: "6", r: "1.7", fill: "currentColor" })
100
+ ] }),
101
+ line: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
102
+ /* @__PURE__ */ jsx("line", { x1: "2", y1: "20", x2: "22", y2: "4" }),
103
+ /* @__PURE__ */ jsx("circle", { cx: "8", cy: "16", r: "1.6", fill: "currentColor" }),
104
+ /* @__PURE__ */ jsx("circle", { cx: "16", cy: "8", r: "1.6", fill: "currentColor" })
105
+ ] }),
106
+ ray: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
107
+ /* @__PURE__ */ jsx("line", { x1: "5", y1: "19", x2: "22", y2: "2" }),
108
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "19", r: "1.7", fill: "currentColor" }),
109
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "1.5", fill: "currentColor" })
110
+ ] }),
111
+ vector: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
112
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "20", y2: "4" }),
113
+ /* @__PURE__ */ jsx("polyline", { points: "14,4 20,4 20,10" })
114
+ ] }),
115
+ perpendicular: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
116
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "18", x2: "21", y2: "18" }),
117
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "18", x2: "12", y2: "4" }),
118
+ /* @__PURE__ */ jsx("rect", { x: "12", y: "14", width: "4", height: "4", fill: "none" })
119
+ ] }),
120
+ parallel: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
121
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "9", x2: "21", y2: "5" }),
122
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "19", x2: "21", y2: "15" })
123
+ ] }),
124
+ perpBisector: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
125
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" }),
126
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "4", x2: "12", y2: "22", strokeDasharray: "3 2" }),
127
+ /* @__PURE__ */ jsx("circle", { cx: "6", cy: "18", r: "1.5", fill: "currentColor" }),
128
+ /* @__PURE__ */ jsx("circle", { cx: "18", cy: "18", r: "1.5", fill: "currentColor" })
129
+ ] }),
130
+ bisector: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
131
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "20", y2: "4" }),
132
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "20", y2: "20" }),
133
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "22", y2: "12", strokeDasharray: "3 2" })
134
+ ] }),
135
+ polygon: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("polygon", { points: "6,6 18,6 22,14 12,22 4,14" }) }),
136
+ regularPolygon: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("polygon", { points: "12,3 20,8 20,17 12,22 4,17 4,8" }) }),
137
+ circleCenter: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
138
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "8" }),
139
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "1.6", fill: "currentColor" })
140
+ ] }),
141
+ circle3: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
142
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "8" }),
143
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "4", r: "1.5", fill: "currentColor" }),
144
+ /* @__PURE__ */ jsx("circle", { cx: "20", cy: "14", r: "1.5", fill: "currentColor" }),
145
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "16", r: "1.5", fill: "currentColor" })
146
+ ] }),
147
+ tangent: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
148
+ /* @__PURE__ */ jsx("circle", { cx: "11", cy: "13", r: "6" }),
149
+ /* @__PURE__ */ jsx("line", { x1: "2", y1: "20", x2: "22", y2: "2" })
150
+ ] }),
151
+ angle: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
152
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "20", y2: "20" }),
153
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "20", y2: "6" }),
154
+ /* @__PURE__ */ jsx("path", { d: "M14 20 A 10 10 0 0 0 11 13" })
155
+ ] }),
156
+ distance: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
157
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
158
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "8", x2: "4", y2: "16" }),
159
+ /* @__PURE__ */ jsx("line", { x1: "20", y1: "8", x2: "20", y2: "16" })
160
+ ] }),
161
+ area: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: /* @__PURE__ */ jsx("polygon", { points: "5,6 19,6 21,14 13,21 3,15", fill: "currentColor", fillOpacity: "0.2" }) }),
162
+ toggleLabel: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
163
+ /* @__PURE__ */ jsx("text", { x: "3", y: "18", fontSize: "16", fontFamily: "serif", fontWeight: "700", fill: "currentColor", stroke: "none", children: "A" }),
164
+ /* @__PURE__ */ jsx("text", { x: "13", y: "14", fontSize: "11", fontFamily: "serif", fontWeight: "700", fill: "currentColor", stroke: "none", children: "A" })
165
+ ] }),
166
+ toggleVisible: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
167
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3.5", fill: "currentColor", fillOpacity: "0.4" }),
168
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3.5" }),
169
+ /* @__PURE__ */ jsx("circle", { cx: "20", cy: "6", r: "1.5", fill: "currentColor" })
170
+ ] }),
171
+ trash: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
172
+ /* @__PURE__ */ jsx("polyline", { points: "3,6 5,6 21,6" }),
173
+ /* @__PURE__ */ jsx("path", { d: "M19 6 l-1 14 a 2 2 0 0 1 -2 2 H 8 a 2 2 0 0 1 -2 -2 l-1 -14" }),
174
+ /* @__PURE__ */ jsx("line", { x1: "10", y1: "11", x2: "10", y2: "17" }),
175
+ /* @__PURE__ */ jsx("line", { x1: "14", y1: "11", x2: "14", y2: "17" })
176
+ ] }),
177
+ translate: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
178
+ /* @__PURE__ */ jsx("path", { d: "M4 4 L20 20" }),
179
+ /* @__PURE__ */ jsx("polygon", { points: "14,4 20,4 20,10", fill: "currentColor" }),
180
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "5", r: "1.5", fill: "currentColor" })
181
+ ] }),
182
+ rotate: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
183
+ /* @__PURE__ */ jsx("path", { d: "M4 12 A8 8 0 1 1 12 20" }),
184
+ /* @__PURE__ */ jsx("polyline", { points: "4,9 4,13 8,13" })
185
+ ] }),
186
+ reflectLine: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
187
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "2", x2: "12", y2: "22", strokeDasharray: "3 2" }),
188
+ /* @__PURE__ */ jsx("polygon", { points: "4,6 9,12 4,18", fill: "currentColor" }),
189
+ /* @__PURE__ */ jsx("polygon", { points: "20,6 15,12 20,18", fill: "currentColor" })
190
+ ] }),
191
+ reflectPoint: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
192
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "1.5", fill: "currentColor" }),
193
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "5", r: "1.6", fill: "currentColor" }),
194
+ /* @__PURE__ */ jsx("circle", { cx: "19", cy: "19", r: "1.6", fill: "currentColor" }),
195
+ /* @__PURE__ */ jsx("line", { x1: "5", y1: "5", x2: "19", y2: "19", strokeDasharray: "2 2" })
196
+ ] }),
197
+ dilate: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", children: [
198
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "1.5", fill: "currentColor" }),
199
+ /* @__PURE__ */ jsx("polygon", { points: "6,18 18,18 12,6", fillOpacity: "0.1", fill: "currentColor" }),
200
+ /* @__PURE__ */ jsx("polygon", { points: "9,15 15,15 12,11", fill: "currentColor" })
201
+ ] })
202
+ };
203
+ var TOOLS = [
204
+ { key: "move", label: "Di chuy\u1EC3n", hint: "K\xE9o \u0111i\u1EC3m ho\u1EB7c xoay n\u1EC1n", icon: Icon.cursor, group: "move", needs: 0 },
205
+ { key: "select", label: "Ch\u1ECDn", hint: "Click \u0111\u1EC3 ch\u1ECDn 1 / Shift+click \u0111\u1EC3 b\u1ECF th\xEAm / K\xE9o n\u1EC1n \u0111\u1EC3 khoanh v\xF9ng / DEL \u0111\u1EC3 xo\xE1", icon: Icon.select, group: "move", needs: 0 },
206
+ { key: "point", label: "\u0110i\u1EC3m m\u1EDBi", hint: "Click \u0111\u1EC3 th\xEAm \u0111i\u1EC3m", icon: Icon.point, group: "point", needs: 1 },
207
+ { key: "midpoint", label: "Trung \u0111i\u1EC3m", hint: "Click 2 \u0111i\u1EC3m c\xF3 s\u1EB5n", icon: Icon.midpoint, group: "point", needs: 2, accepts: ["point", "point"] },
208
+ { key: "segment", label: "\u0110o\u1EA1n th\u1EB3ng", hint: "Click 2 \u0111i\u1EC3m", icon: Icon.segment, group: "line", needs: 2 },
209
+ { key: "line", label: "\u0110\u01B0\u1EDDng th\u1EB3ng qua 2 \u0111i\u1EC3m", hint: "Click 2 \u0111i\u1EC3m", icon: Icon.line, group: "line", needs: 2 },
210
+ { key: "ray", label: "Tia qua 2 \u0111i\u1EC3m", hint: "Click 2 \u0111i\u1EC3m", icon: Icon.ray, group: "line", needs: 2 },
211
+ { key: "vector", label: "Vector", hint: "Click 2 \u0111i\u1EC3m", icon: Icon.vector, group: "line", needs: 2 },
212
+ { key: "perpendicular", label: "\u0110\u01B0\u1EDDng vu\xF4ng g\xF3c", hint: "Click 1 \u0111i\u1EC3m + 1 \u0111\u01B0\u1EDDng c\xF3 s\u1EB5n", icon: Icon.perpendicular, group: "construct", needs: 2, accepts: ["point", "line"] },
213
+ { key: "parallel", label: "\u0110\u01B0\u1EDDng song song", hint: "Click 1 \u0111i\u1EC3m + 1 \u0111\u01B0\u1EDDng c\xF3 s\u1EB5n", icon: Icon.parallel, group: "construct", needs: 2, accepts: ["point", "line"] },
214
+ { key: "perpBisector", label: "\u0110\u01B0\u1EDDng trung tr\u1EF1c", hint: "Click 2 \u0111i\u1EC3m c\xF3 s\u1EB5n", icon: Icon.perpBisector, group: "construct", needs: 2, accepts: ["point", "point"] },
215
+ { key: "angleBisector", label: "\u0110\u01B0\u1EDDng ph\xE2n gi\xE1c", hint: "Click 3 \u0111i\u1EC3m c\xF3 s\u1EB5n (\u0111\u1EC9nh \u1EDF gi\u1EEFa)", icon: Icon.bisector, group: "construct", needs: 3, accepts: ["point", "point", "point"] },
216
+ { key: "polygon", label: "\u0110a gi\xE1c", hint: "Click c\xE1c \u0111i\u1EC3m, click l\u1EA1i \u0111i\u1EC3m \u0111\u1EA7u \u0111\u1EC3 \u0111\xF3ng", icon: Icon.polygon, group: "polygon", needs: -1 },
217
+ { key: "regularPolygon", label: "\u0110a gi\xE1c \u0111\u1EC1u", hint: "Click 2 \u0111i\u1EC3m r\u1ED3i nh\u1EADp s\u1ED1 c\u1EA1nh", icon: Icon.regularPolygon, group: "polygon", needs: 2, accepts: ["point", "point"] },
218
+ { key: "circleCenter", label: "\u0110\u01B0\u1EDDng tr\xF2n (t\xE2m + \u0111i\u1EC3m)", hint: "Click t\xE2m r\u1ED3i 1 \u0111i\u1EC3m tr\xEAn \u0111\u01B0\u1EDDng tr\xF2n", icon: Icon.circleCenter, group: "circle", needs: 2 },
219
+ { key: "circle3", label: "\u0110\u01B0\u1EDDng tr\xF2n qua 3 \u0111i\u1EC3m", hint: "Click 3 \u0111i\u1EC3m", icon: Icon.circle3, group: "circle", needs: 3 },
220
+ { key: "tangent", label: "Ti\u1EBFp tuy\u1EBFn", hint: "Click 1 \u0111i\u1EC3m + 1 \u0111\u01B0\u1EDDng tr\xF2n c\xF3 s\u1EB5n", icon: Icon.tangent, group: "circle", needs: 2, accepts: ["point", "circle"] },
221
+ { key: "angle", label: "G\xF3c", hint: "Click 3 \u0111i\u1EC3m c\xF3 s\u1EB5n (\u0111\u1EC9nh \u1EDF gi\u1EEFa)", icon: Icon.angle, group: "measure", needs: 3, accepts: ["point", "point", "point"] },
222
+ { key: "distance", label: "Kho\u1EA3ng c\xE1ch", hint: "Click 2 \u0111i\u1EC3m c\xF3 s\u1EB5n", icon: Icon.distance, group: "measure", needs: 2, accepts: ["point", "point"] },
223
+ { key: "area", label: "Di\u1EC7n t\xEDch", hint: "Click c\xE1c \u0111\u1EC9nh, click l\u1EA1i \u0111i\u1EC3m \u0111\u1EA7u \u0111\u1EC3 \u0111\xF3ng", icon: Icon.area, group: "measure", needs: -1 },
224
+ { key: "toggleLabel", label: "Hi\u1EC7n/\u1EA9n t\xEAn", hint: "Click v\xE0o \u0111\u1ED1i t\u01B0\u1EE3ng", icon: Icon.toggleLabel, group: "edit", needs: 1, accepts: ["any"] },
225
+ { key: "toggleVisible", label: "Hi\u1EC7n/\u1EA9n \u0111\u1ED1i t\u01B0\u1EE3ng", hint: "Click v\xE0o \u0111\u1ED1i t\u01B0\u1EE3ng", icon: Icon.toggleVisible, group: "edit", needs: 1, accepts: ["any"] },
226
+ { key: "delete", label: "Xo\xE1", hint: "Click v\xE0o \u0111\u1ED1i t\u01B0\u1EE3ng", icon: Icon.trash, group: "edit", needs: 1, accepts: ["any"] },
227
+ { key: "translate", label: "Ph\xE9p t\u1ECBnh ti\u1EBFn", hint: "Click object \u2192 2 \u0111i\u1EC3m t\u1EA1o vector", icon: Icon.translate, group: "transform", needs: 3, accepts: ["any", "point", "point"] },
228
+ { key: "rotate", label: "Quay \u0111\u1ED1i t\u01B0\u1EE3ng", hint: "Click object \u2192 t\xE2m quay \u2192 nh\u1EADp g\xF3c", icon: Icon.rotate, group: "transform", needs: 2, accepts: ["any", "point"] },
229
+ { key: "reflectLine", label: "\u0110\u1ED1i x\u1EE9ng qua \u0111\u01B0\u1EDDng th\u1EB3ng", hint: "Click object \u2192 \u0111\u01B0\u1EDDng th\u1EB3ng", icon: Icon.reflectLine, group: "transform", needs: 2, accepts: ["any", "line"] },
230
+ { key: "reflectPoint", label: "\u0110\u1ED1i x\u1EE9ng qua \u0111i\u1EC3m", hint: "Click object \u2192 t\xE2m \u0111\u1ED1i x\u1EE9ng", icon: Icon.reflectPoint, group: "transform", needs: 2, accepts: ["any", "point"] },
231
+ { key: "dilate", label: "Ph\xE9p v\u1ECB t\u1EF1", hint: "Click object \u2192 t\xE2m \u2192 nh\u1EADp t\u1EF7 s\u1ED1 k", icon: Icon.dilate, group: "transform", needs: 2, accepts: ["any", "point"] }
232
+ ];
233
+ var GROUP_LABELS = {
234
+ move: "C\u01A1 b\u1EA3n",
235
+ point: "\u0110i\u1EC3m",
236
+ line: "\u0110\u01B0\u1EDDng",
237
+ construct: "D\u1EF1ng h\xECnh",
238
+ polygon: "\u0110a gi\xE1c",
239
+ circle: "\u0110\u01B0\u1EDDng tr\xF2n",
240
+ measure: "\u0110o l\u01B0\u1EDDng",
241
+ edit: "Ch\u1EC9nh s\u1EEDa",
242
+ transform: "Ph\xE9p bi\u1EBFn h\xECnh"
243
+ };
244
+ var GROUP_ORDER = [
245
+ "move",
246
+ "point",
247
+ "line",
248
+ "construct",
249
+ "polygon",
250
+ "circle",
251
+ "measure",
252
+ "edit",
253
+ "transform"
254
+ ];
255
+ var A_CODE = "A".charCodeAt(0);
256
+ function letterForGroup(g) {
257
+ const idx = GROUP_ORDER.indexOf(g);
258
+ return idx >= 0 ? String.fromCharCode(A_CODE + idx) : "";
259
+ }
260
+ function objKind(obj) {
261
+ if (!obj) return "other";
262
+ const e = (obj.elType || obj.type || "").toString().toLowerCase();
263
+ if (e === "point" || e === "glider" || e === "midpoint") return "point";
264
+ if (e === "line" || e === "segment" || e === "arrow" || e === "axis" || e === "normal" || e === "parallel" || e === "perpendicular" || e === "tangent" || e === "bisector" || e === "perpendicularsegment") return "line";
265
+ if (e === "circle" || e === "circumcircle") return "circle";
266
+ return "other";
267
+ }
268
+
269
+ // src/stamps/geometry-2d/editor/handlers.ts
270
+ function handleDown(ctx, e) {
271
+ if (!ctx.boardRef.current) return;
272
+ const t = ctx.toolRef.current;
273
+ if (t === "move") {
274
+ const sc = ctx.screenCoordsOf(e);
275
+ if (!sc) return;
276
+ const [sx, sy] = sc;
277
+ ctx.moveDownRef.current = { sx, sy };
278
+ return;
279
+ }
280
+ if (t === "select") {
281
+ const sc = ctx.screenCoordsOf(e);
282
+ if (!sc) return;
283
+ const [sx, sy] = sc;
284
+ const hits2 = ctx.objectsAt(e).map(ctx.promoteLabel).filter((o) => o !== ctx.axisObjsRef.current.x && o !== ctx.axisObjsRef.current.y);
285
+ const obj = hits2.find((o) => objKind(o) === "point") ?? hits2[0] ?? ctx.findNearestPoint(e, 12);
286
+ if (obj) {
287
+ const shift = !!(e.shiftKey || e.altKey);
288
+ ctx.toggleSelect(obj, shift);
289
+ ctx.moveDownRef.current = { sx, sy };
290
+ ctx.marqueeRef.current = null;
291
+ return;
292
+ }
293
+ ctx.marqueeRef.current = { startSx: sx, startSy: sy };
294
+ if (!(e.shiftKey || e.altKey)) ctx.clearSelection();
295
+ return;
296
+ }
297
+ const toolDef = TOOLS.find((td) => td.key === t);
298
+ if (!toolDef) return;
299
+ const coords = ctx.boardRef.current.getUsrCoordsOfMouse(e);
300
+ const x = coords[0], y = coords[1];
301
+ const hits = ctx.objectsAt(e).map(ctx.promoteLabel).filter((o) => o !== ctx.axisObjsRef.current.x && o !== ctx.axisObjsRef.current.y);
302
+ const bestHit = hits.find((o) => objKind(o) === "point") ?? hits[0] ?? null;
303
+ const snapPointForPointSlot = () => bestHit && objKind(bestHit) === "point" ? bestHit : ctx.findNearestPoint(e, 12);
304
+ if (t === "point") {
305
+ const curves = hits.filter((o) => objKind(o) === "line" || objKind(o) === "circle");
306
+ if (curves.length >= 2) {
307
+ const a = curves[0];
308
+ const b = curves[1];
309
+ const aId = ctx.localIdOf(a);
310
+ const bId = ctx.localIdOf(b);
311
+ if (aId && bId) {
312
+ const name2 = ctx.nextLabel();
313
+ const attrs = { name: name2, color: "@stroke", size: 3, fillColor: "@stroke", strokeColor: "@stroke" };
314
+ try {
315
+ const isLineLine = objKind(a) === "line" && objKind(b) === "line";
316
+ if (isLineLine) {
317
+ ctx.create("intersection", [aId, bId, 0], attrs);
318
+ } else {
319
+ const tmp0 = ctx.boardRef.current.create("intersection", [a, b, 0], { visible: false, withLabel: false });
320
+ const tmp1 = ctx.boardRef.current.create("intersection", [a, b, 1], { visible: false, withLabel: false });
321
+ const d0 = Math.hypot((tmp0.X?.() ?? 0) - x, (tmp0.Y?.() ?? 0) - y);
322
+ const d1 = Math.hypot((tmp1.X?.() ?? 0) - x, (tmp1.Y?.() ?? 0) - y);
323
+ safeJsx("handlers.removeObject(intersect.tmp0)", () => ctx.boardRef.current.removeObject(tmp0));
324
+ safeJsx("handlers.removeObject(intersect.tmp1)", () => ctx.boardRef.current.removeObject(tmp1));
325
+ const idx = d0 <= d1 ? 0 : 1;
326
+ ctx.create("intersection", [aId, bId, idx], attrs);
327
+ }
328
+ return;
329
+ } catch {
330
+ }
331
+ }
332
+ }
333
+ const name = ctx.nextLabel();
334
+ ctx.create("point", [x, y], { name, color: "@stroke", size: 3, fillColor: "@stroke", strokeColor: "@stroke" });
335
+ return;
336
+ }
337
+ if (toolDef.needs === 1 && toolDef.accepts) {
338
+ const hit = bestHit ?? ctx.findNearestPoint(e, 12);
339
+ if (hit) ctx.finalize(toolDef, [hit]);
340
+ else ctx.flashWarn("Click v\xE0o m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng \u0111\u1EC3 \xE1p d\u1EE5ng");
341
+ return;
342
+ }
343
+ if (toolDef.needs === -1) {
344
+ const snappedPoint = snapPointForPointSlot();
345
+ if (ctx.pendingRef.current.length >= 3 && snappedPoint && snappedPoint === ctx.pendingRef.current[0]) {
346
+ ctx.clearPreviewSegs();
347
+ ctx.finalize(toolDef, ctx.pendingRef.current);
348
+ ctx.clearPending();
349
+ return;
350
+ }
351
+ if (snappedPoint && ctx.pendingRef.current.includes(snappedPoint)) {
352
+ ctx.flashWarn("\u0110\u1EC9nh n\xE0y \u0111\xE3 c\xF3 \u2014 click \u0111i\u1EC3m kh\xE1c ho\u1EB7c click l\u1EA1i \u0111i\u1EC3m \u0111\u1EA7u \u0111\u1EC3 \u0111\xF3ng");
353
+ return;
354
+ }
355
+ const pick2 = snappedPoint ?? (() => {
356
+ const name = ctx.nextLabel();
357
+ return ctx.create("point", [x, y], { name, color: "@stroke", size: 3 });
358
+ })();
359
+ if (ctx.pendingRef.current.length > 0 && ctx.boardRef.current) {
360
+ const prev = ctx.pendingRef.current[ctx.pendingRef.current.length - 1];
361
+ safeJsx("handlers.createPreviewSegment", () => {
362
+ const seg = ctx.boardRef.current.create("segment", [prev, pick2], {
363
+ strokeColor: "#3b82f6",
364
+ strokeWidth: 1.5,
365
+ strokeOpacity: 0.75,
366
+ fixed: true,
367
+ highlight: false,
368
+ withLabel: false
369
+ });
370
+ ctx.previewSegRef.current.push(seg);
371
+ });
372
+ }
373
+ ctx.pendingRef.current.push(pick2);
374
+ ctx.setPendingCount(ctx.pendingRef.current.length);
375
+ return;
376
+ }
377
+ let pick = null;
378
+ if (toolDef.accepts) {
379
+ const usedKinds = ctx.pendingRef.current.map((p) => objKind(p));
380
+ const remaining = [...toolDef.accepts];
381
+ for (const u of usedKinds) {
382
+ if (u === "other") continue;
383
+ const i = remaining.indexOf(u);
384
+ if (i >= 0) remaining.splice(i, 1);
385
+ }
386
+ const strictPoint = hits.find((o) => objKind(o) === "point") ?? null;
387
+ const lineHit = hits.find((o) => objKind(o) === "line") ?? null;
388
+ const circleHit = hits.find((o) => objKind(o) === "circle") ?? null;
389
+ if (remaining.includes("point") && strictPoint) pick = strictPoint;
390
+ else if (remaining.includes("line") && lineHit) pick = lineHit;
391
+ else if (remaining.includes("circle") && circleHit) pick = circleHit;
392
+ else if (remaining.includes("any") && (strictPoint || lineHit || circleHit)) {
393
+ pick = strictPoint ?? lineHit ?? circleHit;
394
+ } else if (remaining.includes("point")) {
395
+ const near = ctx.findNearestPoint(e, 12);
396
+ if (near) pick = near;
397
+ }
398
+ if (!pick) {
399
+ const needs = remaining.map(
400
+ (k) => k === "point" ? "m\u1ED9t \u0111i\u1EC3m" : k === "line" ? "m\u1ED9t \u0111\u01B0\u1EDDng/\u0111o\u1EA1n" : k === "circle" ? "m\u1ED9t \u0111\u01B0\u1EDDng tr\xF2n" : "m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng"
401
+ );
402
+ ctx.flashWarn(`C\xF2n c\u1EA7n click v\xE0o ${needs.join(" + ")} c\xF3 s\u1EB5n`);
403
+ return;
404
+ }
405
+ if (ctx.pendingRef.current.includes(pick)) {
406
+ ctx.flashWarn("\u0110\xE3 ch\u1ECDn \u0111\u1ED1i t\u01B0\u1EE3ng n\xE0y \u2014 ch\u1ECDn \u0111\u1ED1i t\u01B0\u1EE3ng kh\xE1c");
407
+ return;
408
+ }
409
+ } else {
410
+ const snapped = snapPointForPointSlot();
411
+ if (snapped && ctx.pendingRef.current.includes(snapped)) {
412
+ ctx.flashWarn("\u0110\xE3 ch\u1ECDn \u0111i\u1EC3m n\xE0y \u2014 ch\u1ECDn \u0111i\u1EC3m kh\xE1c ho\u1EB7c click ch\u1ED7 tr\u1ED1ng");
413
+ return;
414
+ }
415
+ if (snapped) pick = snapped;
416
+ else {
417
+ const name = ctx.nextLabel();
418
+ pick = ctx.create("point", [x, y], { name, color: "@stroke", size: 3, fillColor: "@stroke", strokeColor: "@stroke" });
419
+ }
420
+ }
421
+ if (!pick) return;
422
+ ctx.pendingRef.current.push(pick);
423
+ ctx.setPendingCount(ctx.pendingRef.current.length);
424
+ if (ctx.pendingRef.current.length >= toolDef.needs) {
425
+ const tk = toolDef.key;
426
+ if (tk === "rotate" || tk === "dilate") {
427
+ const source = ctx.pendingRef.current[0];
428
+ const center = ctx.pendingRef.current[1];
429
+ const cx = (e.clientX ?? 0) + 8;
430
+ const cy = (e.clientY ?? 0) + 8;
431
+ ctx.pendingTransformRef.current = { tool: tk, source, center, anchorScreen: { x: cx, y: cy } };
432
+ ctx.emitTransform({ tool: tk, anchor: { x: cx, y: cy } });
433
+ return;
434
+ }
435
+ if (tk === "regularPolygon") {
436
+ const p1 = ctx.pendingRef.current[0];
437
+ const p2 = ctx.pendingRef.current[1];
438
+ const cx = (e.clientX ?? 0) + 8;
439
+ const cy = (e.clientY ?? 0) + 8;
440
+ ctx.pendingTransformRef.current = { tool: tk, source: p1, center: p2, anchorScreen: { x: cx, y: cy } };
441
+ ctx.emitTransform({ tool: tk, anchor: { x: cx, y: cy } });
442
+ return;
443
+ }
444
+ if (tk === "translate") {
445
+ const source = ctx.pendingRef.current[0];
446
+ const spec = buildTransformSpec({ kind: "translate", vectorPoints: [ctx.pendingRef.current[1], ctx.pendingRef.current[2]] });
447
+ ctx.finalizeTransformCreate(spec, source);
448
+ ctx.clearPending();
449
+ return;
450
+ }
451
+ if (tk === "reflectLine") {
452
+ const source = ctx.pendingRef.current[0];
453
+ const spec = buildTransformSpec({ kind: "reflectLine", line: ctx.pendingRef.current[1] });
454
+ ctx.finalizeTransformCreate(spec, source);
455
+ ctx.clearPending();
456
+ return;
457
+ }
458
+ if (tk === "reflectPoint") {
459
+ const source = ctx.pendingRef.current[0];
460
+ const spec = buildTransformSpec({ kind: "reflectPoint", center: ctx.pendingRef.current[1] });
461
+ ctx.finalizeTransformCreate(spec, source);
462
+ ctx.clearPending();
463
+ return;
464
+ }
465
+ ctx.finalize(toolDef, ctx.pendingRef.current);
466
+ ctx.clearPending();
467
+ } else {
468
+ ctx.refreshPreview();
469
+ }
470
+ }
471
+ function handleUp(ctx, e) {
472
+ const t = ctx.toolRef.current;
473
+ if (t === "select") {
474
+ const mq = ctx.marqueeRef.current;
475
+ ctx.marqueeRef.current = null;
476
+ ctx.moveDownRef.current = null;
477
+ if (!mq) return;
478
+ const sc2 = ctx.screenCoordsOf(e);
479
+ if (!sc2) return;
480
+ const [ex, ey] = sc2;
481
+ if (mq.rect) {
482
+ safeJsx("handlers.removeObject(marquee.rect)", () => ctx.boardRef.current?.removeObject(mq.rect));
483
+ }
484
+ if (Math.hypot(ex - mq.startSx, ey - mq.startSy) < 4) return;
485
+ const x1 = Math.min(mq.startSx, ex), x2 = Math.max(mq.startSx, ex);
486
+ const y1 = Math.min(mq.startSy, ey), y2 = Math.max(mq.startSy, ey);
487
+ const board = ctx.boardRef.current;
488
+ if (!board) return;
489
+ const list = board.objectsList || [];
490
+ for (const o of list) {
491
+ if (o === ctx.axisObjsRef.current.x || o === ctx.axisObjsRef.current.y) continue;
492
+ const kind = objKind(o);
493
+ if (kind === "point") {
494
+ const pc = o.coords?.scrCoords;
495
+ if (!pc) continue;
496
+ if (pc[1] >= x1 && pc[1] <= x2 && pc[2] >= y1 && pc[2] <= y2) {
497
+ if (!ctx.selectedSetRef.current.has(o)) {
498
+ ctx.selectedSetRef.current.add(o);
499
+ ctx.applySelectionStyle(o);
500
+ }
501
+ }
502
+ } else if (kind === "line" || kind === "circle") {
503
+ const defs = [o.point1, o.point2, o.center, o.midpoint, o.point3].filter(Boolean);
504
+ const anyInside = defs.some((p) => {
505
+ const pc = p?.coords?.scrCoords;
506
+ return pc && pc[1] >= x1 && pc[1] <= x2 && pc[2] >= y1 && pc[2] <= y2;
507
+ });
508
+ if (anyInside && !ctx.selectedSetRef.current.has(o)) {
509
+ ctx.selectedSetRef.current.add(o);
510
+ ctx.applySelectionStyle(o);
511
+ }
512
+ }
513
+ }
514
+ ctx.setSelectionTick((tt) => tt + 1);
515
+ safeJsx("handlers.board.update(marquee)", () => board.update());
516
+ return;
517
+ }
518
+ if (t !== "move") return;
519
+ const start = ctx.moveDownRef.current;
520
+ ctx.moveDownRef.current = null;
521
+ if (!start) return;
522
+ const sc = ctx.screenCoordsOf(e);
523
+ if (!sc) return;
524
+ const [sx, sy] = sc;
525
+ const moved = Math.hypot(sx - start.sx, sy - start.sy);
526
+ if (moved > 4) return;
527
+ const hits = ctx.objectsAt(e).map(ctx.promoteLabel).filter((o) => o !== ctx.axisObjsRef.current.x && o !== ctx.axisObjsRef.current.y);
528
+ const best = hits.find((o) => objKind(o) === "point") ?? hits[0] ?? ctx.findNearestPoint(e, 12);
529
+ if (!best) {
530
+ ctx.lastMoveClickRef.current = { obj: null, time: 0 };
531
+ return;
532
+ }
533
+ const now = Date.now();
534
+ const isDouble = ctx.lastMoveClickRef.current.obj === best && now - ctx.lastMoveClickRef.current.time < 400;
535
+ ctx.lastMoveClickRef.current = { obj: best, time: now };
536
+ if (!isDouble) return;
537
+ const cx = e.clientX ?? e.touches?.[0]?.clientX ?? 0;
538
+ const cy = e.clientY ?? e.touches?.[0]?.clientY ?? 0;
539
+ const snap = ctx.snapshotObject(best, { x: cx + 8, y: cy + 8 });
540
+ if (snap) ctx.emitSelect(snap);
541
+ }
542
+ function handleMove(ctx, e) {
543
+ if (ctx.toolRef.current === "select" && ctx.marqueeRef.current) {
544
+ const sc = ctx.screenCoordsOf(e);
545
+ if (sc && ctx.boardRef.current) {
546
+ const [sx, sy] = sc;
547
+ const { startSx, startSy } = ctx.marqueeRef.current;
548
+ const b = ctx.boardRef.current;
549
+ const ux1 = b.screenCoords2userCoords?.([Math.min(startSx, sx), Math.min(startSy, sy)]) ?? null;
550
+ const ux2 = b.screenCoords2userCoords?.([Math.max(startSx, sx), Math.max(startSy, sy)]) ?? null;
551
+ const toUsr = (px, py) => {
552
+ const ox = b.origin?.scrCoords?.[1] ?? 0;
553
+ const oy = b.origin?.scrCoords?.[2] ?? 0;
554
+ const ux = (px - ox) / b.unitX;
555
+ const uy = (oy - py) / b.unitY;
556
+ return [ux, uy];
557
+ };
558
+ const [x1u, y1u] = ux1 && ux1.length >= 2 ? [ux1[0], ux1[1]] : toUsr(Math.min(startSx, sx), Math.min(startSy, sy));
559
+ const [x2u, y2u] = ux2 && ux2.length >= 2 ? [ux2[0], ux2[1]] : toUsr(Math.max(startSx, sx), Math.max(startSy, sy));
560
+ const rect = ctx.marqueeRef.current.rect;
561
+ if (rect) {
562
+ safeJsx("handlers.removeObject(marquee.prevRect)", () => ctx.boardRef.current.removeObject(rect));
563
+ }
564
+ safeJsx("handlers.createMarqueePolygon", () => {
565
+ ctx.marqueeRef.current.rect = ctx.boardRef.current.create("polygon", [
566
+ [x1u, y1u],
567
+ [x2u, y1u],
568
+ [x2u, y2u],
569
+ [x1u, y2u]
570
+ ], {
571
+ fillColor: "#06b6d4",
572
+ fillOpacity: 0.08,
573
+ borders: { strokeColor: "#06b6d4", strokeWidth: 1, dash: 2 },
574
+ vertices: { visible: false },
575
+ fixed: true,
576
+ highlight: false,
577
+ withLabel: false
578
+ });
579
+ });
580
+ }
581
+ return;
582
+ }
583
+ const ph = ctx.phantomRef.current;
584
+ if (!ph || !ctx.boardRef.current) return;
585
+ if (ctx.previewRafRef.current != null) return;
586
+ ctx.previewRafRef.current = requestAnimationFrame(() => {
587
+ ctx.previewRafRef.current = null;
588
+ if (!ctx.boardRef.current || !ctx.phantomRef.current) return;
589
+ safeJsx("handlers.phantomMove", () => {
590
+ const coords = ctx.boardRef.current.getUsrCoordsOfMouse(e);
591
+ const JXG = ctx.jxgRef.current;
592
+ if (!JXG) return;
593
+ ctx.phantomRef.current.setPositionDirectly(JXG.COORDS_BY_USER, [coords[0], coords[1]]);
594
+ ctx.boardRef.current.update();
595
+ });
596
+ });
597
+ }
598
+ var JSXGraphMiniBoard = ({ onReady, initialState, isDark }) => {
599
+ const isDarkRef = useRef(!!isDark);
600
+ isDarkRef.current = !!isDark;
601
+ const containerId = useId().replace(/:/g, "_") + "_jxgmini";
602
+ const containerRef = useRef(null);
603
+ const boardRef = useRef(null);
604
+ const jxgRef = useRef(null);
605
+ const axisObjsRef = useRef({});
606
+ const creationLogRef = useRef([]);
607
+ const redoStackRef = useRef([]);
608
+ const [tool, setTool] = useState("move");
609
+ const toolRef = useRef("move");
610
+ toolRef.current = tool;
611
+ const [showAxis, setShowAxis] = useState(initialState?.showAxis ?? false);
612
+ const [showGrid, setShowGrid] = useState(initialState?.showGrid ?? false);
613
+ const showAxisRef = useRef(showAxis);
614
+ showAxisRef.current = showAxis;
615
+ const showGridRef = useRef(showGrid);
616
+ showGridRef.current = showGrid;
617
+ const objMapRef = useRef(/* @__PURE__ */ new Map());
618
+ const valueLabelsRef = useRef(/* @__PURE__ */ new Map());
619
+ const pendingRef = useRef([]);
620
+ const [, setPendingCount] = useState(0);
621
+ const selectedSetRef = useRef(/* @__PURE__ */ new Set());
622
+ const selOriginalRef = useRef(/* @__PURE__ */ new Map());
623
+ const [, setSelectionTick] = useState(0);
624
+ const marqueeRef = useRef(null);
625
+ const previewSegRef = useRef([]);
626
+ const phantomRef = useRef(null);
627
+ const previewShapeRef = useRef(null);
628
+ const previewRafRef = useRef(null);
629
+ const [historyTick, setHistoryTick] = useState(0);
630
+ const [, setWarn] = useState(null);
631
+ const warnTimerRef = useRef(null);
632
+ const flashWarn = useCallback((msg) => {
633
+ if (warnTimerRef.current) clearTimeout(warnTimerRef.current);
634
+ setWarn(msg);
635
+ warnTimerRef.current = setTimeout(() => setWarn(null), 1800);
636
+ }, []);
637
+ useEffect(() => () => {
638
+ if (warnTimerRef.current) clearTimeout(warnTimerRef.current);
639
+ }, []);
640
+ const labelIdxRef = useRef(0);
641
+ const nextLabel = useCallback(() => {
642
+ const idx = labelIdxRef.current;
643
+ const suffix = idx >= 26 ? String(Math.floor(idx / 26)) : "";
644
+ const code = "A".charCodeAt(0) + idx % 26;
645
+ labelIdxRef.current = idx + 1;
646
+ return String.fromCharCode(code) + suffix;
647
+ }, []);
648
+ const nextLocalId = useCallback(() => "j" + creationLogRef.current.length, []);
649
+ const resolveArgs = useCallback((args) => {
650
+ return args.map((a) => {
651
+ if (typeof a === "string" && objMapRef.current.has(a)) {
652
+ return objMapRef.current.get(a);
653
+ }
654
+ return a;
655
+ });
656
+ }, []);
657
+ const pushCreationLog = useCallback((entry) => {
658
+ creationLogRef.current.push(entry);
659
+ redoStackRef.current = [];
660
+ }, []);
661
+ const pushLog = useCallback(
662
+ (id, type, args, attrs, obj) => {
663
+ pushCreationLog({ id, type, args, attrs });
664
+ objMapRef.current.set(id, obj);
665
+ setHistoryTick((t) => t + 1);
666
+ },
667
+ [pushCreationLog]
668
+ );
669
+ const create = useCallback(
670
+ (type, args, attrs = {}) => {
671
+ if (!boardRef.current) return null;
672
+ const id = nextLocalId();
673
+ const resolved = resolveArgs(args);
674
+ const resolvedAttrs = resolveAttrColors(attrs, paletteFor(isDarkRef.current));
675
+ const obj = boardRef.current.create(type, resolved, resolvedAttrs);
676
+ pushLog(id, type, args, attrs, obj);
677
+ return obj;
678
+ },
679
+ [nextLocalId, resolveArgs, pushLog]
680
+ );
681
+ const localIdOf = useCallback((obj) => {
682
+ for (const [id, o] of objMapRef.current.entries()) {
683
+ if (o === obj) return id;
684
+ }
685
+ return null;
686
+ }, []);
687
+ const snapshotObject = useCallback((obj, anchorScreen) => {
688
+ const o = obj;
689
+ const k = objKind(o);
690
+ if (k !== "point" && k !== "line" && k !== "circle") return null;
691
+ const v = o.visProp ?? {};
692
+ const showLabel = v.withlabel !== false;
693
+ const showValue = valueLabelsRef.current.has(o);
694
+ return {
695
+ obj: o,
696
+ kind: k,
697
+ name: typeof o.name === "string" ? o.name : "",
698
+ color: v.strokecolor ?? "#1e1e1e",
699
+ dash: typeof v.dash === "number" ? v.dash : 0,
700
+ width: typeof v.strokewidth === "number" ? v.strokewidth : 2,
701
+ face: v.face ?? "o",
702
+ showLabel,
703
+ showValue,
704
+ screenCoords: anchorScreen
705
+ };
706
+ }, []);
707
+ const createValueLabelFor = useCallback((target) => {
708
+ const b = boardRef.current;
709
+ if (!b || !target) return null;
710
+ const k = objKind(target);
711
+ if (k === "line") {
712
+ const p1 = target.point1;
713
+ const p2 = target.point2;
714
+ if (!p1 || !p2) return null;
715
+ const txt = b.create("text", [
716
+ () => (p1.X() + p2.X()) / 2 + 0.15,
717
+ () => (p1.Y() + p2.Y()) / 2 + 0.25,
718
+ () => {
719
+ const dx = p2.X() - p1.X();
720
+ const dy = p2.Y() - p1.Y();
721
+ const len = Math.hypot(dx, dy);
722
+ const name = typeof target.name === "string" && target.name ? target.name : "d";
723
+ return `${name} = ${len.toFixed(2)}`;
724
+ }
725
+ ], { fontSize: 12, color: "#dc2626", fixed: true, highlight: false });
726
+ return txt;
727
+ }
728
+ if (k === "circle") {
729
+ const center = target.center ?? target.midpoint;
730
+ if (!center) return null;
731
+ const txt = b.create("text", [
732
+ () => center.X() + 0.3,
733
+ () => center.Y() + 0.3,
734
+ () => {
735
+ const r = typeof target.Radius === "function" ? target.Radius() : 0;
736
+ const name = typeof target.name === "string" && target.name ? target.name : "r";
737
+ return `${name} = ${r.toFixed(2)}`;
738
+ }
739
+ ], { fontSize: 12, color: "#dc2626", fixed: true, highlight: false });
740
+ return txt;
741
+ }
742
+ return null;
743
+ }, []);
744
+ const mutateObject = useCallback((obj, patch) => {
745
+ if (!boardRef.current) return;
746
+ const o = obj;
747
+ if (patch.remove) {
748
+ const vl = valueLabelsRef.current.get(o);
749
+ if (vl) {
750
+ safeJsx("MiniBoard.removeObject(valueLabel)", () => boardRef.current.removeObject(vl));
751
+ valueLabelsRef.current.delete(o);
752
+ }
753
+ safeJsx("MiniBoard.removeObject(target)", () => boardRef.current.removeObject(o));
754
+ const board = boardRef.current;
755
+ const aliveIds = /* @__PURE__ */ new Set();
756
+ for (const [id, obj2] of objMapRef.current.entries()) {
757
+ const jxgId = obj2?.id;
758
+ if (jxgId && board && board.objects && board.objects[jxgId]) {
759
+ aliveIds.add(id);
760
+ }
761
+ }
762
+ creationLogRef.current = creationLogRef.current.filter((e) => aliveIds.has(e.id));
763
+ for (const id of Array.from(objMapRef.current.keys())) {
764
+ if (!aliveIds.has(id)) objMapRef.current.delete(id);
765
+ }
766
+ setHistoryTick((t) => t + 1);
767
+ return;
768
+ }
769
+ if (typeof patch.valueLabel === "boolean") {
770
+ const has = valueLabelsRef.current.has(o);
771
+ if (patch.valueLabel && !has) {
772
+ const txt = createValueLabelFor(o);
773
+ if (txt) {
774
+ valueLabelsRef.current.set(o, txt);
775
+ const targetId = localIdOf(o);
776
+ if (targetId) {
777
+ const id = nextLocalId();
778
+ pushCreationLog({ id, type: "valueLabel", args: [targetId], attrs: {} });
779
+ objMapRef.current.set(id, txt);
780
+ setHistoryTick((t) => t + 1);
781
+ }
782
+ }
783
+ } else if (!patch.valueLabel && has) {
784
+ const txt = valueLabelsRef.current.get(o);
785
+ valueLabelsRef.current.delete(o);
786
+ if (txt) {
787
+ safeJsx("MiniBoard.removeObject(valueLabel.text)", () => boardRef.current.removeObject(txt));
788
+ const txtId = localIdOf(txt);
789
+ if (txtId) {
790
+ creationLogRef.current = creationLogRef.current.filter((e) => e.id !== txtId);
791
+ objMapRef.current.delete(txtId);
792
+ setHistoryTick((t) => t + 1);
793
+ }
794
+ }
795
+ }
796
+ }
797
+ if (patch.attrs) {
798
+ safeJsx("MiniBoard.setAttribute", () => o.setAttribute(patch.attrs));
799
+ const id = localIdOf(o);
800
+ if (id) {
801
+ const entry = creationLogRef.current.find((e) => e.id === id);
802
+ if (entry) entry.attrs = { ...entry.attrs, ...patch.attrs };
803
+ setHistoryTick((t) => t + 1);
804
+ }
805
+ }
806
+ safeJsx("MiniBoard.board.update(mutate)", () => boardRef.current.update());
807
+ }, [createValueLabelFor, localIdOf, nextLocalId]);
808
+ const clearPreviewSegs = useCallback(() => {
809
+ const b = boardRef.current;
810
+ if (!b) return;
811
+ for (const s of previewSegRef.current) {
812
+ safeJsx("MiniBoard.removeObject(previewSeg)", () => b.removeObject(s));
813
+ }
814
+ previewSegRef.current = [];
815
+ }, []);
816
+ const removePhantom = useCallback(() => {
817
+ const b = boardRef.current;
818
+ if (!b) return;
819
+ if (previewShapeRef.current) {
820
+ safeJsx("MiniBoard.removeObject(previewShape)", () => b.removeObject(previewShapeRef.current));
821
+ previewShapeRef.current = null;
822
+ }
823
+ if (phantomRef.current) {
824
+ safeJsx("MiniBoard.removeObject(phantom)", () => b.removeObject(phantomRef.current));
825
+ phantomRef.current = null;
826
+ }
827
+ }, []);
828
+ const clearPending = useCallback(() => {
829
+ removePhantom();
830
+ clearPreviewSegs();
831
+ pendingRef.current = [];
832
+ setPendingCount(0);
833
+ }, [clearPreviewSegs, removePhantom]);
834
+ const applySelectionStyle = useCallback((obj) => {
835
+ if (!obj || selOriginalRef.current.has(obj)) return;
836
+ safeJsx("MiniBoard.applySelectionStyle", () => {
837
+ const visProp = obj.visProp ?? {};
838
+ selOriginalRef.current.set(obj, {
839
+ strokeColor: visProp.strokecolor,
840
+ strokeWidth: visProp.strokewidth
841
+ });
842
+ const kind = objKind(obj);
843
+ if (kind === "point") {
844
+ obj.setAttribute({ strokeColor: "#06b6d4", strokeWidth: 3 });
845
+ } else {
846
+ obj.setAttribute({ strokeColor: "#06b6d4", strokeWidth: 3 });
847
+ }
848
+ });
849
+ }, []);
850
+ const restoreSelectionStyle = useCallback((obj) => {
851
+ const orig = selOriginalRef.current.get(obj);
852
+ if (!orig) return;
853
+ safeJsx("MiniBoard.restoreSelectionStyle", () => {
854
+ const attrs = {};
855
+ if (orig.strokeColor !== void 0) attrs.strokeColor = orig.strokeColor;
856
+ if (orig.strokeWidth !== void 0) attrs.strokeWidth = orig.strokeWidth;
857
+ obj.setAttribute(attrs);
858
+ });
859
+ selOriginalRef.current.delete(obj);
860
+ }, []);
861
+ const clearSelection = useCallback(() => {
862
+ for (const o of selectedSetRef.current) {
863
+ restoreSelectionStyle(o);
864
+ }
865
+ selectedSetRef.current.clear();
866
+ setSelectionTick((t) => t + 1);
867
+ safeJsx("MiniBoard.board.update(clearSelection)", () => boardRef.current?.update());
868
+ }, [restoreSelectionStyle]);
869
+ const toggleSelect = useCallback((obj, additive) => {
870
+ if (!obj) return;
871
+ if (!additive) {
872
+ for (const o of selectedSetRef.current) {
873
+ if (o !== obj) restoreSelectionStyle(o);
874
+ }
875
+ selectedSetRef.current = /* @__PURE__ */ new Set([obj]);
876
+ applySelectionStyle(obj);
877
+ } else {
878
+ if (selectedSetRef.current.has(obj)) {
879
+ restoreSelectionStyle(obj);
880
+ selectedSetRef.current.delete(obj);
881
+ } else {
882
+ selectedSetRef.current.add(obj);
883
+ applySelectionStyle(obj);
884
+ }
885
+ }
886
+ setSelectionTick((t) => t + 1);
887
+ safeJsx("MiniBoard.board.update(toggleSelect)", () => boardRef.current?.update());
888
+ }, [applySelectionStyle, restoreSelectionStyle]);
889
+ const deleteSelected = useCallback(() => {
890
+ const board = boardRef.current;
891
+ if (!board) return;
892
+ if (selectedSetRef.current.size === 0) return;
893
+ for (const o of selectedSetRef.current) selOriginalRef.current.delete(o);
894
+ for (const o of selectedSetRef.current) {
895
+ safeJsx("MiniBoard.removeObject(selected)", () => board.removeObject(o));
896
+ }
897
+ selectedSetRef.current.clear();
898
+ const aliveIds = /* @__PURE__ */ new Set();
899
+ for (const [id, o] of objMapRef.current.entries()) {
900
+ const jxgId = o?.id;
901
+ if (jxgId && board.objects && board.objects[jxgId]) aliveIds.add(id);
902
+ }
903
+ creationLogRef.current = creationLogRef.current.filter((e) => aliveIds.has(e.id));
904
+ for (const id of Array.from(objMapRef.current.keys())) {
905
+ if (!aliveIds.has(id)) objMapRef.current.delete(id);
906
+ }
907
+ setSelectionTick((t) => t + 1);
908
+ setHistoryTick((t) => t + 1);
909
+ }, []);
910
+ const buildPreview = useCallback((toolDef, picks, phantom) => {
911
+ const b = boardRef.current;
912
+ if (!b) return null;
913
+ const style = { strokeColor: "#3b82f6", strokeWidth: 1.5, strokeOpacity: 0.65, dash: 2, fixed: true, highlight: false, withLabel: false };
914
+ const circStyle = { ...style, fillColor: "none", fillOpacity: 0 };
915
+ try {
916
+ switch (toolDef.key) {
917
+ case "segment":
918
+ case "midpoint":
919
+ case "distance":
920
+ return b.create("segment", [picks[0], phantom], style);
921
+ case "line":
922
+ return b.create("line", [picks[0], phantom], style);
923
+ case "ray":
924
+ return b.create("line", [picks[0], phantom], { ...style, straightFirst: false, straightLast: true });
925
+ case "vector":
926
+ return b.create("arrow", [picks[0], phantom], style);
927
+ case "circleCenter":
928
+ return b.create("circle", [picks[0], phantom], circStyle);
929
+ case "circle3":
930
+ if (picks.length === 1) return b.create("circle", [picks[0], phantom], circStyle);
931
+ if (picks.length === 2) return b.create("circumcircle", [picks[0], picks[1], phantom], circStyle);
932
+ return null;
933
+ case "angle":
934
+ if (picks.length === 1) return b.create("segment", [picks[0], phantom], style);
935
+ if (picks.length === 2) return b.create("angle", [picks[0], picks[1], phantom], { ...style, radius: 1, fillColor: "#22c55e", fillOpacity: 0.15 });
936
+ return null;
937
+ case "perpBisector":
938
+ return b.create("segment", [picks[0], phantom], style);
939
+ case "angleBisector":
940
+ if (picks.length === 1) return b.create("segment", [picks[0], phantom], style);
941
+ if (picks.length === 2) return b.create("bisector", [picks[0], picks[1], phantom], style);
942
+ return null;
943
+ case "perpendicular":
944
+ case "parallel":
945
+ case "tangent":
946
+ if (picks.length === 1) {
947
+ const k = objKind(picks[0]);
948
+ if (k === "line" && toolDef.key !== "tangent") {
949
+ return b.create(toolDef.key, [picks[0], phantom], style);
950
+ }
951
+ if (k === "circle" && toolDef.key === "tangent") {
952
+ const glider = b.create("glider", [phantom.X(), phantom.Y(), picks[0]], { visible: false, withLabel: false });
953
+ return b.create("tangent", [glider], style);
954
+ }
955
+ }
956
+ return null;
957
+ default:
958
+ return null;
959
+ }
960
+ } catch {
961
+ return null;
962
+ }
963
+ }, []);
964
+ const refreshPreview = useCallback(() => {
965
+ const b = boardRef.current;
966
+ if (!b) return;
967
+ if (previewShapeRef.current) {
968
+ safeJsx("MiniBoard.removeObject(refreshPreview)", () => b.removeObject(previewShapeRef.current));
969
+ previewShapeRef.current = null;
970
+ }
971
+ const t = toolRef.current;
972
+ const toolDef = TOOLS.find((td) => td.key === t);
973
+ if (!toolDef) return;
974
+ const picks = pendingRef.current;
975
+ if (picks.length === 0 || toolDef.needs <= 0) return;
976
+ if (picks.length >= toolDef.needs) return;
977
+ if (!phantomRef.current) {
978
+ try {
979
+ phantomRef.current = b.create("point", [0, 0], { visible: false, fixed: true, withLabel: false, name: "" });
980
+ } catch {
981
+ return;
982
+ }
983
+ }
984
+ previewShapeRef.current = buildPreview(toolDef, picks, phantomRef.current);
985
+ }, [buildPreview]);
986
+ const finalize = useCallback((toolDef, picks) => {
987
+ if (!boardRef.current) return;
988
+ const labels = picks.map(localIdOf).filter(Boolean);
989
+ const stroke = { strokeColor: "@stroke", strokeWidth: 2 };
990
+ const strokeOnly = { ...stroke, fillColor: "none", fillOpacity: 0 };
991
+ const lblName = nextLabel();
992
+ switch (toolDef.key) {
993
+ case "midpoint":
994
+ create("midpoint", labels, { name: lblName, color: "@stroke", size: 3 });
995
+ break;
996
+ case "segment":
997
+ create("segment", labels, stroke);
998
+ break;
999
+ case "line":
1000
+ create("line", labels, stroke);
1001
+ break;
1002
+ case "ray": {
1003
+ create("line", labels, { ...stroke, straightFirst: false, straightLast: true });
1004
+ break;
1005
+ }
1006
+ case "vector":
1007
+ create("arrow", labels, stroke);
1008
+ break;
1009
+ case "perpendicular": {
1010
+ const [p, l] = picks[0] && objKind(picks[0]) === "point" ? [labels[0], labels[1]] : [labels[1], labels[0]];
1011
+ create("perpendicular", [l, p], stroke);
1012
+ break;
1013
+ }
1014
+ case "parallel": {
1015
+ const [p, l] = picks[0] && objKind(picks[0]) === "point" ? [labels[0], labels[1]] : [labels[1], labels[0]];
1016
+ create("parallel", [l, p], stroke);
1017
+ break;
1018
+ }
1019
+ case "perpBisector": {
1020
+ const mid = create("midpoint", labels, { visible: false, withLabel: false, name: "" });
1021
+ const seg = create("segment", labels, { visible: false, withLabel: false });
1022
+ const midId = localIdOf(mid);
1023
+ const segId = localIdOf(seg);
1024
+ if (midId && segId) create("perpendicular", [segId, midId], stroke);
1025
+ break;
1026
+ }
1027
+ case "angleBisector":
1028
+ create("bisector", labels, stroke);
1029
+ break;
1030
+ case "circleCenter":
1031
+ create("circle", labels, strokeOnly);
1032
+ break;
1033
+ case "circle3":
1034
+ create("circumcircle", labels, strokeOnly);
1035
+ break;
1036
+ case "tangent": {
1037
+ const firstIsPoint = picks[0] && objKind(picks[0]) === "point";
1038
+ const pointPick = firstIsPoint ? picks[0] : picks[1];
1039
+ const circleLabel = firstIsPoint ? labels[1] : labels[0];
1040
+ if (!pointPick || !circleLabel) break;
1041
+ const px = typeof pointPick.X === "function" ? pointPick.X() : 0;
1042
+ const py = typeof pointPick.Y === "function" ? pointPick.Y() : 0;
1043
+ const glider = create("glider", [px, py, circleLabel], { name: "", size: 2, strokeColor: "#666", visible: false });
1044
+ const gid = localIdOf(glider);
1045
+ if (gid) create("tangent", [gid], stroke);
1046
+ break;
1047
+ }
1048
+ case "angle": {
1049
+ const [pa, pb, pc] = picks;
1050
+ let order = labels;
1051
+ try {
1052
+ const ax = pa.X() - pb.X(), ay = pa.Y() - pb.Y();
1053
+ const cx = pc.X() - pb.X(), cy = pc.Y() - pb.Y();
1054
+ const cross = ax * cy - ay * cx;
1055
+ if (cross < 0) order = [labels[2], labels[1], labels[0]];
1056
+ } catch {
1057
+ }
1058
+ create("angle", order, {
1059
+ radius: 1,
1060
+ fillColor: "#22c55e",
1061
+ fillOpacity: 0.25,
1062
+ strokeColor: "#16a34a",
1063
+ strokeWidth: 1.5,
1064
+ name: "",
1065
+ withLabel: false
1066
+ });
1067
+ break;
1068
+ }
1069
+ case "distance": {
1070
+ const pA = picks[0], pB = picks[1];
1071
+ const dist = Math.hypot(pA.X() - pB.X(), pA.Y() - pB.Y());
1072
+ const midX = (pA.X() + pB.X()) / 2;
1073
+ const midY = (pA.Y() + pB.Y()) / 2;
1074
+ create("text", [midX, midY, `d = ${dist.toFixed(2)}`], { fontSize: 14, color: "#dc2626" });
1075
+ break;
1076
+ }
1077
+ case "polygon": {
1078
+ create("polygon", labels, { fillColor: "#1e3a8a", fillOpacity: 0.1, borders: { strokeColor: "@stroke", strokeWidth: 2 } });
1079
+ break;
1080
+ }
1081
+ case "area": {
1082
+ create("polygon", labels, { fillColor: "#3b82f6", fillOpacity: 0.18, borders: { strokeColor: "#1d4ed8", strokeWidth: 2 } });
1083
+ break;
1084
+ }
1085
+ case "toggleLabel": {
1086
+ const obj = picks[0];
1087
+ safeJsx("MiniBoard.toggleLabel", () => {
1088
+ if (obj.label) {
1089
+ const visible = obj.label.visProp.visible !== false;
1090
+ obj.label.setAttribute({ visible: !visible });
1091
+ } else if (obj.setAttribute) {
1092
+ const cur = obj.visProp.withlabel !== false;
1093
+ obj.setAttribute({ withLabel: !cur });
1094
+ }
1095
+ boardRef.current.update();
1096
+ });
1097
+ break;
1098
+ }
1099
+ case "toggleVisible": {
1100
+ const obj = picks[0];
1101
+ safeJsx("MiniBoard.toggleVisible", () => {
1102
+ const visible = obj.visProp.visible !== false;
1103
+ obj.setAttribute({ visible: !visible });
1104
+ boardRef.current.update();
1105
+ });
1106
+ break;
1107
+ }
1108
+ case "delete": {
1109
+ const obj = picks[0];
1110
+ safeJsx("MiniBoard.deleteOne", () => {
1111
+ boardRef.current.removeObject(obj);
1112
+ const board = boardRef.current;
1113
+ const aliveIds = /* @__PURE__ */ new Set();
1114
+ for (const [id, o] of objMapRef.current.entries()) {
1115
+ const jxgId = o?.id;
1116
+ if (jxgId && board && board.objects && board.objects[jxgId]) {
1117
+ aliveIds.add(id);
1118
+ }
1119
+ }
1120
+ creationLogRef.current = creationLogRef.current.filter((e) => aliveIds.has(e.id));
1121
+ for (const id of Array.from(objMapRef.current.keys())) {
1122
+ if (!aliveIds.has(id)) objMapRef.current.delete(id);
1123
+ }
1124
+ setHistoryTick((t) => t + 1);
1125
+ });
1126
+ break;
1127
+ }
1128
+ }
1129
+ }, [create, localIdOf, nextLabel]);
1130
+ const finalizeTransformCreate = useCallback((spec, source) => {
1131
+ if (!boardRef.current) return;
1132
+ const def = getDefiningPoints(source);
1133
+ if (!def) {
1134
+ flashWarn("Kh\xF4ng th\u1EC3 bi\u1EBFn \u0111\u1ED5i \u0111\u1ED1i t\u01B0\u1EE3ng n\xE0y");
1135
+ return;
1136
+ }
1137
+ const transformObjs = [];
1138
+ const transformIds = [];
1139
+ const steps = spec.chain ?? [{ params: spec.params, attrs: spec.attrs }];
1140
+ for (const step of steps) {
1141
+ const stepLogArgs = [];
1142
+ for (const p of step.params) {
1143
+ if (typeof p === "function") {
1144
+ flashWarn("Tham s\u1ED1 transform kh\xF4ng serialize \u0111\u01B0\u1EE3c \u2014 b\u1ECF qua");
1145
+ return;
1146
+ }
1147
+ if (p && typeof p === "object") {
1148
+ const id = localIdOf(p);
1149
+ if (!id) {
1150
+ flashWarn("\u0110\u1ED1i t\u01B0\u1EE3ng tham chi\u1EBFu kh\xF4ng n\u1EB1m trong board \u2014 kh\xF4ng th\u1EC3 bi\u1EBFn \u0111\u1ED5i");
1151
+ return;
1152
+ }
1153
+ stepLogArgs.push(id);
1154
+ } else {
1155
+ stepLogArgs.push(p);
1156
+ }
1157
+ }
1158
+ const stepId = nextLocalId();
1159
+ const stepObj = boardRef.current.create("transform", step.params, step.attrs);
1160
+ pushCreationLog({ id: stepId, type: "transform", args: stepLogArgs, attrs: step.attrs });
1161
+ objMapRef.current.set(stepId, stepObj);
1162
+ transformObjs.push(stepObj);
1163
+ transformIds.push(stepId);
1164
+ }
1165
+ const transformParent = transformObjs.length === 1 ? transformObjs[0] : transformObjs;
1166
+ const transformLogRef = transformObjs.length === 1 ? transformIds[0] : transformIds;
1167
+ const transformedPoints = def.points.map((src) => {
1168
+ const srcId = localIdOf(src);
1169
+ const id = nextLocalId();
1170
+ const srcName = typeof src.name === "string" ? src.name : "";
1171
+ const newName = srcName ? `${srcName}'` : nextLabel();
1172
+ const attrs = { name: newName, size: 3, color: "#0ea5e9", strokeColor: "#0ea5e9", fillColor: "#0ea5e9" };
1173
+ const obj = boardRef.current.create("point", [src, transformParent], attrs);
1174
+ pushCreationLog({ id, type: "point", args: [srcId ?? src, transformLogRef], attrs });
1175
+ objMapRef.current.set(id, obj);
1176
+ return obj;
1177
+ });
1178
+ const baseStyle = { ...def.attrs, strokeColor: "#0ea5e9" };
1179
+ const strokeOnly = { ...baseStyle, fillColor: "none", fillOpacity: 0 };
1180
+ const ids = transformedPoints.map((p) => localIdOf(p)).filter((s) => !!s);
1181
+ switch (def.kind) {
1182
+ case "point":
1183
+ break;
1184
+ case "segment":
1185
+ create("segment", ids, baseStyle);
1186
+ break;
1187
+ case "line":
1188
+ create("line", ids, baseStyle);
1189
+ break;
1190
+ case "ray":
1191
+ create("line", ids, { ...baseStyle, straightFirst: false, straightLast: true });
1192
+ break;
1193
+ case "arrow":
1194
+ create("arrow", ids, baseStyle);
1195
+ break;
1196
+ case "circleCenter":
1197
+ create("circle", ids, strokeOnly);
1198
+ break;
1199
+ case "circle3":
1200
+ create("circumcircle", ids, strokeOnly);
1201
+ break;
1202
+ }
1203
+ setHistoryTick((t) => t + 1);
1204
+ }, [create, flashWarn, localIdOf, nextLabel, nextLocalId]);
1205
+ const recreateFromLogEntry = useCallback((el) => {
1206
+ const board = boardRef.current;
1207
+ if (!board) return false;
1208
+ const idMap = objMapRef.current;
1209
+ const resolved = el.args.map((a) => typeof a === "string" && idMap.has(a) ? idMap.get(a) : a);
1210
+ try {
1211
+ if (el.type === "valueLabel") {
1212
+ const target = resolved[0];
1213
+ if (!target) return false;
1214
+ const txt = createValueLabelFor(target);
1215
+ if (!txt) return false;
1216
+ idMap.set(el.id, txt);
1217
+ valueLabelsRef.current.set(target, txt);
1218
+ return true;
1219
+ }
1220
+ const themedAttrs = resolveAttrColors({ ...el.attrs }, paletteFor(isDarkRef.current));
1221
+ const obj = board.create(el.type, resolved, themedAttrs);
1222
+ idMap.set(el.id, obj);
1223
+ return true;
1224
+ } catch (err) {
1225
+ console.warn("Recreate failed for", el.type, err);
1226
+ return false;
1227
+ }
1228
+ }, [createValueLabelFor]);
1229
+ const undoLast = useCallback(() => {
1230
+ const b = boardRef.current;
1231
+ if (!b) return;
1232
+ while (creationLogRef.current.length > 0) {
1233
+ const last = creationLogRef.current.pop();
1234
+ if (!last) break;
1235
+ const obj = objMapRef.current.get(last.id);
1236
+ objMapRef.current.delete(last.id);
1237
+ if (obj) {
1238
+ safeJsx("MiniBoard.removeObject(undo)", () => b.removeObject(obj));
1239
+ clearPending();
1240
+ redoStackRef.current.push(last);
1241
+ setHistoryTick((t) => t + 1);
1242
+ safeJsx("MiniBoard.board.update(undo)", () => b.update());
1243
+ return;
1244
+ }
1245
+ }
1246
+ setHistoryTick((t) => t + 1);
1247
+ }, [clearPending]);
1248
+ const redoNext = useCallback(() => {
1249
+ const b = boardRef.current;
1250
+ if (!b) return;
1251
+ const entry = redoStackRef.current.pop();
1252
+ if (!entry) {
1253
+ setHistoryTick((t) => t + 1);
1254
+ return;
1255
+ }
1256
+ const ok = recreateFromLogEntry(entry);
1257
+ if (ok) {
1258
+ creationLogRef.current.push(entry);
1259
+ }
1260
+ setHistoryTick((t) => t + 1);
1261
+ safeJsx("MiniBoard.board.update(redo)", () => b.update());
1262
+ }, [recreateFromLogEntry]);
1263
+ useEffect(() => {
1264
+ const onKey = (e) => {
1265
+ const ae = document.activeElement;
1266
+ const inField = !!(ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable));
1267
+ if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "z" && !e.shiftKey) {
1268
+ if (inField) return;
1269
+ e.preventDefault();
1270
+ e.stopPropagation();
1271
+ undoLastRef.current();
1272
+ return;
1273
+ }
1274
+ if ((e.metaKey || e.ctrlKey) && (e.key.toLowerCase() === "z" && e.shiftKey || e.key.toLowerCase() === "y" && !e.shiftKey)) {
1275
+ if (inField) return;
1276
+ e.preventDefault();
1277
+ e.stopPropagation();
1278
+ redoNextRef.current();
1279
+ return;
1280
+ }
1281
+ if (e.key === "Escape" && !inField) {
1282
+ if (pendingRef.current.length > 0) {
1283
+ e.preventDefault();
1284
+ e.stopPropagation();
1285
+ clearPendingRef.current();
1286
+ }
1287
+ if (selectedSetRef.current.size > 0) {
1288
+ e.preventDefault();
1289
+ e.stopPropagation();
1290
+ clearSelectionRef.current();
1291
+ }
1292
+ }
1293
+ if ((e.key === "Delete" || e.key === "Backspace") && !inField) {
1294
+ if (selectedSetRef.current.size > 0) {
1295
+ e.preventDefault();
1296
+ e.stopPropagation();
1297
+ deleteSelectedRef.current();
1298
+ }
1299
+ }
1300
+ };
1301
+ window.addEventListener("keydown", onKey, { capture: true });
1302
+ return () => window.removeEventListener("keydown", onKey, { capture: true });
1303
+ }, []);
1304
+ const screenCoordsOf = useCallback((evt) => {
1305
+ const b = boardRef.current;
1306
+ if (!b) return null;
1307
+ try {
1308
+ const mp = b.getMousePosition ? b.getMousePosition(evt) : null;
1309
+ if (mp && mp.length >= 2) return [mp[0], mp[1]];
1310
+ } catch {
1311
+ }
1312
+ if (containerRef.current) {
1313
+ const rect = containerRef.current.getBoundingClientRect();
1314
+ const cx = evt.clientX ?? evt.touches?.[0]?.clientX ?? 0;
1315
+ const cy = evt.clientY ?? evt.touches?.[0]?.clientY ?? 0;
1316
+ return [cx - rect.left, cy - rect.top];
1317
+ }
1318
+ return null;
1319
+ }, []);
1320
+ const objectsAt = useCallback((evt) => {
1321
+ const b = boardRef.current;
1322
+ if (!b) return [];
1323
+ const sc = screenCoordsOf(evt);
1324
+ if (!sc) return [];
1325
+ const [sx, sy] = sc;
1326
+ const list = [];
1327
+ safeJsx("MiniBoard.objectsAt.loop", () => {
1328
+ const objs = b.objectsList || [];
1329
+ for (const o of objs) {
1330
+ safeJsx("MiniBoard.objectsAt.hasPoint", () => {
1331
+ if (o.hasPoint && o.hasPoint(sx, sy)) list.push(o);
1332
+ });
1333
+ }
1334
+ });
1335
+ return list;
1336
+ }, [screenCoordsOf]);
1337
+ const findNearestPoint = useCallback((evt, tolPx = 12) => {
1338
+ const b = boardRef.current;
1339
+ if (!b) return null;
1340
+ const sc = screenCoordsOf(evt);
1341
+ if (!sc) return null;
1342
+ const [sx, sy] = sc;
1343
+ const tol2 = tolPx * tolPx;
1344
+ const bestRef = { current: null };
1345
+ safeJsx("MiniBoard.findNearestPoint.loop", () => {
1346
+ const objs = b.objectsList || [];
1347
+ for (const o of objs) {
1348
+ safeJsx("MiniBoard.findNearestPoint.iter", () => {
1349
+ if (objKind(o) !== "point") return;
1350
+ const pc = o.coords?.scrCoords;
1351
+ if (!pc) return;
1352
+ const dx = pc[1] - sx;
1353
+ const dy = pc[2] - sy;
1354
+ const d2 = dx * dx + dy * dy;
1355
+ const cur = bestRef.current;
1356
+ if (d2 <= tol2 && (!cur || d2 < cur.d2)) bestRef.current = { obj: o, d2 };
1357
+ });
1358
+ }
1359
+ });
1360
+ return bestRef.current ? bestRef.current.obj : null;
1361
+ }, [screenCoordsOf]);
1362
+ const promoteLabel = useCallback((o) => {
1363
+ if (!o) return o;
1364
+ const t = (o.elType || o.type || "").toString().toLowerCase();
1365
+ if (t !== "text") return o;
1366
+ const b = boardRef.current;
1367
+ if (!b) return o;
1368
+ const promoted = safeJsx("MiniBoard.promoteLabel", () => {
1369
+ for (const c of b.objectsList || []) {
1370
+ if (c.label === o) return c;
1371
+ }
1372
+ return null;
1373
+ }, null);
1374
+ return promoted ?? o;
1375
+ }, []);
1376
+ const pendingTransformRef = useRef(null);
1377
+ const transformSubsRef = useRef(/* @__PURE__ */ new Set());
1378
+ const emitTransform = useCallback((info) => {
1379
+ transformSubsRef.current.forEach((cb) => {
1380
+ safeJsx("MiniBoard.emitTransform.cb", () => cb(info));
1381
+ });
1382
+ }, []);
1383
+ const selectSubsRef = useRef(/* @__PURE__ */ new Set());
1384
+ const emitSelect = useCallback((snap) => {
1385
+ selectSubsRef.current.forEach((cb) => {
1386
+ safeJsx("MiniBoard.emitSelect.cb", () => cb(snap));
1387
+ });
1388
+ }, []);
1389
+ const moveDownRef = useRef(null);
1390
+ const lastMoveClickRef = useRef({ obj: null, time: 0 });
1391
+ useEffect(() => {
1392
+ if (typeof window === "undefined" || !containerRef.current) return;
1393
+ let cancelled = false;
1394
+ (async () => {
1395
+ const JXG = (await import('jsxgraph')).default;
1396
+ if (cancelled || !containerRef.current) return;
1397
+ jxgRef.current = JXG;
1398
+ safeJsx("MiniBoard.applyJxgOptions", () => {
1399
+ const opts = JXG.Options;
1400
+ if (opts) {
1401
+ opts.text = opts.text || {};
1402
+ opts.text.display = "internal";
1403
+ opts.text.useASCIIMathML = false;
1404
+ opts.text.useMathJax = false;
1405
+ opts.text.useKatex = false;
1406
+ opts.label = opts.label || {};
1407
+ opts.label.display = "internal";
1408
+ opts.label.strokeColor = themeLabel(isDarkRef.current);
1409
+ opts.text.strokeColor = themeLabel(isDarkRef.current);
1410
+ }
1411
+ });
1412
+ const board = JXG.JSXGraph.initBoard(containerId, {
1413
+ boundingbox: initialState?.bbox ?? [-10, 10, 10, -10],
1414
+ axis: false,
1415
+ // We manage axis manually via toggle for clean default
1416
+ grid: false,
1417
+ showCopyright: false,
1418
+ showNavigation: true,
1419
+ // Keep 1:1 user→pixel ratio so circles stay circular regardless of the
1420
+ // container aspect ratio (Excalidraw panel is taller than wide and
1421
+ // without this circles became ellipses after reload).
1422
+ keepAspectRatio: true,
1423
+ pan: { enabled: true, needShift: false },
1424
+ zoom: { wheel: true },
1425
+ // Looser hit-test radius so clicking on a thin segment/line/circle
1426
+ // actually registers without pixel-perfect aim. `precision` is a real
1427
+ // JSXGraph option (Options.precision) but isn't in the d.ts file.
1428
+ ...{ precision: { hasPoint: 8, mouse: 4, touch: 16 } }
1429
+ });
1430
+ boardRef.current = board;
1431
+ if (initialState && initialState.elements.length > 0) {
1432
+ for (const el of initialState.elements) {
1433
+ recreateFromLogEntry(el);
1434
+ }
1435
+ creationLogRef.current = [...initialState.elements];
1436
+ labelIdxRef.current = initialState.elements.filter((e) => e.type === "point").length;
1437
+ }
1438
+ if (showAxisRef.current) {
1439
+ safeJsx("MiniBoard.initAxes", () => {
1440
+ axisObjsRef.current.x = board.create("axis", [[0, 0], [1, 0]], { strokeColor: themeAxis(isDarkRef.current), name: "", withLabel: false });
1441
+ axisObjsRef.current.y = board.create("axis", [[0, 0], [0, 1]], { strokeColor: themeAxis(isDarkRef.current), name: "", withLabel: false });
1442
+ });
1443
+ }
1444
+ if (showGridRef.current) {
1445
+ safeJsx("MiniBoard.initGrid", () => board.create("grid", [], { strokeColor: themeGrid(isDarkRef.current), strokeOpacity: 1 }));
1446
+ }
1447
+ board.on("down", (e) => {
1448
+ const ctx = {
1449
+ boardRef,
1450
+ toolRef,
1451
+ pendingRef,
1452
+ previewSegRef,
1453
+ axisObjsRef,
1454
+ selectedSetRef,
1455
+ marqueeRef,
1456
+ moveDownRef,
1457
+ lastMoveClickRef,
1458
+ pendingTransformRef,
1459
+ phantomRef,
1460
+ previewShapeRef,
1461
+ previewRafRef,
1462
+ jxgRef,
1463
+ screenCoordsOf,
1464
+ objectsAt,
1465
+ promoteLabel,
1466
+ findNearestPoint,
1467
+ toggleSelect,
1468
+ clearSelection,
1469
+ applySelectionStyle,
1470
+ localIdOf,
1471
+ nextLabel,
1472
+ create,
1473
+ finalize,
1474
+ finalizeTransformCreate,
1475
+ clearPending,
1476
+ clearPreviewSegs,
1477
+ refreshPreview,
1478
+ flashWarn,
1479
+ emitTransform,
1480
+ snapshotObject,
1481
+ emitSelect,
1482
+ setPendingCount,
1483
+ setSelectionTick
1484
+ };
1485
+ handleDown(ctx, e);
1486
+ });
1487
+ board.on("up", (e) => {
1488
+ const ctx = {
1489
+ boardRef,
1490
+ toolRef,
1491
+ pendingRef,
1492
+ previewSegRef,
1493
+ axisObjsRef,
1494
+ selectedSetRef,
1495
+ marqueeRef,
1496
+ moveDownRef,
1497
+ lastMoveClickRef,
1498
+ pendingTransformRef,
1499
+ phantomRef,
1500
+ previewShapeRef,
1501
+ previewRafRef,
1502
+ jxgRef,
1503
+ screenCoordsOf,
1504
+ objectsAt,
1505
+ promoteLabel,
1506
+ findNearestPoint,
1507
+ toggleSelect,
1508
+ clearSelection,
1509
+ applySelectionStyle,
1510
+ localIdOf,
1511
+ nextLabel,
1512
+ create,
1513
+ finalize,
1514
+ finalizeTransformCreate,
1515
+ clearPending,
1516
+ clearPreviewSegs,
1517
+ refreshPreview,
1518
+ flashWarn,
1519
+ emitTransform,
1520
+ snapshotObject,
1521
+ emitSelect,
1522
+ setPendingCount,
1523
+ setSelectionTick
1524
+ };
1525
+ handleUp(ctx, e);
1526
+ });
1527
+ board.on("move", (e) => {
1528
+ const ctx = {
1529
+ boardRef,
1530
+ toolRef,
1531
+ pendingRef,
1532
+ previewSegRef,
1533
+ axisObjsRef,
1534
+ selectedSetRef,
1535
+ marqueeRef,
1536
+ moveDownRef,
1537
+ lastMoveClickRef,
1538
+ pendingTransformRef,
1539
+ phantomRef,
1540
+ previewShapeRef,
1541
+ previewRafRef,
1542
+ jxgRef,
1543
+ screenCoordsOf,
1544
+ objectsAt,
1545
+ promoteLabel,
1546
+ findNearestPoint,
1547
+ toggleSelect,
1548
+ clearSelection,
1549
+ applySelectionStyle,
1550
+ localIdOf,
1551
+ nextLabel,
1552
+ create,
1553
+ finalize,
1554
+ finalizeTransformCreate,
1555
+ clearPending,
1556
+ clearPreviewSegs,
1557
+ refreshPreview,
1558
+ flashWarn,
1559
+ emitTransform,
1560
+ snapshotObject,
1561
+ emitSelect,
1562
+ setPendingCount,
1563
+ setSelectionTick
1564
+ };
1565
+ handleMove(ctx, e);
1566
+ });
1567
+ onReady({
1568
+ getContainer: () => containerRef.current,
1569
+ // Sync toạ độ live của free point về log trước khi trả ra. JSXGraph
1570
+ // cho phép drag free point (args=[x,y] không có ref), việc drag chỉ
1571
+ // cập nhật obj.X()/Y() trên board chứ không đụng log → re-edit + Chèn
1572
+ // sẽ serialize toạ độ cũ → SVG không đổi → fileId trùng → user thấy
1573
+ // "k thay đổi". Line/segment/circle/polygon tham chiếu point qua id
1574
+ // nên auto-update theo.
1575
+ getCreationLog: () => creationLogRef.current.map((e) => {
1576
+ if (e.type !== "point") return { ...e };
1577
+ const args = e.args;
1578
+ if (!Array.isArray(args) || args.length !== 2) return { ...e };
1579
+ if (typeof args[0] !== "number" || typeof args[1] !== "number") return { ...e };
1580
+ const obj = objMapRef.current.get(e.id);
1581
+ if (!obj || typeof obj.X !== "function" || typeof obj.Y !== "function") return { ...e };
1582
+ const x = obj.X();
1583
+ const y = obj.Y();
1584
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return { ...e };
1585
+ return { ...e, args: [x, y] };
1586
+ }),
1587
+ getBbox: () => boardRef.current ? boardRef.current.getBoundingBox() : [-10, 10, 10, -10],
1588
+ getShowAxis: () => showAxisRef.current,
1589
+ getShowGrid: () => showGridRef.current,
1590
+ setTool: (t) => handleToolChangeRef.current(t),
1591
+ getTool: () => toolRef.current,
1592
+ setShowAxis: (b) => setShowAxisRef.current(b),
1593
+ setShowGrid: (b) => setShowGridRef.current(b),
1594
+ undo: () => undoLastRef.current(),
1595
+ canUndo: () => creationLogRef.current.length > 0,
1596
+ redo: () => redoNextRef.current(),
1597
+ canRedo: () => redoStackRef.current.length > 0,
1598
+ subscribe: (cb) => {
1599
+ subscribersRef.current.add(cb);
1600
+ return () => {
1601
+ subscribersRef.current.delete(cb);
1602
+ };
1603
+ },
1604
+ snapshotObject,
1605
+ mutateObject,
1606
+ getAllPointNames: () => {
1607
+ const b = boardRef.current;
1608
+ if (!b) return [];
1609
+ const out = [];
1610
+ safeJsx("MiniBoard.getAllPointNames", () => {
1611
+ const objs = b.objectsList || [];
1612
+ for (const o of objs) {
1613
+ if (objKind(o) === "point" && typeof o.name === "string" && o.name) {
1614
+ out.push(o.name);
1615
+ }
1616
+ }
1617
+ });
1618
+ return out;
1619
+ },
1620
+ onSelect: (cb) => {
1621
+ selectSubsRef.current.add(cb);
1622
+ return () => {
1623
+ selectSubsRef.current.delete(cb);
1624
+ };
1625
+ },
1626
+ onTransformParam: (cb) => {
1627
+ transformSubsRef.current.add(cb);
1628
+ return () => {
1629
+ transformSubsRef.current.delete(cb);
1630
+ };
1631
+ },
1632
+ confirmTransformParam: (value) => {
1633
+ const p = pendingTransformRef.current;
1634
+ if (!p) return;
1635
+ if (p.tool === "regularPolygon") {
1636
+ const n = Math.max(3, Math.round(value));
1637
+ const p1Id = localIdOf(p.source);
1638
+ const p2Id = localIdOf(p.center);
1639
+ if (p1Id && p2Id && boardRef.current) {
1640
+ try {
1641
+ create("regularpolygon", [p1Id, p2Id, n], {
1642
+ fillColor: "#1e3a8a",
1643
+ fillOpacity: 0.1,
1644
+ borders: { strokeColor: "@stroke", strokeWidth: 2 }
1645
+ });
1646
+ } catch (err) {
1647
+ console.warn("regularpolygon failed", err);
1648
+ }
1649
+ }
1650
+ pendingTransformRef.current = null;
1651
+ emitTransformRef.current(null);
1652
+ clearPendingRef.current();
1653
+ return;
1654
+ }
1655
+ const spec = p.tool === "rotate" ? buildTransformSpec({ kind: "rotate", center: p.center, angleDeg: value }) : buildTransformSpec({ kind: "dilate", center: p.center, k: value });
1656
+ finalizeTransformCreateRef.current(spec, p.source);
1657
+ pendingTransformRef.current = null;
1658
+ emitTransformRef.current(null);
1659
+ clearPendingRef.current();
1660
+ },
1661
+ cancelTransformParam: () => {
1662
+ pendingTransformRef.current = null;
1663
+ emitTransformRef.current(null);
1664
+ clearPendingRef.current();
1665
+ },
1666
+ getSelectionSize: () => selectedSetRef.current.size,
1667
+ clearSelection: () => clearSelectionRef.current(),
1668
+ deleteSelection: () => deleteSelectedRef.current()
1669
+ });
1670
+ })();
1671
+ return () => {
1672
+ cancelled = true;
1673
+ if (previewRafRef.current != null) {
1674
+ cancelAnimationFrame(previewRafRef.current);
1675
+ previewRafRef.current = null;
1676
+ }
1677
+ if (boardRef.current && jxgRef.current) {
1678
+ safeJsx("MiniBoard.freeBoard", () => jxgRef.current.JSXGraph.freeBoard(boardRef.current));
1679
+ boardRef.current = null;
1680
+ }
1681
+ };
1682
+ }, [containerId]);
1683
+ useEffect(() => {
1684
+ const b = boardRef.current;
1685
+ if (!b) return;
1686
+ safeJsx("MiniBoard.toggleAxis", () => {
1687
+ if (axisObjsRef.current.x) {
1688
+ safeJsx("MiniBoard.removeObject(axisX)", () => b.removeObject(axisObjsRef.current.x));
1689
+ axisObjsRef.current.x = void 0;
1690
+ }
1691
+ if (axisObjsRef.current.y) {
1692
+ safeJsx("MiniBoard.removeObject(axisY)", () => b.removeObject(axisObjsRef.current.y));
1693
+ axisObjsRef.current.y = void 0;
1694
+ }
1695
+ if (showAxis) {
1696
+ axisObjsRef.current.x = b.create("axis", [[0, 0], [1, 0]], { strokeColor: themeAxis(isDarkRef.current), name: "", withLabel: false });
1697
+ axisObjsRef.current.y = b.create("axis", [[0, 0], [0, 1]], { strokeColor: themeAxis(isDarkRef.current), name: "", withLabel: false });
1698
+ }
1699
+ b.update();
1700
+ });
1701
+ }, [showAxis]);
1702
+ useEffect(() => {
1703
+ const b = boardRef.current;
1704
+ if (!b) return;
1705
+ safeJsx("MiniBoard.toggleGrid", () => {
1706
+ const objs = Object.values(b.objects || {});
1707
+ for (const o of objs) {
1708
+ if (o && (o.elType === "grid" || o.type === "grid" || o.visProp && o.visProp.type === "grid")) {
1709
+ safeJsx("MiniBoard.removeObject(grid)", () => b.removeObject(o));
1710
+ }
1711
+ }
1712
+ if (showGrid) {
1713
+ b.create("grid", [], { strokeColor: themeGrid(isDarkRef.current), strokeOpacity: 1 });
1714
+ }
1715
+ b.update();
1716
+ });
1717
+ }, [showGrid]);
1718
+ const handleToolChange = useCallback((t) => {
1719
+ clearPending();
1720
+ toolRef.current = t;
1721
+ setTool(t);
1722
+ const b = boardRef.current;
1723
+ if (b) {
1724
+ safeJsx("MiniBoard.setPanForTool", () => {
1725
+ if (b.attr?.pan) b.attr.pan.enabled = t !== "select";
1726
+ });
1727
+ }
1728
+ }, [clearPending]);
1729
+ const handleToolChangeRef = useRef(handleToolChange);
1730
+ handleToolChangeRef.current = handleToolChange;
1731
+ const subscribersRef = useRef(/* @__PURE__ */ new Set());
1732
+ const notifySubscribers = useCallback(() => {
1733
+ subscribersRef.current.forEach((cb) => {
1734
+ safeJsx("MiniBoard.notifySubscriber.cb", () => cb());
1735
+ });
1736
+ }, []);
1737
+ useEffect(() => {
1738
+ notifySubscribers();
1739
+ }, [tool, showAxis, showGrid, historyTick, notifySubscribers]);
1740
+ const undoLastRef = useRef(undoLast);
1741
+ undoLastRef.current = undoLast;
1742
+ const redoNextRef = useRef(redoNext);
1743
+ redoNextRef.current = redoNext;
1744
+ const clearPendingRef = useRef(clearPending);
1745
+ clearPendingRef.current = clearPending;
1746
+ const finalizeTransformCreateRef = useRef(finalizeTransformCreate);
1747
+ finalizeTransformCreateRef.current = finalizeTransformCreate;
1748
+ const clearSelectionRef = useRef(clearSelection);
1749
+ clearSelectionRef.current = clearSelection;
1750
+ const deleteSelectedRef = useRef(deleteSelected);
1751
+ deleteSelectedRef.current = deleteSelected;
1752
+ const emitTransformRef = useRef(emitTransform);
1753
+ emitTransformRef.current = emitTransform;
1754
+ const setShowAxisRef = useRef(setShowAxis);
1755
+ setShowAxisRef.current = setShowAxis;
1756
+ const setShowGridRef = useRef(setShowGrid);
1757
+ setShowGridRef.current = setShowGrid;
1758
+ return /* @__PURE__ */ jsx(
1759
+ "div",
1760
+ {
1761
+ ref: containerRef,
1762
+ id: containerId,
1763
+ "data-testid": "jxgmini-container",
1764
+ className: "h-full min-h-0 bg-white",
1765
+ style: { touchAction: "none" }
1766
+ }
1767
+ );
1768
+ };
1769
+ var TOOLTIP_DELAY_MS = 400;
1770
+ function Shell({ title, icon, onClose, children, isDark, closeLabel = "\u0110\xF3ng" }) {
1771
+ return /* @__PURE__ */ jsxs(
1772
+ "aside",
1773
+ {
1774
+ role: "complementary",
1775
+ "aria-label": title,
1776
+ "data-testid": "stamp-left-panel",
1777
+ "data-stamp-area": "true",
1778
+ className: [
1779
+ isDark ? "theme--dark " : "",
1780
+ "absolute left-0 top-0 z-30 flex h-full w-60 flex-col border-r border-slate-200 bg-white shadow-md animate-in slide-in-from-left duration-200"
1781
+ ].join(""),
1782
+ children: [
1783
+ /* @__PURE__ */ jsxs("header", { className: "flex items-center justify-between border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white px-3 py-2", children: [
1784
+ /* @__PURE__ */ jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
1785
+ /* @__PURE__ */ jsx("span", { className: "text-base leading-none", children: icon }),
1786
+ title
1787
+ ] }),
1788
+ /* @__PURE__ */ jsx(
1789
+ "button",
1790
+ {
1791
+ onClick: onClose,
1792
+ "aria-label": closeLabel,
1793
+ className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
1794
+ children: /* @__PURE__ */ jsx(CloseIcon, {})
1795
+ }
1796
+ )
1797
+ ] }),
1798
+ /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-4", children })
1799
+ ]
1800
+ }
1801
+ );
1802
+ }
1803
+ function Section({ label, children }) {
1804
+ return /* @__PURE__ */ jsxs("section", { children: [
1805
+ /* @__PURE__ */ jsx("h4", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: label }),
1806
+ children
1807
+ ] });
1808
+ }
1809
+ var GeometryIconHeader = /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
1810
+ /* @__PURE__ */ jsx("polygon", { points: "4,20 20,20 12,5" }),
1811
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "20", r: "1.5", fill: "currentColor", stroke: "none" }),
1812
+ /* @__PURE__ */ jsx("circle", { cx: "20", cy: "20", r: "1.5", fill: "currentColor", stroke: "none" }),
1813
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "5", r: "1.5", fill: "currentColor", stroke: "none" })
1814
+ ] });
1815
+ function CloseIcon() {
1816
+ return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1817
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
1818
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
1819
+ ] });
1820
+ }
1821
+ function UndoIcon() {
1822
+ return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1823
+ /* @__PURE__ */ jsx("polyline", { points: "3 7 3 13 9 13" }),
1824
+ /* @__PURE__ */ jsx("path", { d: "M3.51 13a9 9 0 1 0 2.13-9.36L3 7" })
1825
+ ] });
1826
+ }
1827
+ function RedoIcon() {
1828
+ return /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
1829
+ /* @__PURE__ */ jsx("polyline", { points: "21 7 21 13 15 13" }),
1830
+ /* @__PURE__ */ jsx("path", { d: "M20.49 13a9 9 0 1 1-2.13-9.36L21 7" })
1831
+ ] });
1832
+ }
1833
+ function AxisIcon() {
1834
+ return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
1835
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "20", y2: "20" }),
1836
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "4", y2: "4" }),
1837
+ /* @__PURE__ */ jsx("polyline", { points: "2 6 4 4 6 6" }),
1838
+ /* @__PURE__ */ jsx("polyline", { points: "18 18 20 20 18 22" })
1839
+ ] });
1840
+ }
1841
+ function GridIcon() {
1842
+ return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", children: [
1843
+ /* @__PURE__ */ jsx("rect", { x: "4", y: "4", width: "16", height: "16", rx: "1" }),
1844
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "10", x2: "20", y2: "10" }),
1845
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "16", x2: "20", y2: "16" }),
1846
+ /* @__PURE__ */ jsx("line", { x1: "10", y1: "4", x2: "10", y2: "20" }),
1847
+ /* @__PURE__ */ jsx("line", { x1: "16", y1: "4", x2: "16", y2: "20" })
1848
+ ] });
1849
+ }
1850
+ function useToolHoverTooltip() {
1851
+ const [hover, setHover] = useState(null);
1852
+ const [portalReady, setPortalReady] = useState(false);
1853
+ const hoverTimerRef = useRef(null);
1854
+ useEffect(() => {
1855
+ setPortalReady(true);
1856
+ return () => {
1857
+ if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
1858
+ };
1859
+ }, []);
1860
+ const showHover = useCallback((el, t) => {
1861
+ if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
1862
+ hoverTimerRef.current = setTimeout(() => {
1863
+ const r = el.getBoundingClientRect();
1864
+ setHover({ label: t.label, hint: t.hint, x: r.right, y: r.top + r.height / 2 });
1865
+ }, TOOLTIP_DELAY_MS);
1866
+ }, []);
1867
+ const hideHover = useCallback(() => {
1868
+ if (hoverTimerRef.current) {
1869
+ clearTimeout(hoverTimerRef.current);
1870
+ hoverTimerRef.current = null;
1871
+ }
1872
+ setHover(null);
1873
+ }, []);
1874
+ return { hover, portalReady, showHover, hideHover };
1875
+ }
1876
+ function DesktopGeometryPanel(props) {
1877
+ const { activeTool, onToolChange, showAxis, showGrid, onShowAxisChange, onShowGridChange, onUndo, canUndo, onRedo, canRedo, onClose, isDark, chordGroup } = props;
1878
+ const grouped = useMemo(() => {
1879
+ return TOOLS.reduce((acc, t) => {
1880
+ var _a;
1881
+ (acc[_a = t.group] ?? (acc[_a] = [])).push(t);
1882
+ return acc;
1883
+ }, {});
1884
+ }, []);
1885
+ const groupKeys = useMemo(
1886
+ () => GROUP_ORDER.filter((g) => grouped[g]),
1887
+ [grouped]
1888
+ );
1889
+ const activeGroupTools = chordGroup ? grouped[chordGroup] ?? null : null;
1890
+ const { hover, portalReady, showHover, hideHover } = useToolHoverTooltip();
1891
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1892
+ /* @__PURE__ */ jsxs(Shell, { title: "H\xECnh h\u1ECDc", icon: GeometryIconHeader, onClose, isDark, children: [
1893
+ /* @__PURE__ */ jsx(Section, { label: "B\u1ED1 c\u1EE5c", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 text-[11px] text-slate-700", children: [
1894
+ /* @__PURE__ */ jsxs("label", { className: "inline-flex select-none items-center gap-1.5", children: [
1895
+ /* @__PURE__ */ jsx(
1896
+ "input",
1897
+ {
1898
+ type: "checkbox",
1899
+ checked: showAxis,
1900
+ onChange: (e) => onShowAxisChange(e.target.checked),
1901
+ "data-testid": "toggle-axis"
1902
+ }
1903
+ ),
1904
+ "Tr\u1EE5c to\u1EA1 \u0111\u1ED9"
1905
+ ] }),
1906
+ /* @__PURE__ */ jsxs("label", { className: "inline-flex select-none items-center gap-1.5", children: [
1907
+ /* @__PURE__ */ jsx(
1908
+ "input",
1909
+ {
1910
+ type: "checkbox",
1911
+ checked: showGrid,
1912
+ onChange: (e) => onShowGridChange(e.target.checked),
1913
+ "data-testid": "toggle-grid"
1914
+ }
1915
+ ),
1916
+ "L\u01B0\u1EDBi"
1917
+ ] }),
1918
+ /* @__PURE__ */ jsx(
1919
+ "button",
1920
+ {
1921
+ type: "button",
1922
+ onClick: onUndo,
1923
+ disabled: !canUndo,
1924
+ title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
1925
+ "aria-label": "Ho\xE0n t\xE1c",
1926
+ "data-testid": "undo-btn",
1927
+ className: "ml-auto inline-flex items-center justify-center rounded p-1 text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent",
1928
+ children: /* @__PURE__ */ jsx(UndoIcon, {})
1929
+ }
1930
+ ),
1931
+ /* @__PURE__ */ jsx(
1932
+ "button",
1933
+ {
1934
+ type: "button",
1935
+ onClick: onRedo,
1936
+ disabled: !canRedo,
1937
+ title: "L\xE0m l\u1EA1i (Ctrl/Cmd+Shift+Z)",
1938
+ "aria-label": "L\xE0m l\u1EA1i",
1939
+ "data-testid": "redo-btn",
1940
+ className: "inline-flex items-center justify-center rounded p-1 text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent",
1941
+ children: /* @__PURE__ */ jsx(RedoIcon, {})
1942
+ }
1943
+ )
1944
+ ] }) }),
1945
+ groupKeys.map((group) => {
1946
+ const isChordActive = chordGroup === group;
1947
+ const dimmed = chordGroup !== null && !isChordActive;
1948
+ return /* @__PURE__ */ jsxs(
1949
+ "section",
1950
+ {
1951
+ "data-chord-group": group,
1952
+ "data-chord-active": isChordActive ? "true" : "false",
1953
+ className: [
1954
+ "rounded-md transition",
1955
+ isChordActive ? "bg-emerald-50 ring-1 ring-emerald-400 p-1" : "p-0",
1956
+ dimmed ? "opacity-55" : "opacity-100"
1957
+ ].join(" "),
1958
+ children: [
1959
+ /* @__PURE__ */ jsxs("h4", { className: "mb-1.5 flex items-center justify-between text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: [
1960
+ /* @__PURE__ */ jsx("span", { children: GROUP_LABELS[group] }),
1961
+ /* @__PURE__ */ jsx(
1962
+ "span",
1963
+ {
1964
+ "data-testid": `chord-letter-${group}`,
1965
+ className: [
1966
+ "font-mono text-[10px] leading-none transition",
1967
+ isChordActive ? "text-emerald-700 font-bold" : "text-slate-400"
1968
+ ].join(" "),
1969
+ children: letterForGroup(group)
1970
+ }
1971
+ )
1972
+ ] }),
1973
+ /* @__PURE__ */ jsx("div", { className: "grid grid-cols-4 gap-1", children: grouped[group].map((t, i) => {
1974
+ const active = activeTool === t.key;
1975
+ return /* @__PURE__ */ jsxs(
1976
+ "button",
1977
+ {
1978
+ type: "button",
1979
+ "aria-label": t.label,
1980
+ "aria-pressed": active,
1981
+ "data-tool": t.key,
1982
+ onClick: () => onToolChange(t.key),
1983
+ onMouseEnter: (e) => showHover(e.currentTarget, t),
1984
+ onMouseLeave: hideHover,
1985
+ onFocus: (e) => showHover(e.currentTarget, t),
1986
+ onBlur: hideHover,
1987
+ className: [
1988
+ "relative flex h-8 items-center justify-center rounded-md transition",
1989
+ active ? "bg-emerald-600 text-white shadow-sm" : "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
1990
+ ].join(" "),
1991
+ children: [
1992
+ t.icon,
1993
+ /* @__PURE__ */ jsx(
1994
+ "span",
1995
+ {
1996
+ "data-testid": `chord-num-${t.key}`,
1997
+ className: [
1998
+ "pointer-events-none absolute bottom-0 right-0.5 font-mono text-[9px] leading-none transition",
1999
+ active ? "text-white/70" : isChordActive ? "text-emerald-700 font-bold" : "text-slate-400"
2000
+ ].join(" "),
2001
+ children: i + 1
2002
+ }
2003
+ )
2004
+ ]
2005
+ },
2006
+ t.key
2007
+ );
2008
+ }) })
2009
+ ]
2010
+ },
2011
+ group
2012
+ );
2013
+ }),
2014
+ chordGroup && activeGroupTools && /* @__PURE__ */ jsxs(
2015
+ "div",
2016
+ {
2017
+ "data-testid": "chord-hint",
2018
+ className: "mt-1 rounded border border-emerald-200 bg-emerald-50/60 px-2 py-1 text-[11px] leading-snug text-slate-600",
2019
+ children: [
2020
+ /* @__PURE__ */ jsx("span", { className: "font-mono font-semibold text-emerald-700", children: letterForGroup(chordGroup) }),
2021
+ /* @__PURE__ */ jsx("span", { className: "mx-1 text-slate-400", children: "\u2192" }),
2022
+ activeGroupTools.map((t, i) => /* @__PURE__ */ jsxs("span", { className: "mr-2 inline-block", children: [
2023
+ /* @__PURE__ */ jsx("span", { className: "font-mono font-semibold text-emerald-700", children: i + 1 }),
2024
+ /* @__PURE__ */ jsx("span", { className: "ml-1", children: t.label })
2025
+ ] }, t.key)),
2026
+ /* @__PURE__ */ jsx("span", { className: "text-slate-400", children: "Esc hu\u1EF7" })
2027
+ ]
2028
+ }
2029
+ )
2030
+ ] }),
2031
+ portalReady && hover && typeof document !== "undefined" ? createPortal(
2032
+ /* @__PURE__ */ jsxs(
2033
+ "div",
2034
+ {
2035
+ role: "tooltip",
2036
+ className: "pointer-events-none fixed w-max max-w-[220px] rounded-md bg-slate-900 px-2 py-1 text-left text-[11px] leading-tight text-white shadow-lg",
2037
+ style: {
2038
+ left: hover.x + 8,
2039
+ top: hover.y,
2040
+ transform: "translate(0, -50%)",
2041
+ zIndex: 2147483600
2042
+ },
2043
+ children: [
2044
+ /* @__PURE__ */ jsx("span", { className: "block font-medium", children: hover.label }),
2045
+ hover.hint && /* @__PURE__ */ jsx("span", { className: "mt-0.5 block text-slate-300", children: hover.hint })
2046
+ ]
2047
+ }
2048
+ ),
2049
+ document.body
2050
+ ) : null
2051
+ ] });
2052
+ }
2053
+ function MobileGeometryPanel(props) {
2054
+ const {
2055
+ activeTool,
2056
+ onToolChange,
2057
+ showAxis,
2058
+ showGrid,
2059
+ onShowAxisChange,
2060
+ onShowGridChange,
2061
+ onUndo,
2062
+ canUndo,
2063
+ onRedo,
2064
+ canRedo,
2065
+ isDark,
2066
+ drawerOpen,
2067
+ onDrawerClose
2068
+ } = props;
2069
+ const groups = useMemo(() => {
2070
+ const acc = /* @__PURE__ */ new Map();
2071
+ for (const t of TOOLS) {
2072
+ if (!acc.has(t.group)) acc.set(t.group, []);
2073
+ acc.get(t.group).push(t);
2074
+ }
2075
+ return Array.from(acc.entries()).map(([group, tools]) => ({
2076
+ group,
2077
+ groupLabel: GROUP_LABELS[group],
2078
+ tools: tools.map((t) => ({ key: t.key, label: t.label, icon: t.icon }))
2079
+ }));
2080
+ }, []);
2081
+ return /* @__PURE__ */ jsx(
2082
+ MobileToolDrawer,
2083
+ {
2084
+ title: "H\xECnh h\u1ECDc",
2085
+ headerIcon: GeometryIconHeader,
2086
+ testId: "stamp-left-panel",
2087
+ isDark,
2088
+ drawerOpen: !!drawerOpen,
2089
+ onDrawerClose: () => onDrawerClose?.(),
2090
+ chips: [
2091
+ {
2092
+ label: "Tr\u1EE5c",
2093
+ icon: /* @__PURE__ */ jsx(AxisIcon, {}),
2094
+ pressed: showAxis,
2095
+ onToggle: onShowAxisChange,
2096
+ testId: "toggle-axis"
2097
+ },
2098
+ {
2099
+ label: "L\u01B0\u1EDBi",
2100
+ icon: /* @__PURE__ */ jsx(GridIcon, {}),
2101
+ pressed: showGrid,
2102
+ onToggle: onShowGridChange,
2103
+ testId: "toggle-grid"
2104
+ }
2105
+ ],
2106
+ actions: [
2107
+ {
2108
+ label: "Ho\xE0n t\xE1c",
2109
+ title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
2110
+ icon: /* @__PURE__ */ jsx(UndoIcon, {}),
2111
+ onClick: onUndo,
2112
+ disabled: !canUndo
2113
+ },
2114
+ {
2115
+ label: "L\xE0m l\u1EA1i",
2116
+ title: "L\xE0m l\u1EA1i (Ctrl/Cmd+Shift+Z)",
2117
+ icon: /* @__PURE__ */ jsx(RedoIcon, {}),
2118
+ onClick: onRedo,
2119
+ disabled: !canRedo
2120
+ }
2121
+ ],
2122
+ groups,
2123
+ activeTool,
2124
+ onToolSelect: onToolChange
2125
+ }
2126
+ );
2127
+ }
2128
+ function LeftPanel(props) {
2129
+ if (props.isMobile) {
2130
+ return /* @__PURE__ */ jsx(MobileGeometryPanel, { ...props });
2131
+ }
2132
+ return /* @__PURE__ */ jsx(DesktopGeometryPanel, { ...props });
2133
+ }
2134
+
2135
+ // src/stamps/shared/excalidrawPalette.ts
2136
+ var STROKE_PALETTE = [
2137
+ "#1e1e1e",
2138
+ // black
2139
+ "#e03131",
2140
+ // red
2141
+ "#e8590c",
2142
+ // orange
2143
+ "#f08c00",
2144
+ // yellow
2145
+ "#2f9e44",
2146
+ // green
2147
+ "#1971c2",
2148
+ // blue
2149
+ "#9c36b5",
2150
+ // grape
2151
+ "#868e96"
2152
+ // gray
2153
+ ];
2154
+ var DASH_OPTIONS = [
2155
+ { value: 0, label: "N\xE9t li\u1EC1n" },
2156
+ { value: 2, label: "N\xE9t \u0111\u1EE9t" },
2157
+ { value: 1, label: "N\xE9t ch\u1EA5m" }
2158
+ ];
2159
+ var WIDTH_OPTIONS = [1, 2, 3];
2160
+ var FACE_OPTIONS = [
2161
+ { value: "o", symbol: "\u25CF" },
2162
+ { value: "circle", symbol: "\u25EF" },
2163
+ { value: "cross", symbol: "\u2715" },
2164
+ { value: "plus", symbol: "\u271A" }
2165
+ ];
2166
+ var SUB_DIGITS = ["\u2080", "\u2081", "\u2082", "\u2083", "\u2084", "\u2085", "\u2086", "\u2087", "\u2088", "\u2089"];
2167
+ var SUB_SET = new Set(SUB_DIGITS);
2168
+ function toSubscript(n) {
2169
+ return String(n).split("").map((d) => SUB_DIGITS[+d] ?? d).join("");
2170
+ }
2171
+ function stripTrailingSubscript(s) {
2172
+ let i = s.length;
2173
+ while (i > 0 && SUB_SET.has(s[i - 1])) i--;
2174
+ return s.slice(0, i);
2175
+ }
2176
+ function disambiguateName(name, existing) {
2177
+ if (!name) return name;
2178
+ if (!existing.has(name)) return name;
2179
+ const base = stripTrailingSubscript(name) || name;
2180
+ for (let n = 2; n < 1e3; n++) {
2181
+ const candidate = base + toSubscript(n);
2182
+ if (!existing.has(candidate)) return candidate;
2183
+ }
2184
+ return name;
2185
+ }
2186
+ var Icons = {
2187
+ color: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
2188
+ /* @__PURE__ */ jsx("path", { d: "M19 11 L11 3 L3 11 L11 19 Z" }),
2189
+ /* @__PURE__ */ jsx("path", { d: "M19 11 L21 16 a2 2 0 1 1 -4 0 Z", fill: "currentColor", stroke: "none" })
2190
+ ] }),
2191
+ style: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "5", fill: "currentColor" }) }),
2192
+ size: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: [
2193
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "9", x2: "20", y2: "9", strokeWidth: "1" }),
2194
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "13", x2: "20", y2: "13", strokeWidth: "2" }),
2195
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "17", x2: "20", y2: "17", strokeWidth: "3.2" })
2196
+ ] }),
2197
+ name: /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor", children: [
2198
+ /* @__PURE__ */ jsx("text", { x: "2", y: "17", fontSize: "14", fontFamily: "serif", fontWeight: "700", children: "A" }),
2199
+ /* @__PURE__ */ jsx("text", { x: "12", y: "17", fontSize: "11", fontFamily: "serif", fontWeight: "700", children: "a" })
2200
+ ] }),
2201
+ trash: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
2202
+ /* @__PURE__ */ jsx("polyline", { points: "3,6 5,6 21,6" }),
2203
+ /* @__PURE__ */ jsx("path", { d: "M19 6 l-1 14 a 2 2 0 0 1 -2 2 H 8 a 2 2 0 0 1 -2 -2 l-1 -14" }),
2204
+ /* @__PURE__ */ jsx("line", { x1: "10", y1: "11", x2: "10", y2: "17" }),
2205
+ /* @__PURE__ */ jsx("line", { x1: "14", y1: "11", x2: "14", y2: "17" })
2206
+ ] })
2207
+ };
2208
+ var PropertiesPopover = (props) => {
2209
+ const { anchor, onClose, onMutate, isDark, getAllNames } = props;
2210
+ const rootRef = useRef(null);
2211
+ const [section, setSection] = useState(null);
2212
+ const { isMobile } = useIsMobile();
2213
+ const [clamped, setClamped] = useState(null);
2214
+ useLayoutEffect(() => {
2215
+ if (typeof window === "undefined") return;
2216
+ const margin = 8;
2217
+ if (isMobile) {
2218
+ const rect2 = rootRef.current?.getBoundingClientRect();
2219
+ const w2 = rect2?.width ?? 280;
2220
+ const left2 = Math.max(margin, (window.innerWidth - w2) / 2);
2221
+ const top2 = window.innerHeight - (rect2?.height ?? 80) - margin - 12;
2222
+ setClamped({ left: left2, top: Math.max(margin, top2) });
2223
+ return;
2224
+ }
2225
+ const rect = rootRef.current?.getBoundingClientRect();
2226
+ const w = rect?.width ?? 280;
2227
+ const h = rect?.height ?? 80;
2228
+ const left = Math.max(margin, Math.min(anchor.x, window.innerWidth - w - margin));
2229
+ const top = Math.max(margin, Math.min(anchor.y, window.innerHeight - h - margin));
2230
+ setClamped({ left, top });
2231
+ }, [anchor.x, anchor.y, isMobile, section]);
2232
+ const initialName = props.kind === "point" ? props.currentName : props.kind === "line" || props.kind === "circle" ? props.currentName : "";
2233
+ const [name, setName] = useState(initialName);
2234
+ useEffect(() => {
2235
+ setName(initialName);
2236
+ }, [initialName]);
2237
+ useEffect(() => {
2238
+ const onKey = (e) => {
2239
+ if (e.key === "Escape") {
2240
+ e.preventDefault();
2241
+ onClose();
2242
+ }
2243
+ };
2244
+ const onPointerDown = (e) => {
2245
+ if (!rootRef.current?.contains(e.target)) onClose();
2246
+ };
2247
+ document.addEventListener("keydown", onKey);
2248
+ document.addEventListener("pointerdown", onPointerDown, { capture: true });
2249
+ return () => {
2250
+ document.removeEventListener("keydown", onKey);
2251
+ document.removeEventListener("pointerdown", onPointerDown, { capture: true });
2252
+ };
2253
+ }, [onClose]);
2254
+ const pickColor = (c) => {
2255
+ onMutate({ attrs: { strokeColor: c, fillColor: props.kind === "circle" ? "none" : c, color: c } });
2256
+ };
2257
+ const pickDash = (d) => onMutate({ attrs: { dash: d } });
2258
+ const pickWidth = (w) => onMutate({ attrs: { strokeWidth: w } });
2259
+ const pickFace = (f) => onMutate({ attrs: { face: f } });
2260
+ const currentName = props.kind === "point" || props.kind === "line" || props.kind === "circle" ? props.currentName : "";
2261
+ const commitName = () => {
2262
+ const trimmed = name.trim();
2263
+ if (trimmed === currentName) return;
2264
+ let final = trimmed;
2265
+ if (trimmed) {
2266
+ const others = new Set((getAllNames?.() ?? []).filter((n) => n !== currentName));
2267
+ final = disambiguateName(trimmed, others);
2268
+ }
2269
+ if (final !== name) setName(final);
2270
+ onMutate({ attrs: { name: final } });
2271
+ };
2272
+ const toggleShowLabel = (next) => onMutate({ attrs: { withLabel: next } });
2273
+ const toggleShowValue = (next) => onMutate({ valueLabel: next });
2274
+ const doDelete = () => {
2275
+ onMutate({ remove: true });
2276
+ onClose();
2277
+ };
2278
+ const toggleSection = (s) => setSection((cur) => cur === s ? null : s);
2279
+ const currentColor = props.currentColor;
2280
+ const currentDash = props.currentDash;
2281
+ const currentWidth = props.currentWidth;
2282
+ if (typeof document === "undefined") return null;
2283
+ const PillBtn = ({ id, label, icon, active, onClick, indicatorColor }) => /* @__PURE__ */ jsxs(
2284
+ "button",
2285
+ {
2286
+ type: "button",
2287
+ "data-section": id,
2288
+ "data-pill-btn": id,
2289
+ "aria-label": label,
2290
+ "aria-pressed": !!active,
2291
+ onClick,
2292
+ className: `relative flex h-8 w-8 items-center justify-center rounded-md transition ${active ? "bg-slate-200 text-slate-900" : "text-slate-700 hover:bg-slate-100"}`,
2293
+ children: [
2294
+ icon,
2295
+ indicatorColor && /* @__PURE__ */ jsx(
2296
+ "span",
2297
+ {
2298
+ "aria-hidden": true,
2299
+ className: "absolute bottom-0.5 left-1/2 -translate-x-1/2 h-1 w-4 rounded-full",
2300
+ style: { background: indicatorColor }
2301
+ }
2302
+ )
2303
+ ]
2304
+ }
2305
+ );
2306
+ const colorIndicatorTint = useMemo(() => currentColor, [currentColor]);
2307
+ const pos = clamped ?? { left: anchor.x, top: anchor.y };
2308
+ const node = /* @__PURE__ */ jsxs(
2309
+ "div",
2310
+ {
2311
+ ref: rootRef,
2312
+ "data-stamp-area": "true",
2313
+ className: `${isDark ? "theme--dark " : ""}fixed z-[2147483600] flex flex-col gap-1.5`,
2314
+ style: { left: pos.left, top: pos.top },
2315
+ role: "dialog",
2316
+ "aria-label": "Thu\u1ED9c t\xEDnh \u0111\u1ED1i t\u01B0\u1EE3ng",
2317
+ children: [
2318
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 rounded-full border border-slate-300 bg-white px-1.5 py-1 shadow-lg ring-1 ring-black/5", children: [
2319
+ /* @__PURE__ */ jsx(PillBtn, { id: "color", label: "M\xE0u", icon: Icons.color, active: section === "color", onClick: () => toggleSection("color"), indicatorColor: colorIndicatorTint }),
2320
+ /* @__PURE__ */ jsx(PillBtn, { id: "style", label: "Ki\u1EC3u", icon: Icons.style, active: section === "style", onClick: () => toggleSection("style") }),
2321
+ /* @__PURE__ */ jsx(PillBtn, { id: "size", label: "\u0110\u1ED9 d\xE0y", icon: Icons.size, active: section === "size", onClick: () => toggleSection("size") }),
2322
+ /* @__PURE__ */ jsx(PillBtn, { id: "name", label: "T\xEAn", icon: Icons.name, active: section === "name", onClick: () => toggleSection("name") }),
2323
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "mx-0.5 h-5 w-px bg-slate-200" }),
2324
+ /* @__PURE__ */ jsx(PillBtn, { id: "delete", label: "Xo\xE1", icon: Icons.trash, onClick: doDelete })
2325
+ ] }),
2326
+ section && /* @__PURE__ */ jsxs("div", { className: "w-[220px] rounded-lg border border-slate-300 bg-white p-3 shadow-2xl ring-1 ring-black/5", children: [
2327
+ section === "color" && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
2328
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-slate-500", children: "M\xE0u" }),
2329
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: STROKE_PALETTE.map((c) => /* @__PURE__ */ jsx(
2330
+ "button",
2331
+ {
2332
+ "aria-label": `M\xE0u ${c}`,
2333
+ onClick: () => pickColor(c),
2334
+ className: `h-6 w-6 rounded border ${currentColor === c ? "border-emerald-500 ring-2 ring-emerald-300" : "border-slate-200"}`,
2335
+ style: { background: c }
2336
+ },
2337
+ c
2338
+ )) })
2339
+ ] }),
2340
+ section === "style" && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
2341
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-slate-500", children: "Ki\u1EC3u" }),
2342
+ props.kind === "point" ? /* @__PURE__ */ jsx("div", { className: "flex gap-1", children: FACE_OPTIONS.map((f) => /* @__PURE__ */ jsx(
2343
+ "button",
2344
+ {
2345
+ "aria-label": `H\xECnh ${f.value}`,
2346
+ onClick: () => pickFace(f.value),
2347
+ className: `h-7 w-7 rounded border text-sm ${props.currentFace === f.value ? "border-emerald-500 bg-emerald-50" : "border-slate-300 bg-white"}`,
2348
+ children: f.symbol
2349
+ },
2350
+ f.value
2351
+ )) }) : /* @__PURE__ */ jsx("div", { className: "flex gap-1", children: DASH_OPTIONS.map((d) => /* @__PURE__ */ jsx(
2352
+ "button",
2353
+ {
2354
+ "aria-label": `Ki\u1EC3u ${d.label.toLowerCase()}`,
2355
+ onClick: () => pickDash(d.value),
2356
+ className: `flex-1 rounded border px-1 py-1 text-[11px] ${currentDash === d.value ? "border-emerald-500 bg-emerald-50" : "border-slate-300 bg-white"}`,
2357
+ children: d.label
2358
+ },
2359
+ d.value
2360
+ )) })
2361
+ ] }),
2362
+ section === "size" && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
2363
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-slate-500", children: "\u0110\u1ED9 d\xE0y" }),
2364
+ /* @__PURE__ */ jsx("div", { className: "flex gap-1", children: WIDTH_OPTIONS.map((w) => /* @__PURE__ */ jsx(
2365
+ "button",
2366
+ {
2367
+ "aria-label": `\u0110\u1ED9 d\xE0y ${w}`,
2368
+ onClick: () => pickWidth(w),
2369
+ className: `flex-1 rounded border py-1 ${currentWidth === w ? "border-emerald-500 bg-emerald-50" : "border-slate-300 bg-white"}`,
2370
+ children: /* @__PURE__ */ jsx("span", { className: "inline-block rounded bg-slate-800", style: { width: 30, height: w } })
2371
+ },
2372
+ w
2373
+ )) })
2374
+ ] }),
2375
+ section === "name" && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
2376
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
2377
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-slate-500", children: "T\xEAn" }),
2378
+ /* @__PURE__ */ jsx(
2379
+ "input",
2380
+ {
2381
+ value: name,
2382
+ onChange: (e) => setName(e.target.value),
2383
+ onBlur: commitName,
2384
+ onKeyDown: (e) => {
2385
+ if (e.key === "Enter") {
2386
+ e.preventDefault();
2387
+ commitName();
2388
+ }
2389
+ },
2390
+ autoFocus: true,
2391
+ placeholder: props.kind === "point" ? "A, B, \u2026" : props.kind === "line" ? "a, b, f, \u2026" : "O, c, \u2026",
2392
+ className: "rounded border border-slate-300 bg-white px-2 py-1 text-sm text-slate-800"
2393
+ }
2394
+ ),
2395
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-slate-400", children: "Tr\xF9ng t\xEAn s\u1EBD t\u1EF1 th\xEAm ch\u1EC9 s\u1ED1 (B \u2192 B\u2082)" })
2396
+ ] }),
2397
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center justify-between gap-2 text-[12px] text-slate-700", children: [
2398
+ /* @__PURE__ */ jsx("span", { children: "Hi\u1EC3n th\u1ECB t\xEAn" }),
2399
+ /* @__PURE__ */ jsx(
2400
+ "input",
2401
+ {
2402
+ type: "checkbox",
2403
+ checked: props.currentShowLabel !== false,
2404
+ onChange: (e) => toggleShowLabel(e.target.checked),
2405
+ "aria-label": "Hi\u1EC3n th\u1ECB t\xEAn"
2406
+ }
2407
+ )
2408
+ ] }),
2409
+ (props.kind === "line" || props.kind === "circle") && /* @__PURE__ */ jsxs("label", { className: "flex items-center justify-between gap-2 text-[12px] text-slate-700", children: [
2410
+ /* @__PURE__ */ jsx("span", { children: "Hi\u1EC3n th\u1ECB gi\xE1 tr\u1ECB" }),
2411
+ /* @__PURE__ */ jsx(
2412
+ "input",
2413
+ {
2414
+ type: "checkbox",
2415
+ checked: !!props.currentShowValue,
2416
+ onChange: (e) => toggleShowValue(e.target.checked),
2417
+ "aria-label": "Hi\u1EC3n th\u1ECB gi\xE1 tr\u1ECB"
2418
+ }
2419
+ )
2420
+ ] })
2421
+ ] })
2422
+ ] })
2423
+ ]
2424
+ }
2425
+ );
2426
+ return createPortal(node, document.body);
2427
+ };
2428
+ var LABELS = {
2429
+ rotate: { aria: "G\xF3c quay", label: "G\xF3c (\xB0)", step: 15 },
2430
+ dilate: { aria: "T\u1EF7 s\u1ED1 k", label: "T\u1EF7 s\u1ED1 k", step: 0.5 },
2431
+ regularPolygon: { aria: "S\u1ED1 c\u1EA1nh \u0111a gi\xE1c \u0111\u1EC1u", label: "S\u1ED1 c\u1EA1nh (n \u2265 3)", step: 1, min: 3 }
2432
+ };
2433
+ var TransformParamPopover = ({ kind, anchor, defaultValue, onConfirm, onCancel, isDark }) => {
2434
+ const [value, setValue] = useState(defaultValue);
2435
+ const inputRef = useRef(null);
2436
+ const meta = LABELS[kind];
2437
+ useEffect(() => {
2438
+ inputRef.current?.focus();
2439
+ inputRef.current?.select();
2440
+ }, []);
2441
+ const submit = () => {
2442
+ let v = Number.isFinite(value) ? value : defaultValue;
2443
+ if (kind === "regularPolygon") {
2444
+ v = Math.max(3, Math.round(v));
2445
+ }
2446
+ onConfirm(v);
2447
+ };
2448
+ if (typeof document === "undefined") return null;
2449
+ const node = /* @__PURE__ */ jsxs(
2450
+ "div",
2451
+ {
2452
+ "data-stamp-area": "true",
2453
+ className: `${isDark ? "theme--dark " : ""}fixed z-[2147483600] flex flex-col gap-2 rounded-lg border border-slate-300 bg-white p-3 shadow-2xl ring-1 ring-black/5`,
2454
+ style: { left: anchor.x, top: anchor.y, minWidth: 180 },
2455
+ role: "dialog",
2456
+ "aria-label": meta.aria,
2457
+ children: [
2458
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-slate-700", children: meta.label }),
2459
+ /* @__PURE__ */ jsx(
2460
+ "input",
2461
+ {
2462
+ ref: inputRef,
2463
+ type: "number",
2464
+ value,
2465
+ step: meta.step,
2466
+ min: meta.min,
2467
+ onChange: (e) => setValue(parseFloat(e.target.value)),
2468
+ onKeyDown: (e) => {
2469
+ if (e.key === "Enter") {
2470
+ e.preventDefault();
2471
+ submit();
2472
+ } else if (e.key === "Escape") {
2473
+ e.preventDefault();
2474
+ onCancel();
2475
+ }
2476
+ },
2477
+ className: "rounded border border-slate-300 bg-white px-2 py-1 text-sm text-slate-800"
2478
+ }
2479
+ ),
2480
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
2481
+ /* @__PURE__ */ jsx(
2482
+ "button",
2483
+ {
2484
+ onClick: onCancel,
2485
+ className: "rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-700 hover:bg-slate-100",
2486
+ children: "Hu\u1EF7"
2487
+ }
2488
+ ),
2489
+ /* @__PURE__ */ jsx(
2490
+ "button",
2491
+ {
2492
+ onClick: submit,
2493
+ className: "rounded bg-emerald-600 px-2 py-1 text-xs font-medium text-white hover:bg-emerald-700",
2494
+ children: "\xC1p d\u1EE5ng"
2495
+ }
2496
+ )
2497
+ ] })
2498
+ ]
2499
+ }
2500
+ );
2501
+ return createPortal(node, document.body);
2502
+ };
2503
+ var GeometryEditorPanel = forwardRef(
2504
+ function GeometryEditorPanel2({ initialState, onInsert, onClose, withLeftPanel = false, onStateChange, isDark, isMobile = false, onOpenDrawer, onUndo, onRedo, canUndo, canRedo }, ref) {
2505
+ const handleRef = useRef(null);
2506
+ const [ready, setReady] = useState(false);
2507
+ const [propsPopover, setPropsPopover] = useState(null);
2508
+ const [transformPopover, setTransformPopover] = useState(null);
2509
+ const onStateChangeRef = useRef(onStateChange);
2510
+ useEffect(() => {
2511
+ onStateChangeRef.current = onStateChange;
2512
+ }, [onStateChange]);
2513
+ const emitState = useCallback(() => {
2514
+ const h = handleRef.current;
2515
+ const cb = onStateChangeRef.current;
2516
+ if (!h || !cb) return;
2517
+ cb({
2518
+ tool: h.getTool(),
2519
+ showAxis: h.getShowAxis(),
2520
+ showGrid: h.getShowGrid(),
2521
+ canUndo: h.canUndo(),
2522
+ canRedo: h.canRedo()
2523
+ });
2524
+ }, []);
2525
+ const handleReady = useCallback((h) => {
2526
+ handleRef.current = h;
2527
+ setReady(true);
2528
+ emitState();
2529
+ h.subscribe(emitState);
2530
+ h.onSelect((snap) => setPropsPopover(snap));
2531
+ h.onTransformParam((info) => setTransformPopover(info));
2532
+ }, [emitState]);
2533
+ const performInsert = useCallback(() => {
2534
+ if (!handleRef.current) return false;
2535
+ const log = handleRef.current.getCreationLog();
2536
+ if (log.length === 0) return false;
2537
+ const bbox = handleRef.current.getBbox();
2538
+ const showAxis = handleRef.current.getShowAxis();
2539
+ const showGrid = handleRef.current.getShowGrid();
2540
+ const serialized = serializeBoard(
2541
+ { getBoundingBox: () => bbox, create: () => void 0 },
2542
+ log,
2543
+ { showAxis, showGrid }
2544
+ );
2545
+ const jsonState = JSON.stringify(serialized);
2546
+ void (async () => {
2547
+ try {
2548
+ const svgString = await renderGeometrySvgFromState(jsonState);
2549
+ onInsert(jsonState, svgString);
2550
+ } catch (err) {
2551
+ console.error("Geometry insert failed:", err);
2552
+ }
2553
+ })();
2554
+ return true;
2555
+ }, [onInsert]);
2556
+ const handleInsert = useCallback(() => {
2557
+ performInsert();
2558
+ }, [performInsert]);
2559
+ useImperativeHandle(ref, () => ({
2560
+ setTool: (t) => handleRef.current?.setTool(t),
2561
+ setShowAxis: (b) => handleRef.current?.setShowAxis(b),
2562
+ setShowGrid: (b) => handleRef.current?.setShowGrid(b),
2563
+ undo: () => handleRef.current?.undo(),
2564
+ redo: () => handleRef.current?.redo(),
2565
+ insert: performInsert,
2566
+ hasContent: () => (handleRef.current?.getCreationLog().length ?? 0) > 0
2567
+ }), [performInsert]);
2568
+ const wrapperStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 40 } : {
2569
+ position: "absolute",
2570
+ top: "50%",
2571
+ left: withLeftPanel ? "calc(50% + 120px)" : "50%",
2572
+ transform: "translate(-50%, -50%)",
2573
+ zIndex: 40
2574
+ };
2575
+ return /* @__PURE__ */ jsxs(
2576
+ "div",
2577
+ {
2578
+ role: "dialog",
2579
+ "aria-label": "D\u1EF1ng h\xECnh h\u1ECDc",
2580
+ "data-testid": "geometry-editor-panel",
2581
+ "data-stamp-area": "true",
2582
+ "data-mobile-editor": isMobile ? "true" : void 0,
2583
+ style: wrapperStyle,
2584
+ className: [
2585
+ isDark ? "theme--dark " : "",
2586
+ "flex flex-col overflow-hidden bg-white",
2587
+ isMobile ? "h-full w-full" : "h-[540px] max-h-[85vh] w-[640px] max-w-[calc(100vw-280px)] rounded-lg border border-slate-300 shadow-2xl ring-1 ring-black/5"
2588
+ ].join(" "),
2589
+ children: [
2590
+ /* @__PURE__ */ jsxs("header", { className: "flex items-center gap-2 border-b border-slate-200 bg-gradient-to-r from-emerald-600 to-teal-600 px-3 py-2 text-white", children: [
2591
+ isMobile && /* @__PURE__ */ jsx(
2592
+ "button",
2593
+ {
2594
+ type: "button",
2595
+ onClick: onOpenDrawer,
2596
+ "aria-label": "M\u1EDF ng\u0103n c\xF4ng c\u1EE5",
2597
+ className: "-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15",
2598
+ children: /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2599
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
2600
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
2601
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
2602
+ ] })
2603
+ }
2604
+ ),
2605
+ /* @__PURE__ */ jsxs("h3", { className: "flex flex-1 items-center gap-2 text-sm font-semibold", children: [
2606
+ /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2607
+ /* @__PURE__ */ jsx("polygon", { points: "3,18 12,3 21,18" }),
2608
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "3", r: "1.5", fill: "currentColor" }),
2609
+ /* @__PURE__ */ jsx("circle", { cx: "3", cy: "18", r: "1.5", fill: "currentColor" }),
2610
+ /* @__PURE__ */ jsx("circle", { cx: "21", cy: "18", r: "1.5", fill: "currentColor" })
2611
+ ] }),
2612
+ "D\u1EF1ng h\xECnh h\u1ECDc"
2613
+ ] }),
2614
+ isMobile && /* @__PURE__ */ jsxs(Fragment, { children: [
2615
+ /* @__PURE__ */ jsx(
2616
+ "button",
2617
+ {
2618
+ type: "button",
2619
+ onClick: onUndo,
2620
+ disabled: !canUndo,
2621
+ "aria-label": "Ho\xE0n t\xE1c",
2622
+ title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
2623
+ "data-testid": "undo-btn-mobile",
2624
+ className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15 disabled:opacity-40",
2625
+ children: /* @__PURE__ */ jsx(UndoIcon, {})
2626
+ }
2627
+ ),
2628
+ /* @__PURE__ */ jsx(
2629
+ "button",
2630
+ {
2631
+ type: "button",
2632
+ onClick: onRedo,
2633
+ disabled: !canRedo,
2634
+ "aria-label": "L\xE0m l\u1EA1i",
2635
+ title: "L\xE0m l\u1EA1i (Ctrl/Cmd+Shift+Z)",
2636
+ "data-testid": "redo-btn-mobile",
2637
+ className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15 disabled:opacity-40",
2638
+ children: /* @__PURE__ */ jsx(RedoIcon, {})
2639
+ }
2640
+ ),
2641
+ /* @__PURE__ */ jsx(
2642
+ "button",
2643
+ {
2644
+ type: "button",
2645
+ onClick: handleInsert,
2646
+ disabled: !ready,
2647
+ "data-testid": "geometry-insert-btn-mobile",
2648
+ className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
2649
+ children: "Ch\xE8n"
2650
+ }
2651
+ )
2652
+ ] }),
2653
+ /* @__PURE__ */ jsx("button", { onClick: onClose, "aria-label": "\u0110\xF3ng", className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15", children: /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2654
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
2655
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
2656
+ ] }) })
2657
+ ] }),
2658
+ /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1", style: isMobile ? void 0 : { height: "420px" }, children: /* @__PURE__ */ jsx(
2659
+ JSXGraphMiniBoard,
2660
+ {
2661
+ onReady: handleReady,
2662
+ initialState,
2663
+ isDark
2664
+ }
2665
+ ) }),
2666
+ propsPopover && (propsPopover.kind === "point" ? /* @__PURE__ */ jsx(
2667
+ PropertiesPopover,
2668
+ {
2669
+ kind: "point",
2670
+ anchor: propsPopover.screenCoords,
2671
+ isDark,
2672
+ currentName: propsPopover.name,
2673
+ currentColor: propsPopover.color,
2674
+ currentDash: propsPopover.dash,
2675
+ currentWidth: propsPopover.width,
2676
+ currentFace: propsPopover.face,
2677
+ currentShowLabel: propsPopover.showLabel,
2678
+ getAllNames: () => handleRef.current?.getAllPointNames() ?? [],
2679
+ onClose: () => setPropsPopover(null),
2680
+ onMutate: (patch) => {
2681
+ handleRef.current?.mutateObject(propsPopover.obj, patch);
2682
+ if (patch.remove) setPropsPopover(null);
2683
+ if (typeof patch.valueLabel === "boolean" || patch.attrs) {
2684
+ setPropsPopover((cur) => cur ? { ...cur, showValue: patch.valueLabel ?? cur.showValue } : cur);
2685
+ }
2686
+ }
2687
+ }
2688
+ ) : /* @__PURE__ */ jsx(
2689
+ PropertiesPopover,
2690
+ {
2691
+ kind: propsPopover.kind,
2692
+ anchor: propsPopover.screenCoords,
2693
+ isDark,
2694
+ currentName: propsPopover.name,
2695
+ currentColor: propsPopover.color,
2696
+ currentDash: propsPopover.dash,
2697
+ currentWidth: propsPopover.width,
2698
+ currentShowLabel: propsPopover.showLabel,
2699
+ currentShowValue: propsPopover.showValue,
2700
+ getAllNames: () => handleRef.current?.getAllPointNames() ?? [],
2701
+ onClose: () => setPropsPopover(null),
2702
+ onMutate: (patch) => {
2703
+ handleRef.current?.mutateObject(propsPopover.obj, patch);
2704
+ if (patch.remove) setPropsPopover(null);
2705
+ if (typeof patch.valueLabel === "boolean") {
2706
+ setPropsPopover((cur) => cur ? { ...cur, showValue: patch.valueLabel ?? cur.showValue } : cur);
2707
+ }
2708
+ if (patch.attrs && "withLabel" in patch.attrs) {
2709
+ setPropsPopover((cur) => cur ? { ...cur, showLabel: !!patch.attrs?.withLabel } : cur);
2710
+ }
2711
+ }
2712
+ }
2713
+ )),
2714
+ transformPopover && /* @__PURE__ */ jsx(
2715
+ TransformParamPopover,
2716
+ {
2717
+ kind: transformPopover.tool,
2718
+ anchor: transformPopover.anchor,
2719
+ defaultValue: transformPopover.tool === "rotate" ? 90 : transformPopover.tool === "dilate" ? 2 : 6,
2720
+ isDark,
2721
+ onConfirm: (v) => {
2722
+ handleRef.current?.confirmTransformParam(v);
2723
+ setTransformPopover(null);
2724
+ },
2725
+ onCancel: () => {
2726
+ handleRef.current?.cancelTransformParam();
2727
+ setTransformPopover(null);
2728
+ }
2729
+ }
2730
+ ),
2731
+ !isMobile && /* @__PURE__ */ jsxs("footer", { className: "flex items-center justify-between border-t border-slate-200 bg-slate-50 px-3 py-2", children: [
2732
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-slate-500", children: "Ch\u1ECDn c\xF4ng c\u1EE5 b\xEAn tr\xE1i, click tr\xEAn b\u1EA3ng \u0111\u1EC3 d\u1EF1ng h\xECnh." }),
2733
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
2734
+ /* @__PURE__ */ jsx(
2735
+ "button",
2736
+ {
2737
+ onClick: onClose,
2738
+ className: "rounded border border-slate-300 bg-white px-3 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100",
2739
+ children: "Hu\u1EF7"
2740
+ }
2741
+ ),
2742
+ /* @__PURE__ */ jsx(
2743
+ "button",
2744
+ {
2745
+ onClick: handleInsert,
2746
+ disabled: !ready,
2747
+ "data-testid": "geometry-insert-btn",
2748
+ className: "rounded bg-emerald-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-emerald-700 disabled:opacity-50",
2749
+ children: "Ch\xE8n"
2750
+ }
2751
+ )
2752
+ ] })
2753
+ ] })
2754
+ ]
2755
+ }
2756
+ );
2757
+ }
2758
+ );
2759
+ var INITIAL_GEOM_STATE = {
2760
+ tool: "move",
2761
+ showAxis: false,
2762
+ showGrid: false,
2763
+ canUndo: false,
2764
+ canRedo: false
2765
+ };
2766
+ var GeometryStampHost = forwardRef(
2767
+ function GeometryStampHost2({ api, editingElement, onClose, isDark }, ref) {
2768
+ const panelRef = useRef(null);
2769
+ const [geomState, setGeomState] = useState(INITIAL_GEOM_STATE);
2770
+ const { isMobile } = useIsMobile();
2771
+ const [drawerOpen, setDrawerOpen] = useState(false);
2772
+ const { chordGroup } = useChordShortcut({
2773
+ groupOrder: GROUP_ORDER,
2774
+ tools: TOOLS,
2775
+ onSelect: (key) => panelRef.current?.setTool(key),
2776
+ enabled: !isMobile
2777
+ });
2778
+ const initialState = useMemo(() => {
2779
+ if (!editingElement) return null;
2780
+ if (!isGeometryCustomData(editingElement.customData)) return null;
2781
+ try {
2782
+ return JSON.parse(editingElement.customData.jsonState);
2783
+ } catch {
2784
+ console.warn("GeometryStampHost: customData jsonState corrupted");
2785
+ return null;
2786
+ }
2787
+ }, [editingElement]);
2788
+ const handleInsert = useCallback(
2789
+ async (jsonState, svgString) => {
2790
+ if (!api) return;
2791
+ try {
2792
+ await insertStampImage(api, {
2793
+ svgString,
2794
+ makeCustomData: (width, height) => ({
2795
+ kind: "geometry",
2796
+ version: 1,
2797
+ jsonState,
2798
+ svgWidth: width,
2799
+ svgHeight: height
2800
+ }),
2801
+ editingElementId: editingElement?.id ?? null
2802
+ });
2803
+ } catch (err) {
2804
+ console.error("Geometry insert failed:", err);
2805
+ }
2806
+ onClose();
2807
+ },
2808
+ [api, editingElement?.id, onClose]
2809
+ );
2810
+ useImperativeHandle(
2811
+ ref,
2812
+ () => ({
2813
+ tryInsert: () => panelRef.current?.insert() ?? false,
2814
+ hasContent: () => panelRef.current?.hasContent() ?? false
2815
+ }),
2816
+ []
2817
+ );
2818
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
2819
+ /* @__PURE__ */ jsx(
2820
+ LeftPanel,
2821
+ {
2822
+ activeTool: geomState.tool,
2823
+ onToolChange: (t) => panelRef.current?.setTool(t),
2824
+ showAxis: geomState.showAxis,
2825
+ showGrid: geomState.showGrid,
2826
+ onShowAxisChange: (b) => panelRef.current?.setShowAxis(b),
2827
+ onShowGridChange: (b) => panelRef.current?.setShowGrid(b),
2828
+ onUndo: () => panelRef.current?.undo(),
2829
+ canUndo: geomState.canUndo,
2830
+ onRedo: () => panelRef.current?.redo(),
2831
+ canRedo: geomState.canRedo,
2832
+ onClose,
2833
+ isDark,
2834
+ isMobile,
2835
+ drawerOpen,
2836
+ onDrawerClose: () => setDrawerOpen(false),
2837
+ chordGroup
2838
+ }
2839
+ ),
2840
+ /* @__PURE__ */ jsx(
2841
+ GeometryEditorPanel,
2842
+ {
2843
+ ref: panelRef,
2844
+ initialState,
2845
+ onInsert: handleInsert,
2846
+ onClose,
2847
+ onStateChange: setGeomState,
2848
+ withLeftPanel: !isMobile,
2849
+ isDark,
2850
+ isMobile,
2851
+ onOpenDrawer: () => setDrawerOpen(true),
2852
+ onUndo: () => panelRef.current?.undo(),
2853
+ onRedo: () => panelRef.current?.redo(),
2854
+ canUndo: geomState.canUndo,
2855
+ canRedo: geomState.canRedo
2856
+ }
2857
+ )
2858
+ ] });
2859
+ }
2860
+ );
2861
+
2862
+ export { GeometryStampHost };
2863
+ //# sourceMappingURL=host-VDNAJMLC.mjs.map
2864
+ //# sourceMappingURL=host-VDNAJMLC.mjs.map