@xom11/whiteboard 0.11.0 → 0.24.0

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