@xom11/whiteboard 0.6.4 → 0.7.0

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