@xom11/whiteboard 0.11.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +67 -0
  2. package/dist/{ExcalidrawWithMenus-EAVPOPJZ.mjs → ExcalidrawWithMenus-WENZRYYE.mjs} +2 -3
  3. package/dist/ExcalidrawWithMenus-WENZRYYE.mjs.map +1 -0
  4. package/dist/catalog.json +57 -0
  5. package/dist/chunk-4D5CSIJO.mjs +1167 -0
  6. package/dist/chunk-4D5CSIJO.mjs.map +1 -0
  7. package/dist/chunk-5UTGXHLJ.mjs +57 -0
  8. package/dist/chunk-5UTGXHLJ.mjs.map +1 -0
  9. package/dist/chunk-6V4SH4JJ.mjs +1801 -0
  10. package/dist/chunk-6V4SH4JJ.mjs.map +1 -0
  11. package/dist/chunk-AZIARTGX.mjs +23 -0
  12. package/dist/chunk-AZIARTGX.mjs.map +1 -0
  13. package/dist/chunk-BKSXPNPQ.mjs +348 -0
  14. package/dist/chunk-BKSXPNPQ.mjs.map +1 -0
  15. package/dist/{chunk-YVJP7NRG.mjs → chunk-CRAPWQKJ.mjs} +7 -9
  16. package/dist/chunk-CRAPWQKJ.mjs.map +1 -0
  17. package/dist/chunk-CSCF3YFZ.mjs +388 -0
  18. package/dist/chunk-CSCF3YFZ.mjs.map +1 -0
  19. package/dist/chunk-HNQLZIEP.mjs +78 -0
  20. package/dist/chunk-HNQLZIEP.mjs.map +1 -0
  21. package/dist/chunk-IBTRMWD6.mjs +28 -0
  22. package/dist/chunk-IBTRMWD6.mjs.map +1 -0
  23. package/dist/chunk-ICR4CVOE.mjs +57 -0
  24. package/dist/chunk-ICR4CVOE.mjs.map +1 -0
  25. package/dist/chunk-LVNCYP4J.mjs +57 -0
  26. package/dist/chunk-LVNCYP4J.mjs.map +1 -0
  27. package/dist/chunk-MFOGFFIL.mjs +95 -0
  28. package/dist/chunk-MFOGFFIL.mjs.map +1 -0
  29. package/dist/chunk-NVJ7K3DK.mjs +29 -0
  30. package/dist/chunk-NVJ7K3DK.mjs.map +1 -0
  31. package/dist/chunk-O4WIZFRQ.mjs +11 -0
  32. package/dist/chunk-O4WIZFRQ.mjs.map +1 -0
  33. package/dist/{chunk-C6SCVOMC.mjs → chunk-QGNU34T7.mjs} +5 -41
  34. package/dist/chunk-QGNU34T7.mjs.map +1 -0
  35. package/dist/chunk-R5FL6S7L.mjs +22 -0
  36. package/dist/chunk-R5FL6S7L.mjs.map +1 -0
  37. package/dist/{chunk-7P7SQFOW.mjs → chunk-SGFJLHHG.mjs} +3 -3
  38. package/dist/chunk-SGFJLHHG.mjs.map +1 -0
  39. package/dist/{chunk-PWIMZIB6.mjs → chunk-WWMQ2VHZ.mjs} +7 -8
  40. package/dist/chunk-WWMQ2VHZ.mjs.map +1 -0
  41. package/dist/chunk-YIPI3WUL.mjs +61 -0
  42. package/dist/chunk-YIPI3WUL.mjs.map +1 -0
  43. package/dist/chunk-ZBJBQKJ2.mjs +330 -0
  44. package/dist/chunk-ZBJBQKJ2.mjs.map +1 -0
  45. package/dist/geometry-2d.d.mts +3 -6
  46. package/dist/geometry-2d.d.ts +3 -6
  47. package/dist/geometry-2d.js +7007 -2633
  48. package/dist/geometry-2d.js.map +1 -1
  49. package/dist/geometry-2d.mjs +8 -4
  50. package/dist/geometry-3d.d.mts +4 -7
  51. package/dist/geometry-3d.d.ts +4 -7
  52. package/dist/geometry-3d.js +5446 -2507
  53. package/dist/geometry-3d.js.map +1 -1
  54. package/dist/geometry-3d.mjs +7 -4
  55. package/dist/graph-2d.d.mts +4 -7
  56. package/dist/graph-2d.d.ts +4 -7
  57. package/dist/graph-2d.js +5300 -1677
  58. package/dist/graph-2d.js.map +1 -1
  59. package/dist/graph-2d.mjs +10 -3
  60. package/dist/host-DOAYVL35.mjs +3199 -0
  61. package/dist/host-DOAYVL35.mjs.map +1 -0
  62. package/dist/host-GKNQBBUE.mjs +1142 -0
  63. package/dist/host-GKNQBBUE.mjs.map +1 -0
  64. package/dist/{host-Z3TEJKZA.mjs → host-QS2EOTRJ.mjs} +4 -4
  65. package/dist/{host-Z3TEJKZA.mjs.map → host-QS2EOTRJ.mjs.map} +1 -1
  66. package/dist/host-TLIXN4CF.mjs +2374 -0
  67. package/dist/host-TLIXN4CF.mjs.map +1 -0
  68. package/dist/index.css +4 -1
  69. package/dist/index.css.map +1 -1
  70. package/dist/index.d.mts +659 -19
  71. package/dist/index.d.ts +659 -19
  72. package/dist/index.js +13736 -9491
  73. package/dist/index.js.map +1 -1
  74. package/dist/index.mjs +1465 -342
  75. package/dist/index.mjs.map +1 -1
  76. package/dist/latex.d.mts +3 -4
  77. package/dist/latex.d.ts +3 -4
  78. package/dist/latex.js +33 -18
  79. package/dist/latex.js.map +1 -1
  80. package/dist/latex.mjs +2 -3
  81. package/dist/render-SA4JTOW3.mjs +8 -0
  82. package/dist/render-SA4JTOW3.mjs.map +1 -0
  83. package/dist/serialize-3NZS6A6Q.mjs +6 -0
  84. package/dist/serialize-3NZS6A6Q.mjs.map +1 -0
  85. package/dist/{types-CinstD7T.d.mts → types-rA4slL08.d.mts} +69 -4
  86. package/dist/{types-CinstD7T.d.ts → types-rA4slL08.d.ts} +69 -4
  87. package/package.json +34 -6
  88. package/dist/ExcalidrawWithMenus-EAVPOPJZ.mjs.map +0 -1
  89. package/dist/chunk-74VEEZBV.mjs +0 -619
  90. package/dist/chunk-74VEEZBV.mjs.map +0 -1
  91. package/dist/chunk-7P7SQFOW.mjs.map +0 -1
  92. package/dist/chunk-BJTO5JO5.mjs +0 -11
  93. package/dist/chunk-BJTO5JO5.mjs.map +0 -1
  94. package/dist/chunk-C6SCVOMC.mjs.map +0 -1
  95. package/dist/chunk-D257NCQW.mjs +0 -58
  96. package/dist/chunk-D257NCQW.mjs.map +0 -1
  97. package/dist/chunk-G7FR3AIV.mjs +0 -193
  98. package/dist/chunk-G7FR3AIV.mjs.map +0 -1
  99. package/dist/chunk-HTBLO5JO.mjs +0 -41
  100. package/dist/chunk-HTBLO5JO.mjs.map +0 -1
  101. package/dist/chunk-PWIMZIB6.mjs.map +0 -1
  102. package/dist/chunk-SBDMF4NQ.mjs +0 -212
  103. package/dist/chunk-SBDMF4NQ.mjs.map +0 -1
  104. package/dist/chunk-WQOABS6N.mjs +0 -197
  105. package/dist/chunk-WQOABS6N.mjs.map +0 -1
  106. package/dist/chunk-YVJP7NRG.mjs.map +0 -1
  107. package/dist/host-N6ACNJKI.mjs +0 -3226
  108. package/dist/host-N6ACNJKI.mjs.map +0 -1
  109. package/dist/host-NKGV6RF2.mjs +0 -1134
  110. package/dist/host-NKGV6RF2.mjs.map +0 -1
  111. package/dist/host-XVK7UCRE.mjs +0 -2908
  112. package/dist/host-XVK7UCRE.mjs.map +0 -1
@@ -0,0 +1,3199 @@
1
+ "use client";
2
+ import { useToolStateMachine } from './chunk-NVJ7K3DK.mjs';
3
+ import { serializeBoard, renderGeometrySvgFromState, isGeometryCustomData, deserializeBoard } from './chunk-MFOGFFIL.mjs';
4
+ import { JxgRenderer } from './chunk-BKSXPNPQ.mjs';
5
+ import { useChordShortcut } from './chunk-HNQLZIEP.mjs';
6
+ import { safeJsx, initJxgBoard, attachJxgWheelZoom, useToast, ToastHost, STAMP_PANEL_DESKTOP, ToastProvider, useStampStore, StampLeftPanel } from './chunk-4D5CSIJO.mjs';
7
+ import { themeLabel, paletteFor, themeAxis, themeGrid } from './chunk-R5FL6S7L.mjs';
8
+ import './chunk-ICR4CVOE.mjs';
9
+ import { DEFAULT_VIEW_2D, nextLabel, useEditorState, listObjects } from './chunk-6V4SH4JJ.mjs';
10
+ import './chunk-ZBJBQKJ2.mjs';
11
+ import { useIsMobile } from './chunk-P2AOIF7S.mjs';
12
+ import { insertStampImage } from './chunk-QGNU34T7.mjs';
13
+ import './chunk-5UTGXHLJ.mjs';
14
+ import { forwardRef, useRef, useId, useState, useEffect, useCallback, useImperativeHandle, useLayoutEffect, useMemo } from 'react';
15
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
16
+ import { createPortal } from 'react-dom';
17
+
18
+ var C_POINT = "#2563eb";
19
+ var C_CONSTRUCT = "#dc2626";
20
+ var C_FILL = "#f59e0b";
21
+ var C_ARC = "#059669";
22
+ var Icon = {
23
+ // ===== Basic =====
24
+ cursor: /* @__PURE__ */ jsx("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: /* @__PURE__ */ jsx(
25
+ "path",
26
+ {
27
+ d: "M5 3 L5 18 L9.5 14 L12 20 L14 19.2 L11.5 13.5 L17.5 13.5 Z",
28
+ fill: "currentColor",
29
+ fillOpacity: "0.12",
30
+ stroke: "currentColor",
31
+ strokeWidth: "1.4",
32
+ strokeLinejoin: "round"
33
+ }
34
+ ) }),
35
+ select: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinejoin: "round", strokeLinecap: "round", children: [
36
+ /* @__PURE__ */ jsx("rect", { x: "2.5", y: "2.5", width: "14", height: "14", rx: "0.5", stroke: "currentColor", strokeWidth: "1.4", strokeDasharray: "2 1.8" }),
37
+ /* @__PURE__ */ jsx("path", { d: "M10 10 L21 14.5 L14.5 16 L12.5 22 Z", fill: "currentColor", fillOpacity: "0.5", stroke: "currentColor", strokeWidth: "1.2" })
38
+ ] }),
39
+ // ===== Point =====
40
+ point: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
41
+ /* @__PURE__ */ jsx("circle", { cx: "9.5", cy: "14", r: "3.2", fill: C_POINT }),
42
+ /* @__PURE__ */ jsx("text", { x: "13.5", y: "10.5", fontSize: "9.5", fontFamily: "serif", fontStyle: "italic", fontWeight: "600", fill: C_POINT, children: "A" })
43
+ ] }),
44
+ midpoint: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
45
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12", stroke: "currentColor", strokeWidth: "1.5" }),
46
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "12", r: "1.8", fill: C_POINT }),
47
+ /* @__PURE__ */ jsx("circle", { cx: "20", cy: "12", r: "1.8", fill: C_POINT }),
48
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "2.6", fill: C_CONSTRUCT })
49
+ ] }),
50
+ perpFoot: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
51
+ /* @__PURE__ */ jsx("line", { x1: "2", y1: "17", x2: "22", y2: "17", stroke: "currentColor", strokeWidth: "1.5" }),
52
+ /* @__PURE__ */ jsx("line", { x1: "9", y1: "5", x2: "9", y2: "17", stroke: C_CONSTRUCT, strokeWidth: "1.4", strokeDasharray: "2.5 2" }),
53
+ /* @__PURE__ */ jsx("rect", { x: "9", y: "13.5", width: "3.5", height: "3.5", fill: "none", stroke: "currentColor", strokeWidth: "1" }),
54
+ /* @__PURE__ */ jsx("circle", { cx: "9", cy: "5", r: "2", fill: C_POINT }),
55
+ /* @__PURE__ */ jsx("circle", { cx: "9", cy: "17", r: "2.4", fill: C_CONSTRUCT })
56
+ ] }),
57
+ intersect: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
58
+ /* @__PURE__ */ jsx("path", { d: "M3 5 L21 19", stroke: "currentColor", strokeWidth: "1.6" }),
59
+ /* @__PURE__ */ jsx("path", { d: "M21 5 L3 19", stroke: "currentColor", strokeWidth: "1.6" }),
60
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "2.6", fill: C_CONSTRUCT })
61
+ ] }),
62
+ // ===== Line =====
63
+ segment: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
64
+ /* @__PURE__ */ jsx("line", { x1: "5", y1: "18", x2: "19", y2: "6", stroke: "currentColor", strokeWidth: "1.6" }),
65
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "18", r: "2.2", fill: C_POINT }),
66
+ /* @__PURE__ */ jsx("circle", { cx: "19", cy: "6", r: "2.2", fill: C_POINT })
67
+ ] }),
68
+ line: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
69
+ /* @__PURE__ */ jsx("line", { x1: "2", y1: "21", x2: "22", y2: "3", stroke: "currentColor", strokeWidth: "1.6" }),
70
+ /* @__PURE__ */ jsx("circle", { cx: "8.5", cy: "15.5", r: "1.9", fill: C_POINT }),
71
+ /* @__PURE__ */ jsx("circle", { cx: "15.5", cy: "8.5", r: "1.9", fill: C_POINT })
72
+ ] }),
73
+ ray: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", children: [
74
+ /* @__PURE__ */ jsx("line", { x1: "5", y1: "19", x2: "22", y2: "2", stroke: "currentColor", strokeWidth: "1.6" }),
75
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "19", r: "2.2", fill: C_POINT }),
76
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "1.7", fill: C_POINT })
77
+ ] }),
78
+ vector: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", children: [
79
+ /* @__PURE__ */ jsx("line", { x1: "5", y1: "19", x2: "18", y2: "6" }),
80
+ /* @__PURE__ */ jsx("polyline", { points: "13,5 19,5 19,11" }),
81
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "19", r: "2", fill: C_POINT, stroke: "none" })
82
+ ] }),
83
+ // ===== Construct =====
84
+ perpendicular: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
85
+ /* @__PURE__ */ jsx("line", { x1: "2", y1: "17", x2: "22", y2: "17", stroke: "currentColor", strokeWidth: "1.5" }),
86
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "3", x2: "12", y2: "17", stroke: C_CONSTRUCT, strokeWidth: "1.5" }),
87
+ /* @__PURE__ */ jsx("rect", { x: "12", y: "13.5", width: "3.5", height: "3.5", fill: "none", stroke: "currentColor", strokeWidth: "1" }),
88
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "17", r: "1.8", fill: C_POINT })
89
+ ] }),
90
+ parallel: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
91
+ /* @__PURE__ */ jsx("line", { x1: "2", y1: "9", x2: "22", y2: "6", stroke: "currentColor", strokeWidth: "1.5" }),
92
+ /* @__PURE__ */ jsx("line", { x1: "2", y1: "19", x2: "22", y2: "16", stroke: C_CONSTRUCT, strokeWidth: "1.5" }),
93
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "17.5", r: "1.9", fill: C_POINT })
94
+ ] }),
95
+ perpBisector: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
96
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "16", x2: "20", y2: "16", stroke: "currentColor", strokeWidth: "1.5" }),
97
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "3", x2: "12", y2: "22", stroke: C_CONSTRUCT, strokeWidth: "1.4", strokeDasharray: "2.5 2" }),
98
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "16", r: "2", fill: C_POINT }),
99
+ /* @__PURE__ */ jsx("circle", { cx: "20", cy: "16", r: "2", fill: C_POINT })
100
+ ] }),
101
+ bisector: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
102
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "22", y2: "20", stroke: "currentColor", strokeWidth: "1.5" }),
103
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "22", y2: "4", stroke: "currentColor", strokeWidth: "1.5" }),
104
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "23", y2: "11", stroke: C_CONSTRUCT, strokeWidth: "1.4", strokeDasharray: "2.5 2" })
105
+ ] }),
106
+ // ===== Polygon =====
107
+ polygon: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinejoin: "round", children: [
108
+ /* @__PURE__ */ jsx("polygon", { points: "6,5 18,6 22,14 13,21 4,16", fill: C_FILL, fillOpacity: "0.28", stroke: C_FILL, strokeWidth: "1.5" }),
109
+ /* @__PURE__ */ jsx("circle", { cx: "6", cy: "5", r: "1.5", fill: C_POINT }),
110
+ /* @__PURE__ */ jsx("circle", { cx: "18", cy: "6", r: "1.5", fill: C_POINT }),
111
+ /* @__PURE__ */ jsx("circle", { cx: "22", cy: "14", r: "1.5", fill: C_POINT }),
112
+ /* @__PURE__ */ jsx("circle", { cx: "13", cy: "21", r: "1.5", fill: C_POINT }),
113
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "16", r: "1.5", fill: C_POINT })
114
+ ] }),
115
+ regularPolygon: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinejoin: "round", children: [
116
+ /* @__PURE__ */ jsx("polygon", { points: "12,3 20.5,8 20.5,16 12,21 3.5,16 3.5,8", fill: C_FILL, fillOpacity: "0.28", stroke: C_FILL, strokeWidth: "1.5" }),
117
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "3", r: "1.7", fill: C_POINT }),
118
+ /* @__PURE__ */ jsx("circle", { cx: "20.5", cy: "8", r: "1.7", fill: C_POINT })
119
+ ] }),
120
+ // ===== Circle =====
121
+ circleCenter: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
122
+ /* @__PURE__ */ jsx("circle", { cx: "11", cy: "13", r: "8", stroke: "currentColor", strokeWidth: "1.5" }),
123
+ /* @__PURE__ */ jsx("circle", { cx: "11", cy: "13", r: "1.7", fill: C_POINT }),
124
+ /* @__PURE__ */ jsx("circle", { cx: "19", cy: "13", r: "1.7", fill: C_POINT })
125
+ ] }),
126
+ semicircle: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
127
+ /* @__PURE__ */ jsx("path", { d: "M 4 16 A 8 8 0 0 1 20 16", stroke: C_ARC, strokeWidth: "1.6", fill: "none" }),
128
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "16", x2: "20", y2: "16", stroke: "currentColor", strokeWidth: "1.2", strokeDasharray: "2 1.6" }),
129
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "16", r: "1.9", fill: C_POINT }),
130
+ /* @__PURE__ */ jsx("circle", { cx: "20", cy: "16", r: "1.9", fill: C_POINT })
131
+ ] }),
132
+ arcCenter: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
133
+ /* @__PURE__ */ jsx("path", { d: "M 6 6 A 9 9 0 0 1 18 18", stroke: C_ARC, strokeWidth: "1.7", fill: "none" }),
134
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "12", x2: "6", y2: "6", stroke: "currentColor", strokeWidth: "1", strokeDasharray: "1.5 1.5", opacity: "0.5" }),
135
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "12", x2: "18", y2: "18", stroke: "currentColor", strokeWidth: "1", strokeDasharray: "1.5 1.5", opacity: "0.5" }),
136
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "2", fill: C_POINT }),
137
+ /* @__PURE__ */ jsx("circle", { cx: "6", cy: "6", r: "1.4", fill: C_POINT }),
138
+ /* @__PURE__ */ jsx("circle", { cx: "18", cy: "18", r: "1.4", fill: C_POINT })
139
+ ] }),
140
+ arc3: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
141
+ /* @__PURE__ */ jsx("path", { d: "M 4 17 A 8 8 0 0 1 20 17", stroke: C_ARC, strokeWidth: "1.8", fill: "none" }),
142
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "17", r: "1.9", fill: C_POINT }),
143
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "9", r: "1.9", fill: C_POINT }),
144
+ /* @__PURE__ */ jsx("circle", { cx: "20", cy: "17", r: "1.9", fill: C_POINT })
145
+ ] }),
146
+ sectorCenter: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinejoin: "round", children: [
147
+ /* @__PURE__ */ jsx(
148
+ "path",
149
+ {
150
+ d: "M 12 12 L 5 7 A 8.6 8.6 0 0 1 19 7 Z",
151
+ fill: C_FILL,
152
+ fillOpacity: "0.25",
153
+ stroke: C_ARC,
154
+ strokeWidth: "1.6"
155
+ }
156
+ ),
157
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "12", x2: "5", y2: "7", stroke: "currentColor", strokeWidth: "1.3" }),
158
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "12", x2: "19", y2: "7", stroke: "currentColor", strokeWidth: "1.3" }),
159
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "1.8", fill: C_POINT }),
160
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "7", r: "1.4", fill: C_POINT }),
161
+ /* @__PURE__ */ jsx("circle", { cx: "19", cy: "7", r: "1.4", fill: C_POINT })
162
+ ] }),
163
+ circle3: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
164
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "13", r: "8", stroke: "currentColor", strokeWidth: "1.5" }),
165
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "5", r: "1.7", fill: C_POINT }),
166
+ /* @__PURE__ */ jsx("circle", { cx: "19", cy: "16", r: "1.7", fill: C_POINT }),
167
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "16", r: "1.7", fill: C_POINT })
168
+ ] }),
169
+ tangent: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
170
+ /* @__PURE__ */ jsx("circle", { cx: "10", cy: "13", r: "5", stroke: "currentColor", strokeWidth: "1.5" }),
171
+ /* @__PURE__ */ jsx("line", { x1: "20", y1: "4", x2: "14.5", y2: "15.2", stroke: C_CONSTRUCT, strokeWidth: "1.6" }),
172
+ /* @__PURE__ */ jsx("line", { x1: "20", y1: "4", x2: "8.3", y2: "8.3", stroke: C_CONSTRUCT, strokeWidth: "1.6" }),
173
+ /* @__PURE__ */ jsx("circle", { cx: "20", cy: "4", r: "1.9", fill: C_POINT }),
174
+ /* @__PURE__ */ jsx("circle", { cx: "14.5", cy: "15.2", r: "1.4", fill: C_CONSTRUCT }),
175
+ /* @__PURE__ */ jsx("circle", { cx: "8.3", cy: "8.3", r: "1.4", fill: C_CONSTRUCT })
176
+ ] }),
177
+ // ===== Triangle centers =====
178
+ centroid: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinejoin: "round", children: [
179
+ /* @__PURE__ */ jsx("polygon", { points: "4,20 20,20 12,4", fill: C_FILL, fillOpacity: "0.18", stroke: "currentColor", strokeWidth: "1.4" }),
180
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "4", x2: "12", y2: "20", stroke: "#94a3b8", strokeWidth: "1", strokeDasharray: "1.5 1.5" }),
181
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "16", y2: "12", stroke: "#94a3b8", strokeWidth: "1", strokeDasharray: "1.5 1.5" }),
182
+ /* @__PURE__ */ jsx("line", { x1: "20", y1: "20", x2: "8", y2: "12", stroke: "#94a3b8", strokeWidth: "1", strokeDasharray: "1.5 1.5" }),
183
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "20", r: "1.5", fill: C_POINT }),
184
+ /* @__PURE__ */ jsx("circle", { cx: "20", cy: "20", r: "1.5", fill: C_POINT }),
185
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "4", r: "1.5", fill: C_POINT }),
186
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "14.67", r: "2.4", fill: C_CONSTRUCT })
187
+ ] }),
188
+ circumcenter: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
189
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "9", stroke: "currentColor", strokeWidth: "1.4" }),
190
+ /* @__PURE__ */ jsx("polygon", { points: "12,3 21,16 3,16", fill: "none", stroke: "currentColor", strokeWidth: "1.3" }),
191
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "3", r: "1.6", fill: C_POINT }),
192
+ /* @__PURE__ */ jsx("circle", { cx: "21", cy: "16", r: "1.6", fill: C_POINT }),
193
+ /* @__PURE__ */ jsx("circle", { cx: "3", cy: "16", r: "1.6", fill: C_POINT }),
194
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "2.4", fill: C_CONSTRUCT })
195
+ ] }),
196
+ incenter: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
197
+ /* @__PURE__ */ jsx("polygon", { points: "3,20 21,20 12,4", fill: "none", stroke: "currentColor", strokeWidth: "1.4" }),
198
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "14", r: "4.5", stroke: "currentColor", strokeWidth: "1.3" }),
199
+ /* @__PURE__ */ jsx("circle", { cx: "3", cy: "20", r: "1.6", fill: C_POINT }),
200
+ /* @__PURE__ */ jsx("circle", { cx: "21", cy: "20", r: "1.6", fill: C_POINT }),
201
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "4", r: "1.6", fill: C_POINT }),
202
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "14", r: "2.2", fill: C_CONSTRUCT })
203
+ ] }),
204
+ orthocenter: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", children: [
205
+ /* @__PURE__ */ jsx("polygon", { points: "3,20 21,20 8,5", fill: "none", stroke: "currentColor", strokeWidth: "1.4" }),
206
+ /* @__PURE__ */ jsx("line", { x1: "8", y1: "5", x2: "8", y2: "20", stroke: C_CONSTRUCT, strokeWidth: "1.2", strokeDasharray: "2 1.6" }),
207
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "20", x2: "14.5", y2: "11.5", stroke: C_CONSTRUCT, strokeWidth: "1.2", strokeDasharray: "2 1.6" }),
208
+ /* @__PURE__ */ jsx("circle", { cx: "3", cy: "20", r: "1.6", fill: C_POINT }),
209
+ /* @__PURE__ */ jsx("circle", { cx: "21", cy: "20", r: "1.6", fill: C_POINT }),
210
+ /* @__PURE__ */ jsx("circle", { cx: "8", cy: "5", r: "1.6", fill: C_POINT }),
211
+ /* @__PURE__ */ jsx("circle", { cx: "8", cy: "14.5", r: "2.4", fill: C_CONSTRUCT })
212
+ ] }),
213
+ // ===== Measure =====
214
+ angle: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", children: [
215
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "22", y2: "20", stroke: "currentColor", strokeWidth: "1.5" }),
216
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "22", y2: "6", stroke: "currentColor", strokeWidth: "1.5" }),
217
+ /* @__PURE__ */ jsx("path", { d: "M14 20 A 10 10 0 0 0 11 13.4", stroke: C_ARC, strokeWidth: "1.6", fill: "none" }),
218
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "20", r: "1.7", fill: C_POINT })
219
+ ] }),
220
+ distance: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
221
+ /* @__PURE__ */ jsx("line", { x1: "5", y1: "16", x2: "19", y2: "16", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" }),
222
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "16", r: "2", fill: C_POINT }),
223
+ /* @__PURE__ */ jsx("circle", { cx: "19", cy: "16", r: "2", fill: C_POINT }),
224
+ /* @__PURE__ */ jsx("text", { x: "8.5", y: "11", fontSize: "7", fontFamily: "serif", fontStyle: "italic", fontWeight: "600", fill: "currentColor", children: "cm" })
225
+ ] }),
226
+ area: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinejoin: "round", children: [
227
+ /* @__PURE__ */ jsx("polygon", { points: "4,10 14,3.5 21,12 16,21 5,18", fill: C_FILL, fillOpacity: "0.32", stroke: C_FILL, strokeWidth: "1.4" }),
228
+ /* @__PURE__ */ jsx("text", { x: "1.5", y: "8", fontSize: "6.5", fontFamily: "serif", fontStyle: "italic", fontWeight: "600", fill: "currentColor", children: "cm\xB2" })
229
+ ] }),
230
+ // ===== Edit =====
231
+ toggleLabel: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
232
+ /* @__PURE__ */ jsx("text", { x: "1.5", y: "19", fontSize: "15", fontFamily: "serif", fontWeight: "700", fill: "currentColor", children: "A" }),
233
+ /* @__PURE__ */ jsx("text", { x: "12", y: "19", fontSize: "15", fontFamily: "serif", fontWeight: "700", fill: "currentColor", fillOpacity: "0.35", children: "A" })
234
+ ] }),
235
+ toggleVisible: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
236
+ /* @__PURE__ */ jsx("circle", { cx: "8", cy: "12", r: "3.2", fill: C_POINT }),
237
+ /* @__PURE__ */ jsx("circle", { cx: "17", cy: "12", r: "3.2", fill: "none", stroke: C_POINT, strokeWidth: "1.6" })
238
+ ] }),
239
+ trash: (
240
+ // Eraser hình bình hành — GeoGebra dùng eraser, không phải trash bin.
241
+ /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinejoin: "round", strokeLinecap: "round", children: [
242
+ /* @__PURE__ */ jsx("path", { d: "M14.5 3 L21 9.5 L11.5 19 L4 19 L4 11.5 Z", fill: "currentColor", fillOpacity: "0.14", stroke: "currentColor", strokeWidth: "1.5" }),
243
+ /* @__PURE__ */ jsx("line", { x1: "9.5", y1: "8", x2: "16", y2: "14.5", stroke: "currentColor", strokeWidth: "1.4" })
244
+ ] })
245
+ ),
246
+ // ===== Transform =====
247
+ // Style chung: BLUE = input (điểm gốc / tâm), RED = output (ảnh sau biến hình).
248
+ // Mảnh, ít chi tiết — focus vào quan hệ điểm gốc ↔ ảnh.
249
+ translate: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", children: [
250
+ /* @__PURE__ */ jsx("line", { x1: "7", y1: "21", x2: "12.5", y2: "16.25", stroke: "currentColor", strokeWidth: "1.4" }),
251
+ /* @__PURE__ */ jsx("polyline", { points: "11.94,17.85 12.5,16.25 10.83,16.57", stroke: "currentColor", strokeWidth: "1.4", fill: "none" }),
252
+ /* @__PURE__ */ jsx("line", { x1: "7", y1: "15", x2: "18", y2: "5.5", stroke: "#94a3b8", strokeWidth: "1.4" }),
253
+ /* @__PURE__ */ jsx("polyline", { points: "15.54,5.97 18,5.5 17.18,7.86", stroke: "#94a3b8", strokeWidth: "1.4", fill: "none" }),
254
+ /* @__PURE__ */ jsx("circle", { cx: "7", cy: "15", r: "2", fill: C_POINT }),
255
+ /* @__PURE__ */ jsx("circle", { cx: "18", cy: "5.5", r: "2", fill: C_CONSTRUCT })
256
+ ] }),
257
+ rotate: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", strokeLinecap: "round", strokeLinejoin: "round", children: [
258
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "18", x2: "20", y2: "6", stroke: "currentColor", strokeWidth: "1", opacity: "0.55" }),
259
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "18", x2: "4", y2: "9", stroke: "currentColor", strokeWidth: "1", opacity: "0.55" }),
260
+ /* @__PURE__ */ jsx("path", { d: "M 14.22 14.67 A 4 4 0 0 0 9.34 15.01", stroke: "currentColor", strokeWidth: "1.2", fill: "none" }),
261
+ /* @__PURE__ */ jsx("text", { x: "10", y: "14", fontSize: "7", fontFamily: "serif", fontStyle: "italic", fontWeight: "700", fill: "currentColor", children: "\u03B1" }),
262
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "18", r: "2.4", fill: C_POINT, stroke: "#fff", strokeWidth: "0.8" }),
263
+ /* @__PURE__ */ jsx("circle", { cx: "20", cy: "6", r: "1.9", fill: C_POINT }),
264
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "9", r: "1.9", fill: C_CONSTRUCT })
265
+ ] }),
266
+ reflectLine: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
267
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "21", x2: "21", y2: "3", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }),
268
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "11", r: "2", fill: C_POINT }),
269
+ /* @__PURE__ */ jsx("circle", { cx: "13", cy: "19", r: "2", fill: C_CONSTRUCT })
270
+ ] }),
271
+ reflectPoint: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
272
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "20", y2: "4", stroke: "currentColor", strokeWidth: "0.9", strokeDasharray: "1.5 1.5", opacity: "0.45" }),
273
+ /* @__PURE__ */ jsx("circle", { cx: "5", cy: "19", r: "1.9", fill: C_POINT }),
274
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "2.6", fill: C_POINT, stroke: "#fff", strokeWidth: "0.8" }),
275
+ /* @__PURE__ */ jsx("circle", { cx: "19", cy: "5", r: "1.9", fill: C_CONSTRUCT })
276
+ ] }),
277
+ dilate: /* @__PURE__ */ jsxs("svg", { width: "22", height: "22", viewBox: "0 0 24 24", fill: "none", children: [
278
+ /* @__PURE__ */ jsx("line", { x1: "3", y1: "20", x2: "21", y2: "4", stroke: "currentColor", strokeWidth: "0.9", strokeDasharray: "1.5 1.5", opacity: "0.5" }),
279
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "19", r: "2.2", fill: C_POINT, stroke: "#fff", strokeWidth: "0.8" }),
280
+ /* @__PURE__ */ jsx("circle", { cx: "8", cy: "15.5", r: "1.7", fill: C_POINT }),
281
+ /* @__PURE__ */ jsx("circle", { cx: "19", cy: "5.7", r: "1.9", fill: C_CONSTRUCT }),
282
+ /* @__PURE__ */ jsx("text", { x: "10.5", y: "10.5", fontSize: "8", fontFamily: "serif", fontStyle: "italic", fontWeight: "700", fill: "currentColor", children: "k" })
283
+ ] })
284
+ };
285
+ var GeometryIconHeader = /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
286
+ /* @__PURE__ */ jsx("polygon", { points: "4,20 20,20 12,5" }),
287
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "20", r: "1.5", fill: "currentColor", stroke: "none" }),
288
+ /* @__PURE__ */ jsx("circle", { cx: "20", cy: "20", r: "1.5", fill: "currentColor", stroke: "none" }),
289
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "5", r: "1.5", fill: "currentColor", stroke: "none" })
290
+ ] });
291
+ function UndoIcon() {
292
+ return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
293
+ /* @__PURE__ */ jsx("polyline", { points: "3 7 3 13 9 13" }),
294
+ /* @__PURE__ */ jsx("path", { d: "M3.51 13a9 9 0 1 0 2.13-9.36L3 7" })
295
+ ] });
296
+ }
297
+ function RedoIcon() {
298
+ return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
299
+ /* @__PURE__ */ jsx("polyline", { points: "21 7 21 13 15 13" }),
300
+ /* @__PURE__ */ jsx("path", { d: "M20.49 13a9 9 0 1 1-2.12-9.36L21 7" })
301
+ ] });
302
+ }
303
+
304
+ // src/stamps/geometry-2d/editor/tools.tsx
305
+ var TOOLS = [
306
+ { key: "move", label: "Di chuy\u1EC3n", hint: "K\xE9o \u0111i\u1EC3m ho\u1EB7c xoay n\u1EC1n", icon: Icon.cursor, group: "move", needs: 0 },
307
+ { 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 },
308
+ { key: "point", label: "\u0110i\u1EC3m m\u1EDBi", hint: "Click \u0111\u1EC3 th\xEAm \u0111i\u1EC3m", icon: Icon.point, group: "point", needs: 1 },
309
+ { 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"] },
310
+ { key: "perpFoot", label: "Ch\xE2n \u0111\u01B0\u1EDDng vu\xF4ng g\xF3c", hint: "Click 1 \u0111i\u1EC3m + 1 \u0111\u01B0\u1EDDng c\xF3 s\u1EB5n", icon: Icon.perpFoot, group: "point", needs: 2, accepts: ["point", "line"] },
311
+ { key: "intersect", label: "Giao \u0111i\u1EC3m c\u1EE7a 2 \u0111\u1ED1i t\u01B0\u1EE3ng", hint: "Click 2 \u0111\u01B0\u1EDDng/\u0111\u01B0\u1EDDng tr\xF2n c\xF3 s\u1EB5n", icon: Icon.intersect, group: "point", needs: 2, accepts: ["lineOrCircle", "lineOrCircle"] },
312
+ { key: "segment", label: "\u0110o\u1EA1n th\u1EB3ng", hint: "Click 2 \u0111i\u1EC3m", icon: Icon.segment, group: "line", needs: 2 },
313
+ { key: "line", label: "\u0110\u01B0\u1EDDng th\u1EB3ng qua 2 \u0111i\u1EC3m", hint: "Click 2 \u0111i\u1EC3m", icon: Icon.line, group: "line", needs: 2 },
314
+ { key: "ray", label: "Tia qua 2 \u0111i\u1EC3m", hint: "Click 2 \u0111i\u1EC3m", icon: Icon.ray, group: "line", needs: 2 },
315
+ { key: "vector", label: "Vector", hint: "Click 2 \u0111i\u1EC3m", icon: Icon.vector, group: "line", needs: 2 },
316
+ { 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"] },
317
+ { 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"] },
318
+ { 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"] },
319
+ { key: "angleBisector", label: "\u0110\u01B0\u1EDDng ph\xE2n gi\xE1c", hint: "Click 3 \u0111i\u1EC3m (\u0111\u1EC9nh \u1EDF gi\u1EEFa) ho\u1EB7c 2 \u0111\u01B0\u1EDDng/\u0111o\u1EA1n (s\u1EBD t\u1EA1o 2 tia ph\xE2n gi\xE1c)", icon: Icon.bisector, group: "construct", needs: 3, accepts: ["pointOrLine", "pointOrLine", "pointOrLine"] },
320
+ { 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 },
321
+ { 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"] },
322
+ { 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 },
323
+ { key: "semicircle", label: "N\u1EEDa \u0111\u01B0\u1EDDng tr\xF2n (\u0111\u01B0\u1EDDng k\xEDnh)", hint: "Click 2 \u0111i\u1EC3m \u2014 b\xE1n nguy\u1EC7t qua \u0111\u01B0\u1EDDng k\xEDnh", icon: Icon.semicircle, group: "circle", needs: 2 },
324
+ { key: "arcCenter", label: "Cung tr\xF2n (t\xE2m + 2 \u0111i\u1EC3m)", hint: "Click t\xE2m O \u2192 A \u2192 B (cung t\u1EEB A \u0111\u1EBFn B)", icon: Icon.arcCenter, group: "circle", needs: 3 },
325
+ { key: "arc3", label: "Cung tr\xF2n qua 3 \u0111i\u1EC3m", hint: "Click 3 \u0111i\u1EC3m tr\xEAn cung", icon: Icon.arc3, group: "circle", needs: 3 },
326
+ { key: "sectorCenter", label: "H\xECnh qu\u1EA1t (t\xE2m + 2 \u0111i\u1EC3m)", hint: "Click t\xE2m O \u2192 A \u2192 B (qu\u1EA1t OAB)", icon: Icon.sectorCenter, group: "circle", needs: 3 },
327
+ { key: "circle3", label: "\u0110\u01B0\u1EDDng tr\xF2n qua 3 \u0111i\u1EC3m", hint: "Click 3 \u0111i\u1EC3m", icon: Icon.circle3, group: "circle", needs: 3 },
328
+ { 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"] },
329
+ { key: "centroid", label: "Tr\u1ECDng t\xE2m tam gi\xE1c", hint: "Click 3 \u0111\u1EC9nh tam gi\xE1c", icon: Icon.centroid, group: "triangle", needs: 3, accepts: ["point", "point", "point"] },
330
+ { key: "circumcenter", label: "T\xE2m \u0111\u01B0\u1EDDng tr\xF2n ngo\u1EA1i ti\u1EBFp", hint: "Click 3 \u0111\u1EC9nh tam gi\xE1c", icon: Icon.circumcenter, group: "triangle", needs: 3, accepts: ["point", "point", "point"] },
331
+ { key: "incenter", label: "T\xE2m \u0111\u01B0\u1EDDng tr\xF2n n\u1ED9i ti\u1EBFp", hint: "Click 3 \u0111\u1EC9nh tam gi\xE1c", icon: Icon.incenter, group: "triangle", needs: 3, accepts: ["point", "point", "point"] },
332
+ { key: "orthocenter", label: "Tr\u1EF1c t\xE2m tam gi\xE1c", hint: "Click 3 \u0111\u1EC9nh tam gi\xE1c", icon: Icon.orthocenter, group: "triangle", needs: 3, accepts: ["point", "point", "point"] },
333
+ { 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"] },
334
+ { 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"] },
335
+ { 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 },
336
+ { 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"] },
337
+ { 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"] },
338
+ { key: "delete", label: "Xo\xE1", hint: "Click v\xE0o \u0111\u1ED1i t\u01B0\u1EE3ng", icon: Icon.trash, group: "edit", needs: 1, accepts: ["any"] },
339
+ { 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"] },
340
+ { 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"] },
341
+ { 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"] },
342
+ { 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"] },
343
+ { 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"] }
344
+ ];
345
+ var GROUP_LABELS = {
346
+ move: "C\u01A1 b\u1EA3n",
347
+ point: "\u0110i\u1EC3m",
348
+ line: "\u0110\u01B0\u1EDDng",
349
+ construct: "D\u1EF1ng h\xECnh",
350
+ polygon: "\u0110a gi\xE1c",
351
+ circle: "\u0110\u01B0\u1EDDng tr\xF2n",
352
+ triangle: "Tam gi\xE1c",
353
+ measure: "\u0110o l\u01B0\u1EDDng",
354
+ edit: "Ch\u1EC9nh s\u1EEDa",
355
+ transform: "Ph\xE9p bi\u1EBFn h\xECnh"
356
+ };
357
+ var GROUP_ORDER = [
358
+ "move",
359
+ "point",
360
+ "line",
361
+ "construct",
362
+ "polygon",
363
+ "circle",
364
+ "triangle",
365
+ "measure",
366
+ "edit",
367
+ "transform"
368
+ ];
369
+ var A_CODE = "A".charCodeAt(0);
370
+ function letterForGroup(g) {
371
+ const idx = GROUP_ORDER.indexOf(g);
372
+ return idx >= 0 ? String.fromCharCode(A_CODE + idx) : "";
373
+ }
374
+ function objKind(obj) {
375
+ if (!obj) return "other";
376
+ const ec = typeof obj.elementClass === "number" ? obj.elementClass : null;
377
+ if (ec === 1) return "point";
378
+ if (ec === 2) return "line";
379
+ if (ec === 3) return "circle";
380
+ const e = (obj.elType || obj.type || "").toString().toLowerCase();
381
+ if (e === "point" || e === "glider" || e === "midpoint" || e === "intersection" || e === "otherintersection" || e === "reflection" || e === "mirrorpoint" || e === "mirrorelement" || e === "orthogonalprojection" || e === "parallelpoint") return "point";
382
+ if (e === "line" || e === "segment" || e === "arrow" || e === "axis" || e === "normal" || e === "parallel" || e === "perpendicular" || e === "tangent" || e === "bisector" || e === "perpendicularsegment") return "line";
383
+ if (e === "circle" || e === "circumcircle") return "circle";
384
+ return "other";
385
+ }
386
+
387
+ // src/stamps/geometry-2d/editor/handlers/pointerDown/move.ts
388
+ function handleMoveTool(ctx, e) {
389
+ const sc = ctx.screenCoordsOf(e);
390
+ if (!sc) return;
391
+ const [sx, sy] = sc;
392
+ ctx.moveDownRef.current = { sx, sy };
393
+ const cx = e.clientX ?? e.touches?.[0]?.clientX ?? 0;
394
+ const cy = e.clientY ?? e.touches?.[0]?.clientY ?? 0;
395
+ ctx.lastClickClientRef.current = { x: cx, y: cy };
396
+ const hits = ctx.objectsAt(e).map(ctx.promoteLabel).filter((o) => o !== ctx.axisObjsRef.current.x && o !== ctx.axisObjsRef.current.y).filter((o) => ctx.jxgIdToSceneId(o) != null);
397
+ const obj = hits.find((o) => objKind(o) === "point") ?? ctx.findNearestPointJxg(e, 12) ?? hits[0];
398
+ if (obj) {
399
+ const sid = ctx.jxgIdToSceneId(obj);
400
+ if (sid) {
401
+ const shift = !!(e.shiftKey || e.altKey);
402
+ ctx.toggleSelect(sid, shift);
403
+ }
404
+ return;
405
+ }
406
+ if (!(e.shiftKey || e.altKey)) ctx.clearSelection();
407
+ }
408
+
409
+ // src/stamps/geometry-2d/editor/handlers/pointerDown/select.ts
410
+ function handleSelectTool(ctx, e) {
411
+ const sc = ctx.screenCoordsOf(e);
412
+ if (!sc) return;
413
+ const [sx, sy] = sc;
414
+ const cx = e.clientX ?? e.touches?.[0]?.clientX ?? 0;
415
+ const cy = e.clientY ?? e.touches?.[0]?.clientY ?? 0;
416
+ ctx.lastClickClientRef.current = { x: cx, y: cy };
417
+ const hits = ctx.objectsAt(e).map(ctx.promoteLabel).filter((o) => o !== ctx.axisObjsRef.current.x && o !== ctx.axisObjsRef.current.y).filter((o) => ctx.jxgIdToSceneId(o) != null);
418
+ const obj = hits.find((o) => objKind(o) === "point") ?? ctx.findNearestPointJxg(e, 12) ?? hits[0];
419
+ if (obj) {
420
+ const sid = ctx.jxgIdToSceneId(obj);
421
+ if (sid) {
422
+ const shift = !!(e.shiftKey || e.altKey);
423
+ ctx.toggleSelect(sid, shift);
424
+ }
425
+ ctx.moveDownRef.current = { sx, sy };
426
+ ctx.marqueeRef.current = null;
427
+ return;
428
+ }
429
+ ctx.marqueeRef.current = { startSx: sx, startSy: sy };
430
+ if (!(e.shiftKey || e.altKey)) ctx.clearSelection();
431
+ }
432
+
433
+ // src/stamps/geometry-2d/editor/handlers/utils.ts
434
+ function freshId(ctx, prefix) {
435
+ const counter = ctx.store.getState().counter;
436
+ let n = counter + 1;
437
+ let id = `${prefix}_${n}`;
438
+ const objs = ctx.store.getState().objects;
439
+ while (id in objs) {
440
+ n += 1;
441
+ id = `${prefix}_${n}`;
442
+ }
443
+ return id;
444
+ }
445
+ function mkSceneObj(id, kind, label, attrs) {
446
+ return {
447
+ id,
448
+ kind,
449
+ label,
450
+ visible: true,
451
+ locked: false,
452
+ layer: "default",
453
+ schemaVersion: 1,
454
+ attrs
455
+ };
456
+ }
457
+ function dispatchAddFreePoint(ctx, x, y) {
458
+ const id = freshId(ctx, "p");
459
+ const label = ctx.nextLabel("point");
460
+ const obj = mkSceneObj(id, "point", label, { constraint: { kind: "free", x, y } });
461
+ ctx.store.dispatch({ type: "ADD", payload: { obj } });
462
+ return id;
463
+ }
464
+ function dispatchAddIntersection(ctx, attrs) {
465
+ const id = freshId(ctx, "X");
466
+ const label = ctx.nextLabel("intersection");
467
+ const obj = mkSceneObj(id, "intersection", label, attrs);
468
+ ctx.store.dispatch({ type: "ADD", payload: { obj } });
469
+ return id;
470
+ }
471
+
472
+ // src/stamps/geometry-2d/editor/handlers/pointerDown/point.ts
473
+ function handlePointTool(ctx, _e, x, y, hits) {
474
+ const curves = hits.filter((o) => objKind(o) === "line" || objKind(o) === "circle");
475
+ if (curves.length >= 2) {
476
+ const a = curves[0];
477
+ const b = curves[1];
478
+ const aId = ctx.jxgIdToSceneId(a);
479
+ const bId = ctx.jxgIdToSceneId(b);
480
+ if (aId && bId) {
481
+ try {
482
+ const aKind = objKind(a);
483
+ const bKind = objKind(b);
484
+ if (aKind === "line" && bKind === "line") {
485
+ dispatchAddIntersection(ctx, { kind: "lineLine", ref1: aId, ref2: bId });
486
+ return;
487
+ }
488
+ const tmp0 = ctx.boardRef.current.create("intersection", [a, b, 0], { visible: false, withLabel: false });
489
+ const tmp1 = ctx.boardRef.current.create("intersection", [a, b, 1], { visible: false, withLabel: false });
490
+ const d0 = Math.hypot((tmp0.X?.() ?? 0) - x, (tmp0.Y?.() ?? 0) - y);
491
+ const d1 = Math.hypot((tmp1.X?.() ?? 0) - x, (tmp1.Y?.() ?? 0) - y);
492
+ safeJsx("handlers.removeObject(intersect.tmp0)", () => ctx.boardRef.current.removeObject(tmp0));
493
+ safeJsx("handlers.removeObject(intersect.tmp1)", () => ctx.boardRef.current.removeObject(tmp1));
494
+ const branch = d0 <= d1 ? 0 : 1;
495
+ const isLineCircle = aKind === "line" && bKind === "circle" || aKind === "circle" && bKind === "line";
496
+ if (isLineCircle) {
497
+ dispatchAddIntersection(ctx, { kind: "lineCircle", ref1: aId, ref2: bId, branch });
498
+ } else {
499
+ dispatchAddIntersection(ctx, { kind: "circleCircle", ref1: aId, ref2: bId, branch });
500
+ }
501
+ return;
502
+ } catch {
503
+ }
504
+ }
505
+ }
506
+ dispatchAddFreePoint(ctx, x, y);
507
+ }
508
+
509
+ // src/stamps/geometry-2d/editor/handlers/pointerDown/singleTarget.ts
510
+ function handleSingleTargetTool(ctx, t, toolDef, e, bestHit) {
511
+ if (!(toolDef.needs === 1 && toolDef.accepts)) return false;
512
+ const hit = bestHit ?? ctx.findNearestPointJxg(e, 12);
513
+ if (!hit) {
514
+ ctx.flashWarn("Click v\xE0o m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng \u0111\u1EC3 \xE1p d\u1EE5ng");
515
+ return true;
516
+ }
517
+ const sid = ctx.jxgIdToSceneId(hit);
518
+ if (!sid) return true;
519
+ if (t === "delete") {
520
+ ctx.store.dispatch({ type: "DELETE", payload: { id: sid } });
521
+ return true;
522
+ }
523
+ if (t === "toggleLabel") {
524
+ const obj = ctx.store.getState().objects[sid];
525
+ if (!obj) return true;
526
+ const cur = obj.attrs.showLabel;
527
+ const next = !(cur ?? false);
528
+ ctx.store.dispatch({ type: "UPDATE_ATTRS", payload: { id: sid, patch: { showLabel: next } } });
529
+ return true;
530
+ }
531
+ if (t === "toggleVisible") {
532
+ const obj = ctx.store.getState().objects[sid];
533
+ if (!obj) return true;
534
+ ctx.store.dispatch({ type: "UPDATE", payload: { id: sid, patch: { visible: !obj.visible } } });
535
+ return true;
536
+ }
537
+ return true;
538
+ }
539
+
540
+ // src/stamps/geometry-2d/editor/handlers/pointerDown/polygon.ts
541
+ function handlePolygonTool(ctx, t, toolDef, e, x, y, bestHit) {
542
+ if (toolDef.needs !== -1) return false;
543
+ const snappedPoint = bestHit && objKind(bestHit) === "point" ? bestHit : ctx.findNearestPointJxg(e, 12);
544
+ const snappedId = snappedPoint ? ctx.jxgIdToSceneId(snappedPoint) : null;
545
+ if (ctx.pendingIdsRef.current.length >= 3 && snappedId && snappedId === ctx.pendingIdsRef.current[0]) {
546
+ ctx.clearPreviewSegs();
547
+ const vertices = ctx.pendingIdsRef.current.slice();
548
+ const isArea = t === "area";
549
+ const id = freshId(ctx, isArea ? "area" : "poly");
550
+ const label = ctx.nextLabel("polygon");
551
+ const attrs = { vertices };
552
+ if (isArea) {
553
+ attrs.showValue = true;
554
+ attrs.fillOpacity = 0.18;
555
+ attrs.color = "#1d4ed8";
556
+ }
557
+ ctx.store.dispatch({
558
+ type: "ADD",
559
+ payload: { obj: mkSceneObj(id, "polygon", label, attrs) }
560
+ });
561
+ ctx.clearPending();
562
+ return true;
563
+ }
564
+ if (snappedId && ctx.pendingIdsRef.current.includes(snappedId)) {
565
+ 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");
566
+ return true;
567
+ }
568
+ let pickId = snappedId;
569
+ let pickJxg = snappedPoint;
570
+ if (!pickId) {
571
+ pickId = dispatchAddFreePoint(ctx, x, y);
572
+ pickJxg = ctx.jxgFromSceneId(pickId);
573
+ }
574
+ if (ctx.pendingRef.current.length > 0 && ctx.boardRef.current && pickJxg) {
575
+ const prev = ctx.pendingRef.current[ctx.pendingRef.current.length - 1];
576
+ safeJsx("handlers.createPreviewSegment", () => {
577
+ const seg = ctx.boardRef.current.create("segment", [prev, pickJxg], {
578
+ strokeColor: "#3b82f6",
579
+ strokeWidth: 1.5,
580
+ strokeOpacity: 0.75,
581
+ fixed: true,
582
+ highlight: false,
583
+ withLabel: false
584
+ });
585
+ ctx.previewSegRef.current.push(seg);
586
+ });
587
+ }
588
+ if (pickJxg) ctx.pendingRef.current.push(pickJxg);
589
+ if (pickId) ctx.pendingIdsRef.current.push(pickId);
590
+ ctx.setPendingCount(ctx.pendingIdsRef.current.length);
591
+ return true;
592
+ }
593
+
594
+ // src/stamps/geometry-2d/editor/handlers/classifyPointVsCircle.ts
595
+ function classifyPointVsCircle(point, circle) {
596
+ if (!point || !circle || !circle.center) return "inside";
597
+ const dx = point.X() - circle.center.X();
598
+ const dy = point.Y() - circle.center.Y();
599
+ const d = Math.hypot(dx, dy);
600
+ const r = typeof circle.Radius === "function" ? circle.Radius() : Number(circle.radius);
601
+ if (!Number.isFinite(d) || !Number.isFinite(r)) return "inside";
602
+ const eps = Math.max(1e-9, 1e-6 * r);
603
+ if (Math.abs(d - r) <= eps) return "on";
604
+ return d < r ? "inside" : "outside";
605
+ }
606
+
607
+ // src/stamps/geometry-2d/editor/handlers/finalizeShape.ts
608
+ function findPickIdByKind(ctx, kind) {
609
+ const picks = ctx.pendingRef.current;
610
+ const ids = ctx.pendingIdsRef.current;
611
+ for (let i = 0; i < picks.length; i += 1) {
612
+ if (objKind(picks[i]) === kind && ids[i]) return ids[i];
613
+ }
614
+ return null;
615
+ }
616
+ function finalizeShape(ctx, toolDef) {
617
+ const ids = ctx.pendingIdsRef.current;
618
+ const key = toolDef.key;
619
+ switch (key) {
620
+ case "segment": {
621
+ const id = freshId(ctx, "s");
622
+ const label = ctx.nextLabel("segment");
623
+ ctx.store.dispatch({
624
+ type: "ADD",
625
+ payload: { obj: mkSceneObj(id, "segment", label, { p1: ids[0], p2: ids[1] }) }
626
+ });
627
+ return;
628
+ }
629
+ case "line": {
630
+ const id = freshId(ctx, "l");
631
+ const label = ctx.nextLabel("line");
632
+ ctx.store.dispatch({
633
+ type: "ADD",
634
+ payload: { obj: mkSceneObj(id, "line", label, { p1: ids[0], p2: ids[1] }) }
635
+ });
636
+ return;
637
+ }
638
+ case "perpendicular":
639
+ case "parallel": {
640
+ const throughPoint = findPickIdByKind(ctx, "point");
641
+ const toLine = findPickIdByKind(ctx, "line");
642
+ if (!throughPoint || !toLine) return;
643
+ const id = freshId(ctx, key === "perpendicular" ? "perp" : "par");
644
+ const label = ctx.nextLabel("line");
645
+ ctx.store.dispatch({
646
+ type: "ADD",
647
+ payload: { obj: mkSceneObj(id, "line", label, {
648
+ construction: { kind: key, throughPoint, toLine }
649
+ }) }
650
+ });
651
+ return;
652
+ }
653
+ case "perpBisector": {
654
+ const id = freshId(ctx, "pb");
655
+ const label = ctx.nextLabel("line");
656
+ ctx.store.dispatch({
657
+ type: "ADD",
658
+ payload: { obj: mkSceneObj(id, "line", label, {
659
+ construction: { kind: "perpBisector", p1: ids[0], p2: ids[1] }
660
+ }) }
661
+ });
662
+ return;
663
+ }
664
+ case "angleBisector": {
665
+ const picks = ctx.pendingRef.current;
666
+ if (picks.length === 2 && objKind(picks[0]) === "line" && objKind(picks[1]) === "line") {
667
+ for (const branch of [0, 1]) {
668
+ const id2 = freshId(ctx, "ab");
669
+ const label2 = ctx.nextLabel("line");
670
+ ctx.store.dispatch({
671
+ type: "ADD",
672
+ payload: { obj: mkSceneObj(id2, "line", label2, {
673
+ construction: { kind: "angleBisectorLines", line1: ids[0], line2: ids[1], branch }
674
+ }) }
675
+ });
676
+ }
677
+ return;
678
+ }
679
+ const id = freshId(ctx, "ab");
680
+ const label = ctx.nextLabel("line");
681
+ ctx.store.dispatch({
682
+ type: "ADD",
683
+ payload: { obj: mkSceneObj(id, "line", label, {
684
+ construction: { kind: "angleBisector", p1: ids[0], vertex: ids[1], p2: ids[2] }
685
+ }) }
686
+ });
687
+ return;
688
+ }
689
+ case "tangent": {
690
+ const throughId = findPickIdByKind(ctx, "point");
691
+ const circleId = findPickIdByKind(ctx, "circle");
692
+ if (!throughId || !circleId) return;
693
+ const picks = ctx.pendingRef.current;
694
+ const ids2 = ctx.pendingIdsRef.current;
695
+ const through = picks[ids2.indexOf(throughId)];
696
+ const circle = picks[ids2.indexOf(circleId)];
697
+ const pos = classifyPointVsCircle(through, circle);
698
+ if (pos === "inside") {
699
+ ctx.toast?.("\u0110i\u1EC3m n\u1EB1m trong \u0111\u01B0\u1EDDng tr\xF2n \u2014 kh\xF4ng c\xF3 ti\u1EBFp tuy\u1EBFn", {
700
+ variant: "warning",
701
+ id: "tangent-invalid-inside"
702
+ });
703
+ return;
704
+ }
705
+ if (pos === "on") {
706
+ const id = freshId(ctx, "t");
707
+ const label = ctx.nextLabel("line");
708
+ ctx.store.dispatch({
709
+ type: "ADD",
710
+ payload: { obj: mkSceneObj(id, "line", label, {
711
+ construction: { kind: "tangent", throughPoint: throughId, toCircle: circleId, branch: "on" }
712
+ }) }
713
+ });
714
+ return;
715
+ }
716
+ for (const branch of [0, 1]) {
717
+ const id = freshId(ctx, "t");
718
+ const label = ctx.nextLabel("line");
719
+ ctx.store.dispatch({
720
+ type: "ADD",
721
+ payload: { obj: mkSceneObj(id, "line", label, {
722
+ construction: { kind: "tangent", throughPoint: throughId, toCircle: circleId, branch }
723
+ }) }
724
+ });
725
+ }
726
+ return;
727
+ }
728
+ case "ray": {
729
+ const id = freshId(ctx, "r");
730
+ const label = ctx.nextLabel("ray");
731
+ ctx.store.dispatch({
732
+ type: "ADD",
733
+ payload: { obj: mkSceneObj(id, "ray", label, { origin: ids[0], through: ids[1] }) }
734
+ });
735
+ return;
736
+ }
737
+ case "vector": {
738
+ const id = freshId(ctx, "v");
739
+ const label = ctx.nextLabel("vector");
740
+ ctx.store.dispatch({
741
+ type: "ADD",
742
+ payload: { obj: mkSceneObj(id, "vector", label, { from: ids[0], to: ids[1] }) }
743
+ });
744
+ return;
745
+ }
746
+ case "circleCenter": {
747
+ const id = freshId(ctx, "c");
748
+ const label = ctx.nextLabel("circle");
749
+ ctx.store.dispatch({
750
+ type: "ADD",
751
+ payload: {
752
+ obj: mkSceneObj(id, "circle", label, {
753
+ center: ids[0],
754
+ surfacePoint: ids[1]
755
+ })
756
+ }
757
+ });
758
+ return;
759
+ }
760
+ case "circle3": {
761
+ const id = freshId(ctx, "cc");
762
+ const label = ctx.nextLabel("circle");
763
+ ctx.store.dispatch({
764
+ type: "ADD",
765
+ payload: {
766
+ obj: mkSceneObj(id, "circle", label, {
767
+ construction: { kind: "circumscribed", p1: ids[0], p2: ids[1], p3: ids[2] }
768
+ })
769
+ }
770
+ });
771
+ return;
772
+ }
773
+ case "semicircle": {
774
+ if (ids[0] === ids[1]) {
775
+ ctx.toast?.("C\u1EA7n 2 \u0111i\u1EC3m ph\xE2n bi\u1EC7t", { variant: "warning", id: "semicircle-dup" });
776
+ return;
777
+ }
778
+ const id = freshId(ctx, "arc");
779
+ const label = ctx.nextLabel("arc");
780
+ ctx.store.dispatch({
781
+ type: "ADD",
782
+ payload: {
783
+ obj: mkSceneObj(id, "arc", label, {
784
+ construction: { kind: "semicircle", p1: ids[0], p2: ids[1] }
785
+ })
786
+ }
787
+ });
788
+ return;
789
+ }
790
+ case "arcCenter": {
791
+ if (ids[0] === ids[1] || ids[0] === ids[2] || ids[1] === ids[2]) {
792
+ ctx.toast?.("C\u1EA7n 3 \u0111i\u1EC3m ph\xE2n bi\u1EC7t", { variant: "warning", id: "arc-center-dup" });
793
+ return;
794
+ }
795
+ const id = freshId(ctx, "arc");
796
+ const label = ctx.nextLabel("arc");
797
+ ctx.store.dispatch({
798
+ type: "ADD",
799
+ payload: {
800
+ obj: mkSceneObj(id, "arc", label, {
801
+ construction: { kind: "byCenter", center: ids[0], p1: ids[1], p2: ids[2] }
802
+ })
803
+ }
804
+ });
805
+ return;
806
+ }
807
+ case "arc3": {
808
+ if (ids[0] === ids[1] || ids[0] === ids[2] || ids[1] === ids[2]) {
809
+ ctx.toast?.("C\u1EA7n 3 \u0111i\u1EC3m ph\xE2n bi\u1EC7t", { variant: "warning", id: "arc3-dup" });
810
+ return;
811
+ }
812
+ const picks = ctx.pendingRef.current;
813
+ const ax = picks[0].X(), ay = picks[0].Y();
814
+ const bx = picks[1].X(), by = picks[1].Y();
815
+ const cx = picks[2].X(), cy = picks[2].Y();
816
+ const cross = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax);
817
+ if (Math.abs(cross) < 1e-6) {
818
+ ctx.toast?.("Kh\xF4ng v\u1EBD \u0111\u01B0\u1EE3c cung qua 3 \u0111i\u1EC3m th\u1EB3ng h\xE0ng", {
819
+ variant: "warning",
820
+ id: "arc3-collinear"
821
+ });
822
+ return;
823
+ }
824
+ const id = freshId(ctx, "arc");
825
+ const label = ctx.nextLabel("arc");
826
+ ctx.store.dispatch({
827
+ type: "ADD",
828
+ payload: {
829
+ obj: mkSceneObj(id, "arc", label, {
830
+ construction: { kind: "by3Points", p1: ids[0], p2: ids[1], p3: ids[2] }
831
+ })
832
+ }
833
+ });
834
+ return;
835
+ }
836
+ case "sectorCenter": {
837
+ if (ids[0] === ids[1] || ids[0] === ids[2] || ids[1] === ids[2]) {
838
+ ctx.toast?.("C\u1EA7n 3 \u0111i\u1EC3m ph\xE2n bi\u1EC7t", { variant: "warning", id: "sector-center-dup" });
839
+ return;
840
+ }
841
+ const id = freshId(ctx, "sec");
842
+ const label = ctx.nextLabel("sector");
843
+ ctx.store.dispatch({
844
+ type: "ADD",
845
+ payload: {
846
+ obj: mkSceneObj(id, "sector", label, {
847
+ construction: { kind: "byCenter", center: ids[0], p1: ids[1], p2: ids[2] }
848
+ })
849
+ }
850
+ });
851
+ return;
852
+ }
853
+ case "midpoint": {
854
+ const id = freshId(ctx, "mp");
855
+ const label = ctx.nextLabel("point");
856
+ ctx.store.dispatch({
857
+ type: "ADD",
858
+ payload: { obj: mkSceneObj(id, "point", label, {
859
+ constraint: { kind: "midpoint", p1: ids[0], p2: ids[1] }
860
+ }) }
861
+ });
862
+ return;
863
+ }
864
+ case "perpFoot": {
865
+ const fromPoint = findPickIdByKind(ctx, "point");
866
+ const onLine = findPickIdByKind(ctx, "line");
867
+ if (!fromPoint || !onLine) return;
868
+ const id = freshId(ctx, "pf");
869
+ const label = ctx.nextLabel("point");
870
+ ctx.store.dispatch({
871
+ type: "ADD",
872
+ payload: { obj: mkSceneObj(id, "point", label, {
873
+ constraint: { kind: "perpFoot", from: fromPoint, onLine }
874
+ }) }
875
+ });
876
+ return;
877
+ }
878
+ case "centroid": {
879
+ const id = freshId(ctx, "g");
880
+ const label = ctx.nextLabel("point");
881
+ ctx.store.dispatch({
882
+ type: "ADD",
883
+ payload: { obj: mkSceneObj(id, "point", label, {
884
+ constraint: { kind: "centroid", vertices: [ids[0], ids[1], ids[2]] }
885
+ }) }
886
+ });
887
+ return;
888
+ }
889
+ case "circumcenter": {
890
+ const id = freshId(ctx, "o");
891
+ const label = ctx.nextLabel("point");
892
+ ctx.store.dispatch({
893
+ type: "ADD",
894
+ payload: { obj: mkSceneObj(id, "point", label, {
895
+ constraint: { kind: "circumcenter", vertices: [ids[0], ids[1], ids[2]] }
896
+ }) }
897
+ });
898
+ return;
899
+ }
900
+ case "incenter": {
901
+ const id = freshId(ctx, "i");
902
+ const label = ctx.nextLabel("point");
903
+ ctx.store.dispatch({
904
+ type: "ADD",
905
+ payload: { obj: mkSceneObj(id, "point", label, {
906
+ constraint: { kind: "incenter", vertices: [ids[0], ids[1], ids[2]] }
907
+ }) }
908
+ });
909
+ return;
910
+ }
911
+ case "orthocenter": {
912
+ const id = freshId(ctx, "h");
913
+ const label = ctx.nextLabel("point");
914
+ ctx.store.dispatch({
915
+ type: "ADD",
916
+ payload: { obj: mkSceneObj(id, "point", label, {
917
+ constraint: { kind: "orthocenter", vertices: [ids[0], ids[1], ids[2]] }
918
+ }) }
919
+ });
920
+ return;
921
+ }
922
+ case "angle": {
923
+ const id = freshId(ctx, "ang");
924
+ const label = ctx.nextLabel("angle");
925
+ ctx.store.dispatch({
926
+ type: "ADD",
927
+ payload: { obj: mkSceneObj(id, "angle", label, {
928
+ p1: ids[0],
929
+ vertex: ids[1],
930
+ p2: ids[2]
931
+ }) }
932
+ });
933
+ return;
934
+ }
935
+ case "distance": {
936
+ const id = freshId(ctx, "d");
937
+ const label = ctx.nextLabel("distance");
938
+ ctx.store.dispatch({
939
+ type: "ADD",
940
+ payload: { obj: mkSceneObj(id, "distance", label, { p1: ids[0], p2: ids[1] }) }
941
+ });
942
+ return;
943
+ }
944
+ case "intersect": {
945
+ const picks = ctx.pendingRef.current;
946
+ const pendIds = ctx.pendingIdsRef.current;
947
+ const aIdx = pendIds.indexOf(ids[0]);
948
+ const bIdx = pendIds.indexOf(ids[1]);
949
+ if (aIdx < 0 || bIdx < 0) return;
950
+ const aKind = objKind(picks[aIdx]);
951
+ const bKind = objKind(picks[bIdx]);
952
+ if (aKind === "line" && bKind === "line") {
953
+ dispatchAddIntersection(ctx, { kind: "lineLine", ref1: ids[0], ref2: ids[1] });
954
+ return;
955
+ }
956
+ const isLineCircle = aKind === "line" && bKind === "circle" || aKind === "circle" && bKind === "line";
957
+ const isCircleCircle = aKind === "circle" && bKind === "circle";
958
+ if (!isLineCircle && !isCircleCircle) return;
959
+ for (const branch of [0, 1]) {
960
+ dispatchAddIntersection(ctx, {
961
+ kind: isLineCircle ? "lineCircle" : "circleCircle",
962
+ ref1: ids[0],
963
+ ref2: ids[1],
964
+ branch
965
+ });
966
+ }
967
+ return;
968
+ }
969
+ default:
970
+ return;
971
+ }
972
+ }
973
+
974
+ // src/stamps/geometry-2d/editor/transforms.ts
975
+ function getDefiningPoints(obj, _state) {
976
+ if (obj.kind === "point" || obj.kind === "intersection") return [obj.id];
977
+ if (obj.kind === "segment" || obj.kind === "line") {
978
+ const a = obj.attrs;
979
+ return [a.p1, a.p2];
980
+ }
981
+ if (obj.kind === "ray") {
982
+ const a = obj.attrs;
983
+ return [a.origin, a.through];
984
+ }
985
+ if (obj.kind === "vector") {
986
+ const a = obj.attrs;
987
+ return [a.from, a.to];
988
+ }
989
+ if (obj.kind === "circle") {
990
+ const a = obj.attrs;
991
+ return [a.center, a.surfacePoint];
992
+ }
993
+ if (obj.kind === "polygon") {
994
+ return [...obj.attrs.vertices];
995
+ }
996
+ return [];
997
+ }
998
+
999
+ // src/stamps/geometry-2d/editor/handlers/transform.ts
1000
+ function finalizeTransform(ctx, tool, pendingIds, value) {
1001
+ if (tool === "regularPolygon") {
1002
+ const n = Math.max(3, Math.round(value));
1003
+ const id = freshId(ctx, "rpoly");
1004
+ const label = ctx.nextLabel("polygon");
1005
+ ctx.store.dispatch({
1006
+ type: "ADD",
1007
+ payload: { obj: mkSceneObj(id, "polygon", label, {
1008
+ construction: { kind: "regular", p1: pendingIds[0], p2: pendingIds[1], n }
1009
+ }) }
1010
+ });
1011
+ return;
1012
+ }
1013
+ const sourceId = pendingIds[0];
1014
+ const state = ctx.store.getState();
1015
+ const source = state.objects[sourceId];
1016
+ if (!source) {
1017
+ ctx.flashWarn("\u0110\u1ED1i t\u01B0\u1EE3ng ngu\u1ED3n kh\xF4ng c\xF2n");
1018
+ return;
1019
+ }
1020
+ const defining = getDefiningPoints(source);
1021
+ if (defining.length === 0) {
1022
+ ctx.flashWarn("Kh\xF4ng th\u1EC3 bi\u1EBFn \u0111\u1ED5i \u0111\u1ED1i t\u01B0\u1EE3ng n\xE0y");
1023
+ return;
1024
+ }
1025
+ let transformDef = null;
1026
+ if (tool === "translate") {
1027
+ const a = ctx.jxgFromSceneId(pendingIds[1]);
1028
+ const b = ctx.jxgFromSceneId(pendingIds[2]);
1029
+ if (!a || !b || typeof a.X !== "function" || typeof b.X !== "function") {
1030
+ ctx.flashWarn("Kh\xF4ng \u0111\u1ECDc \u0111\u01B0\u1EE3c to\u1EA1 \u0111\u1ED9 vector");
1031
+ return;
1032
+ }
1033
+ transformDef = { kind: "translate", dx: b.X() - a.X(), dy: b.Y() - a.Y() };
1034
+ } else if (tool === "rotate") {
1035
+ transformDef = { kind: "rotate", angleRad: value * Math.PI / 180, center: pendingIds[1] };
1036
+ } else if (tool === "reflectLine") {
1037
+ transformDef = { kind: "reflectLine", line: pendingIds[1] };
1038
+ } else if (tool === "reflectPoint") {
1039
+ transformDef = { kind: "reflectPoint", center: pendingIds[1] };
1040
+ } else if (tool === "dilate") {
1041
+ transformDef = { kind: "dilate", k: value, center: pendingIds[1] };
1042
+ }
1043
+ if (!transformDef) return;
1044
+ const newPointIds = [];
1045
+ for (const defId of defining) {
1046
+ const newId = freshId(ctx, "p");
1047
+ const newLabel = ctx.nextLabel("point");
1048
+ ctx.store.dispatch({
1049
+ type: "ADD",
1050
+ payload: { obj: mkSceneObj(newId, "point", newLabel, {
1051
+ constraint: { kind: "transformed", source: defId, transform: transformDef },
1052
+ color: "#0ea5e9"
1053
+ }) }
1054
+ });
1055
+ newPointIds.push(newId);
1056
+ }
1057
+ recreateFromTransformedPoints(ctx, source, newPointIds);
1058
+ }
1059
+ function recreateFromTransformedPoints(ctx, source, pointIds) {
1060
+ const k = source.kind;
1061
+ if (k === "point" || k === "intersection") return;
1062
+ if (k === "segment" && pointIds.length === 2) {
1063
+ const id = freshId(ctx, "s");
1064
+ const label = ctx.nextLabel("segment");
1065
+ ctx.store.dispatch({
1066
+ type: "ADD",
1067
+ payload: { obj: mkSceneObj(id, "segment", label, { p1: pointIds[0], p2: pointIds[1], color: "#0ea5e9" }) }
1068
+ });
1069
+ return;
1070
+ }
1071
+ if (k === "line" && pointIds.length === 2) {
1072
+ const id = freshId(ctx, "l");
1073
+ const label = ctx.nextLabel("line");
1074
+ ctx.store.dispatch({
1075
+ type: "ADD",
1076
+ payload: { obj: mkSceneObj(id, "line", label, { p1: pointIds[0], p2: pointIds[1], color: "#0ea5e9" }) }
1077
+ });
1078
+ return;
1079
+ }
1080
+ if (k === "ray" && pointIds.length === 2) {
1081
+ const id = freshId(ctx, "r");
1082
+ const label = ctx.nextLabel("ray");
1083
+ ctx.store.dispatch({
1084
+ type: "ADD",
1085
+ payload: { obj: mkSceneObj(id, "ray", label, { origin: pointIds[0], through: pointIds[1], color: "#0ea5e9" }) }
1086
+ });
1087
+ return;
1088
+ }
1089
+ if (k === "vector" && pointIds.length === 2) {
1090
+ const id = freshId(ctx, "v");
1091
+ const label = ctx.nextLabel("vector");
1092
+ ctx.store.dispatch({
1093
+ type: "ADD",
1094
+ payload: { obj: mkSceneObj(id, "vector", label, { from: pointIds[0], to: pointIds[1], color: "#0ea5e9" }) }
1095
+ });
1096
+ return;
1097
+ }
1098
+ if (k === "circle" && pointIds.length === 2) {
1099
+ const id = freshId(ctx, "c");
1100
+ const label = ctx.nextLabel("circle");
1101
+ ctx.store.dispatch({
1102
+ type: "ADD",
1103
+ payload: { obj: mkSceneObj(id, "circle", label, { center: pointIds[0], surfacePoint: pointIds[1], color: "#0ea5e9" }) }
1104
+ });
1105
+ return;
1106
+ }
1107
+ if (k === "polygon" && pointIds.length >= 3) {
1108
+ const id = freshId(ctx, "poly");
1109
+ const label = ctx.nextLabel("polygon");
1110
+ ctx.store.dispatch({
1111
+ type: "ADD",
1112
+ payload: { obj: mkSceneObj(id, "polygon", label, { vertices: pointIds, color: "#0ea5e9" }) }
1113
+ });
1114
+ return;
1115
+ }
1116
+ }
1117
+
1118
+ // src/stamps/geometry-2d/editor/handlers/pointerDown/multiClick.ts
1119
+ function handleMultiClickTool(ctx, toolDef, e, x, y, hits, bestHit) {
1120
+ let pick = null;
1121
+ let pickId = null;
1122
+ if (toolDef.accepts) {
1123
+ const usedKinds = ctx.pendingRef.current.map((p) => objKind(p));
1124
+ const remaining = [...toolDef.accepts];
1125
+ for (const u of usedKinds) {
1126
+ if (u === "other") continue;
1127
+ let i = remaining.indexOf(u);
1128
+ if (i < 0 && (u === "line" || u === "circle")) i = remaining.indexOf("lineOrCircle");
1129
+ if (i < 0 && (u === "point" || u === "line")) i = remaining.indexOf("pointOrLine");
1130
+ if (i >= 0) remaining.splice(i, 1);
1131
+ }
1132
+ const strictPoint = hits.find((o) => objKind(o) === "point") ?? null;
1133
+ const lineHit = hits.find((o) => objKind(o) === "line") ?? null;
1134
+ const circleHit = hits.find((o) => objKind(o) === "circle") ?? null;
1135
+ if (remaining.includes("point") && strictPoint) pick = strictPoint;
1136
+ else if (remaining.includes("line") && lineHit) pick = lineHit;
1137
+ else if (remaining.includes("circle") && circleHit) pick = circleHit;
1138
+ else if (remaining.includes("lineOrCircle") && (lineHit || circleHit)) {
1139
+ pick = lineHit ?? circleHit;
1140
+ } else if (remaining.includes("pointOrLine") && (strictPoint || lineHit)) {
1141
+ pick = strictPoint ?? lineHit;
1142
+ } else if (remaining.includes("any") && (strictPoint || lineHit || circleHit)) {
1143
+ pick = strictPoint ?? lineHit ?? circleHit;
1144
+ } else if (remaining.includes("point") || remaining.includes("pointOrLine")) {
1145
+ const near = ctx.findNearestPointJxg(e, 12);
1146
+ if (near) {
1147
+ pick = near;
1148
+ } else {
1149
+ if (toolDef.key === "angleBisector" && ctx.pendingRef.current.length > 0 && objKind(ctx.pendingRef.current[0]) === "line") {
1150
+ ctx.flashWarn("\u0110\xE3 ch\u1ECDn \u0111\u01B0\u1EDDng \u2014 click th\xEAm 1 \u0111\u01B0\u1EDDng/\u0111o\u1EA1n n\u1EEFa \u0111\u1EC3 t\u1EA1o 2 tia ph\xE2n gi\xE1c");
1151
+ return;
1152
+ }
1153
+ pickId = dispatchAddFreePoint(ctx, x, y);
1154
+ pick = ctx.jxgFromSceneId(pickId);
1155
+ }
1156
+ }
1157
+ if (!pick) {
1158
+ const needs = remaining.map(
1159
+ (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" : k === "lineOrCircle" ? "m\u1ED9t \u0111\u01B0\u1EDDng ho\u1EB7c \u0111\u01B0\u1EDDng tr\xF2n" : k === "pointOrLine" ? "m\u1ED9t \u0111i\u1EC3m ho\u1EB7c \u0111\u01B0\u1EDDng/\u0111o\u1EA1n" : "m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng"
1160
+ );
1161
+ ctx.flashWarn(`C\xF2n c\u1EA7n click v\xE0o ${needs.join(" + ")} c\xF3 s\u1EB5n`);
1162
+ return;
1163
+ }
1164
+ if (ctx.pendingRef.current.includes(pick)) {
1165
+ ctx.flashWarn("\u0110\xE3 ch\u1ECDn \u0111\u1ED1i t\u01B0\u1EE3ng n\xE0y \u2014 ch\u1ECDn \u0111\u1ED1i t\u01B0\u1EE3ng kh\xE1c");
1166
+ return;
1167
+ }
1168
+ if (toolDef.key === "angleBisector" && ctx.pendingRef.current.length > 0) {
1169
+ const firstKind = objKind(ctx.pendingRef.current[0]);
1170
+ const newKind = objKind(pick);
1171
+ if (firstKind === "line" && newKind !== "line") {
1172
+ ctx.flashWarn("\u0110\xE3 ch\u1ECDn \u0111\u01B0\u1EDDng \u2014 ch\u1EC9 click th\xEAm 1 \u0111\u01B0\u1EDDng/\u0111o\u1EA1n n\u1EEFa");
1173
+ return;
1174
+ }
1175
+ if (firstKind === "point" && newKind !== "point") {
1176
+ ctx.flashWarn("\u0110\xE3 ch\u1ECDn \u0111i\u1EC3m \u2014 click th\xEAm \u0111i\u1EC3m (\u0111\u1EC9nh \u1EDF gi\u1EEFa)");
1177
+ return;
1178
+ }
1179
+ }
1180
+ if (!pickId) pickId = ctx.jxgIdToSceneId(pick);
1181
+ } else {
1182
+ const snapped = bestHit && objKind(bestHit) === "point" ? bestHit : ctx.findNearestPointJxg(e, 12);
1183
+ if (snapped && ctx.pendingRef.current.includes(snapped)) {
1184
+ ctx.flashWarn("\u0110\xE3 ch\u1ECDn \u0111i\u1EC3m n\xE0y \u2014 ch\u1ECDn \u0111i\u1EC3m kh\xE1c ho\u1EB7c click ch\u1ED7 tr\u1ED1ng");
1185
+ return;
1186
+ }
1187
+ if (snapped) {
1188
+ pick = snapped;
1189
+ pickId = ctx.jxgIdToSceneId(snapped);
1190
+ } else {
1191
+ pickId = dispatchAddFreePoint(ctx, x, y);
1192
+ pick = ctx.jxgFromSceneId(pickId);
1193
+ }
1194
+ }
1195
+ if (!pick) return;
1196
+ ctx.pendingRef.current.push(pick);
1197
+ if (pickId) ctx.pendingIdsRef.current.push(pickId);
1198
+ ctx.setPendingCount(ctx.pendingIdsRef.current.length);
1199
+ if (toolDef.key === "angleBisector" && ctx.pendingIdsRef.current.length === 2 && objKind(ctx.pendingRef.current[0]) === "line") {
1200
+ finalizeShape(ctx, toolDef);
1201
+ ctx.clearPending();
1202
+ return;
1203
+ }
1204
+ if (ctx.pendingIdsRef.current.length >= toolDef.needs) {
1205
+ const tk = toolDef.key;
1206
+ if (tk === "rotate" || tk === "dilate" || tk === "regularPolygon") {
1207
+ const cx = (e.clientX ?? 0) + 8;
1208
+ const cy = (e.clientY ?? 0) + 8;
1209
+ ctx.pendingTransformRef.current = {
1210
+ tool: tk,
1211
+ pendingIds: ctx.pendingIdsRef.current.slice(),
1212
+ anchorScreen: { x: cx, y: cy }
1213
+ };
1214
+ ctx.emitTransform({ tool: tk, anchor: { x: cx, y: cy } });
1215
+ return;
1216
+ }
1217
+ if (tk === "translate" || tk === "reflectLine" || tk === "reflectPoint") {
1218
+ finalizeTransform(ctx, tk, ctx.pendingIdsRef.current.slice(), 0);
1219
+ ctx.clearPending();
1220
+ return;
1221
+ }
1222
+ finalizeShape(ctx, toolDef);
1223
+ ctx.clearPending();
1224
+ } else {
1225
+ ctx.refreshPreview();
1226
+ }
1227
+ }
1228
+
1229
+ // src/stamps/geometry-2d/editor/handlers/pointerDown/index.ts
1230
+ function handleDown(ctx, e) {
1231
+ if (!ctx.boardRef.current) return;
1232
+ const t = ctx.toolRef.current;
1233
+ if (t === "move") return handleMoveTool(ctx, e);
1234
+ if (t === "select") return handleSelectTool(ctx, e);
1235
+ const toolDef = TOOLS.find((td) => td.key === t);
1236
+ if (!toolDef) return;
1237
+ const coords = ctx.boardRef.current.getUsrCoordsOfMouse(e);
1238
+ const x = coords[0], y = coords[1];
1239
+ const hits = ctx.objectsAt(e).map(ctx.promoteLabel).filter((o) => o !== ctx.axisObjsRef.current.x && o !== ctx.axisObjsRef.current.y).filter((o) => ctx.jxgIdToSceneId(o) != null);
1240
+ const bestHit = hits.find((o) => objKind(o) === "point") ?? hits[0] ?? null;
1241
+ if (t === "point") return handlePointTool(ctx, e, x, y, hits);
1242
+ if (handleSingleTargetTool(ctx, t, toolDef, e, bestHit)) return;
1243
+ if (handlePolygonTool(ctx, t, toolDef, e, x, y, bestHit)) return;
1244
+ handleMultiClickTool(ctx, toolDef, e, x, y, hits, bestHit);
1245
+ }
1246
+
1247
+ // src/stamps/geometry-2d/editor/handlers/pointerMove.ts
1248
+ function handleMove(ctx, e) {
1249
+ if (ctx.toolRef.current === "select" && ctx.marqueeRef.current) {
1250
+ const sc = ctx.screenCoordsOf(e);
1251
+ if (sc && ctx.boardRef.current) {
1252
+ const [sx, sy] = sc;
1253
+ const { startSx, startSy } = ctx.marqueeRef.current;
1254
+ const b = ctx.boardRef.current;
1255
+ const ux1 = b.screenCoords2userCoords?.([Math.min(startSx, sx), Math.min(startSy, sy)]) ?? null;
1256
+ const ux2 = b.screenCoords2userCoords?.([Math.max(startSx, sx), Math.max(startSy, sy)]) ?? null;
1257
+ const toUsr = (px, py) => {
1258
+ const ox = b.origin?.scrCoords?.[1] ?? 0;
1259
+ const oy = b.origin?.scrCoords?.[2] ?? 0;
1260
+ const ux = (px - ox) / b.unitX;
1261
+ const uy = (oy - py) / b.unitY;
1262
+ return [ux, uy];
1263
+ };
1264
+ const [x1u, y1u] = ux1 && ux1.length >= 2 ? [ux1[0], ux1[1]] : toUsr(Math.min(startSx, sx), Math.min(startSy, sy));
1265
+ const [x2u, y2u] = ux2 && ux2.length >= 2 ? [ux2[0], ux2[1]] : toUsr(Math.max(startSx, sx), Math.max(startSy, sy));
1266
+ const rect = ctx.marqueeRef.current.rect;
1267
+ if (rect) {
1268
+ safeJsx("handlers.removeObject(marquee.prevRect)", () => ctx.boardRef.current.removeObject(rect));
1269
+ }
1270
+ safeJsx("handlers.createMarqueePolygon", () => {
1271
+ ctx.marqueeRef.current.rect = ctx.boardRef.current.create("polygon", [
1272
+ [x1u, y1u],
1273
+ [x2u, y1u],
1274
+ [x2u, y2u],
1275
+ [x1u, y2u]
1276
+ ], {
1277
+ fillColor: "#06b6d4",
1278
+ fillOpacity: 0.08,
1279
+ borders: { strokeColor: "#06b6d4", strokeWidth: 1, dash: 2 },
1280
+ vertices: { visible: false },
1281
+ fixed: true,
1282
+ highlight: false,
1283
+ withLabel: false
1284
+ });
1285
+ });
1286
+ }
1287
+ return;
1288
+ }
1289
+ const ph = ctx.phantomRef.current;
1290
+ if (!ph || !ctx.boardRef.current) return;
1291
+ if (ctx.previewRafRef.current != null) return;
1292
+ ctx.previewRafRef.current = requestAnimationFrame(() => {
1293
+ ctx.previewRafRef.current = null;
1294
+ if (!ctx.boardRef.current || !ctx.phantomRef.current) return;
1295
+ safeJsx("handlers.phantomMove", () => {
1296
+ const coords = ctx.boardRef.current.getUsrCoordsOfMouse(e);
1297
+ const JXG = ctx.jxgRef.current;
1298
+ if (!JXG) return;
1299
+ ctx.phantomRef.current.setPositionDirectly(JXG.COORDS_BY_USER, [coords[0], coords[1]]);
1300
+ ctx.boardRef.current.update();
1301
+ });
1302
+ });
1303
+ }
1304
+
1305
+ // src/stamps/geometry-2d/editor/handlers/pointerUp.ts
1306
+ function handleUp(ctx, e) {
1307
+ const t = ctx.toolRef.current;
1308
+ if (t === "select") {
1309
+ const mq = ctx.marqueeRef.current;
1310
+ ctx.marqueeRef.current = null;
1311
+ ctx.moveDownRef.current = null;
1312
+ if (!mq) return;
1313
+ const sc2 = ctx.screenCoordsOf(e);
1314
+ if (!sc2) return;
1315
+ const [ex, ey] = sc2;
1316
+ if (mq.rect) {
1317
+ safeJsx("handlers.removeObject(marquee.rect)", () => ctx.boardRef.current?.removeObject(mq.rect));
1318
+ }
1319
+ if (Math.hypot(ex - mq.startSx, ey - mq.startSy) < 4) return;
1320
+ const x1 = Math.min(mq.startSx, ex), x2 = Math.max(mq.startSx, ex);
1321
+ const y1 = Math.min(mq.startSy, ey), y2 = Math.max(mq.startSy, ey);
1322
+ const board = ctx.boardRef.current;
1323
+ if (!board) return;
1324
+ const list = board.objectsList || [];
1325
+ for (const o of list) {
1326
+ if (o === ctx.axisObjsRef.current.x || o === ctx.axisObjsRef.current.y) continue;
1327
+ const kind = objKind(o);
1328
+ if (kind === "point") {
1329
+ const pc = o.coords?.scrCoords;
1330
+ if (!pc) continue;
1331
+ if (pc[1] >= x1 && pc[1] <= x2 && pc[2] >= y1 && pc[2] <= y2) {
1332
+ const sid = ctx.jxgIdToSceneId(o);
1333
+ if (sid && !ctx.selectedSetRef.current.has(sid)) {
1334
+ ctx.selectedSetRef.current.add(sid);
1335
+ }
1336
+ }
1337
+ } else if (kind === "line" || kind === "circle") {
1338
+ const defs = [o.point1, o.point2, o.center, o.midpoint, o.point3].filter(Boolean);
1339
+ const anyInside = defs.some((p) => {
1340
+ const pc = p?.coords?.scrCoords;
1341
+ return pc && pc[1] >= x1 && pc[1] <= x2 && pc[2] >= y1 && pc[2] <= y2;
1342
+ });
1343
+ if (anyInside) {
1344
+ const sid = ctx.jxgIdToSceneId(o);
1345
+ if (sid && !ctx.selectedSetRef.current.has(sid)) {
1346
+ ctx.selectedSetRef.current.add(sid);
1347
+ }
1348
+ }
1349
+ }
1350
+ }
1351
+ ctx.setSelectionTick((tt) => tt + 1);
1352
+ safeJsx("handlers.board.update(marquee)", () => board.update());
1353
+ return;
1354
+ }
1355
+ if (t !== "move") return;
1356
+ const start = ctx.moveDownRef.current;
1357
+ ctx.moveDownRef.current = null;
1358
+ if (!start) return;
1359
+ const sc = ctx.screenCoordsOf(e);
1360
+ if (!sc) return;
1361
+ const [sx, sy] = sc;
1362
+ const moved = Math.hypot(sx - start.sx, sy - start.sy);
1363
+ if (moved > 4) return;
1364
+ const hits = ctx.objectsAt(e).map(ctx.promoteLabel).filter((o) => o !== ctx.axisObjsRef.current.x && o !== ctx.axisObjsRef.current.y).filter((o) => ctx.jxgIdToSceneId(o) != null);
1365
+ const best = hits.find((o) => objKind(o) === "point") ?? ctx.findNearestPointJxg(e, 12) ?? hits[0] ?? null;
1366
+ if (!best) {
1367
+ ctx.lastMoveClickRef.current = { id: null, time: 0 };
1368
+ return;
1369
+ }
1370
+ const bestId = ctx.jxgIdToSceneId(best);
1371
+ const now = Date.now();
1372
+ const isDouble = bestId !== null && ctx.lastMoveClickRef.current.id === bestId && now - ctx.lastMoveClickRef.current.time < 400;
1373
+ ctx.lastMoveClickRef.current = { id: bestId, time: now };
1374
+ if (!isDouble) return;
1375
+ const cx = e.clientX ?? e.touches?.[0]?.clientX ?? 0;
1376
+ const cy = e.clientY ?? e.touches?.[0]?.clientY ?? 0;
1377
+ if (!bestId) return;
1378
+ ctx.emitSelect({ id: bestId, anchorScreen: { x: cx + 8, y: cy + 8 } });
1379
+ }
1380
+
1381
+ // src/stamps/geometry-2d/editor/hitTest.ts
1382
+ function findNearestPoint(state, pointCoord, x, y, tolPx, excludeIds = /* @__PURE__ */ new Set()) {
1383
+ let best = null;
1384
+ let bestDistSq = tolPx * tolPx;
1385
+ for (const obj of listObjects(state)) {
1386
+ if (obj.kind !== "point" && obj.kind !== "intersection") continue;
1387
+ if (excludeIds.has(obj.id)) continue;
1388
+ const coord = pointCoord(obj.id);
1389
+ if (!coord) continue;
1390
+ const dx = coord[0] - x;
1391
+ const dy = coord[1] - y;
1392
+ const d2 = dx * dx + dy * dy;
1393
+ if (d2 < bestDistSq) {
1394
+ bestDistSq = d2;
1395
+ best = obj;
1396
+ }
1397
+ }
1398
+ return best;
1399
+ }
1400
+ function screenCoordsOf(board, container, evt) {
1401
+ if (!board) return null;
1402
+ try {
1403
+ const mp = board.getMousePosition ? board.getMousePosition(evt) : null;
1404
+ if (mp && mp.length >= 2) return [mp[0], mp[1]];
1405
+ } catch {
1406
+ }
1407
+ if (container) {
1408
+ const rect = container.getBoundingClientRect();
1409
+ const cx = evt.clientX ?? evt.touches?.[0]?.clientX ?? 0;
1410
+ const cy = evt.clientY ?? evt.touches?.[0]?.clientY ?? 0;
1411
+ return [cx - rect.left, cy - rect.top];
1412
+ }
1413
+ return null;
1414
+ }
1415
+ function objectsAt(board, container, evt, excludes) {
1416
+ const sc = screenCoordsOf(board, container, evt);
1417
+ if (!board || !sc) return [];
1418
+ const [sx, sy] = sc;
1419
+ const excludeSet = /* @__PURE__ */ new Set();
1420
+ for (const e of excludes) if (e) excludeSet.add(e);
1421
+ const out = [];
1422
+ safeJsx("hitTest.objectsAt", () => {
1423
+ for (const o of board.objectsList || []) {
1424
+ if (excludeSet.has(o)) continue;
1425
+ if (o && typeof o.hasPoint === "function" && o.hasPoint(sx, sy)) out.push(o);
1426
+ }
1427
+ });
1428
+ return out;
1429
+ }
1430
+ function findNearestJxgPoint(board, container, state, jxgFromSceneId2, evt, tolPx = 12) {
1431
+ const sc = screenCoordsOf(board, container, evt);
1432
+ if (!board || !sc) return null;
1433
+ const [sx, sy] = sc;
1434
+ const pointCoord = (id) => {
1435
+ const j = jxgFromSceneId2(id);
1436
+ const sc2 = j?.coords?.scrCoords;
1437
+ return sc2 ? [sc2[1], sc2[2]] : null;
1438
+ };
1439
+ const result = findNearestPoint(state, pointCoord, sx, sy, tolPx);
1440
+ return result ? jxgFromSceneId2(result.id) : null;
1441
+ }
1442
+ function promoteToLabelOwner(board, o) {
1443
+ if (!o) return o;
1444
+ const t = (o.elType || o.type || "").toString().toLowerCase();
1445
+ if (t !== "text" || !board) return o;
1446
+ const promoted = safeJsx("hitTest.promoteToLabelOwner", () => {
1447
+ for (const c of board.objectsList || []) {
1448
+ if (c.label === o) return c;
1449
+ }
1450
+ return null;
1451
+ }, null);
1452
+ return promoted ?? o;
1453
+ }
1454
+
1455
+ // src/stamps/geometry-2d/editor/snapshot.ts
1456
+ function buildObjectSnapshot(state, id, anchorScreen) {
1457
+ const obj = state.objects[id];
1458
+ if (!obj) return null;
1459
+ const k = obj.kind;
1460
+ if (k !== "point" && k !== "line" && k !== "circle" && k !== "segment" && k !== "ray" && k !== "vector") {
1461
+ return null;
1462
+ }
1463
+ const a = obj.attrs;
1464
+ const jKind = k === "point" ? "point" : k === "circle" ? "circle" : "line";
1465
+ return {
1466
+ id,
1467
+ kind: jKind,
1468
+ name: obj.label,
1469
+ color: a.color ?? "#0f172a",
1470
+ width: a.width ?? 2,
1471
+ dash: a.dash ?? 0,
1472
+ face: a.face ?? "o",
1473
+ showLabel: a.showLabel ?? true,
1474
+ showValue: a.showValue ?? false,
1475
+ screenCoords: anchorScreen
1476
+ };
1477
+ }
1478
+
1479
+ // src/stamps/geometry-2d/editor/preview.ts
1480
+ var PREVIEW_STYLE = {
1481
+ strokeColor: "#3b82f6",
1482
+ strokeWidth: 1.5,
1483
+ strokeOpacity: 0.65,
1484
+ dash: 2,
1485
+ fixed: true,
1486
+ highlight: false,
1487
+ withLabel: false
1488
+ };
1489
+ var CIRCLE_PREVIEW_STYLE = {
1490
+ ...PREVIEW_STYLE,
1491
+ fillColor: "none",
1492
+ fillOpacity: 0
1493
+ };
1494
+ function buildPreviewShape(board, toolDef, picks, phantom) {
1495
+ if (!board) return null;
1496
+ return safeJsx("preview.buildPreviewShape", () => {
1497
+ switch (toolDef.key) {
1498
+ case "segment":
1499
+ case "midpoint":
1500
+ case "distance":
1501
+ return board.create("segment", [picks[0], phantom], PREVIEW_STYLE);
1502
+ case "line":
1503
+ return board.create("line", [picks[0], phantom], PREVIEW_STYLE);
1504
+ case "ray":
1505
+ return board.create("line", [picks[0], phantom], {
1506
+ ...PREVIEW_STYLE,
1507
+ straightFirst: false,
1508
+ straightLast: true
1509
+ });
1510
+ case "vector":
1511
+ return board.create("arrow", [picks[0], phantom], PREVIEW_STYLE);
1512
+ case "circleCenter":
1513
+ return board.create("circle", [picks[0], phantom], CIRCLE_PREVIEW_STYLE);
1514
+ case "circle3":
1515
+ if (picks.length === 1) return board.create("circle", [picks[0], phantom], CIRCLE_PREVIEW_STYLE);
1516
+ if (picks.length === 2) {
1517
+ return board.create("circumcircle", [picks[0], picks[1], phantom], CIRCLE_PREVIEW_STYLE);
1518
+ }
1519
+ return null;
1520
+ case "angle":
1521
+ if (picks.length === 1) return board.create("segment", [picks[0], phantom], PREVIEW_STYLE);
1522
+ if (picks.length === 2) {
1523
+ return board.create("angle", [picks[0], picks[1], phantom], {
1524
+ ...PREVIEW_STYLE,
1525
+ radius: 1,
1526
+ fillColor: "#22c55e",
1527
+ fillOpacity: 0.15
1528
+ });
1529
+ }
1530
+ return null;
1531
+ case "perpBisector":
1532
+ return board.create("segment", [picks[0], phantom], PREVIEW_STYLE);
1533
+ case "angleBisector":
1534
+ if (picks.length === 1) return board.create("segment", [picks[0], phantom], PREVIEW_STYLE);
1535
+ if (picks.length === 2) return board.create("bisector", [picks[0], picks[1], phantom], PREVIEW_STYLE);
1536
+ return null;
1537
+ case "perpendicular":
1538
+ case "parallel":
1539
+ case "tangent": {
1540
+ if (picks.length !== 1) return null;
1541
+ const k = objKind(picks[0]);
1542
+ if (k === "line" && toolDef.key !== "tangent") {
1543
+ return board.create(toolDef.key, [picks[0], phantom], PREVIEW_STYLE);
1544
+ }
1545
+ if (k === "circle" && toolDef.key === "tangent") {
1546
+ const glider = board.create("glider", [phantom.X(), phantom.Y(), picks[0]], {
1547
+ visible: false,
1548
+ withLabel: false
1549
+ });
1550
+ return board.create("tangent", [glider], PREVIEW_STYLE);
1551
+ }
1552
+ return null;
1553
+ }
1554
+ default:
1555
+ return null;
1556
+ }
1557
+ }, null);
1558
+ }
1559
+ function createPhantomPoint(board) {
1560
+ if (!board) return null;
1561
+ return safeJsx("preview.createPhantomPoint", () => board.create("point", [0, 0], {
1562
+ visible: false,
1563
+ fixed: true,
1564
+ withLabel: false,
1565
+ name: ""
1566
+ }), null);
1567
+ }
1568
+
1569
+ // src/stamps/geometry-2d/editor/previewActions.ts
1570
+ function clearPreviewSegs(board, previewSegRef) {
1571
+ if (!board) return;
1572
+ for (const s of previewSegRef.current) {
1573
+ safeJsx("previewActions.removePreviewSeg", () => board.removeObject(s));
1574
+ }
1575
+ previewSegRef.current = [];
1576
+ }
1577
+ function removePhantom(board, refs) {
1578
+ if (!board) return;
1579
+ if (refs.previewShapeRef.current) {
1580
+ safeJsx("previewActions.removePreviewShape", () => board.removeObject(refs.previewShapeRef.current));
1581
+ refs.previewShapeRef.current = null;
1582
+ }
1583
+ if (refs.phantomRef.current) {
1584
+ safeJsx("previewActions.removePhantom", () => board.removeObject(refs.phantomRef.current));
1585
+ refs.phantomRef.current = null;
1586
+ }
1587
+ }
1588
+ function refreshPreviewShape(board, toolSM, refs) {
1589
+ if (!board) return;
1590
+ if (refs.previewShapeRef.current) {
1591
+ safeJsx("previewActions.removePreviewShape", () => board.removeObject(refs.previewShapeRef.current));
1592
+ refs.previewShapeRef.current = null;
1593
+ }
1594
+ const t = toolSM.toolRef.current;
1595
+ const toolDef = TOOLS.find((td) => td.key === t);
1596
+ if (!toolDef) return;
1597
+ const picks = refs.pendingRef.current;
1598
+ if (picks.length === 0 || toolDef.needs <= 0) return;
1599
+ if (picks.length >= toolDef.needs) return;
1600
+ if (!refs.phantomRef.current) {
1601
+ refs.phantomRef.current = createPhantomPoint(board);
1602
+ if (!refs.phantomRef.current) return;
1603
+ }
1604
+ refs.previewShapeRef.current = buildPreviewShape(board, toolDef, picks, refs.phantomRef.current);
1605
+ }
1606
+
1607
+ // src/stamps/geometry-2d/editor/attrMapping.ts
1608
+ function applyMutatePatch(store, id, patch) {
1609
+ if (patch.remove) {
1610
+ store.dispatch({ type: "DELETE", payload: { id } });
1611
+ return;
1612
+ }
1613
+ if (!patch.attrs) return;
1614
+ const { name, withLabel, strokeColor, fillColor, strokeWidth, ...rest } = patch.attrs;
1615
+ if (typeof name === "string") {
1616
+ store.dispatch({ type: "UPDATE", payload: { id, patch: { label: name } } });
1617
+ }
1618
+ const mapped = { ...rest };
1619
+ if (strokeColor !== void 0 && mapped.color === void 0) mapped.color = strokeColor;
1620
+ if (fillColor !== void 0 && mapped.color === void 0) mapped.color = fillColor;
1621
+ if (strokeWidth !== void 0 && mapped.width === void 0) mapped.width = strokeWidth;
1622
+ if (withLabel !== void 0 && mapped.showLabel === void 0) mapped.showLabel = withLabel;
1623
+ if (Object.keys(mapped).length > 0) {
1624
+ store.dispatch({ type: "UPDATE_ATTRS", payload: { id, patch: mapped } });
1625
+ }
1626
+ }
1627
+
1628
+ // src/stamps/geometry-2d/editor/buildHandle.ts
1629
+ function buildMiniBoardHandle(d) {
1630
+ return {
1631
+ getContainer: () => d.containerRef.current,
1632
+ getBbox: () => d.boardRef.current?.getBoundingBox() ?? [-10, 10, 10, -10],
1633
+ getState: () => d.store.getState(),
1634
+ getStore: () => d.store,
1635
+ highlight: (id) => {
1636
+ d.rendererRef.current?.highlight(id);
1637
+ },
1638
+ snapshotObject: (id, anchorScreen) => buildObjectSnapshot(d.store.getState(), id, anchorScreen),
1639
+ mutateObject: (id, patch) => applyMutatePatch(d.store, id, patch),
1640
+ getAllPointNames: () => listObjects(d.store.getState()).filter((o) => o.kind === "point" || o.kind === "intersection").map((o) => o.label),
1641
+ onSelect: (cb) => {
1642
+ d.selectSubsRef.current.add(cb);
1643
+ return () => {
1644
+ d.selectSubsRef.current.delete(cb);
1645
+ };
1646
+ },
1647
+ onTransformParam: (cb) => {
1648
+ d.transformSubsRef.current.add(cb);
1649
+ return () => {
1650
+ d.transformSubsRef.current.delete(cb);
1651
+ };
1652
+ },
1653
+ onSelectionState: (cb) => {
1654
+ d.selectionStateSubsRef.current.add(cb);
1655
+ return () => {
1656
+ d.selectionStateSubsRef.current.delete(cb);
1657
+ };
1658
+ },
1659
+ confirmTransformParam: (value) => {
1660
+ const info = d.pendingTransformRef.current;
1661
+ if (info && d.ctxRef.current) {
1662
+ safeJsx(
1663
+ "buildHandle.finalizeTransform",
1664
+ () => finalizeTransform(d.ctxRef.current, info.tool, info.pendingIds, value)
1665
+ );
1666
+ }
1667
+ d.pendingTransformRef.current = null;
1668
+ d.emitTransform(null);
1669
+ d.clearPending();
1670
+ },
1671
+ cancelTransformParam: () => {
1672
+ d.pendingTransformRef.current = null;
1673
+ d.emitTransform(null);
1674
+ d.clearPending();
1675
+ },
1676
+ getSelectionSize: () => d.selectedSetRef.current.size,
1677
+ clearSelection: d.clearSelection,
1678
+ deleteSelection: d.deleteSelection
1679
+ };
1680
+ }
1681
+
1682
+ // src/stamps/geometry-2d/editor/idResolvers.ts
1683
+ function jxgFromSceneId(renderer, id) {
1684
+ if (!renderer) return null;
1685
+ const elements = renderer.elements;
1686
+ if (!elements) return null;
1687
+ const m = /^(.+):border:(\d+)$/.exec(id);
1688
+ if (m) {
1689
+ const poly = elements.get(m[1]);
1690
+ const idx = parseInt(m[2], 10);
1691
+ const borders = poly?.borders;
1692
+ if (Array.isArray(borders) && borders[idx]) return borders[idx];
1693
+ return null;
1694
+ }
1695
+ return elements.get(id) ?? null;
1696
+ }
1697
+ function jxgIdToSceneId(renderer, idMap, jxgObj) {
1698
+ if (!jxgObj?.id) return null;
1699
+ const direct = idMap.get(String(jxgObj.id));
1700
+ if (direct) return direct;
1701
+ if (!renderer) return null;
1702
+ const elements = renderer.elements;
1703
+ if (!elements) return null;
1704
+ for (const [sid, el] of elements) {
1705
+ if (el === jxgObj) return sid;
1706
+ const borders = el?.borders;
1707
+ if (Array.isArray(borders)) {
1708
+ const idx = borders.indexOf(jxgObj);
1709
+ if (idx >= 0) return `${sid}:border:${idx}`;
1710
+ }
1711
+ }
1712
+ return null;
1713
+ }
1714
+ function useAxisGridSync({
1715
+ boardRef,
1716
+ axisObjsRef,
1717
+ isDarkRef,
1718
+ showAxis,
1719
+ showGrid
1720
+ }) {
1721
+ useEffect(() => {
1722
+ const b = boardRef.current;
1723
+ if (!b) return;
1724
+ safeJsx("useAxisGridSync.toggleAxis", () => {
1725
+ if (axisObjsRef.current.x) {
1726
+ safeJsx("useAxisGridSync.removeAxisX", () => b.removeObject(axisObjsRef.current.x));
1727
+ axisObjsRef.current.x = void 0;
1728
+ }
1729
+ if (axisObjsRef.current.y) {
1730
+ safeJsx("useAxisGridSync.removeAxisY", () => b.removeObject(axisObjsRef.current.y));
1731
+ axisObjsRef.current.y = void 0;
1732
+ }
1733
+ if (showAxis) {
1734
+ axisObjsRef.current.x = b.create("axis", [[0, 0], [1, 0]], { strokeColor: themeAxis(isDarkRef.current), name: "", withLabel: false });
1735
+ axisObjsRef.current.y = b.create("axis", [[0, 0], [0, 1]], { strokeColor: themeAxis(isDarkRef.current), name: "", withLabel: false });
1736
+ }
1737
+ b.update();
1738
+ });
1739
+ }, [showAxis]);
1740
+ useEffect(() => {
1741
+ const b = boardRef.current;
1742
+ if (!b) return;
1743
+ safeJsx("useAxisGridSync.toggleGrid", () => {
1744
+ for (const o of Object.values(b.objects || {})) {
1745
+ if (o && (o.elType === "grid" || o.type === "grid" || o.visProp && o.visProp.type === "grid")) {
1746
+ safeJsx("useAxisGridSync.removeGrid", () => b.removeObject(o));
1747
+ }
1748
+ }
1749
+ if (showGrid) b.create("grid", [], { strokeColor: themeGrid(isDarkRef.current), strokeOpacity: 1 });
1750
+ b.update();
1751
+ });
1752
+ }, [showGrid]);
1753
+ }
1754
+ function useEditorShortcuts({
1755
+ store,
1756
+ pendingIdsRef,
1757
+ selectedSetRef,
1758
+ clearPending,
1759
+ clearSelection,
1760
+ deleteSelection
1761
+ }) {
1762
+ useEffect(() => {
1763
+ const onKey = (e) => {
1764
+ const ae = document.activeElement;
1765
+ const inField = !!(ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable));
1766
+ const lk = e.key.toLowerCase();
1767
+ if ((e.metaKey || e.ctrlKey) && lk === "z" && !e.shiftKey) {
1768
+ if (inField) return;
1769
+ e.preventDefault();
1770
+ e.stopPropagation();
1771
+ store.undo();
1772
+ return;
1773
+ }
1774
+ if ((e.metaKey || e.ctrlKey) && (lk === "z" && e.shiftKey || lk === "y" && !e.shiftKey)) {
1775
+ if (inField) return;
1776
+ e.preventDefault();
1777
+ e.stopPropagation();
1778
+ store.redo();
1779
+ return;
1780
+ }
1781
+ if (e.key === "Escape" && !inField) {
1782
+ if (pendingIdsRef.current.length > 0) {
1783
+ e.preventDefault();
1784
+ e.stopPropagation();
1785
+ clearPending();
1786
+ }
1787
+ if (selectedSetRef.current.size > 0) {
1788
+ e.preventDefault();
1789
+ e.stopPropagation();
1790
+ clearSelection();
1791
+ }
1792
+ }
1793
+ if ((e.key === "Delete" || e.key === "Backspace") && !inField && selectedSetRef.current.size > 0) {
1794
+ e.preventDefault();
1795
+ e.stopPropagation();
1796
+ deleteSelection();
1797
+ }
1798
+ };
1799
+ window.addEventListener("keydown", onKey, { capture: true });
1800
+ return () => window.removeEventListener("keydown", onKey, { capture: true });
1801
+ }, [store, pendingIdsRef, selectedSetRef, clearPending, clearSelection, deleteSelection]);
1802
+ }
1803
+ function useJxgSceneIdMap({ store, rendererRef }) {
1804
+ const jxgIdToSceneRef = useRef(/* @__PURE__ */ new Map());
1805
+ useEffect(() => {
1806
+ const rebuild = () => {
1807
+ const r = rendererRef.current;
1808
+ if (!r) return;
1809
+ const elements = r.elements;
1810
+ const next = /* @__PURE__ */ new Map();
1811
+ if (elements) {
1812
+ for (const [sid, jxg] of elements) {
1813
+ const jid = jxg?.id;
1814
+ if (jid) next.set(String(jid), sid);
1815
+ }
1816
+ }
1817
+ jxgIdToSceneRef.current = next;
1818
+ };
1819
+ rebuild();
1820
+ return store.subscribe(() => rebuild());
1821
+ }, [store, rendererRef]);
1822
+ return { jxgIdToSceneRef };
1823
+ }
1824
+ var MiniBoard2D = forwardRef(function MiniBoard2D2({ onReady, store, selectedTool, showAxis, showGrid, isDark, toast }, ref) {
1825
+ const isDarkRef = useRef(!!isDark);
1826
+ isDarkRef.current = !!isDark;
1827
+ const containerId = useId().replace(/:/g, "_") + "_jxgmini";
1828
+ const containerRef = useRef(null);
1829
+ const boardRef = useRef(null);
1830
+ const jxgRef = useRef(null);
1831
+ const rendererRef = useRef(null);
1832
+ const axisObjsRef = useRef({});
1833
+ const toolSM = useToolStateMachine(selectedTool);
1834
+ const initialMeta = store.getState().meta;
1835
+ const initialView = initialMeta.domain === "2d" ? initialMeta.view : DEFAULT_VIEW_2D;
1836
+ const showAxisRef = useRef(showAxis);
1837
+ showAxisRef.current = showAxis;
1838
+ const showGridRef = useRef(showGrid);
1839
+ showGridRef.current = showGrid;
1840
+ const selectedSetRef = useRef(/* @__PURE__ */ new Set());
1841
+ const [selectionTick, setSelectionTick] = useState(0);
1842
+ useEffect(() => {
1843
+ const ids = [...selectedSetRef.current];
1844
+ rendererRef.current?.highlight(ids);
1845
+ const anchor = lastClickClientRef.current ?? { x: 0, y: 0 };
1846
+ const snap = ids.length === 0 ? null : { ids, anchor: { x: anchor.x + 8, y: anchor.y + 8 } };
1847
+ selectionStateSubsRef.current.forEach(
1848
+ (cb) => safeJsx("MiniBoard.emitSelectionState.cb", () => cb(snap))
1849
+ );
1850
+ }, [selectionTick]);
1851
+ const pendingRef = useRef([]);
1852
+ const previewSegRef = useRef([]);
1853
+ const phantomRef = useRef(null);
1854
+ const previewShapeRef = useRef(null);
1855
+ const previewRafRef = useRef(null);
1856
+ const marqueeRef = useRef(null);
1857
+ const moveDownRef = useRef(null);
1858
+ const lastMoveClickRef = useRef({ id: null, time: 0 });
1859
+ const lastClickClientRef = useRef(null);
1860
+ const pendingTransformRef = useRef(null);
1861
+ const selectSubsRef = useRef(/* @__PURE__ */ new Set());
1862
+ const transformSubsRef = useRef(/* @__PURE__ */ new Set());
1863
+ const selectionStateSubsRef = useRef(/* @__PURE__ */ new Set());
1864
+ const { jxgIdToSceneRef } = useJxgSceneIdMap({ store, rendererRef });
1865
+ const jxgFromSceneId2 = useCallback(
1866
+ (id) => jxgFromSceneId(rendererRef.current, id),
1867
+ []
1868
+ );
1869
+ const jxgIdToSceneId2 = useCallback(
1870
+ (jxgObj) => jxgIdToSceneId(rendererRef.current, jxgIdToSceneRef.current, jxgObj),
1871
+ [jxgIdToSceneRef]
1872
+ );
1873
+ const screenCoordsOf2 = useCallback(
1874
+ (evt) => screenCoordsOf(boardRef.current, containerRef.current, evt),
1875
+ []
1876
+ );
1877
+ const objectsAt2 = useCallback(
1878
+ (evt) => objectsAt(boardRef.current, containerRef.current, evt, [
1879
+ phantomRef.current,
1880
+ previewShapeRef.current,
1881
+ ...previewSegRef.current
1882
+ ]),
1883
+ []
1884
+ );
1885
+ const findNearestPointJxg = useCallback(
1886
+ (evt, tolPx = 12) => findNearestJxgPoint(boardRef.current, containerRef.current, store.getState(), jxgFromSceneId2, evt, tolPx),
1887
+ [jxgFromSceneId2, store]
1888
+ );
1889
+ const promoteLabel = useCallback(
1890
+ (o) => promoteToLabelOwner(boardRef.current, o),
1891
+ []
1892
+ );
1893
+ const toggleSelect = useCallback((id, additive) => {
1894
+ if (!additive) {
1895
+ selectedSetRef.current.clear();
1896
+ selectedSetRef.current.add(id);
1897
+ } else if (selectedSetRef.current.has(id)) selectedSetRef.current.delete(id);
1898
+ else selectedSetRef.current.add(id);
1899
+ rendererRef.current?.highlight([...selectedSetRef.current]);
1900
+ setSelectionTick((t) => t + 1);
1901
+ }, []);
1902
+ const clearSelection = useCallback(() => {
1903
+ selectedSetRef.current.clear();
1904
+ rendererRef.current?.highlight(null);
1905
+ setSelectionTick((t) => t + 1);
1906
+ }, []);
1907
+ const deleteSelection = useCallback(() => {
1908
+ if (selectedSetRef.current.size === 0) return;
1909
+ store.transaction((dispatch) => {
1910
+ for (const id of selectedSetRef.current) dispatch({ type: "DELETE", payload: { id } });
1911
+ });
1912
+ selectedSetRef.current.clear();
1913
+ rendererRef.current?.highlight(null);
1914
+ setSelectionTick((t) => t + 1);
1915
+ }, [store]);
1916
+ const clearPreviewSegs2 = useCallback(
1917
+ () => clearPreviewSegs(boardRef.current, previewSegRef),
1918
+ []
1919
+ );
1920
+ const removePhantom2 = useCallback(
1921
+ () => removePhantom(boardRef.current, { previewShapeRef, phantomRef }),
1922
+ []
1923
+ );
1924
+ const clearPending = useCallback(() => {
1925
+ removePhantom2();
1926
+ clearPreviewSegs2();
1927
+ pendingRef.current = [];
1928
+ toolSM.clearPending();
1929
+ }, [clearPreviewSegs2, removePhantom2, toolSM]);
1930
+ const refreshPreview = useCallback(
1931
+ () => refreshPreviewShape(boardRef.current, toolSM, {
1932
+ phantomRef,
1933
+ previewShapeRef,
1934
+ pendingRef
1935
+ }),
1936
+ [toolSM]
1937
+ );
1938
+ const [, setWarn] = useState(null);
1939
+ const warnTimerRef = useRef(null);
1940
+ const flashWarn = useCallback((msg) => {
1941
+ if (warnTimerRef.current) clearTimeout(warnTimerRef.current);
1942
+ setWarn(msg);
1943
+ warnTimerRef.current = setTimeout(() => setWarn(null), 1800);
1944
+ }, []);
1945
+ useEffect(() => () => {
1946
+ if (warnTimerRef.current) clearTimeout(warnTimerRef.current);
1947
+ }, []);
1948
+ const nextLabelFor = useCallback(
1949
+ (kind) => nextLabel(store.getState(), kind),
1950
+ [store]
1951
+ );
1952
+ const buildSnapshot = useCallback(
1953
+ (id, anchorScreen) => buildObjectSnapshot(store.getState(), id, anchorScreen),
1954
+ [store]
1955
+ );
1956
+ const emitSelect = useCallback((info) => {
1957
+ const snap = buildSnapshot(info.id, info.anchorScreen);
1958
+ if (!snap) return;
1959
+ selectSubsRef.current.forEach((cb) => safeJsx("MiniBoard.emitSelect.cb", () => cb(snap)));
1960
+ }, [buildSnapshot]);
1961
+ const emitTransform = useCallback((info) => {
1962
+ transformSubsRef.current.forEach((cb) => safeJsx("MiniBoard.emitTransform.cb", () => cb(info)));
1963
+ }, []);
1964
+ const ctxRef = useRef(null);
1965
+ ctxRef.current = {
1966
+ boardRef,
1967
+ toolRef: toolSM.toolRef,
1968
+ pendingRef,
1969
+ pendingIdsRef: toolSM.pendingIdsRef,
1970
+ previewSegRef,
1971
+ axisObjsRef,
1972
+ selectedSetRef,
1973
+ marqueeRef,
1974
+ moveDownRef,
1975
+ lastMoveClickRef,
1976
+ lastClickClientRef,
1977
+ pendingTransformRef,
1978
+ phantomRef,
1979
+ previewShapeRef,
1980
+ previewRafRef,
1981
+ jxgRef,
1982
+ store,
1983
+ jxgIdToSceneId: jxgIdToSceneId2,
1984
+ jxgFromSceneId: jxgFromSceneId2,
1985
+ screenCoordsOf: screenCoordsOf2,
1986
+ objectsAt: objectsAt2,
1987
+ promoteLabel,
1988
+ findNearestPointJxg,
1989
+ toggleSelect,
1990
+ clearSelection,
1991
+ nextLabel: nextLabelFor,
1992
+ clearPending,
1993
+ clearPreviewSegs: clearPreviewSegs2,
1994
+ refreshPreview,
1995
+ flashWarn,
1996
+ toast,
1997
+ emitTransform,
1998
+ emitSelect,
1999
+ setPendingCount: () => {
2000
+ },
2001
+ setSelectionTick: (fn) => setSelectionTick(fn)
2002
+ };
2003
+ useEditorShortcuts({
2004
+ store,
2005
+ pendingIdsRef: toolSM.pendingIdsRef,
2006
+ selectedSetRef,
2007
+ clearPending,
2008
+ clearSelection,
2009
+ deleteSelection
2010
+ });
2011
+ useAxisGridSync({ boardRef, axisObjsRef, isDarkRef, showAxis, showGrid });
2012
+ const handleToolChange = useCallback((t) => {
2013
+ clearPending();
2014
+ toolSM.setTool(t);
2015
+ const b = boardRef.current;
2016
+ if (b) safeJsx("MiniBoard.setPanForTool", () => {
2017
+ if (b.attr?.pan) b.attr.pan.enabled = t !== "select";
2018
+ });
2019
+ }, [clearPending, toolSM]);
2020
+ useEffect(() => {
2021
+ if (toolSM.toolRef.current !== selectedTool) handleToolChange(selectedTool);
2022
+ }, [selectedTool]);
2023
+ useEffect(() => {
2024
+ if (typeof window === "undefined" || !containerRef.current) return;
2025
+ let cancelled = false;
2026
+ let wheelCleanup = null;
2027
+ let freeBoard = null;
2028
+ void (async () => {
2029
+ const { JXG, board, cleanup } = await initJxgBoard(containerId, {
2030
+ label: "MiniBoard.2d",
2031
+ defaults: { disableElementHighlight: true },
2032
+ boardOptions: {
2033
+ boundingbox: initialView.bbox,
2034
+ axis: false,
2035
+ grid: false,
2036
+ showCopyright: false,
2037
+ showNavigation: true,
2038
+ keepAspectRatio: true,
2039
+ pan: { enabled: true, needShift: false },
2040
+ zoom: { wheel: false },
2041
+ precision: { hasPoint: 8, mouse: 4, touch: 16 }
2042
+ },
2043
+ extraOptionTweaks: (opts) => {
2044
+ if (opts.label) opts.label.strokeColor = themeLabel(isDarkRef.current);
2045
+ if (opts.text) opts.text.strokeColor = themeLabel(isDarkRef.current);
2046
+ }
2047
+ });
2048
+ if (cancelled || !containerRef.current) {
2049
+ cleanup();
2050
+ return;
2051
+ }
2052
+ jxgRef.current = JXG;
2053
+ boardRef.current = board;
2054
+ freeBoard = cleanup;
2055
+ rendererRef.current = new JxgRenderer(store, board, {
2056
+ theme: paletteFor(isDarkRef.current)
2057
+ });
2058
+ if (containerRef.current) {
2059
+ wheelCleanup = attachJxgWheelZoom(containerRef.current, board, "MiniBoard.2d");
2060
+ }
2061
+ if (showAxisRef.current) safeJsx("MiniBoard.initAxes", () => {
2062
+ axisObjsRef.current.x = board.create("axis", [[0, 0], [1, 0]], { strokeColor: themeAxis(isDarkRef.current), name: "", withLabel: false });
2063
+ axisObjsRef.current.y = board.create("axis", [[0, 0], [0, 1]], { strokeColor: themeAxis(isDarkRef.current), name: "", withLabel: false });
2064
+ });
2065
+ if (showGridRef.current) safeJsx(
2066
+ "MiniBoard.initGrid",
2067
+ () => board.create("grid", [], { strokeColor: themeGrid(isDarkRef.current), strokeOpacity: 1 })
2068
+ );
2069
+ const fire = (h) => (e) => {
2070
+ if (ctxRef.current) h(ctxRef.current, e);
2071
+ };
2072
+ board.on("down", fire(handleDown));
2073
+ board.on("up", fire(handleUp));
2074
+ board.on("move", fire(handleMove));
2075
+ board.on("move", (e) => {
2076
+ const container = containerRef.current;
2077
+ if (!container) return;
2078
+ const hits = objectsAt(boardRef.current, container, e, [
2079
+ phantomRef.current,
2080
+ previewShapeRef.current,
2081
+ ...previewSegRef.current
2082
+ ]);
2083
+ container.style.cursor = hits.length > 0 ? "pointer" : "";
2084
+ });
2085
+ onReady?.();
2086
+ })();
2087
+ return () => {
2088
+ cancelled = true;
2089
+ if (wheelCleanup) {
2090
+ wheelCleanup();
2091
+ wheelCleanup = null;
2092
+ }
2093
+ if (previewRafRef.current != null) {
2094
+ cancelAnimationFrame(previewRafRef.current);
2095
+ previewRafRef.current = null;
2096
+ }
2097
+ rendererRef.current?.dispose();
2098
+ rendererRef.current = null;
2099
+ if (freeBoard) {
2100
+ freeBoard();
2101
+ freeBoard = null;
2102
+ }
2103
+ boardRef.current = null;
2104
+ };
2105
+ }, [containerId]);
2106
+ useImperativeHandle(
2107
+ ref,
2108
+ () => buildMiniBoardHandle({
2109
+ containerRef,
2110
+ boardRef,
2111
+ rendererRef,
2112
+ selectSubsRef,
2113
+ transformSubsRef,
2114
+ selectionStateSubsRef,
2115
+ selectedSetRef,
2116
+ pendingTransformRef,
2117
+ ctxRef,
2118
+ store,
2119
+ clearPending,
2120
+ clearSelection,
2121
+ deleteSelection,
2122
+ emitTransform
2123
+ }),
2124
+ [store, clearSelection, deleteSelection, clearPending, emitTransform]
2125
+ );
2126
+ return /* @__PURE__ */ jsx(
2127
+ "div",
2128
+ {
2129
+ ref: containerRef,
2130
+ id: containerId,
2131
+ "data-testid": "jxgmini-container",
2132
+ className: "h-full min-h-0 bg-white",
2133
+ style: { touchAction: "none" }
2134
+ }
2135
+ );
2136
+ });
2137
+
2138
+ // src/stamps/shared/excalidrawPalette.ts
2139
+ var STROKE_PALETTE = [
2140
+ "#1e1e1e",
2141
+ // black
2142
+ "#e03131",
2143
+ // red
2144
+ "#e8590c",
2145
+ // orange
2146
+ "#f08c00",
2147
+ // yellow
2148
+ "#2f9e44",
2149
+ // green
2150
+ "#1971c2",
2151
+ // blue
2152
+ "#9c36b5",
2153
+ // grape
2154
+ "#868e96"
2155
+ // gray
2156
+ ];
2157
+ var DASH_OPTIONS = [
2158
+ { value: 0, label: "N\xE9t li\u1EC1n" },
2159
+ { value: 2, label: "N\xE9t \u0111\u1EE9t" },
2160
+ { value: 1, label: "N\xE9t ch\u1EA5m" }
2161
+ ];
2162
+ var WIDTH_OPTIONS = [1, 2, 3];
2163
+ var FACE_OPTIONS = [
2164
+ { value: "o", symbol: "\u25CF" },
2165
+ { value: "circle", symbol: "\u25EF" },
2166
+ { value: "cross", symbol: "\u2715" },
2167
+ { value: "plus", symbol: "\u271A" }
2168
+ ];
2169
+ var SUB_DIGITS = ["\u2080", "\u2081", "\u2082", "\u2083", "\u2084", "\u2085", "\u2086", "\u2087", "\u2088", "\u2089"];
2170
+ var SUB_SET = new Set(SUB_DIGITS);
2171
+ function toSubscript(n) {
2172
+ return String(n).split("").map((d) => SUB_DIGITS[+d] ?? d).join("");
2173
+ }
2174
+ function stripTrailingSubscript(s) {
2175
+ let i = s.length;
2176
+ while (i > 0 && SUB_SET.has(s[i - 1])) i--;
2177
+ return s.slice(0, i);
2178
+ }
2179
+ function disambiguateName(name, existing) {
2180
+ if (!name) return name;
2181
+ if (!existing.has(name)) return name;
2182
+ const base = stripTrailingSubscript(name) || name;
2183
+ for (let n = 2; n < 1e3; n++) {
2184
+ const candidate = base + toSubscript(n);
2185
+ if (!existing.has(candidate)) return candidate;
2186
+ }
2187
+ return name;
2188
+ }
2189
+ var Icons = {
2190
+ color: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
2191
+ /* @__PURE__ */ jsx("path", { d: "M19 11 L11 3 L3 11 L11 19 Z" }),
2192
+ /* @__PURE__ */ jsx("path", { d: "M19 11 L21 16 a2 2 0 1 1 -4 0 Z", fill: "currentColor", stroke: "none" })
2193
+ ] }),
2194
+ style: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "5", fill: "currentColor" }) }),
2195
+ size: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", children: [
2196
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "9", x2: "20", y2: "9", strokeWidth: "1" }),
2197
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "13", x2: "20", y2: "13", strokeWidth: "2" }),
2198
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "17", x2: "20", y2: "17", strokeWidth: "3.2" })
2199
+ ] }),
2200
+ name: /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor", children: [
2201
+ /* @__PURE__ */ jsx("text", { x: "2", y: "17", fontSize: "14", fontFamily: "serif", fontWeight: "700", children: "A" }),
2202
+ /* @__PURE__ */ jsx("text", { x: "12", y: "17", fontSize: "11", fontFamily: "serif", fontWeight: "700", children: "a" })
2203
+ ] }),
2204
+ trash: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
2205
+ /* @__PURE__ */ jsx("polyline", { points: "3,6 5,6 21,6" }),
2206
+ /* @__PURE__ */ jsx("path", { d: "M19 6 l-1 14 a 2 2 0 0 1 -2 2 H 8 a 2 2 0 0 1 -2 -2 l-1 -14" }),
2207
+ /* @__PURE__ */ jsx("line", { x1: "10", y1: "11", x2: "10", y2: "17" }),
2208
+ /* @__PURE__ */ jsx("line", { x1: "14", y1: "11", x2: "14", y2: "17" })
2209
+ ] })
2210
+ };
2211
+ var PropertiesPopover = (props) => {
2212
+ const { anchor, onClose, onMutate, isDark, getAllNames } = props;
2213
+ const rootRef = useRef(null);
2214
+ const [section, setSection] = useState(null);
2215
+ const { isMobile } = useIsMobile();
2216
+ const [clamped, setClamped] = useState(null);
2217
+ useLayoutEffect(() => {
2218
+ if (typeof window === "undefined") return;
2219
+ const margin = 8;
2220
+ if (isMobile) {
2221
+ const rect2 = rootRef.current?.getBoundingClientRect();
2222
+ const w2 = rect2?.width ?? 280;
2223
+ const left2 = Math.max(margin, (window.innerWidth - w2) / 2);
2224
+ const top2 = window.innerHeight - (rect2?.height ?? 80) - margin - 12;
2225
+ setClamped({ left: left2, top: Math.max(margin, top2) });
2226
+ return;
2227
+ }
2228
+ const rect = rootRef.current?.getBoundingClientRect();
2229
+ const w = rect?.width ?? 280;
2230
+ const h = rect?.height ?? 80;
2231
+ const left = Math.max(margin, Math.min(anchor.x, window.innerWidth - w - margin));
2232
+ const top = Math.max(margin, Math.min(anchor.y, window.innerHeight - h - margin));
2233
+ setClamped({ left, top });
2234
+ }, [anchor.x, anchor.y, isMobile, section]);
2235
+ const initialName = props.kind === "point" ? props.currentName : props.kind === "line" || props.kind === "circle" ? props.currentName : "";
2236
+ const [name, setName] = useState(initialName);
2237
+ useEffect(() => {
2238
+ setName(initialName);
2239
+ }, [initialName]);
2240
+ useEffect(() => {
2241
+ const onKey = (e) => {
2242
+ if (e.key === "Escape") {
2243
+ e.preventDefault();
2244
+ onClose();
2245
+ }
2246
+ };
2247
+ const onPointerDown = (e) => {
2248
+ if (!rootRef.current?.contains(e.target)) onClose();
2249
+ };
2250
+ document.addEventListener("keydown", onKey);
2251
+ document.addEventListener("pointerdown", onPointerDown, { capture: true });
2252
+ return () => {
2253
+ document.removeEventListener("keydown", onKey);
2254
+ document.removeEventListener("pointerdown", onPointerDown, { capture: true });
2255
+ };
2256
+ }, [onClose]);
2257
+ const pickColor = (c) => {
2258
+ onMutate({ attrs: { strokeColor: c, fillColor: props.kind === "circle" ? "none" : c, color: c } });
2259
+ };
2260
+ const pickDash = (d) => onMutate({ attrs: { dash: d } });
2261
+ const pickWidth = (w) => onMutate({ attrs: { strokeWidth: w } });
2262
+ const pickFace = (f) => onMutate({ attrs: { face: f } });
2263
+ const currentName = props.kind === "point" || props.kind === "line" || props.kind === "circle" ? props.currentName : "";
2264
+ const commitName = () => {
2265
+ const trimmed = name.trim();
2266
+ if (trimmed === currentName) return;
2267
+ let final = trimmed;
2268
+ if (trimmed) {
2269
+ const others = new Set((getAllNames?.() ?? []).filter((n) => n !== currentName));
2270
+ final = disambiguateName(trimmed, others);
2271
+ }
2272
+ if (final !== name) setName(final);
2273
+ onMutate({ attrs: { name: final } });
2274
+ };
2275
+ const toggleShowLabel = (next) => onMutate({ attrs: { withLabel: next } });
2276
+ const toggleShowValue = (next) => onMutate({ valueLabel: next });
2277
+ const doDelete = () => {
2278
+ onMutate({ remove: true });
2279
+ onClose();
2280
+ };
2281
+ const toggleSection = (s) => setSection((cur) => cur === s ? null : s);
2282
+ const currentColor = props.currentColor;
2283
+ const currentDash = props.currentDash;
2284
+ const currentWidth = props.currentWidth;
2285
+ const colorIndicatorTint = useMemo(() => currentColor, [currentColor]);
2286
+ if (typeof document === "undefined") return null;
2287
+ const PillBtn = ({ id, label, icon, active, onClick, indicatorColor }) => /* @__PURE__ */ jsxs(
2288
+ "button",
2289
+ {
2290
+ type: "button",
2291
+ "data-section": id,
2292
+ "data-pill-btn": id,
2293
+ "aria-label": label,
2294
+ "aria-pressed": !!active,
2295
+ onClick,
2296
+ 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"}`,
2297
+ children: [
2298
+ icon,
2299
+ indicatorColor && /* @__PURE__ */ jsx(
2300
+ "span",
2301
+ {
2302
+ "aria-hidden": true,
2303
+ className: "absolute bottom-0.5 left-1/2 -translate-x-1/2 h-1 w-4 rounded-full",
2304
+ style: { background: indicatorColor }
2305
+ }
2306
+ )
2307
+ ]
2308
+ }
2309
+ );
2310
+ const pos = clamped ?? { left: anchor.x, top: anchor.y };
2311
+ const node = /* @__PURE__ */ jsxs(
2312
+ "div",
2313
+ {
2314
+ ref: rootRef,
2315
+ "data-stamp-area": "true",
2316
+ className: `${isDark ? "theme--dark " : ""}fixed z-[2147483600] flex flex-col gap-1.5`,
2317
+ style: { left: pos.left, top: pos.top },
2318
+ role: "dialog",
2319
+ "aria-label": "Thu\u1ED9c t\xEDnh \u0111\u1ED1i t\u01B0\u1EE3ng",
2320
+ children: [
2321
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 rounded-full border border-slate-300 bg-white px-1.5 py-1 shadow-lg ring-1 ring-black/5", children: [
2322
+ /* @__PURE__ */ jsx(PillBtn, { id: "color", label: "M\xE0u", icon: Icons.color, active: section === "color", onClick: () => toggleSection("color"), indicatorColor: colorIndicatorTint }),
2323
+ /* @__PURE__ */ jsx(PillBtn, { id: "style", label: "Ki\u1EC3u", icon: Icons.style, active: section === "style", onClick: () => toggleSection("style") }),
2324
+ /* @__PURE__ */ jsx(PillBtn, { id: "size", label: "\u0110\u1ED9 d\xE0y", icon: Icons.size, active: section === "size", onClick: () => toggleSection("size") }),
2325
+ /* @__PURE__ */ jsx(PillBtn, { id: "name", label: "T\xEAn", icon: Icons.name, active: section === "name", onClick: () => toggleSection("name") }),
2326
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "mx-0.5 h-5 w-px bg-slate-200" }),
2327
+ /* @__PURE__ */ jsx(PillBtn, { id: "delete", label: "Xo\xE1", icon: Icons.trash, onClick: doDelete })
2328
+ ] }),
2329
+ section && /* @__PURE__ */ jsxs("div", { className: "w-[220px] rounded-lg border border-slate-300 bg-white p-3 shadow-2xl ring-1 ring-black/5", children: [
2330
+ section === "color" && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
2331
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-slate-500", children: "M\xE0u" }),
2332
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: STROKE_PALETTE.map((c) => /* @__PURE__ */ jsx(
2333
+ "button",
2334
+ {
2335
+ "aria-label": `M\xE0u ${c}`,
2336
+ onClick: () => pickColor(c),
2337
+ className: `h-6 w-6 rounded border ${currentColor === c ? "border-emerald-500 ring-2 ring-emerald-300" : "border-slate-200"}`,
2338
+ style: { background: c }
2339
+ },
2340
+ c
2341
+ )) })
2342
+ ] }),
2343
+ section === "style" && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
2344
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-slate-500", children: "Ki\u1EC3u" }),
2345
+ props.kind === "point" ? /* @__PURE__ */ jsx("div", { className: "flex gap-1", children: FACE_OPTIONS.map((f) => /* @__PURE__ */ jsx(
2346
+ "button",
2347
+ {
2348
+ "aria-label": `H\xECnh ${f.value}`,
2349
+ onClick: () => pickFace(f.value),
2350
+ className: `h-7 w-7 rounded border text-sm ${props.currentFace === f.value ? "border-emerald-500 bg-emerald-50" : "border-slate-300 bg-white"}`,
2351
+ children: f.symbol
2352
+ },
2353
+ f.value
2354
+ )) }) : /* @__PURE__ */ jsx("div", { className: "flex gap-1", children: DASH_OPTIONS.map((d) => /* @__PURE__ */ jsx(
2355
+ "button",
2356
+ {
2357
+ "aria-label": `Ki\u1EC3u ${d.label.toLowerCase()}`,
2358
+ onClick: () => pickDash(d.value),
2359
+ 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"}`,
2360
+ children: d.label
2361
+ },
2362
+ d.value
2363
+ )) })
2364
+ ] }),
2365
+ section === "size" && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
2366
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-slate-500", children: "\u0110\u1ED9 d\xE0y" }),
2367
+ /* @__PURE__ */ jsx("div", { className: "flex gap-1", children: WIDTH_OPTIONS.map((w) => /* @__PURE__ */ jsx(
2368
+ "button",
2369
+ {
2370
+ "aria-label": `\u0110\u1ED9 d\xE0y ${w}`,
2371
+ onClick: () => pickWidth(w),
2372
+ className: `flex-1 rounded border py-1 ${currentWidth === w ? "border-emerald-500 bg-emerald-50" : "border-slate-300 bg-white"}`,
2373
+ children: /* @__PURE__ */ jsx("span", { className: "inline-block rounded bg-slate-800", style: { width: 30, height: w } })
2374
+ },
2375
+ w
2376
+ )) })
2377
+ ] }),
2378
+ section === "name" && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
2379
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
2380
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] font-medium text-slate-500", children: "T\xEAn" }),
2381
+ /* @__PURE__ */ jsx(
2382
+ "input",
2383
+ {
2384
+ value: name,
2385
+ onChange: (e) => setName(e.target.value),
2386
+ onBlur: commitName,
2387
+ onKeyDown: (e) => {
2388
+ if (e.key === "Enter") {
2389
+ e.preventDefault();
2390
+ commitName();
2391
+ }
2392
+ },
2393
+ autoFocus: true,
2394
+ placeholder: props.kind === "point" ? "A, B, \u2026" : props.kind === "line" ? "a, b, f, \u2026" : "O, c, \u2026",
2395
+ className: "rounded border border-slate-300 bg-white px-2 py-1 text-sm text-slate-800"
2396
+ }
2397
+ ),
2398
+ /* @__PURE__ */ jsx("span", { className: "text-[10px] text-slate-400", children: "Tr\xF9ng t\xEAn s\u1EBD t\u1EF1 th\xEAm ch\u1EC9 s\u1ED1 (B \u2192 B\u2082)" })
2399
+ ] }),
2400
+ /* @__PURE__ */ jsxs("label", { className: "flex items-center justify-between gap-2 text-[12px] text-slate-700", children: [
2401
+ /* @__PURE__ */ jsx("span", { children: "Hi\u1EC3n th\u1ECB t\xEAn" }),
2402
+ /* @__PURE__ */ jsx(
2403
+ "input",
2404
+ {
2405
+ type: "checkbox",
2406
+ checked: props.currentShowLabel !== false,
2407
+ onChange: (e) => toggleShowLabel(e.target.checked),
2408
+ "aria-label": "Hi\u1EC3n th\u1ECB t\xEAn"
2409
+ }
2410
+ )
2411
+ ] }),
2412
+ (props.kind === "line" || props.kind === "circle") && /* @__PURE__ */ jsxs("label", { className: "flex items-center justify-between gap-2 text-[12px] text-slate-700", children: [
2413
+ /* @__PURE__ */ jsx("span", { children: "Hi\u1EC3n th\u1ECB gi\xE1 tr\u1ECB" }),
2414
+ /* @__PURE__ */ jsx(
2415
+ "input",
2416
+ {
2417
+ type: "checkbox",
2418
+ checked: !!props.currentShowValue,
2419
+ onChange: (e) => toggleShowValue(e.target.checked),
2420
+ "aria-label": "Hi\u1EC3n th\u1ECB gi\xE1 tr\u1ECB"
2421
+ }
2422
+ )
2423
+ ] })
2424
+ ] })
2425
+ ] })
2426
+ ]
2427
+ }
2428
+ );
2429
+ return createPortal(node, document.body);
2430
+ };
2431
+ var Icons2 = {
2432
+ color: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
2433
+ /* @__PURE__ */ jsx("path", { d: "M19 11 L11 3 L3 11 L11 19 Z" }),
2434
+ /* @__PURE__ */ jsx("path", { d: "M19 11 L21 16 a2 2 0 1 1 -4 0 Z", fill: "currentColor", stroke: "none" })
2435
+ ] }),
2436
+ trash: /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
2437
+ /* @__PURE__ */ jsx("polyline", { points: "3,6 5,6 21,6" }),
2438
+ /* @__PURE__ */ jsx("path", { d: "M19 6 l-1 14 a 2 2 0 0 1 -2 2 H 8 a 2 2 0 0 1 -2 -2 l-1 -14" }),
2439
+ /* @__PURE__ */ jsx("line", { x1: "10", y1: "11", x2: "10", y2: "17" }),
2440
+ /* @__PURE__ */ jsx("line", { x1: "14", y1: "11", x2: "14", y2: "17" })
2441
+ ] })
2442
+ };
2443
+ var MultiPropertiesPopover = (props) => {
2444
+ const { anchor, count, isDark, onColor, onDelete, onClose } = props;
2445
+ const rootRef = useRef(null);
2446
+ const [section, setSection] = useState(null);
2447
+ const { isMobile } = useIsMobile();
2448
+ const [clamped, setClamped] = useState(null);
2449
+ useLayoutEffect(() => {
2450
+ if (typeof window === "undefined") return;
2451
+ const margin = 8;
2452
+ if (isMobile) {
2453
+ const rect2 = rootRef.current?.getBoundingClientRect();
2454
+ const w2 = rect2?.width ?? 220;
2455
+ const left2 = Math.max(margin, (window.innerWidth - w2) / 2);
2456
+ const top2 = window.innerHeight - (rect2?.height ?? 80) - margin - 12;
2457
+ setClamped({ left: left2, top: Math.max(margin, top2) });
2458
+ return;
2459
+ }
2460
+ const rect = rootRef.current?.getBoundingClientRect();
2461
+ const w = rect?.width ?? 220;
2462
+ const h = rect?.height ?? 80;
2463
+ const left = Math.max(margin, Math.min(anchor.x, window.innerWidth - w - margin));
2464
+ const top = Math.max(margin, Math.min(anchor.y, window.innerHeight - h - margin));
2465
+ setClamped({ left, top });
2466
+ }, [anchor.x, anchor.y, isMobile, section]);
2467
+ useEffect(() => {
2468
+ const onKey = (e) => {
2469
+ if (e.key === "Escape") {
2470
+ e.preventDefault();
2471
+ onClose();
2472
+ }
2473
+ };
2474
+ const onPointerDown = (e) => {
2475
+ if (!rootRef.current?.contains(e.target)) onClose();
2476
+ };
2477
+ document.addEventListener("keydown", onKey);
2478
+ document.addEventListener("pointerdown", onPointerDown, { capture: true });
2479
+ return () => {
2480
+ document.removeEventListener("keydown", onKey);
2481
+ document.removeEventListener("pointerdown", onPointerDown, { capture: true });
2482
+ };
2483
+ }, [onClose]);
2484
+ const toggleSection = (s) => setSection((cur) => cur === s ? null : s);
2485
+ const doDelete = () => {
2486
+ onDelete();
2487
+ onClose();
2488
+ };
2489
+ if (typeof document === "undefined") return null;
2490
+ const PillBtn = ({ id, label, icon, active, onClick }) => /* @__PURE__ */ jsx(
2491
+ "button",
2492
+ {
2493
+ type: "button",
2494
+ "data-section": id,
2495
+ "data-pill-btn": id,
2496
+ "aria-label": label,
2497
+ "aria-pressed": !!active,
2498
+ onClick,
2499
+ 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"}`,
2500
+ children: icon
2501
+ }
2502
+ );
2503
+ const pos = clamped ?? { left: anchor.x, top: anchor.y };
2504
+ const node = /* @__PURE__ */ jsxs(
2505
+ "div",
2506
+ {
2507
+ ref: rootRef,
2508
+ "data-stamp-area": "true",
2509
+ "data-testid": "multi-properties-popover",
2510
+ className: `${isDark ? "theme--dark " : ""}fixed z-[2147483600] flex flex-col gap-1.5`,
2511
+ style: { left: pos.left, top: pos.top },
2512
+ role: "dialog",
2513
+ "aria-label": `Thu\u1ED9c t\xEDnh (${count} \u0111\u1ED1i t\u01B0\u1EE3ng)`,
2514
+ children: [
2515
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 rounded-full border border-slate-300 bg-white px-1.5 py-1 shadow-lg ring-1 ring-black/5", children: [
2516
+ /* @__PURE__ */ jsxs("span", { className: "px-1 text-[11px] font-medium text-slate-500", children: [
2517
+ count,
2518
+ " \u0111\xE3 ch\u1ECDn"
2519
+ ] }),
2520
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "mx-0.5 h-5 w-px bg-slate-200" }),
2521
+ /* @__PURE__ */ jsx(PillBtn, { id: "color", label: "\u0110\u1ED5i m\xE0u", icon: Icons2.color, active: section === "color", onClick: () => toggleSection("color") }),
2522
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "mx-0.5 h-5 w-px bg-slate-200" }),
2523
+ /* @__PURE__ */ jsx(PillBtn, { id: "delete", label: "Xo\xE1 t\u1EA5t c\u1EA3", icon: Icons2.trash, onClick: doDelete })
2524
+ ] }),
2525
+ section === "color" && /* @__PURE__ */ jsx("div", { className: "w-[220px] rounded-lg border border-slate-300 bg-white p-3 shadow-2xl ring-1 ring-black/5", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
2526
+ /* @__PURE__ */ jsxs("span", { className: "text-[11px] font-medium text-slate-500", children: [
2527
+ "M\xE0u (\xE1p cho ",
2528
+ count,
2529
+ " \u0111\u1ED1i t\u01B0\u1EE3ng)"
2530
+ ] }),
2531
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: STROKE_PALETTE.map((c) => /* @__PURE__ */ jsx(
2532
+ "button",
2533
+ {
2534
+ "aria-label": `M\xE0u ${c}`,
2535
+ onClick: () => onColor(c),
2536
+ className: "h-6 w-6 rounded border border-slate-200",
2537
+ style: { background: c }
2538
+ },
2539
+ c
2540
+ )) })
2541
+ ] }) })
2542
+ ]
2543
+ }
2544
+ );
2545
+ return createPortal(node, document.body);
2546
+ };
2547
+ var LABELS = {
2548
+ rotate: { aria: "G\xF3c quay", label: "G\xF3c (\xB0)", step: 15 },
2549
+ dilate: { aria: "T\u1EF7 s\u1ED1 k", label: "T\u1EF7 s\u1ED1 k", step: 0.5 },
2550
+ regularPolygon: { aria: "S\u1ED1 c\u1EA1nh \u0111a gi\xE1c \u0111\u1EC1u", label: "S\u1ED1 c\u1EA1nh (n \u2265 3)", step: 1, min: 3 }
2551
+ };
2552
+ var TransformParamPopover = ({ kind, anchor, defaultValue, onConfirm, onCancel, isDark }) => {
2553
+ const [value, setValue] = useState(defaultValue);
2554
+ const inputRef = useRef(null);
2555
+ const meta = LABELS[kind];
2556
+ useEffect(() => {
2557
+ inputRef.current?.focus();
2558
+ inputRef.current?.select();
2559
+ }, []);
2560
+ const submit = () => {
2561
+ let v = Number.isFinite(value) ? value : defaultValue;
2562
+ if (kind === "regularPolygon") {
2563
+ v = Math.max(3, Math.round(v));
2564
+ }
2565
+ onConfirm(v);
2566
+ };
2567
+ if (typeof document === "undefined") return null;
2568
+ const node = /* @__PURE__ */ jsxs(
2569
+ "div",
2570
+ {
2571
+ "data-stamp-area": "true",
2572
+ 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`,
2573
+ style: { left: anchor.x, top: anchor.y, minWidth: 180 },
2574
+ role: "dialog",
2575
+ "aria-label": meta.aria,
2576
+ children: [
2577
+ /* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-slate-700", children: meta.label }),
2578
+ /* @__PURE__ */ jsx(
2579
+ "input",
2580
+ {
2581
+ ref: inputRef,
2582
+ type: "number",
2583
+ value,
2584
+ step: meta.step,
2585
+ min: meta.min,
2586
+ onChange: (e) => setValue(parseFloat(e.target.value)),
2587
+ onKeyDown: (e) => {
2588
+ if (e.key === "Enter") {
2589
+ e.preventDefault();
2590
+ submit();
2591
+ } else if (e.key === "Escape") {
2592
+ e.preventDefault();
2593
+ onCancel();
2594
+ }
2595
+ },
2596
+ className: "rounded border border-slate-300 bg-white px-2 py-1 text-sm text-slate-800"
2597
+ }
2598
+ ),
2599
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
2600
+ /* @__PURE__ */ jsx(
2601
+ "button",
2602
+ {
2603
+ onClick: onCancel,
2604
+ className: "rounded border border-slate-300 bg-white px-2 py-1 text-xs text-slate-700 hover:bg-slate-100",
2605
+ children: "Hu\u1EF7"
2606
+ }
2607
+ ),
2608
+ /* @__PURE__ */ jsx(
2609
+ "button",
2610
+ {
2611
+ onClick: submit,
2612
+ className: "rounded bg-emerald-600 px-2 py-1 text-xs font-medium text-white hover:bg-emerald-700",
2613
+ children: "\xC1p d\u1EE5ng"
2614
+ }
2615
+ )
2616
+ ] })
2617
+ ]
2618
+ }
2619
+ );
2620
+ return createPortal(node, document.body);
2621
+ };
2622
+ function useAiFigure(generator) {
2623
+ const [prompt, setPrompt] = useState("");
2624
+ const [isLoading, setIsLoading] = useState(false);
2625
+ const [error, setError] = useState(null);
2626
+ const abortRef = useRef(null);
2627
+ const requestIdRef = useRef(0);
2628
+ useEffect(() => () => abortRef.current?.abort(), []);
2629
+ const submit = useCallback(async () => {
2630
+ const problem = prompt.trim();
2631
+ if (!problem) {
2632
+ setError("Nh\u1EADp \u0111\u1EC1 b\xE0i c\u1EA7n d\u1EF1ng h\xECnh.");
2633
+ return null;
2634
+ }
2635
+ if (!generator) {
2636
+ setError("T\xEDnh n\u0103ng d\u1EF1ng h\xECnh AI ch\u01B0a \u0111\u01B0\u1EE3c c\u1EA5u h\xECnh.");
2637
+ return null;
2638
+ }
2639
+ abortRef.current?.abort();
2640
+ const controller = new AbortController();
2641
+ const requestId = ++requestIdRef.current;
2642
+ abortRef.current = controller;
2643
+ setIsLoading(true);
2644
+ setError(null);
2645
+ try {
2646
+ const generated = await generator(problem, { signal: controller.signal });
2647
+ if (controller.signal.aborted || requestId !== requestIdRef.current) return null;
2648
+ if (!generated.ok) {
2649
+ setError(generated.message);
2650
+ return null;
2651
+ }
2652
+ return generated.state;
2653
+ } catch (caught) {
2654
+ if (controller.signal.aborted || caught instanceof DOMException && caught.name === "AbortError") {
2655
+ return null;
2656
+ }
2657
+ if (requestId === requestIdRef.current) {
2658
+ setError(caught instanceof Error && caught.message ? caught.message : "Kh\xF4ng th\u1EC3 d\u1EF1ng h\xECnh b\u1EB1ng AI.");
2659
+ }
2660
+ return null;
2661
+ } finally {
2662
+ if (requestId === requestIdRef.current) {
2663
+ abortRef.current = null;
2664
+ setIsLoading(false);
2665
+ }
2666
+ }
2667
+ }, [generator, prompt]);
2668
+ return { prompt, setPrompt, isLoading, error, submit };
2669
+ }
2670
+ function AiFigurePrompt({ generator, onGenerated }) {
2671
+ const {
2672
+ prompt,
2673
+ setPrompt,
2674
+ isLoading,
2675
+ error,
2676
+ submit
2677
+ } = useAiFigure(generator);
2678
+ const handleSubmit = useCallback(async (event) => {
2679
+ event.preventDefault();
2680
+ const generated = await submit();
2681
+ if (generated) onGenerated(generated);
2682
+ }, [onGenerated, submit]);
2683
+ return /* @__PURE__ */ jsxs(
2684
+ "form",
2685
+ {
2686
+ "data-testid": "geometry-ai-form",
2687
+ onSubmit: (event) => {
2688
+ void handleSubmit(event);
2689
+ },
2690
+ className: "border-b border-slate-200 bg-slate-50 px-3 py-2",
2691
+ children: [
2692
+ /* @__PURE__ */ jsx("label", { htmlFor: "geometry-ai-prompt", className: "mb-1 block text-xs font-medium text-slate-600", children: "D\u1EF1ng h\xECnh b\u1EB1ng AI" }),
2693
+ /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-2", children: [
2694
+ /* @__PURE__ */ jsx(
2695
+ "textarea",
2696
+ {
2697
+ id: "geometry-ai-prompt",
2698
+ "aria-label": "\u0110\u1EC1 b\xE0i cho AI",
2699
+ value: prompt,
2700
+ onChange: (event) => setPrompt(event.target.value),
2701
+ disabled: isLoading,
2702
+ rows: 2,
2703
+ placeholder: "V\xED d\u1EE5: Cho tam gi\xE1c ABC, d\u1EF1ng \u0111\u01B0\u1EDDng cao AH.",
2704
+ className: "min-h-12 flex-1 resize-none rounded border border-slate-300 bg-white px-2 py-1.5 text-xs text-slate-800 outline-none focus:border-emerald-500 disabled:opacity-60"
2705
+ }
2706
+ ),
2707
+ /* @__PURE__ */ jsx(
2708
+ "button",
2709
+ {
2710
+ type: "submit",
2711
+ disabled: isLoading || !prompt.trim(),
2712
+ className: "rounded bg-emerald-600 px-3 py-2 text-xs font-medium text-white transition hover:bg-emerald-700 disabled:opacity-50",
2713
+ children: isLoading ? "\u0110ang d\u1EF1ng..." : "D\u1EF1ng b\u1EB1ng AI"
2714
+ }
2715
+ )
2716
+ ] }),
2717
+ error && /* @__PURE__ */ jsx("p", { role: "alert", className: "mt-1 text-xs text-red-600", children: error })
2718
+ ]
2719
+ }
2720
+ );
2721
+ }
2722
+ var GeometryEditorPanelInner = forwardRef(
2723
+ function GeometryEditorPanelInner2({
2724
+ store,
2725
+ onInsert,
2726
+ onClose,
2727
+ withLeftPanel = false,
2728
+ selectedTool,
2729
+ showAxis,
2730
+ showGrid,
2731
+ onHistoryChange,
2732
+ isDark,
2733
+ isMobile = false,
2734
+ onOpenDrawer,
2735
+ onUndo,
2736
+ onRedo,
2737
+ canUndo,
2738
+ canRedo,
2739
+ onSelectionChange,
2740
+ generateGeometryFigure
2741
+ }, ref) {
2742
+ const { showToast } = useToast();
2743
+ const handleRef = useRef(null);
2744
+ const [ready, setReady] = useState(false);
2745
+ const [hasContent, setHasContent] = useState(false);
2746
+ const [propsPopover, setPropsPopover] = useState(null);
2747
+ const [multiSelection, setMultiSelection] = useState(null);
2748
+ const [transformPopover, setTransformPopover] = useState(null);
2749
+ const onSelectionChangeRef = useRef(onSelectionChange);
2750
+ useEffect(() => {
2751
+ onSelectionChangeRef.current = onSelectionChange;
2752
+ }, [onSelectionChange]);
2753
+ useEditorState({ store, onHistoryChange });
2754
+ useEffect(() => {
2755
+ const sync = () => setHasContent(Object.keys(store.getState().objects).length > 0);
2756
+ sync();
2757
+ return store.subscribe(sync);
2758
+ }, [store]);
2759
+ const handleReady = useCallback(() => {
2760
+ const h = handleRef.current;
2761
+ if (!h) return;
2762
+ setReady(true);
2763
+ h.onSelect((snap) => {
2764
+ setPropsPopover(snap);
2765
+ setMultiSelection(null);
2766
+ onSelectionChangeRef.current?.(snap.id);
2767
+ });
2768
+ h.onTransformParam((info) => setTransformPopover(info));
2769
+ h.onSelectionState((snap) => {
2770
+ if (!snap || snap.ids.length === 0) {
2771
+ setPropsPopover(null);
2772
+ setMultiSelection(null);
2773
+ onSelectionChangeRef.current?.(void 0);
2774
+ return;
2775
+ }
2776
+ if (snap.ids.length === 1) {
2777
+ const id = snap.ids[0];
2778
+ const single = buildObjectSnapshot(store.getState(), id, snap.anchor);
2779
+ if (single) {
2780
+ setPropsPopover(single);
2781
+ setMultiSelection(null);
2782
+ onSelectionChangeRef.current?.(id);
2783
+ }
2784
+ return;
2785
+ }
2786
+ setMultiSelection(snap);
2787
+ setPropsPopover(null);
2788
+ onSelectionChangeRef.current?.(void 0);
2789
+ });
2790
+ }, [store]);
2791
+ const dismissPropsPopover = useCallback(() => {
2792
+ setPropsPopover(null);
2793
+ onSelectionChangeRef.current?.(void 0);
2794
+ }, []);
2795
+ const dismissMultiPopover = useCallback(() => {
2796
+ setMultiSelection(null);
2797
+ handleRef.current?.clearSelection();
2798
+ onSelectionChangeRef.current?.(void 0);
2799
+ }, []);
2800
+ const applyMultiColor = useCallback((color) => {
2801
+ const ids = multiSelection?.ids ?? [];
2802
+ const h = handleRef.current;
2803
+ if (!h) return;
2804
+ for (const id of ids) {
2805
+ h.mutateObject(id, { attrs: { strokeColor: color, color } });
2806
+ }
2807
+ }, [multiSelection]);
2808
+ const applyMultiDelete = useCallback(() => {
2809
+ const ids = multiSelection?.ids ?? [];
2810
+ const h = handleRef.current;
2811
+ if (!h) return;
2812
+ for (const id of ids) {
2813
+ h.mutateObject(id, { remove: true });
2814
+ }
2815
+ h.clearSelection();
2816
+ setMultiSelection(null);
2817
+ onSelectionChangeRef.current?.(void 0);
2818
+ }, [multiSelection]);
2819
+ const performInsert = useCallback(() => {
2820
+ if (!handleRef.current) return false;
2821
+ const h = handleRef.current;
2822
+ const state = h.getState();
2823
+ if (Object.keys(state.objects).length === 0) return false;
2824
+ const bbox = h.getBbox();
2825
+ const jsonState = serializeBoard(state, { bbox, showAxis, showGrid });
2826
+ void (async () => {
2827
+ try {
2828
+ const svgString = await renderGeometrySvgFromState(jsonState);
2829
+ onInsert(jsonState, svgString);
2830
+ } catch (err) {
2831
+ console.error("Geometry insert failed:", err);
2832
+ }
2833
+ })();
2834
+ return true;
2835
+ }, [onInsert, showAxis, showGrid]);
2836
+ const handleInsert = useCallback(() => {
2837
+ performInsert();
2838
+ }, [performInsert]);
2839
+ const loadAiFigure = useCallback((generated) => {
2840
+ handleRef.current?.clearSelection();
2841
+ setPropsPopover(null);
2842
+ setMultiSelection(null);
2843
+ setTransformPopover(null);
2844
+ onSelectionChangeRef.current?.(void 0);
2845
+ const current = store.getState();
2846
+ store.dispatch({
2847
+ type: "LOAD",
2848
+ payload: { state: { ...generated, meta: current.meta } }
2849
+ });
2850
+ }, [store]);
2851
+ useImperativeHandle(ref, () => ({
2852
+ insert: performInsert,
2853
+ hasContent: () => Object.keys(handleRef.current?.getState().objects ?? {}).length > 0,
2854
+ selectObject: (id) => handleRef.current?.highlight(id)
2855
+ }), [performInsert]);
2856
+ const wrapperStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 40 } : {
2857
+ position: "absolute",
2858
+ top: "50%",
2859
+ left: withLeftPanel ? "calc(50% + 120px)" : "50%",
2860
+ transform: "translate(-50%, -50%)",
2861
+ zIndex: 40
2862
+ };
2863
+ return /* @__PURE__ */ jsxs(
2864
+ "div",
2865
+ {
2866
+ role: "dialog",
2867
+ "aria-label": "D\u1EF1ng h\xECnh h\u1ECDc",
2868
+ "data-testid": "geometry-editor-panel",
2869
+ "data-stamp-area": "true",
2870
+ "data-mobile-editor": isMobile ? "true" : void 0,
2871
+ style: wrapperStyle,
2872
+ className: [
2873
+ isDark ? "theme--dark " : "",
2874
+ "relative flex flex-col overflow-hidden bg-white",
2875
+ isMobile ? "h-full w-full" : `${STAMP_PANEL_DESKTOP} rounded-lg border border-slate-300 shadow-2xl ring-1 ring-black/5`
2876
+ ].join(" "),
2877
+ children: [
2878
+ /* @__PURE__ */ jsxs("header", { className: "flex items-center gap-2 border-b border-slate-200 bg-gradient-to-r from-emerald-600 to-teal-600 px-3 py-2 text-white", children: [
2879
+ isMobile && /* @__PURE__ */ jsx(
2880
+ "button",
2881
+ {
2882
+ type: "button",
2883
+ onClick: onOpenDrawer,
2884
+ "aria-label": "M\u1EDF ng\u0103n c\xF4ng c\u1EE5",
2885
+ className: "-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15",
2886
+ children: /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2887
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
2888
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
2889
+ /* @__PURE__ */ jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
2890
+ ] })
2891
+ }
2892
+ ),
2893
+ /* @__PURE__ */ jsxs("h3", { className: "flex flex-1 items-center gap-2 text-sm font-semibold", children: [
2894
+ /* @__PURE__ */ jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2895
+ /* @__PURE__ */ jsx("polygon", { points: "3,18 12,3 21,18" }),
2896
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "3", r: "1.5", fill: "currentColor" }),
2897
+ /* @__PURE__ */ jsx("circle", { cx: "3", cy: "18", r: "1.5", fill: "currentColor" }),
2898
+ /* @__PURE__ */ jsx("circle", { cx: "21", cy: "18", r: "1.5", fill: "currentColor" })
2899
+ ] }),
2900
+ "D\u1EF1ng h\xECnh h\u1ECDc"
2901
+ ] }),
2902
+ isMobile && /* @__PURE__ */ jsxs(Fragment, { children: [
2903
+ /* @__PURE__ */ jsx(
2904
+ "button",
2905
+ {
2906
+ type: "button",
2907
+ onClick: onUndo,
2908
+ disabled: !canUndo,
2909
+ "aria-label": "Ho\xE0n t\xE1c",
2910
+ title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
2911
+ "data-testid": "undo-btn-mobile",
2912
+ className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15 disabled:opacity-40",
2913
+ children: /* @__PURE__ */ jsx(UndoIcon, {})
2914
+ }
2915
+ ),
2916
+ /* @__PURE__ */ jsx(
2917
+ "button",
2918
+ {
2919
+ type: "button",
2920
+ onClick: onRedo,
2921
+ disabled: !canRedo,
2922
+ "aria-label": "L\xE0m l\u1EA1i",
2923
+ title: "L\xE0m l\u1EA1i (Ctrl/Cmd+Shift+Z)",
2924
+ "data-testid": "redo-btn-mobile",
2925
+ className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15 disabled:opacity-40",
2926
+ children: /* @__PURE__ */ jsx(RedoIcon, {})
2927
+ }
2928
+ ),
2929
+ /* @__PURE__ */ jsx(
2930
+ "button",
2931
+ {
2932
+ type: "button",
2933
+ onClick: handleInsert,
2934
+ disabled: !ready || !hasContent,
2935
+ title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
2936
+ "data-testid": "geometry-insert-btn-mobile",
2937
+ className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
2938
+ children: "Ch\xE8n"
2939
+ }
2940
+ )
2941
+ ] }),
2942
+ /* @__PURE__ */ jsx("button", { onClick: onClose, "aria-label": "\u0110\xF3ng", className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15", children: /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2943
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
2944
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
2945
+ ] }) })
2946
+ ] }),
2947
+ generateGeometryFigure && /* @__PURE__ */ jsx(AiFigurePrompt, { generator: generateGeometryFigure, onGenerated: loadAiFigure }),
2948
+ /* @__PURE__ */ jsx("div", { className: "flex min-h-0 flex-1", children: /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(
2949
+ MiniBoard2D,
2950
+ {
2951
+ ref: handleRef,
2952
+ store,
2953
+ selectedTool,
2954
+ showAxis,
2955
+ showGrid,
2956
+ onReady: handleReady,
2957
+ isDark,
2958
+ toast: showToast
2959
+ }
2960
+ ) }) }),
2961
+ propsPopover && (propsPopover.kind === "point" ? /* @__PURE__ */ jsx(
2962
+ PropertiesPopover,
2963
+ {
2964
+ kind: "point",
2965
+ anchor: propsPopover.screenCoords,
2966
+ isDark,
2967
+ currentName: propsPopover.name,
2968
+ currentColor: propsPopover.color,
2969
+ currentDash: propsPopover.dash,
2970
+ currentWidth: propsPopover.width,
2971
+ currentFace: propsPopover.face,
2972
+ currentShowLabel: propsPopover.showLabel,
2973
+ getAllNames: () => handleRef.current?.getAllPointNames() ?? [],
2974
+ onClose: dismissPropsPopover,
2975
+ onMutate: (patch) => {
2976
+ handleRef.current?.mutateObject(propsPopover.id, patch);
2977
+ if (patch.remove) dismissPropsPopover();
2978
+ if (typeof patch.valueLabel === "boolean" || patch.attrs) {
2979
+ setPropsPopover((cur) => cur ? { ...cur, showValue: patch.valueLabel ?? cur.showValue } : cur);
2980
+ }
2981
+ }
2982
+ }
2983
+ ) : /* @__PURE__ */ jsx(
2984
+ PropertiesPopover,
2985
+ {
2986
+ kind: propsPopover.kind,
2987
+ anchor: propsPopover.screenCoords,
2988
+ isDark,
2989
+ currentName: propsPopover.name,
2990
+ currentColor: propsPopover.color,
2991
+ currentDash: propsPopover.dash,
2992
+ currentWidth: propsPopover.width,
2993
+ currentShowLabel: propsPopover.showLabel,
2994
+ currentShowValue: propsPopover.showValue,
2995
+ getAllNames: () => handleRef.current?.getAllPointNames() ?? [],
2996
+ onClose: dismissPropsPopover,
2997
+ onMutate: (patch) => {
2998
+ handleRef.current?.mutateObject(propsPopover.id, patch);
2999
+ if (patch.remove) dismissPropsPopover();
3000
+ if (typeof patch.valueLabel === "boolean") {
3001
+ setPropsPopover((cur) => cur ? { ...cur, showValue: patch.valueLabel ?? cur.showValue } : cur);
3002
+ }
3003
+ if (patch.attrs && "withLabel" in patch.attrs) {
3004
+ setPropsPopover((cur) => cur ? { ...cur, showLabel: !!patch.attrs?.withLabel } : cur);
3005
+ }
3006
+ }
3007
+ }
3008
+ )),
3009
+ multiSelection && multiSelection.ids.length > 1 && /* @__PURE__ */ jsx(
3010
+ MultiPropertiesPopover,
3011
+ {
3012
+ anchor: multiSelection.anchor,
3013
+ count: multiSelection.ids.length,
3014
+ isDark,
3015
+ onColor: applyMultiColor,
3016
+ onDelete: applyMultiDelete,
3017
+ onClose: dismissMultiPopover
3018
+ }
3019
+ ),
3020
+ transformPopover && (transformPopover.tool === "rotate" || transformPopover.tool === "dilate" || transformPopover.tool === "regularPolygon") && /* @__PURE__ */ jsx(
3021
+ TransformParamPopover,
3022
+ {
3023
+ kind: transformPopover.tool,
3024
+ anchor: transformPopover.anchor,
3025
+ defaultValue: transformPopover.tool === "rotate" ? 90 : transformPopover.tool === "dilate" ? 2 : 6,
3026
+ isDark,
3027
+ onConfirm: (v) => {
3028
+ handleRef.current?.confirmTransformParam(v);
3029
+ setTransformPopover(null);
3030
+ },
3031
+ onCancel: () => {
3032
+ handleRef.current?.cancelTransformParam();
3033
+ setTransformPopover(null);
3034
+ }
3035
+ }
3036
+ ),
3037
+ !isMobile && /* @__PURE__ */ jsxs("footer", { className: "flex items-center justify-between border-t border-slate-200 bg-slate-50 px-3 py-2", children: [
3038
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-slate-500", children: "Ch\u1ECDn c\xF4ng c\u1EE5 b\xEAn tr\xE1i, click tr\xEAn b\u1EA3ng \u0111\u1EC3 d\u1EF1ng h\xECnh." }),
3039
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
3040
+ /* @__PURE__ */ jsx(
3041
+ "button",
3042
+ {
3043
+ onClick: onClose,
3044
+ className: "rounded border border-slate-300 bg-white px-3 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100",
3045
+ children: "Hu\u1EF7"
3046
+ }
3047
+ ),
3048
+ /* @__PURE__ */ jsx(
3049
+ "button",
3050
+ {
3051
+ onClick: handleInsert,
3052
+ disabled: !ready || !hasContent,
3053
+ title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
3054
+ "data-testid": "geometry-insert-btn",
3055
+ className: "rounded bg-emerald-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-emerald-700 disabled:opacity-50",
3056
+ children: "Ch\xE8n"
3057
+ }
3058
+ )
3059
+ ] })
3060
+ ] }),
3061
+ /* @__PURE__ */ jsx(ToastHost, {})
3062
+ ]
3063
+ }
3064
+ );
3065
+ }
3066
+ );
3067
+ var GeometryEditorPanel = forwardRef(
3068
+ function GeometryEditorPanel2(props, ref) {
3069
+ return /* @__PURE__ */ jsx(ToastProvider, { children: /* @__PURE__ */ jsx(GeometryEditorPanelInner, { ...props, ref }) });
3070
+ }
3071
+ );
3072
+ function parseInitialState(data) {
3073
+ if (!isGeometryCustomData(data)) return null;
3074
+ return deserializeBoard(data.jsonState);
3075
+ }
3076
+ var GeometryStampHost = forwardRef(
3077
+ function GeometryStampHost2({ api, editingElement, onClose, isDark, generateGeometryFigure }, ref) {
3078
+ const panelRef = useRef(null);
3079
+ const { isMobile } = useIsMobile();
3080
+ const [drawerOpen, setDrawerOpen] = useState(false);
3081
+ const sceneStore = useStampStore("2d", editingElement, parseInitialState);
3082
+ const initialMeta = sceneStore.getState().meta;
3083
+ const initialView = initialMeta.domain === "2d" ? initialMeta.view : DEFAULT_VIEW_2D;
3084
+ const [selectedTool, setSelectedTool] = useState("move");
3085
+ const [showAxis, setShowAxis] = useState(initialView.showAxis);
3086
+ const [showGrid, setShowGrid] = useState(initialView.showGrid);
3087
+ const [canUndo, setCanUndo] = useState(false);
3088
+ const [canRedo, setCanRedo] = useState(false);
3089
+ const [selectedObjectId, setSelectedObjectId] = useState(void 0);
3090
+ const handleHistoryChange = useCallback((u, r) => {
3091
+ setCanUndo(u);
3092
+ setCanRedo(r);
3093
+ }, []);
3094
+ const handleUndo = useCallback(() => sceneStore.undo(), [sceneStore]);
3095
+ const handleRedo = useCallback(() => sceneStore.redo(), [sceneStore]);
3096
+ const { chordGroup } = useChordShortcut({
3097
+ groupOrder: GROUP_ORDER,
3098
+ tools: TOOLS,
3099
+ onSelect: (key) => setSelectedTool(key),
3100
+ enabled: !isMobile
3101
+ });
3102
+ const handleInsert = useCallback(
3103
+ async (jsonState, svgString) => {
3104
+ if (!api) return;
3105
+ try {
3106
+ await insertStampImage(api, {
3107
+ svgString,
3108
+ makeCustomData: () => ({
3109
+ kind: "geometry",
3110
+ version: 1,
3111
+ jsonState
3112
+ }),
3113
+ editingElementId: editingElement?.id ?? null
3114
+ });
3115
+ } catch (err) {
3116
+ console.error("Geometry insert failed:", err);
3117
+ }
3118
+ onClose();
3119
+ },
3120
+ [api, editingElement?.id, onClose]
3121
+ );
3122
+ useImperativeHandle(
3123
+ ref,
3124
+ () => ({
3125
+ tryInsert: () => panelRef.current?.insert() ?? false,
3126
+ hasContent: () => panelRef.current?.hasContent() ?? false
3127
+ }),
3128
+ []
3129
+ );
3130
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
3131
+ /* @__PURE__ */ jsx(
3132
+ StampLeftPanel,
3133
+ {
3134
+ title: "H\xECnh h\u1ECDc",
3135
+ icon: GeometryIconHeader,
3136
+ onClose,
3137
+ isDark,
3138
+ testId: "stamp-left-panel",
3139
+ tools: TOOLS,
3140
+ groupOrder: GROUP_ORDER,
3141
+ groupLabels: GROUP_LABELS,
3142
+ activeTool: selectedTool,
3143
+ onToolChange: setSelectedTool,
3144
+ view: {
3145
+ showAxis,
3146
+ showGrid,
3147
+ onShowAxisChange: setShowAxis,
3148
+ onShowGridChange: setShowGrid
3149
+ },
3150
+ history: {
3151
+ onUndo: handleUndo,
3152
+ canUndo,
3153
+ onRedo: handleRedo,
3154
+ canRedo
3155
+ },
3156
+ chord: { activeGroup: chordGroup, letterForGroup },
3157
+ objects: {
3158
+ store: sceneStore,
3159
+ selectedObjectId,
3160
+ onObjectSelect: (id) => {
3161
+ setSelectedObjectId(id ?? void 0);
3162
+ panelRef.current?.selectObject(id);
3163
+ }
3164
+ },
3165
+ isMobile,
3166
+ drawerOpen,
3167
+ onDrawerClose: () => setDrawerOpen(false)
3168
+ }
3169
+ ),
3170
+ /* @__PURE__ */ jsx(
3171
+ GeometryEditorPanel,
3172
+ {
3173
+ ref: panelRef,
3174
+ store: sceneStore,
3175
+ onInsert: handleInsert,
3176
+ onClose,
3177
+ selectedTool,
3178
+ showAxis,
3179
+ showGrid,
3180
+ onHistoryChange: handleHistoryChange,
3181
+ withLeftPanel: !isMobile,
3182
+ isDark,
3183
+ isMobile,
3184
+ onOpenDrawer: () => setDrawerOpen(true),
3185
+ onUndo: handleUndo,
3186
+ onRedo: handleRedo,
3187
+ canUndo,
3188
+ canRedo,
3189
+ onSelectionChange: setSelectedObjectId,
3190
+ generateGeometryFigure
3191
+ }
3192
+ )
3193
+ ] });
3194
+ }
3195
+ );
3196
+
3197
+ export { GeometryStampHost };
3198
+ //# sourceMappingURL=host-DOAYVL35.mjs.map
3199
+ //# sourceMappingURL=host-DOAYVL35.mjs.map