@xom11/whiteboard 0.11.0 → 0.24.1

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