@xom11/whiteboard 0.6.5 → 0.9.1

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