@xom11/whiteboard 0.11.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +67 -0
  2. package/dist/{ExcalidrawWithMenus-EAVPOPJZ.mjs → ExcalidrawWithMenus-KBLDWPM2.mjs} +2 -3
  3. package/dist/ExcalidrawWithMenus-KBLDWPM2.mjs.map +1 -0
  4. package/dist/catalog.json +57 -0
  5. package/dist/{chunk-PWIMZIB6.mjs → chunk-2SKXRBGS.mjs} +7 -8
  6. package/dist/chunk-2SKXRBGS.mjs.map +1 -0
  7. package/dist/chunk-33PEN2WC.mjs +57 -0
  8. package/dist/chunk-33PEN2WC.mjs.map +1 -0
  9. package/dist/chunk-3KBL77M6.mjs +127 -0
  10. package/dist/chunk-3KBL77M6.mjs.map +1 -0
  11. package/dist/chunk-5UTGXHLJ.mjs +57 -0
  12. package/dist/chunk-5UTGXHLJ.mjs.map +1 -0
  13. package/dist/chunk-6XUPIGVD.mjs +467 -0
  14. package/dist/chunk-6XUPIGVD.mjs.map +1 -0
  15. package/dist/chunk-7WG2KDRF.mjs +28 -0
  16. package/dist/chunk-7WG2KDRF.mjs.map +1 -0
  17. package/dist/chunk-FZY33J6Z.mjs +95 -0
  18. package/dist/chunk-FZY33J6Z.mjs.map +1 -0
  19. package/dist/chunk-HNQLZIEP.mjs +78 -0
  20. package/dist/chunk-HNQLZIEP.mjs.map +1 -0
  21. package/dist/chunk-NVJ7K3DK.mjs +29 -0
  22. package/dist/chunk-NVJ7K3DK.mjs.map +1 -0
  23. package/dist/chunk-O4WIZFRQ.mjs +11 -0
  24. package/dist/chunk-O4WIZFRQ.mjs.map +1 -0
  25. package/dist/{chunk-YVJP7NRG.mjs → chunk-O6QTYAKE.mjs} +7 -9
  26. package/dist/chunk-O6QTYAKE.mjs.map +1 -0
  27. package/dist/chunk-R5FL6S7L.mjs +22 -0
  28. package/dist/chunk-R5FL6S7L.mjs.map +1 -0
  29. package/dist/chunk-RBUILBX3.mjs +388 -0
  30. package/dist/chunk-RBUILBX3.mjs.map +1 -0
  31. package/dist/chunk-RD34F5PM.mjs +57 -0
  32. package/dist/chunk-RD34F5PM.mjs.map +1 -0
  33. package/dist/{chunk-7P7SQFOW.mjs → chunk-RXOFO64U.mjs} +3 -3
  34. package/dist/chunk-RXOFO64U.mjs.map +1 -0
  35. package/dist/chunk-TOOHCAWP.mjs +1167 -0
  36. package/dist/chunk-TOOHCAWP.mjs.map +1 -0
  37. package/dist/{chunk-C6SCVOMC.mjs → chunk-TQYQVXNW.mjs} +5 -41
  38. package/dist/chunk-TQYQVXNW.mjs.map +1 -0
  39. package/dist/chunk-VBJLUHCY.mjs +23 -0
  40. package/dist/chunk-VBJLUHCY.mjs.map +1 -0
  41. package/dist/chunk-VRWZILTG.mjs +205 -0
  42. package/dist/chunk-VRWZILTG.mjs.map +1 -0
  43. package/dist/chunk-XVSO7FBM.mjs +61 -0
  44. package/dist/chunk-XVSO7FBM.mjs.map +1 -0
  45. package/dist/geometry-2d.d.mts +3 -6
  46. package/dist/geometry-2d.d.ts +3 -6
  47. package/dist/geometry-2d.js +5069 -2651
  48. package/dist/geometry-2d.js.map +1 -1
  49. package/dist/geometry-2d.mjs +8 -4
  50. package/dist/geometry-3d.d.mts +4 -7
  51. package/dist/geometry-3d.d.ts +4 -7
  52. package/dist/geometry-3d.js +3053 -2150
  53. package/dist/geometry-3d.js.map +1 -1
  54. package/dist/geometry-3d.mjs +7 -4
  55. package/dist/graph-2d.d.mts +4 -7
  56. package/dist/graph-2d.d.ts +4 -7
  57. package/dist/graph-2d.js +3363 -1670
  58. package/dist/graph-2d.js.map +1 -1
  59. package/dist/graph-2d.mjs +10 -3
  60. package/dist/host-3N4E4KJH.mjs +1142 -0
  61. package/dist/host-3N4E4KJH.mjs.map +1 -0
  62. package/dist/{host-Z3TEJKZA.mjs → host-6SNSZ332.mjs} +4 -4
  63. package/dist/{host-Z3TEJKZA.mjs.map → host-6SNSZ332.mjs.map} +1 -1
  64. package/dist/host-EVJT3LIF.mjs +3198 -0
  65. package/dist/host-EVJT3LIF.mjs.map +1 -0
  66. package/dist/host-HN4X3TBC.mjs +2374 -0
  67. package/dist/host-HN4X3TBC.mjs.map +1 -0
  68. package/dist/index.css +4 -1
  69. package/dist/index.css.map +1 -1
  70. package/dist/index.d.mts +659 -19
  71. package/dist/index.d.ts +659 -19
  72. package/dist/index.js +11741 -9420
  73. package/dist/index.js.map +1 -1
  74. package/dist/index.mjs +1467 -336
  75. package/dist/index.mjs.map +1 -1
  76. package/dist/latex.d.mts +3 -4
  77. package/dist/latex.d.ts +3 -4
  78. package/dist/latex.js +33 -18
  79. package/dist/latex.js.map +1 -1
  80. package/dist/latex.mjs +2 -3
  81. package/dist/render-OCVGDKK6.mjs +8 -0
  82. package/dist/render-OCVGDKK6.mjs.map +1 -0
  83. package/dist/serialize-GKN6OVPM.mjs +6 -0
  84. package/dist/serialize-GKN6OVPM.mjs.map +1 -0
  85. package/dist/{types-CinstD7T.d.mts → types-rA4slL08.d.mts} +69 -4
  86. package/dist/{types-CinstD7T.d.ts → types-rA4slL08.d.ts} +69 -4
  87. package/package.json +24 -5
  88. package/dist/ExcalidrawWithMenus-EAVPOPJZ.mjs.map +0 -1
  89. package/dist/chunk-74VEEZBV.mjs +0 -619
  90. package/dist/chunk-74VEEZBV.mjs.map +0 -1
  91. package/dist/chunk-7P7SQFOW.mjs.map +0 -1
  92. package/dist/chunk-BJTO5JO5.mjs +0 -11
  93. package/dist/chunk-BJTO5JO5.mjs.map +0 -1
  94. package/dist/chunk-C6SCVOMC.mjs.map +0 -1
  95. package/dist/chunk-D257NCQW.mjs +0 -58
  96. package/dist/chunk-D257NCQW.mjs.map +0 -1
  97. package/dist/chunk-G7FR3AIV.mjs +0 -193
  98. package/dist/chunk-G7FR3AIV.mjs.map +0 -1
  99. package/dist/chunk-HTBLO5JO.mjs +0 -41
  100. package/dist/chunk-HTBLO5JO.mjs.map +0 -1
  101. package/dist/chunk-PWIMZIB6.mjs.map +0 -1
  102. package/dist/chunk-SBDMF4NQ.mjs +0 -212
  103. package/dist/chunk-SBDMF4NQ.mjs.map +0 -1
  104. package/dist/chunk-WQOABS6N.mjs +0 -197
  105. package/dist/chunk-WQOABS6N.mjs.map +0 -1
  106. package/dist/chunk-YVJP7NRG.mjs.map +0 -1
  107. package/dist/host-N6ACNJKI.mjs +0 -3226
  108. package/dist/host-N6ACNJKI.mjs.map +0 -1
  109. package/dist/host-NKGV6RF2.mjs +0 -1134
  110. package/dist/host-NKGV6RF2.mjs.map +0 -1
  111. package/dist/host-XVK7UCRE.mjs +0 -2908
  112. package/dist/host-XVK7UCRE.mjs.map +0 -1
@@ -1,3226 +0,0 @@
1
- "use client";
2
- import { VIEW3D_ATTRS, DEFAULT_VIEW3D, GROUND_PLANE_RANGE, GROUND_PLANE_ATTRS, paletteFor, serializeBoard3D, renderGeometry3DSvgFromState, isGeometry3DCustomData, parseSerializedBoard3D } from './chunk-WQOABS6N.mjs';
3
- import { useChordShortcut, MobileToolDrawer } from './chunk-SBDMF4NQ.mjs';
4
- import './chunk-HTBLO5JO.mjs';
5
- import { useIsMobile } from './chunk-P2AOIF7S.mjs';
6
- import { insertStampImage } from './chunk-C6SCVOMC.mjs';
7
- import './chunk-BJTO5JO5.mjs';
8
- import * as React2 from 'react';
9
- import { forwardRef, useRef, useState, useCallback, useEffect, useMemo, useImperativeHandle } from 'react';
10
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
11
- import { createPortal } from 'react-dom';
12
-
13
- // src/stamps/geometry-3d/editor/tools/handlers/_ensurePoint.ts
14
- function hitToConstraint(hit) {
15
- switch (hit.kind) {
16
- case "onGround":
17
- return { kind: "onGround", x: hit.world[0], y: hit.world[1] };
18
- case "onAxis":
19
- return { kind: "onAxis", axis: hit.axis, t: hit.t };
20
- case "onPlane":
21
- return { kind: "onPlane", planeId: hit.planeId, u: hit.u, v: hit.v };
22
- case "onLine":
23
- return { kind: "onLine", lineId: hit.lineId, t: hit.t };
24
- case "onPolygon":
25
- return { kind: "onPolygon", polygonId: hit.polygonId, u: hit.u, v: hit.v };
26
- case "onSphere":
27
- return { kind: "onSphere", sphereId: hit.sphereId, theta: hit.theta, phi: hit.phi };
28
- default:
29
- return null;
30
- }
31
- }
32
- function ensurePoint(hit, scene) {
33
- if (hit.kind === "existingPoint") return hit.pointId;
34
- const c = hitToConstraint(hit);
35
- if (!c) return null;
36
- return scene.addPoint(c);
37
- }
38
-
39
- // src/stamps/geometry-3d/editor/tools/handlers/point.ts
40
- function buildPoint(args, scene) {
41
- const hit = args[0]?.hit;
42
- if (!hit) return null;
43
- if (hit.kind === "existingPoint") return hit.pointId;
44
- const c = hitToConstraint(hit);
45
- if (!c) return null;
46
- return scene.addPoint(c);
47
- }
48
- var buildPointOnObject = buildPoint;
49
-
50
- // src/stamps/geometry-3d/editor/tools/handlers/segment.ts
51
- function buildSegment(args, scene) {
52
- if (args.length < 2 || !args[0].hit || !args[1].hit) return null;
53
- const p1 = ensurePoint(args[0].hit, scene);
54
- const p2 = ensurePoint(args[1].hit, scene);
55
- if (!p1 || !p2 || p1 === p2) return null;
56
- return scene.addObject("segment", { p1, p2 });
57
- }
58
- function buildLine(args, scene) {
59
- if (args.length < 2 || !args[0].hit || !args[1].hit) return null;
60
- const p1 = ensurePoint(args[0].hit, scene);
61
- const p2 = ensurePoint(args[1].hit, scene);
62
- if (!p1 || !p2 || p1 === p2) return null;
63
- return scene.addObject("line", { p1, p2 });
64
- }
65
- function buildRay(args, scene) {
66
- if (args.length < 2 || !args[0].hit || !args[1].hit) return null;
67
- const origin = ensurePoint(args[0].hit, scene);
68
- const through = ensurePoint(args[1].hit, scene);
69
- if (!origin || !through || origin === through) return null;
70
- return scene.addObject("ray", { origin, through });
71
- }
72
- function buildVector(args, scene) {
73
- if (args.length < 2 || !args[0].hit || !args[1].hit) return null;
74
- const from = ensurePoint(args[0].hit, scene);
75
- const to = ensurePoint(args[1].hit, scene);
76
- if (!from || !to || from === to) return null;
77
- return scene.addObject("vector", { from, to });
78
- }
79
-
80
- // src/stamps/geometry-3d/editor/tools/handlers/polygon.ts
81
- function buildPolygon(args, scene) {
82
- const vertexArgs = args.filter((a) => a.step.type === "point");
83
- const vertexIds = vertexArgs.map((a) => a.hit ? ensurePoint(a.hit, scene) : null).filter((x) => !!x);
84
- if (vertexIds.length < 3) return null;
85
- return scene.addObject("polygon", { vertices: vertexIds });
86
- }
87
-
88
- // src/stamps/geometry-3d/editor/scene/constraintMath.ts
89
- function sub(a, b) {
90
- return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
91
- }
92
- function add(a, b) {
93
- return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
94
- }
95
- function scale(a, k) {
96
- return [a[0] * k, a[1] * k, a[2] * k];
97
- }
98
- function dot(a, b) {
99
- return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
100
- }
101
- function cross(a, b) {
102
- return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
103
- }
104
- function norm(a) {
105
- return Math.sqrt(dot(a, a));
106
- }
107
- function normalize(a) {
108
- const n = norm(a);
109
- return n === 0 ? a : scale(a, 1 / n);
110
- }
111
- function getPointWorld(id, scene) {
112
- const obj = scene.get(id);
113
- if (!obj || obj.kind !== "point") {
114
- throw new Error(`constraintMath: point ${id} not found`);
115
- }
116
- return constraintToWorld(obj.constraint, scene);
117
- }
118
- function getPlaneBasis(planeObj, scene) {
119
- const p1 = getPointWorld(planeObj.p1, scene);
120
- const p2 = getPointWorld(planeObj.p2, scene);
121
- const p3 = getPointWorld(planeObj.p3, scene);
122
- const basis1 = sub(p2, p1);
123
- const tmp = sub(p3, p1);
124
- const normal = normalize(cross(basis1, tmp));
125
- const basis2 = cross(normal, basis1);
126
- return { origin: p1, basis1, basis2, normal };
127
- }
128
- function constraintToWorld(c, scene) {
129
- switch (c.kind) {
130
- case "free":
131
- return [c.x, c.y, c.z];
132
- case "onGround":
133
- return [c.x, c.y, 0];
134
- case "onAxis": {
135
- if (c.axis === "x") return [c.t, 0, 0];
136
- if (c.axis === "y") return [0, c.t, 0];
137
- return [0, 0, c.t];
138
- }
139
- case "onPlane": {
140
- const plane = scene.get(c.planeId);
141
- if (!plane || plane.kind !== "plane") throw new Error("onPlane: plane missing");
142
- const { origin, basis1, basis2 } = getPlaneBasis(plane, scene);
143
- return add(add(origin, scale(basis1, c.u)), scale(basis2, c.v));
144
- }
145
- case "onLine": {
146
- const line = scene.get(c.lineId);
147
- if (!line || line.kind !== "line" && line.kind !== "segment" && line.kind !== "ray") {
148
- throw new Error("onLine: parent missing");
149
- }
150
- const p1Id = line.kind === "ray" ? line.origin : line.p1;
151
- const p2Id = line.kind === "ray" ? line.through : line.p2;
152
- const p1 = getPointWorld(p1Id, scene);
153
- const p2 = getPointWorld(p2Id, scene);
154
- const dir = sub(p2, p1);
155
- return add(p1, scale(dir, c.t));
156
- }
157
- case "onPolygon": {
158
- const pg = scene.get(c.polygonId);
159
- if (!pg || pg.kind !== "polygon") throw new Error("onPolygon: parent missing");
160
- const v = pg.vertices;
161
- if (v.length < 3) throw new Error("onPolygon: < 3 vertices");
162
- const p1 = getPointWorld(v[0], scene);
163
- const p2 = getPointWorld(v[1], scene);
164
- const p3 = getPointWorld(v[2], scene);
165
- const basis1 = sub(p2, p1);
166
- const tmp = sub(p3, p1);
167
- const normal = normalize(cross(basis1, tmp));
168
- const basis2 = cross(normal, basis1);
169
- return add(add(p1, scale(basis1, c.u)), scale(basis2, c.v));
170
- }
171
- case "onSphere": {
172
- const sph = scene.get(c.sphereId);
173
- if (!sph || sph.kind !== "sphere") throw new Error("onSphere: parent missing");
174
- const center = getPointWorld(sph.center, scene);
175
- const surface = getPointWorld(sph.surfacePoint, scene);
176
- const radius = norm(sub(surface, center));
177
- const x = center[0] + radius * Math.sin(c.phi) * Math.cos(c.theta);
178
- const y = center[1] + radius * Math.sin(c.phi) * Math.sin(c.theta);
179
- const z = center[2] + radius * Math.cos(c.phi);
180
- return [x, y, z];
181
- }
182
- }
183
- }
184
-
185
- // src/stamps/geometry-3d/editor/scene/geometryChecks.ts
186
- var EPS = 1e-6;
187
- function getWorld(id, scene) {
188
- const obj = scene.get(id);
189
- if (!obj || obj.kind !== "point") return null;
190
- return constraintToWorld(obj.constraint, scene);
191
- }
192
- function areCollinear3(p1Id, p2Id, p3Id, scene) {
193
- const p1 = getWorld(p1Id, scene);
194
- const p2 = getWorld(p2Id, scene);
195
- const p3 = getWorld(p3Id, scene);
196
- if (!p1 || !p2 || !p3) return true;
197
- const a = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];
198
- const b = [p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2]];
199
- const c = [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
200
- return Math.hypot(c[0], c[1], c[2]) < EPS;
201
- }
202
- function apexCoplanarWithBase(baseIds, apexId, scene) {
203
- if (baseIds.length < 3) return false;
204
- const p1 = getWorld(baseIds[0], scene);
205
- const p2 = getWorld(baseIds[1], scene);
206
- const p3 = getWorld(baseIds[2], scene);
207
- const apex = getWorld(apexId, scene);
208
- if (!p1 || !p2 || !p3 || !apex) return false;
209
- const a = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];
210
- const b = [p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2]];
211
- const n = [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
212
- const d = [apex[0] - p1[0], apex[1] - p1[1], apex[2] - p1[2]];
213
- const dotND = n[0] * d[0] + n[1] * d[1] + n[2] * d[2];
214
- return Math.abs(dotND) < EPS;
215
- }
216
-
217
- // src/stamps/geometry-3d/editor/tools/handlers/plane.ts
218
- function buildPlane(args, scene) {
219
- if (args.length < 3 || !args[0].hit || !args[1].hit || !args[2].hit) return null;
220
- const p1 = ensurePoint(args[0].hit, scene);
221
- const p2 = ensurePoint(args[1].hit, scene);
222
- const p3 = ensurePoint(args[2].hit, scene);
223
- if (!p1 || !p2 || !p3) return null;
224
- if (p1 === p2 || p2 === p3 || p1 === p3) return null;
225
- if (areCollinear3(p1, p2, p3, scene)) return null;
226
- return scene.addObject("plane", { p1, p2, p3 });
227
- }
228
-
229
- // src/stamps/geometry-3d/editor/tools/handlers/pyramid.ts
230
- function buildPyramid(args, scene) {
231
- const pointArgs = args.filter((a) => a.step.type === "point");
232
- const baseArgs = pointArgs.slice(0, -1);
233
- const apexArg = pointArgs.slice(-1)[0];
234
- if (baseArgs.length < 3 || !apexArg?.hit) return null;
235
- const baseIds = baseArgs.map((a) => a.hit ? ensurePoint(a.hit, scene) : null).filter((x) => !!x);
236
- const apexId = ensurePoint(apexArg.hit, scene);
237
- if (!apexId || baseIds.length < 3) return null;
238
- if (apexCoplanarWithBase(baseIds, apexId, scene)) return null;
239
- const vertices = [...baseIds, apexId];
240
- const apexIdx = vertices.length - 1;
241
- const faces = [baseIds.map((_, i) => i)];
242
- for (let i = 0; i < baseIds.length; i++) {
243
- faces.push([i, (i + 1) % baseIds.length, apexIdx]);
244
- }
245
- return scene.addObject("polyhedron", { flavor: "pyramid", vertices, faces });
246
- }
247
-
248
- // src/stamps/geometry-3d/editor/tools/handlers/prism.ts
249
- function buildPrism(args, scene) {
250
- const baseArgs = args.filter((a) => a.step.type === "point");
251
- const numberArg = args.find((a) => a.step.type === "number");
252
- if (baseArgs.length < 3 || !numberArg || typeof numberArg.value !== "number") return null;
253
- const height = numberArg.value;
254
- if (height <= 0) return null;
255
- const baseIds = baseArgs.map((a) => a.hit ? ensurePoint(a.hit, scene) : null).filter((x) => !!x);
256
- if (baseIds.length < 3) return null;
257
- const topIds = [];
258
- for (const id of baseIds) {
259
- const p = scene.get(id);
260
- if (!p || p.kind !== "point") return null;
261
- const w = constraintToWorld(p.constraint, scene);
262
- topIds.push(scene.addPoint({ kind: "free", x: w[0], y: w[1], z: w[2] + height }));
263
- }
264
- const n = baseIds.length;
265
- const vertices = [...baseIds, ...topIds];
266
- const faces = [
267
- baseIds.map((_, i) => i),
268
- topIds.map((_, i) => n + i)
269
- ];
270
- for (let i = 0; i < n; i++) {
271
- faces.push([i, (i + 1) % n, n + (i + 1) % n, n + i]);
272
- }
273
- return scene.addObject("polyhedron", { flavor: "prism", vertices, faces });
274
- }
275
-
276
- // src/stamps/geometry-3d/editor/tools/handlers/tetrahedron.ts
277
- function buildTetrahedron(args, scene) {
278
- if (args.length < 2 || !args[0].hit || !args[1].hit) return null;
279
- const p1Id = ensurePoint(args[0].hit, scene);
280
- const p2Id = ensurePoint(args[1].hit, scene);
281
- if (!p1Id || !p2Id || p1Id === p2Id) return null;
282
- const p1Obj = scene.get(p1Id);
283
- const p2Obj = scene.get(p2Id);
284
- if (!p1Obj || p1Obj.kind !== "point" || !p2Obj || p2Obj.kind !== "point") return null;
285
- const p1 = constraintToWorld(p1Obj.constraint, scene);
286
- const p2 = constraintToWorld(p2Obj.constraint, scene);
287
- const z0 = Math.min(p1[2], p2[2]);
288
- const baseA = [p1[0], p1[1], z0];
289
- const baseB = [p2[0], p2[1], z0];
290
- const dx = baseB[0] - baseA[0];
291
- const dy = baseB[1] - baseA[1];
292
- const edge = Math.hypot(dx, dy);
293
- if (edge < 1e-9) return null;
294
- const mid = [(baseA[0] + baseB[0]) / 2, (baseA[1] + baseB[1]) / 2, z0];
295
- const perpX = -dy;
296
- const perpY = dx;
297
- const perpLen = Math.hypot(perpX, perpY);
298
- const height = edge * Math.sqrt(3) / 2;
299
- const baseC = [mid[0] + perpX / perpLen * height, mid[1] + perpY / perpLen * height, z0];
300
- const centroid = [
301
- (baseA[0] + baseB[0] + baseC[0]) / 3,
302
- (baseA[1] + baseB[1] + baseC[1]) / 3,
303
- z0
304
- ];
305
- const apexHeight = edge * Math.sqrt(2 / 3);
306
- const apex = [centroid[0], centroid[1], z0 + apexHeight];
307
- const cId = scene.addPoint({ kind: "free", x: baseC[0], y: baseC[1], z: baseC[2] });
308
- const apexId = scene.addPoint({ kind: "free", x: apex[0], y: apex[1], z: apex[2] });
309
- const vertices = [p1Id, p2Id, cId, apexId];
310
- const faces = [
311
- [0, 1, 2],
312
- // base
313
- [0, 1, 3],
314
- // face p1-p2-apex
315
- [1, 2, 3],
316
- // face p2-c-apex
317
- [2, 0, 3]
318
- // face c-p1-apex
319
- ];
320
- return scene.addObject("polyhedron", { flavor: "tetrahedron", vertices, faces });
321
- }
322
-
323
- // src/stamps/geometry-3d/editor/tools/handlers/cube.ts
324
- function buildCube(args, scene) {
325
- if (args.length < 2 || !args[0].hit || !args[1].hit) return null;
326
- const p1Id = ensurePoint(args[0].hit, scene);
327
- const p2Id = ensurePoint(args[1].hit, scene);
328
- if (!p1Id || !p2Id || p1Id === p2Id) return null;
329
- const p1Obj = scene.get(p1Id);
330
- const p2Obj = scene.get(p2Id);
331
- if (!p1Obj || p1Obj.kind !== "point" || !p2Obj || p2Obj.kind !== "point") return null;
332
- const p1 = constraintToWorld(p1Obj.constraint, scene);
333
- const p2 = constraintToWorld(p2Obj.constraint, scene);
334
- if (Math.abs(p1[2]) > 1e-6 || Math.abs(p2[2]) > 1e-6) return null;
335
- const dx = p2[0] - p1[0];
336
- const dy = p2[1] - p1[1];
337
- const edge = Math.hypot(dx, dy);
338
- if (edge < 1e-9) return null;
339
- const perpX = -dy;
340
- const perpY = dx;
341
- const p3 = [p2[0] + perpX, p2[1] + perpY, 0];
342
- const p4 = [p1[0] + perpX, p1[1] + perpY, 0];
343
- const t1 = [p1[0], p1[1], edge];
344
- const t2 = [p2[0], p2[1], edge];
345
- const t3 = [p3[0], p3[1], edge];
346
- const t4 = [p4[0], p4[1], edge];
347
- const p3Id = scene.addPoint({ kind: "onGround", x: p3[0], y: p3[1] });
348
- const p4Id = scene.addPoint({ kind: "onGround", x: p4[0], y: p4[1] });
349
- const t1Id = scene.addPoint({ kind: "free", x: t1[0], y: t1[1], z: t1[2] });
350
- const t2Id = scene.addPoint({ kind: "free", x: t2[0], y: t2[1], z: t2[2] });
351
- const t3Id = scene.addPoint({ kind: "free", x: t3[0], y: t3[1], z: t3[2] });
352
- const t4Id = scene.addPoint({ kind: "free", x: t4[0], y: t4[1], z: t4[2] });
353
- const vertices = [p1Id, p2Id, p3Id, p4Id, t1Id, t2Id, t3Id, t4Id];
354
- const faces = [
355
- [0, 1, 2, 3],
356
- // bottom
357
- [4, 5, 6, 7],
358
- // top
359
- [0, 1, 5, 4],
360
- // front
361
- [1, 2, 6, 5],
362
- // right
363
- [2, 3, 7, 6],
364
- // back
365
- [3, 0, 4, 7]
366
- // left
367
- ];
368
- return scene.addObject("polyhedron", { flavor: "cube", vertices, faces });
369
- }
370
-
371
- // src/stamps/geometry-3d/editor/tools/handlers/sphere.ts
372
- function buildSphere(args, scene) {
373
- if (args.length < 2 || !args[0].hit || !args[1].hit) return null;
374
- const center = ensurePoint(args[0].hit, scene);
375
- const surface = ensurePoint(args[1].hit, scene);
376
- if (!center || !surface || center === surface) return null;
377
- return scene.addObject("sphere", { center, surfacePoint: surface });
378
- }
379
-
380
- // src/stamps/geometry-3d/editor/tools/handlers/cylinder.ts
381
- function buildCylinder(args, scene) {
382
- const points = args.filter((a) => a.step.type === "point");
383
- const numberArg = args.find((a) => a.step.type === "number");
384
- if (points.length < 2 || !points[0].hit || !points[1].hit || !numberArg || typeof numberArg.value !== "number") return null;
385
- const radius = numberArg.value;
386
- if (radius <= 0) return null;
387
- const baseCenter = ensurePoint(points[0].hit, scene);
388
- const topCenter = ensurePoint(points[1].hit, scene);
389
- if (!baseCenter || !topCenter || baseCenter === topCenter) return null;
390
- return scene.addObject("cylinder", { baseCenter, topCenter, radius });
391
- }
392
-
393
- // src/stamps/geometry-3d/editor/tools/handlers/cone.ts
394
- function buildCone(args, scene) {
395
- const points = args.filter((a) => a.step.type === "point");
396
- const numberArg = args.find((a) => a.step.type === "number");
397
- if (points.length < 2 || !points[0].hit || !points[1].hit || !numberArg || typeof numberArg.value !== "number") return null;
398
- const radius = numberArg.value;
399
- if (radius <= 0) return null;
400
- const baseCenter = ensurePoint(points[0].hit, scene);
401
- const apex = ensurePoint(points[1].hit, scene);
402
- if (!baseCenter || !apex || baseCenter === apex) return null;
403
- return scene.addObject("cone", { baseCenter, apex, radius });
404
- }
405
-
406
- // src/stamps/geometry-3d/editor/tools/spec.ts
407
- var stubBuild = () => null;
408
- var ALL_SURFACES = ["ground", "axis", "plane", "line", "polygon", "sphere"];
409
- var OBJECT_ONLY = ["plane", "line", "polygon", "sphere"];
410
- var NO_SURFACE = ["ground", "axis", "plane"];
411
- var TOOLS = [
412
- {
413
- key: "move",
414
- label: "Di chuy\u1EC3n",
415
- hintIdle: "K\xE9o \u0111i\u1EC3m ho\u1EB7c xoay khung",
416
- steps: [],
417
- build: stubBuild
418
- },
419
- {
420
- key: "point",
421
- label: "\u0110i\u1EC3m",
422
- hintIdle: "Click tr\xEAn m\u1EB7t ph\u1EB3ng Oxy ho\u1EB7c tr\xEAn tr\u1EE5c \u0111\u1EC3 \u0111\u1EB7t \u0111i\u1EC3m",
423
- steps: [
424
- {
425
- type: "point",
426
- allowExisting: false,
427
- // GeoGebra-style: a new point must lie on the XY ground plane or on
428
- // one of the coordinate axes (Oz lets you place points off the plane).
429
- allowNewOn: ["ground", "axis"],
430
- hint: "Click tr\xEAn m\u1EB7t ph\u1EB3ng Oxy ho\u1EB7c tr\u1EE5c Ox/Oy/Oz"
431
- }
432
- ],
433
- build: buildPoint,
434
- repeatAfterBuild: true
435
- },
436
- {
437
- key: "pointOnObject",
438
- label: "\u0110i\u1EC3m tr\xEAn \u0111\u1ED1i t\u01B0\u1EE3ng",
439
- hintIdle: "Ch\u1ECDn m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng \u0111\u1EC3 \u0111\u1EB7t \u0111i\u1EC3m",
440
- steps: [{ type: "point", allowExisting: false, allowNewOn: OBJECT_ONLY, hint: "Click l\xEAn m\u1EB7t / \u0111\u01B0\u1EDDng \u0111\u1EC3 \u0111\u1EB7t \u0111i\u1EC3m" }],
441
- build: buildPointOnObject
442
- },
443
- {
444
- key: "segment",
445
- label: "\u0110o\u1EA1n th\u1EB3ng",
446
- hintIdle: "Ch\u1ECDn 2 \u0111i\u1EC3m \u0111\u1EC3 v\u1EBD \u0111o\u1EA1n th\u1EB3ng",
447
- steps: [
448
- { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m th\u1EE9 nh\u1EA5t" },
449
- { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m th\u1EE9 hai" }
450
- ],
451
- build: buildSegment
452
- },
453
- {
454
- key: "line",
455
- label: "\u0110\u01B0\u1EDDng th\u1EB3ng",
456
- hintIdle: "Ch\u1ECDn 2 \u0111i\u1EC3m \u0111\u1EC3 v\u1EBD \u0111\u01B0\u1EDDng th\u1EB3ng",
457
- steps: [
458
- { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m th\u1EE9 nh\u1EA5t" },
459
- { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m th\u1EE9 hai" }
460
- ],
461
- build: buildLine
462
- },
463
- {
464
- key: "ray",
465
- label: "Tia",
466
- hintIdle: "Ch\u1ECDn \u0111i\u1EC3m g\u1ED1c r\u1ED3i \u0111i\u1EC3m tr\xEAn tia",
467
- steps: [
468
- { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m g\u1ED1c" },
469
- { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m tr\xEAn tia" }
470
- ],
471
- build: buildRay
472
- },
473
- {
474
- key: "vector",
475
- label: "Vector",
476
- hintIdle: "Ch\u1ECDn 2 \u0111i\u1EC3m \u0111\u1EC3 v\u1EBD vector",
477
- steps: [
478
- { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m \u0111\u1EA7u" },
479
- { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m cu\u1ED1i" }
480
- ],
481
- build: buildVector
482
- },
483
- {
484
- key: "polygon",
485
- label: "\u0110a gi\xE1c",
486
- hintIdle: "Ch\u1ECDn c\xE1c \u0111\u1EC9nh; click \u0111i\u1EC3m \u0111\u1EA7u \u0111\u1EC3 \u0111\xF3ng",
487
- steps: [
488
- { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111\u1EC9nh th\u1EE9 1" },
489
- { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111\u1EC9nh th\u1EE9 2" },
490
- { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111\u1EC9nh th\u1EE9 3" },
491
- { type: "closingPoint", hint: "Click \u0111i\u1EC3m \u0111\u1EA7u \u0111\u1EC3 \u0111\xF3ng (ho\u1EB7c ch\u1ECDn th\xEAm \u0111\u1EC9nh)" }
492
- ],
493
- build: buildPolygon
494
- },
495
- {
496
- key: "plane",
497
- label: "M\u1EB7t ph\u1EB3ng (3 \u0111i\u1EC3m)",
498
- hintIdle: "Ch\u1ECDn 3 \u0111i\u1EC3m \u0111\u1EC3 x\xE1c \u0111\u1ECBnh m\u1EB7t ph\u1EB3ng",
499
- steps: [
500
- { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m th\u1EE9 1 c\u1EE7a m\u1EB7t ph\u1EB3ng" },
501
- { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m th\u1EE9 2" },
502
- { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m th\u1EE9 3" }
503
- ],
504
- build: buildPlane
505
- },
506
- {
507
- key: "pyramid",
508
- label: "H\xECnh ch\xF3p",
509
- hintIdle: "Ch\u1ECDn \u0111\xE1y \u0111a gi\xE1c r\u1ED3i ch\u1ECDn \u0111\u1EC9nh ch\xF3p",
510
- steps: [
511
- { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111\u1EC9nh \u0111\xE1y 1" },
512
- { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111\u1EC9nh \u0111\xE1y 2" },
513
- { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111\u1EC9nh \u0111\xE1y 3" },
514
- { type: "closingPoint", hint: "Click \u0111\u1EC9nh \u0111\xE1y \u0111\u1EA7u ti\xEAn \u0111\u1EC3 \u0111\xF3ng (ho\u1EB7c ch\u1ECDn th\xEAm \u0111\u1EC9nh)" },
515
- { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111\u1EC9nh ch\xF3p" }
516
- ],
517
- build: buildPyramid
518
- },
519
- {
520
- key: "prism",
521
- label: "L\u0103ng tr\u1EE5",
522
- hintIdle: "Ch\u1ECDn \u0111\xE1y \u0111a gi\xE1c r\u1ED3i nh\u1EADp chi\u1EC1u cao",
523
- steps: [
524
- { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111\u1EC9nh \u0111\xE1y 1" },
525
- { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111\u1EC9nh \u0111\xE1y 2" },
526
- { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111\u1EC9nh \u0111\xE1y 3" },
527
- { type: "closingPoint", hint: "Click \u0111\u1EC9nh \u0111\u1EA7u \u0111\u1EC3 \u0111\xF3ng \u0111\xE1y" },
528
- { type: "number", prompt: "Chi\u1EC1u cao (theo tr\u1EE5c z)", min: 1e-4 }
529
- ],
530
- build: buildPrism
531
- },
532
- {
533
- key: "tetrahedron",
534
- label: "T\u1EE9 di\u1EC7n \u0111\u1EC1u",
535
- hintIdle: "Ch\u1ECDn 2 \u0111i\u1EC3m x\xE1c \u0111\u1ECBnh c\u1EA1nh c\u1EE7a t\u1EE9 di\u1EC7n",
536
- steps: [
537
- { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111i\u1EC3m 1" },
538
- { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111i\u1EC3m 2" }
539
- ],
540
- build: buildTetrahedron
541
- },
542
- {
543
- key: "cube",
544
- label: "L\u1EADp ph\u01B0\u01A1ng",
545
- hintIdle: "Ch\u1ECDn 2 \u0111i\u1EC3m tr\xEAn n\u1EC1n x\xE1c \u0111\u1ECBnh c\u1EA1nh",
546
- steps: [
547
- { type: "point", allowExisting: true, allowNewOn: ["ground"], hint: "Ch\u1ECDn \u0111i\u1EC3m 1 (tr\xEAn n\u1EC1n)" },
548
- { type: "point", allowExisting: true, allowNewOn: ["ground"], hint: "Ch\u1ECDn \u0111i\u1EC3m 2 (tr\xEAn n\u1EC1n)" }
549
- ],
550
- build: buildCube
551
- },
552
- {
553
- key: "sphere",
554
- label: "M\u1EB7t c\u1EA7u",
555
- hintIdle: "Ch\u1ECDn t\xE2m r\u1ED3i \u0111i\u1EC3m tr\xEAn m\u1EB7t c\u1EA7u",
556
- steps: [
557
- { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn t\xE2m m\u1EB7t c\u1EA7u" },
558
- { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m tr\xEAn m\u1EB7t c\u1EA7u" }
559
- ],
560
- build: buildSphere
561
- },
562
- {
563
- key: "cylinder",
564
- label: "H\xECnh tr\u1EE5",
565
- hintIdle: "Ch\u1ECDn t\xE2m \u0111\xE1y, t\xE2m tr\xEAn, r\u1ED3i nh\u1EADp b\xE1n k\xEDnh",
566
- steps: [
567
- { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn t\xE2m \u0111\xE1y" },
568
- { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn t\xE2m tr\xEAn" },
569
- { type: "number", prompt: "B\xE1n k\xEDnh", min: 1e-4 }
570
- ],
571
- build: buildCylinder
572
- },
573
- {
574
- key: "cone",
575
- label: "H\xECnh n\xF3n",
576
- hintIdle: "Ch\u1ECDn t\xE2m \u0111\xE1y, \u0111\u1EC9nh, r\u1ED3i nh\u1EADp b\xE1n k\xEDnh",
577
- steps: [
578
- { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn t\xE2m \u0111\xE1y" },
579
- { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111\u1EC9nh" },
580
- { type: "number", prompt: "B\xE1n k\xEDnh", min: 1e-4 }
581
- ],
582
- build: buildCone
583
- }
584
- ];
585
-
586
- // src/stamps/geometry-3d/editor/tools/controller.ts
587
- function stepHint(step) {
588
- return step.type === "number" ? step.prompt : step.hint;
589
- }
590
- var ToolController = class {
591
- constructor(scene) {
592
- this.scene = scene;
593
- this.state = { tool: null, stepIndex: 0, collected: [], hint: "" };
594
- this.listeners = /* @__PURE__ */ new Set();
595
- this.selectTool("move");
596
- }
597
- getState() {
598
- return this.state;
599
- }
600
- on(cb) {
601
- this.listeners.add(cb);
602
- return () => {
603
- this.listeners.delete(cb);
604
- };
605
- }
606
- selectTool(key) {
607
- const tool = TOOLS.find((t) => t.key === key) ?? TOOLS.find((t) => t.key === "move");
608
- const firstStep = tool.steps[0];
609
- this.state = {
610
- tool,
611
- stepIndex: 0,
612
- collected: [],
613
- hint: firstStep ? stepHint(firstStep) : tool.hintIdle
614
- };
615
- this.notify();
616
- }
617
- cancel() {
618
- this.selectTool("move");
619
- }
620
- consumeHit(hit) {
621
- const tool = this.state.tool;
622
- if (!tool) return false;
623
- const step = tool.steps[this.state.stepIndex];
624
- if (!step) return false;
625
- if (step.type === "closingPoint") {
626
- if (hit.kind === "empty") return false;
627
- if (hit.kind === "existingPoint") {
628
- this.state.collected.push({ step, hit });
629
- this.state.stepIndex++;
630
- this.advance();
631
- return true;
632
- }
633
- const prevStep = tool.steps[this.state.stepIndex - 1];
634
- if (!prevStep || prevStep.type !== "point") return false;
635
- if (!this.hitMatchesStep(hit, prevStep)) return false;
636
- this.state.collected.push({ step: prevStep, hit });
637
- this.notify();
638
- return true;
639
- }
640
- if (!this.hitMatchesStep(hit, step)) return false;
641
- this.state.collected.push({ step, hit });
642
- this.state.stepIndex++;
643
- this.advance();
644
- return true;
645
- }
646
- consumeNumber(value) {
647
- const tool = this.state.tool;
648
- if (!tool) return false;
649
- const step = tool.steps[this.state.stepIndex];
650
- if (!step || step.type !== "number") return false;
651
- if (step.min != null && value < step.min) return false;
652
- if (step.max != null && value > step.max) return false;
653
- this.state.collected.push({ step, value });
654
- this.state.stepIndex++;
655
- this.advance();
656
- return true;
657
- }
658
- hitMatchesStep(hit, step) {
659
- if (step.type !== "point" && step.type !== "closingPoint") return false;
660
- if (hit.kind === "empty") return false;
661
- if (step.type === "closingPoint") return hit.kind === "existingPoint";
662
- if (hit.kind === "existingPoint") return step.allowExisting;
663
- const surfaceMap = {
664
- onGround: "ground",
665
- onAxis: "axis",
666
- onPlane: "plane",
667
- onLine: "line",
668
- onPolygon: "polygon",
669
- onSphere: "sphere"
670
- };
671
- const k = surfaceMap[hit.kind];
672
- return k != null && step.type === "point" && step.allowNewOn.includes(k);
673
- }
674
- advance() {
675
- const tool = this.state.tool;
676
- if (this.state.stepIndex >= tool.steps.length) {
677
- tool.build(this.state.collected, this.scene);
678
- if (tool.repeatAfterBuild) {
679
- this.state.stepIndex = 0;
680
- this.state.collected = [];
681
- this.state.hint = stepHint(tool.steps[0]);
682
- this.notify();
683
- } else {
684
- this.selectTool("move");
685
- }
686
- return;
687
- }
688
- this.state.hint = stepHint(tool.steps[this.state.stepIndex]);
689
- this.notify();
690
- }
691
- notify() {
692
- for (const cb of this.listeners) cb(this.state);
693
- }
694
- };
695
-
696
- // src/stamps/geometry-3d/editor/renderer/faceted.ts
697
- var CURVED_SEGMENTS = 16;
698
- function cylinderFaces(center, top, radius) {
699
- const baseRing = [];
700
- const topRing = [];
701
- for (let i = 0; i < CURVED_SEGMENTS; i++) {
702
- const theta = i / CURVED_SEGMENTS * Math.PI * 2;
703
- const dx = radius * Math.cos(theta);
704
- const dy = radius * Math.sin(theta);
705
- baseRing.push([center[0] + dx, center[1] + dy, center[2]]);
706
- topRing.push([top[0] + dx, top[1] + dy, top[2]]);
707
- }
708
- const vertices = [...baseRing, ...topRing];
709
- const faces = [];
710
- faces.push(baseRing.map((_, i) => i));
711
- faces.push(topRing.map((_, i) => CURVED_SEGMENTS + i));
712
- for (let i = 0; i < CURVED_SEGMENTS; i++) {
713
- const next = (i + 1) % CURVED_SEGMENTS;
714
- faces.push([i, next, CURVED_SEGMENTS + next, CURVED_SEGMENTS + i]);
715
- }
716
- return { vertices, faces };
717
- }
718
- function coneFaces(baseCenter, apex, radius) {
719
- const baseRing = [];
720
- for (let i = 0; i < CURVED_SEGMENTS; i++) {
721
- const theta = i / CURVED_SEGMENTS * Math.PI * 2;
722
- baseRing.push([
723
- baseCenter[0] + radius * Math.cos(theta),
724
- baseCenter[1] + radius * Math.sin(theta),
725
- baseCenter[2]
726
- ]);
727
- }
728
- const apexIdx = baseRing.length;
729
- const vertices = [...baseRing, apex];
730
- const faces = [baseRing.map((_, i) => i)];
731
- for (let i = 0; i < CURVED_SEGMENTS; i++) {
732
- faces.push([i, (i + 1) % CURVED_SEGMENTS, apexIdx]);
733
- }
734
- return { vertices, faces };
735
- }
736
-
737
- // src/stamps/geometry-3d/editor/renderer/JxgRenderer.ts
738
- var JxgRenderer = class {
739
- constructor(scene, view) {
740
- this.scene = scene;
741
- this.view = view;
742
- this.map = /* @__PURE__ */ new Map();
743
- this.unsubAdd = scene.on("add", (o) => this.handleAdd(o));
744
- this.unsubChange = scene.on("change", (o) => this.handleChange(o));
745
- this.unsubDelete = scene.on("delete", (id) => this.handleDelete(id));
746
- this.unsubReset = scene.on("reset", () => this.handleReset());
747
- for (const obj of scene.list()) this.handleAdd(obj);
748
- }
749
- dispose() {
750
- this.unsubAdd();
751
- this.unsubChange();
752
- this.unsubDelete();
753
- this.unsubReset();
754
- for (const [id, j] of this.map) {
755
- try {
756
- j.remove?.();
757
- } catch {
758
- }
759
- this.map.delete(id);
760
- }
761
- }
762
- handleAdd(obj) {
763
- if (this.map.has(obj.id)) return;
764
- if (obj.kind === "point") {
765
- const world = constraintToWorld(obj.constraint, this.scene);
766
- const attrs = { id: obj.id, name: obj.label, size: 4, visible: obj.visible, fixed: true };
767
- const jxg = this.view.create("point3d", world, attrs);
768
- this.map.set(obj.id, jxg);
769
- return;
770
- }
771
- if (obj.kind === "segment") {
772
- const a = this.map.get(obj.p1);
773
- const b = this.map.get(obj.p2);
774
- const attrs = {
775
- id: obj.id,
776
- straightFirst: false,
777
- straightLast: false,
778
- visible: obj.visible,
779
- strokeColor: obj.color ?? "#0066cc",
780
- strokeWidth: 2
781
- };
782
- this.map.set(obj.id, this.view.create("line3d", [a, b], attrs));
783
- return;
784
- }
785
- if (obj.kind === "line") {
786
- const attrs = {
787
- id: obj.id,
788
- visible: obj.visible,
789
- strokeColor: obj.color ?? "#0066cc",
790
- strokeWidth: 2
791
- };
792
- this.map.set(
793
- obj.id,
794
- this.view.create("line3d", [this.map.get(obj.p1), this.map.get(obj.p2)], attrs)
795
- );
796
- return;
797
- }
798
- if (obj.kind === "ray") {
799
- const attrs = { id: obj.id, straightFirst: false, visible: obj.visible };
800
- this.map.set(
801
- obj.id,
802
- this.view.create("line3d", [this.map.get(obj.origin), this.map.get(obj.through)], attrs)
803
- );
804
- return;
805
- }
806
- if (obj.kind === "vector") {
807
- const attrs = {
808
- id: obj.id,
809
- lastArrow: true,
810
- straightFirst: false,
811
- straightLast: false,
812
- visible: obj.visible
813
- };
814
- this.map.set(
815
- obj.id,
816
- this.view.create("line3d", [this.map.get(obj.from), this.map.get(obj.to)], attrs)
817
- );
818
- return;
819
- }
820
- if (obj.kind === "plane") {
821
- const attrs = { id: obj.id, fillOpacity: 0.2, visible: obj.visible };
822
- this.map.set(
823
- obj.id,
824
- this.view.create(
825
- "plane3d",
826
- [this.map.get(obj.p1), this.map.get(obj.p2), this.map.get(obj.p3)],
827
- attrs
828
- )
829
- );
830
- return;
831
- }
832
- if (obj.kind === "polygon") {
833
- const refs = obj.vertices.map((v) => this.map.get(v));
834
- const attrs = { id: obj.id, fillOpacity: 0.3, visible: obj.visible };
835
- this.map.set(obj.id, this.view.create("polygon3d", [refs], attrs));
836
- return;
837
- }
838
- if (obj.kind === "sphere") {
839
- const attrs = { id: obj.id, fillOpacity: 0.25, visible: obj.visible };
840
- this.map.set(
841
- obj.id,
842
- this.view.create("sphere3d", [this.map.get(obj.center), this.map.get(obj.surfacePoint)], attrs)
843
- );
844
- return;
845
- }
846
- if (obj.kind === "polyhedron") {
847
- const verts = obj.vertices.map((id) => this.map.get(id));
848
- const faceJxgs = obj.faces.map(
849
- (face) => this.view.create("polygon3d", [face.map((idx) => verts[idx])], {
850
- id: `${obj.id}.face${face.join("-")}`,
851
- fillOpacity: 0.25,
852
- strokeColor: "#0066cc",
853
- strokeWidth: 1.5,
854
- visible: obj.visible
855
- })
856
- );
857
- this.map.set(obj.id, {
858
- _faces: faceJxgs,
859
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
860
- remove: () => faceJxgs.forEach((f) => f.remove?.())
861
- });
862
- return;
863
- }
864
- if (obj.kind === "cylinder" || obj.kind === "cone") {
865
- const baseCenterPt = this.scene.get(obj.baseCenter);
866
- if (!baseCenterPt || baseCenterPt.kind !== "point") return;
867
- const base = constraintToWorld(baseCenterPt.constraint, this.scene);
868
- let secondPt;
869
- if (obj.kind === "cylinder") {
870
- const topCenterPt = this.scene.get(obj.topCenter);
871
- if (!topCenterPt || topCenterPt.kind !== "point") return;
872
- secondPt = constraintToWorld(topCenterPt.constraint, this.scene);
873
- } else {
874
- const apexPt = this.scene.get(obj.apex);
875
- if (!apexPt || apexPt.kind !== "point") return;
876
- secondPt = constraintToWorld(apexPt.constraint, this.scene);
877
- }
878
- const geom = obj.kind === "cylinder" ? cylinderFaces(base, secondPt, obj.radius) : coneFaces(base, secondPt, obj.radius);
879
- const vertJxgs = geom.vertices.map(
880
- (v, i) => this.view.create("point3d", v, {
881
- id: `${obj.id}.v${i}`,
882
- visible: false,
883
- fixed: true,
884
- withLabel: false
885
- })
886
- );
887
- const faceJxgs = geom.faces.map(
888
- (face) => this.view.create("polygon3d", [face.map((idx) => vertJxgs[idx])], {
889
- id: `${obj.id}.face${face.join("-")}`,
890
- fillOpacity: 0.25,
891
- strokeColor: "#0066cc",
892
- strokeWidth: 1.5,
893
- visible: obj.visible
894
- })
895
- );
896
- this.map.set(obj.id, {
897
- _verts: vertJxgs,
898
- _faces: faceJxgs,
899
- remove: () => {
900
- faceJxgs.forEach((f) => f.remove?.());
901
- vertJxgs.forEach((v) => v.remove?.());
902
- }
903
- });
904
- return;
905
- }
906
- }
907
- handleChange(obj) {
908
- const j = this.map.get(obj.id);
909
- if (!j) return;
910
- if (obj.kind === "point" && typeof j.moveTo === "function") {
911
- const w = constraintToWorld(obj.constraint, this.scene);
912
- try {
913
- j.moveTo([w[0], w[1], w[2]], 0);
914
- } catch {
915
- }
916
- }
917
- }
918
- handleDelete(id) {
919
- const j = this.map.get(id);
920
- if (!j) return;
921
- try {
922
- j.remove?.();
923
- } catch {
924
- }
925
- this.map.delete(id);
926
- }
927
- handleReset() {
928
- for (const [, j] of this.map) {
929
- try {
930
- j.remove?.();
931
- } catch {
932
- }
933
- }
934
- this.map.clear();
935
- }
936
- };
937
-
938
- // src/stamps/geometry-3d/editor/hitTest/rayCast.ts
939
- function screenToRay(screen, view) {
940
- const near = unproject(screen, view, 20);
941
- const far = unproject(screen, view, -20);
942
- const dir = [far[0] - near[0], far[1] - near[1], far[2] - near[2]];
943
- const n = Math.sqrt(dir[0] ** 2 + dir[1] ** 2 + dir[2] ** 2);
944
- const norm3 = n === 0 ? [0, 0, -1] : [dir[0] / n, dir[1] / n, dir[2] / n];
945
- return { origin: near, dir: norm3 };
946
- }
947
- function unproject(screen, view, depth) {
948
- if (typeof view.unprojectScreen === "function") {
949
- const v = view.unprojectScreen(screen.x, screen.y, depth);
950
- return [v[0], v[1], v[2]];
951
- }
952
- if (typeof view.project3DTo2D === "function") {
953
- const p0 = view.project3DTo2D(0, 0, 0);
954
- const px = view.project3DTo2D(1, 0, 0);
955
- const py = view.project3DTo2D(0, 1, 0);
956
- const pz = view.project3DTo2D(0, 0, 1);
957
- const ox = p0[1], oy = p0[2];
958
- const a = px[1] - ox, b = py[1] - ox, c = pz[1] - ox;
959
- const d = px[2] - oy, e = py[2] - oy, f = pz[2] - oy;
960
- const rhsX = screen.x - ox - c * depth;
961
- const rhsY = screen.y - oy - f * depth;
962
- const det = a * e - b * d;
963
- if (Math.abs(det) < 1e-9) return [0, 0, depth];
964
- const x = (e * rhsX - b * rhsY) / det;
965
- const y = (-d * rhsX + a * rhsY) / det;
966
- return [x, y, depth];
967
- }
968
- throw new Error("rayCast: view has neither unprojectScreen nor project3DTo2D");
969
- }
970
-
971
- // src/stamps/geometry-3d/editor/hitTest/intersect.ts
972
- var EPS2 = 1e-9;
973
- function dot2(a, b) {
974
- return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
975
- }
976
- function sub2(a, b) {
977
- return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
978
- }
979
- function add2(a, b) {
980
- return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
981
- }
982
- function scale2(a, k) {
983
- return [a[0] * k, a[1] * k, a[2] * k];
984
- }
985
- function norm2(a) {
986
- return dot2(a, a);
987
- }
988
- function rayPlane(ray, plane) {
989
- const denom = dot2(ray.dir, plane.normal);
990
- if (Math.abs(denom) < EPS2) return null;
991
- const t = dot2(sub2(plane.point, ray.origin), plane.normal) / denom;
992
- if (t < 0) return null;
993
- return { point: add2(ray.origin, scale2(ray.dir, t)), t };
994
- }
995
- function rayGround(ray) {
996
- return rayPlane(ray, { point: [0, 0, 0], normal: [0, 0, 1] });
997
- }
998
- function raySphere(ray, sphere) {
999
- const oc = sub2(ray.origin, sphere.center);
1000
- const b = dot2(oc, ray.dir);
1001
- const c = dot2(oc, oc) - sphere.radius * sphere.radius;
1002
- const disc = b * b - c;
1003
- if (disc < 0) return null;
1004
- const sqrtD = Math.sqrt(disc);
1005
- const t1 = -b - sqrtD;
1006
- const t2 = -b + sqrtD;
1007
- const t = t1 >= 0 ? t1 : t2;
1008
- if (t < 0) return null;
1009
- return { point: add2(ray.origin, scale2(ray.dir, t)), t };
1010
- }
1011
- function rayLineSegment(ray, seg, maxDistance) {
1012
- const u = ray.dir;
1013
- const v = sub2(seg.b, seg.a);
1014
- const w0 = sub2(ray.origin, seg.a);
1015
- const a = dot2(u, u);
1016
- const bb = dot2(u, v);
1017
- const cc = dot2(v, v);
1018
- const d = dot2(u, w0);
1019
- const e = dot2(v, w0);
1020
- const denom = a * cc - bb * bb;
1021
- if (Math.abs(denom) < EPS2) return null;
1022
- const sc = (bb * e - cc * d) / denom;
1023
- const tc = (a * e - bb * d) / denom;
1024
- if (sc < 0 || tc < 0 || tc > 1) return null;
1025
- const pRay = add2(ray.origin, scale2(u, sc));
1026
- const pSeg = add2(seg.a, scale2(v, tc));
1027
- const dist2 = norm2(sub2(pRay, pSeg));
1028
- if (dist2 > maxDistance * maxDistance) return null;
1029
- return { point: pSeg, t: sc, tOnSegment: tc };
1030
- }
1031
-
1032
- // src/stamps/geometry-3d/editor/hitTest/snapping.ts
1033
- function findSnapPoint(screen, view, scene, pixelRadius = 8) {
1034
- const board = view?.board;
1035
- const ux = typeof board?.unitX === "number" && board.unitX > 0 ? board.unitX : 1;
1036
- const uy = typeof board?.unitY === "number" && board.unitY > 0 ? board.unitY : ux;
1037
- const rxUser = pixelRadius / ux;
1038
- const ryUser = pixelRadius / uy;
1039
- let best = null;
1040
- for (const obj of scene.list()) {
1041
- if (obj.kind !== "point") continue;
1042
- if (!obj.visible) continue;
1043
- const world = constraintToWorld(obj.constraint, scene);
1044
- const proj = view.project3DTo2D?.(world[0], world[1], world[2]);
1045
- if (!proj) continue;
1046
- const dxN = (proj[1] - screen.x) / rxUser;
1047
- const dyN = (proj[2] - screen.y) / ryUser;
1048
- const d2 = dxN * dxN + dyN * dyN;
1049
- if (d2 <= 1 && (best === null || d2 < best.d2)) {
1050
- best = { id: obj.id, d2 };
1051
- }
1052
- }
1053
- return best?.id ?? null;
1054
- }
1055
-
1056
- // src/stamps/geometry-3d/editor/hitTest/hitTest.ts
1057
- var AXIS_PIXEL_THRESHOLD = 12;
1058
- function hitTest(screen, view, scene) {
1059
- const board = view?.board;
1060
- const ux = typeof board?.unitX === "number" && board.unitX > 0 ? board.unitX : 1;
1061
- const axisThresholdUser = AXIS_PIXEL_THRESHOLD / ux;
1062
- const snap = findSnapPoint(screen, view, scene);
1063
- if (snap) return { kind: "existingPoint", pointId: snap };
1064
- const ray = screenToRay(screen, view);
1065
- let bestSphere = null;
1066
- for (const obj of scene.list()) {
1067
- if (obj.kind !== "sphere" || !obj.visible) continue;
1068
- const centerPoint = scene.get(obj.center);
1069
- const surfacePoint = scene.get(obj.surfacePoint);
1070
- if (!centerPoint || centerPoint.kind !== "point") continue;
1071
- if (!surfacePoint || surfacePoint.kind !== "point") continue;
1072
- const center = constraintToWorld(centerPoint.constraint, scene);
1073
- const surface = constraintToWorld(surfacePoint.constraint, scene);
1074
- const radius = Math.hypot(
1075
- surface[0] - center[0],
1076
- surface[1] - center[1],
1077
- surface[2] - center[2]
1078
- );
1079
- const sh = raySphere(ray, { center, radius });
1080
- if (sh && (bestSphere === null || sh.t < bestSphere.t)) {
1081
- bestSphere = { id: obj.id, t: sh.t, world: sh.point };
1082
- }
1083
- }
1084
- if (view.project3DTo2D) {
1085
- const axes = [
1086
- { axis: "x", a: [-10, 0, 0], b: [10, 0, 0] },
1087
- { axis: "y", a: [0, -10, 0], b: [0, 10, 0] },
1088
- { axis: "z", a: [0, 0, -10], b: [0, 0, 10] }
1089
- ];
1090
- for (const ax of axes) {
1091
- const pa = view.project3DTo2D(ax.a[0], ax.a[1], ax.a[2]);
1092
- const pb = view.project3DTo2D(ax.b[0], ax.b[1], ax.b[2]);
1093
- const d = distScreenPointToSegment(screen, [pa[1], pa[2]], [pb[1], pb[2]]);
1094
- if (d <= axisThresholdUser) {
1095
- const hit = rayLineSegment(ray, { a: ax.a, b: ax.b }, 1e3);
1096
- if (hit) {
1097
- const t = ax.axis === "x" ? hit.point[0] : ax.axis === "y" ? hit.point[1] : hit.point[2];
1098
- return { kind: "onAxis", axis: ax.axis, t, world: hit.point };
1099
- }
1100
- }
1101
- }
1102
- }
1103
- let bestPlane = null;
1104
- for (const obj of scene.list()) {
1105
- if (obj.kind !== "plane" || !obj.visible) continue;
1106
- const basis = planeBasis(obj, scene);
1107
- if (!basis) continue;
1108
- const ph = rayPlane(ray, { point: basis.origin, normal: basis.normal });
1109
- if (ph && (bestPlane === null || ph.t < bestPlane.t)) {
1110
- bestPlane = { id: obj.id, t: ph.t, world: ph.point, basis };
1111
- }
1112
- }
1113
- if (bestPlane && (!bestSphere || bestPlane.t < bestSphere.t)) {
1114
- const rel = [
1115
- bestPlane.world[0] - bestPlane.basis.origin[0],
1116
- bestPlane.world[1] - bestPlane.basis.origin[1],
1117
- bestPlane.world[2] - bestPlane.basis.origin[2]
1118
- ];
1119
- const b1n = dot3(bestPlane.basis.basis1, bestPlane.basis.basis1);
1120
- const b2n = dot3(bestPlane.basis.basis2, bestPlane.basis.basis2);
1121
- const u = b1n === 0 ? 0 : dot3(rel, bestPlane.basis.basis1) / b1n;
1122
- const v = b2n === 0 ? 0 : dot3(rel, bestPlane.basis.basis2) / b2n;
1123
- return { kind: "onPlane", planeId: bestPlane.id, u, v, world: bestPlane.world };
1124
- }
1125
- if (bestSphere) {
1126
- const sph = scene.get(bestSphere.id);
1127
- if (sph && sph.kind === "sphere") {
1128
- const centerPt = scene.get(sph.center);
1129
- if (centerPt && centerPt.kind === "point") {
1130
- const center = constraintToWorld(centerPt.constraint, scene);
1131
- const relX = bestSphere.world[0] - center[0];
1132
- const relY = bestSphere.world[1] - center[1];
1133
- const relZ = bestSphere.world[2] - center[2];
1134
- const r = Math.hypot(relX, relY, relZ);
1135
- const phi = r === 0 ? 0 : Math.acos(relZ / r);
1136
- const theta = Math.atan2(relY, relX);
1137
- return { kind: "onSphere", sphereId: bestSphere.id, theta, phi, world: bestSphere.world };
1138
- }
1139
- }
1140
- }
1141
- const g = rayGround(ray);
1142
- if (g) return { kind: "onGround", world: g.point };
1143
- return { kind: "empty" };
1144
- }
1145
- function distScreenPointToSegment(p, a, b) {
1146
- const vx = b[0] - a[0];
1147
- const vy = b[1] - a[1];
1148
- const wx = p.x - a[0];
1149
- const wy = p.y - a[1];
1150
- const c1 = vx * wx + vy * wy;
1151
- if (c1 <= 0) return Math.hypot(wx, wy);
1152
- const c2 = vx * vx + vy * vy;
1153
- if (c2 <= c1) return Math.hypot(p.x - b[0], p.y - b[1]);
1154
- const t = c1 / c2;
1155
- const px = a[0] + t * vx;
1156
- const py = a[1] + t * vy;
1157
- return Math.hypot(p.x - px, p.y - py);
1158
- }
1159
- function planeBasis(planeObj, scene) {
1160
- const p1Obj = scene.get(planeObj.p1);
1161
- const p2Obj = scene.get(planeObj.p2);
1162
- const p3Obj = scene.get(planeObj.p3);
1163
- if (!p1Obj || p1Obj.kind !== "point") return null;
1164
- if (!p2Obj || p2Obj.kind !== "point") return null;
1165
- if (!p3Obj || p3Obj.kind !== "point") return null;
1166
- const p1 = constraintToWorld(p1Obj.constraint, scene);
1167
- const p2 = constraintToWorld(p2Obj.constraint, scene);
1168
- const p3 = constraintToWorld(p3Obj.constraint, scene);
1169
- const basis1 = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];
1170
- const tmp = [p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2]];
1171
- const cx = basis1[1] * tmp[2] - basis1[2] * tmp[1];
1172
- const cy = basis1[2] * tmp[0] - basis1[0] * tmp[2];
1173
- const cz = basis1[0] * tmp[1] - basis1[1] * tmp[0];
1174
- const cLen = Math.hypot(cx, cy, cz);
1175
- if (cLen === 0) return null;
1176
- const normal = [cx / cLen, cy / cLen, cz / cLen];
1177
- const basis2 = [
1178
- normal[1] * basis1[2] - normal[2] * basis1[1],
1179
- normal[2] * basis1[0] - normal[0] * basis1[2],
1180
- normal[0] * basis1[1] - normal[1] * basis1[0]
1181
- ];
1182
- return { origin: p1, basis1, basis2, normal };
1183
- }
1184
- function dot3(a, b) {
1185
- return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
1186
- }
1187
- var MiniBoard3D = React2.forwardRef(
1188
- function MiniBoard3D2(props, ref) {
1189
- const containerRef = React2.useRef(null);
1190
- const boardRef = React2.useRef(null);
1191
- const viewRef = React2.useRef(null);
1192
- const {
1193
- isDark,
1194
- onView3DReady,
1195
- onPointerClick,
1196
- onPointerMove,
1197
- onPointerLeave,
1198
- shouldStartPointDrag,
1199
- onPointerDrag,
1200
- onPointerDragEnd
1201
- } = props;
1202
- const onView3DReadyRef = React2.useRef(onView3DReady);
1203
- const onPointerClickRef = React2.useRef(onPointerClick);
1204
- const onPointerMoveRef = React2.useRef(onPointerMove);
1205
- const onPointerLeaveRef = React2.useRef(onPointerLeave);
1206
- const shouldStartPointDragRef = React2.useRef(shouldStartPointDrag);
1207
- const onPointerDragRef = React2.useRef(onPointerDrag);
1208
- const onPointerDragEndRef = React2.useRef(onPointerDragEnd);
1209
- onView3DReadyRef.current = onView3DReady;
1210
- onPointerClickRef.current = onPointerClick;
1211
- onPointerMoveRef.current = onPointerMove;
1212
- onPointerLeaveRef.current = onPointerLeave;
1213
- shouldStartPointDragRef.current = shouldStartPointDrag;
1214
- onPointerDragRef.current = onPointerDrag;
1215
- onPointerDragEndRef.current = onPointerDragEnd;
1216
- React2.useImperativeHandle(
1217
- ref,
1218
- () => ({
1219
- getBoard: () => boardRef.current,
1220
- getView3D: () => viewRef.current,
1221
- getSvgElement: () => containerRef.current?.querySelector("svg") ?? null
1222
- }),
1223
- []
1224
- );
1225
- React2.useEffect(() => {
1226
- const div = containerRef.current;
1227
- if (!div) return;
1228
- let cancelled = false;
1229
- let JXG = null;
1230
- let board = null;
1231
- let svgEl = null;
1232
- let handlePointerDown = null;
1233
- let handlePointerMove = null;
1234
- let handlePointerUp = null;
1235
- let handlePointerLeave = null;
1236
- void (async () => {
1237
- try {
1238
- JXG = (await import('jsxgraph')).default;
1239
- } catch {
1240
- return;
1241
- }
1242
- if (cancelled || !containerRef.current) return;
1243
- try {
1244
- JXG.Options.text.display = "internal";
1245
- } catch {
1246
- }
1247
- try {
1248
- board = JXG.JSXGraph.initBoard(div, {
1249
- boundingbox: [-6, 6, 6, -6],
1250
- keepaspectratio: true,
1251
- axis: false,
1252
- showCopyright: false,
1253
- showNavigation: false,
1254
- renderer: "svg"
1255
- });
1256
- } catch {
1257
- return;
1258
- }
1259
- if (cancelled || !board) return;
1260
- boardRef.current = board;
1261
- let view = null;
1262
- try {
1263
- const baseAttrs = VIEW3D_ATTRS(isDark);
1264
- view = board.create(
1265
- "view3d",
1266
- [
1267
- [-5, -5],
1268
- [10, 10],
1269
- [
1270
- [DEFAULT_VIEW3D.bbox3D[0], DEFAULT_VIEW3D.bbox3D[3]],
1271
- [DEFAULT_VIEW3D.bbox3D[1], DEFAULT_VIEW3D.bbox3D[4]],
1272
- [DEFAULT_VIEW3D.bbox3D[2], DEFAULT_VIEW3D.bbox3D[5]]
1273
- ]
1274
- ],
1275
- {
1276
- ...baseAttrs,
1277
- // JSXGraph view3d đọc giá trị khởi tạo từ az.slider.start (không
1278
- // phải az.value). Pass nhầm `value` → JSXGraph dùng default
1279
- // 1.0/0.3, khiến DEFAULT_VIEW3D bị bỏ qua.
1280
- az: { ...baseAttrs.az, slider: { ...baseAttrs.az.slider, start: DEFAULT_VIEW3D.azimuth } },
1281
- el: { ...baseAttrs.el, slider: { ...baseAttrs.el.slider, start: DEFAULT_VIEW3D.elevation } }
1282
- }
1283
- );
1284
- } catch {
1285
- }
1286
- viewRef.current = view;
1287
- if (view) {
1288
- try {
1289
- view.create(
1290
- "plane3d",
1291
- [
1292
- [0, 0, 0],
1293
- [1, 0, 0],
1294
- [0, 1, 0],
1295
- GROUND_PLANE_RANGE,
1296
- GROUND_PLANE_RANGE
1297
- ],
1298
- GROUND_PLANE_ATTRS(isDark)
1299
- );
1300
- } catch {
1301
- }
1302
- onView3DReadyRef.current?.(view, board);
1303
- }
1304
- svgEl = containerRef.current?.querySelector("svg") ?? null;
1305
- if (svgEl) {
1306
- const p2 = paletteFor(isDark);
1307
- svgEl.style.background = p2.view3dBg;
1308
- const pixelToUser = (e) => {
1309
- const rect = svgEl.getBoundingClientRect();
1310
- const px = e.clientX - rect.left;
1311
- const py = e.clientY - rect.top;
1312
- const b = board;
1313
- if (!b || !b.origin || !b.origin.scrCoords) {
1314
- return { x: px, y: py };
1315
- }
1316
- const ox = b.origin.scrCoords[1];
1317
- const oy = b.origin.scrCoords[2];
1318
- const ux = b.unitX || 1;
1319
- const uy = b.unitY || 1;
1320
- return { x: (px - ox) / ux, y: (oy - py) / uy };
1321
- };
1322
- const DRAG_THRESHOLD = 4;
1323
- const AZ_PER_PX = 0.01;
1324
- const EL_PER_PX = 0.01;
1325
- const EL_LIMIT = Math.PI / 2 - 0.05;
1326
- let dragStart = null;
1327
- let dragging = false;
1328
- let pointDragMode = false;
1329
- let startAz = 0;
1330
- let startEl = 0;
1331
- const readAng = (s) => {
1332
- if (!s) return 0;
1333
- if (typeof s.Value === "function") {
1334
- try {
1335
- return s.Value();
1336
- } catch {
1337
- }
1338
- }
1339
- return typeof s.value === "number" ? s.value : 0;
1340
- };
1341
- const setAng = (s, v) => {
1342
- if (!s) return;
1343
- if (typeof s.setValue === "function") {
1344
- try {
1345
- s.setValue(v);
1346
- return;
1347
- } catch {
1348
- }
1349
- }
1350
- s.value = v;
1351
- };
1352
- handlePointerDown = (e) => {
1353
- if (!svgEl) return;
1354
- dragStart = { x: e.clientX, y: e.clientY };
1355
- dragging = false;
1356
- pointDragMode = false;
1357
- const screen = pixelToUser(e);
1358
- try {
1359
- pointDragMode = shouldStartPointDragRef.current?.(screen) ?? false;
1360
- } catch {
1361
- pointDragMode = false;
1362
- }
1363
- if (!pointDragMode) {
1364
- const v = viewRef.current;
1365
- startAz = readAng(v?.az_slide ?? v?.az);
1366
- startEl = readAng(v?.el_slide ?? v?.el);
1367
- }
1368
- try {
1369
- svgEl.setPointerCapture?.(e.pointerId);
1370
- } catch {
1371
- }
1372
- };
1373
- handlePointerMove = (e) => {
1374
- if (!svgEl) return;
1375
- if (dragStart) {
1376
- const dx = e.clientX - dragStart.x;
1377
- const dy = e.clientY - dragStart.y;
1378
- if (!dragging && Math.hypot(dx, dy) > DRAG_THRESHOLD) dragging = true;
1379
- if (dragging) {
1380
- if (pointDragMode) {
1381
- onPointerDragRef.current?.(pixelToUser(e));
1382
- return;
1383
- }
1384
- const v = viewRef.current;
1385
- const newAz = startAz + dx * AZ_PER_PX;
1386
- let newEl = startEl - dy * EL_PER_PX;
1387
- if (newEl > EL_LIMIT) newEl = EL_LIMIT;
1388
- if (newEl < -EL_LIMIT) newEl = -EL_LIMIT;
1389
- setAng(v?.az_slide ?? v?.az, newAz);
1390
- setAng(v?.el_slide ?? v?.el, newEl);
1391
- try {
1392
- v?.board?.update?.();
1393
- } catch {
1394
- }
1395
- return;
1396
- }
1397
- }
1398
- onPointerMoveRef.current?.(pixelToUser(e));
1399
- };
1400
- handlePointerUp = (e) => {
1401
- if (!svgEl) return;
1402
- const wasDrag = dragging;
1403
- const hadDown = dragStart !== null;
1404
- const wasPointDrag = pointDragMode;
1405
- dragStart = null;
1406
- dragging = false;
1407
- pointDragMode = false;
1408
- try {
1409
- svgEl.releasePointerCapture?.(e.pointerId);
1410
- } catch {
1411
- }
1412
- if (hadDown && wasPointDrag) {
1413
- onPointerDragEndRef.current?.(pixelToUser(e));
1414
- return;
1415
- }
1416
- if (hadDown && !wasDrag) {
1417
- onPointerClickRef.current?.(pixelToUser(e));
1418
- }
1419
- };
1420
- handlePointerLeave = () => {
1421
- if (pointDragMode) {
1422
- try {
1423
- onPointerDragEndRef.current?.({ x: 0, y: 0 });
1424
- } catch {
1425
- }
1426
- }
1427
- dragStart = null;
1428
- dragging = false;
1429
- pointDragMode = false;
1430
- onPointerLeaveRef.current?.();
1431
- };
1432
- svgEl.addEventListener("pointerdown", handlePointerDown);
1433
- svgEl.addEventListener("pointermove", handlePointerMove);
1434
- svgEl.addEventListener("pointerup", handlePointerUp);
1435
- svgEl.addEventListener("pointercancel", handlePointerUp);
1436
- svgEl.addEventListener("pointerleave", handlePointerLeave);
1437
- }
1438
- })();
1439
- return () => {
1440
- cancelled = true;
1441
- if (svgEl) {
1442
- if (handlePointerDown) svgEl.removeEventListener("pointerdown", handlePointerDown);
1443
- if (handlePointerMove) svgEl.removeEventListener("pointermove", handlePointerMove);
1444
- if (handlePointerUp) {
1445
- svgEl.removeEventListener("pointerup", handlePointerUp);
1446
- svgEl.removeEventListener("pointercancel", handlePointerUp);
1447
- }
1448
- if (handlePointerLeave) svgEl.removeEventListener("pointerleave", handlePointerLeave);
1449
- }
1450
- try {
1451
- if (board && JXG) JXG.JSXGraph.freeBoard(board);
1452
- } catch {
1453
- }
1454
- boardRef.current = null;
1455
- viewRef.current = null;
1456
- };
1457
- }, [isDark]);
1458
- const p = paletteFor(isDark);
1459
- return /* @__PURE__ */ jsx(
1460
- "div",
1461
- {
1462
- "data-testid": "mini-board-3d",
1463
- ref: containerRef,
1464
- style: {
1465
- width: "100%",
1466
- height: "100%",
1467
- minHeight: 400,
1468
- background: p.view3dBg,
1469
- position: "relative",
1470
- // Clip JSXGraph mesh3d paths projecting outside the container.
1471
- overflow: "hidden"
1472
- }
1473
- }
1474
- );
1475
- }
1476
- );
1477
- function StatusHint(props) {
1478
- const { hint, hoverLabel } = props;
1479
- return /* @__PURE__ */ jsxs(
1480
- "div",
1481
- {
1482
- "data-testid": "status-hint",
1483
- className: "border-t border-zinc-200 bg-zinc-50 px-3 py-1.5 text-xs text-zinc-700 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300",
1484
- children: [
1485
- /* @__PURE__ */ jsxs("span", { children: [
1486
- "\u{1F4D0} ",
1487
- hint || "Ch\u1ECDn c\xF4ng c\u1EE5 trong b\u1EA3ng b\xEAn tr\xE1i"
1488
- ] }),
1489
- hoverLabel ? /* @__PURE__ */ jsxs("span", { className: "ml-3 text-zinc-500", children: [
1490
- "\u2014 \u0111ang tr\xEAn: ",
1491
- hoverLabel
1492
- ] }) : null
1493
- ]
1494
- }
1495
- );
1496
- }
1497
-
1498
- // src/stamps/geometry-3d/editor/scene/labels.ts
1499
- var A = "A".charCodeAt(0);
1500
- function nextPointLabel(existing) {
1501
- const used = new Set(existing);
1502
- for (let suffix = 0; suffix < 1e3; suffix++) {
1503
- for (let i = 0; i < 26; i++) {
1504
- const letter = String.fromCharCode(A + i);
1505
- const candidate = suffix === 0 ? letter : `${letter}_${suffix}`;
1506
- if (!used.has(candidate)) return candidate;
1507
- }
1508
- }
1509
- return `P_${used.size}`;
1510
- }
1511
- var LOWERCASE_KINDS = ["segment", "line", "ray", "vector"];
1512
- var PREFIX = {
1513
- sphere: "s",
1514
- polyhedron: "h",
1515
- cylinder: "c",
1516
- cone: "k",
1517
- polygon: "g",
1518
- plane: "\u03C0"
1519
- };
1520
- function nextDerivedLabel(kind, existing) {
1521
- const used = new Set(existing);
1522
- if (LOWERCASE_KINDS.includes(kind)) {
1523
- for (let i = 0; i < 26; i++) {
1524
- const c = String.fromCharCode("a".charCodeAt(0) + i);
1525
- if (!used.has(c)) return c;
1526
- }
1527
- for (let n = 1; n < 1e3; n++) {
1528
- const c = `a_${n}`;
1529
- if (!used.has(c)) return c;
1530
- }
1531
- }
1532
- const prefix = PREFIX[kind] ?? kind[0];
1533
- for (let n = 1; n < 1e3; n++) {
1534
- const candidate = `${prefix}_${n}`;
1535
- if (!used.has(candidate)) return candidate;
1536
- }
1537
- return `${prefix}_x`;
1538
- }
1539
-
1540
- // src/stamps/geometry-3d/editor/scene/Scene3D.ts
1541
- var Scene3D = class {
1542
- constructor() {
1543
- this.objects = /* @__PURE__ */ new Map();
1544
- this.order = [];
1545
- this.counter = 0;
1546
- this.listeners = {
1547
- add: /* @__PURE__ */ new Set(),
1548
- change: /* @__PURE__ */ new Set(),
1549
- delete: /* @__PURE__ */ new Set(),
1550
- reset: /* @__PURE__ */ new Set()
1551
- };
1552
- this.historyPast = [];
1553
- this.historyFuture = [];
1554
- this.historySuspended = false;
1555
- this.historyListeners = /* @__PURE__ */ new Set();
1556
- }
1557
- on(event, cb) {
1558
- const set = this.listeners[event];
1559
- set.add(cb);
1560
- return () => {
1561
- set.delete(cb);
1562
- };
1563
- }
1564
- nextId(prefix) {
1565
- this.counter += 1;
1566
- return `${prefix}${this.counter}`;
1567
- }
1568
- addPoint(constraint, label, color) {
1569
- this.capture();
1570
- const id = this.nextId("p");
1571
- const existingLabels = this.list().filter((o) => o.kind === "point").map((o) => o.label);
1572
- const autoLabel = label ?? nextPointLabel(existingLabels);
1573
- const obj = {
1574
- kind: "point",
1575
- id,
1576
- label: autoLabel,
1577
- visible: true,
1578
- color,
1579
- constraint
1580
- };
1581
- this.objects.set(id, obj);
1582
- this.order.push(id);
1583
- this.listeners.add.forEach((cb) => cb(obj));
1584
- return id;
1585
- }
1586
- addObject(kind, spec, label) {
1587
- this.capture();
1588
- const id = this.nextId(kind[0]);
1589
- const existingLabels = this.list().filter((o) => o.kind === kind).map((o) => o.label);
1590
- const autoLabel = label ?? nextDerivedLabel(kind, existingLabels);
1591
- const obj = { id, label: autoLabel, visible: true, kind, ...spec };
1592
- this.objects.set(id, obj);
1593
- this.order.push(id);
1594
- this.listeners.add.forEach((cb) => cb(obj));
1595
- return id;
1596
- }
1597
- insert(obj) {
1598
- this.capture();
1599
- if (this.objects.has(obj.id)) {
1600
- throw new Error(`Scene3D.insert: id ${obj.id} already exists`);
1601
- }
1602
- this.objects.set(obj.id, obj);
1603
- this.order.push(obj.id);
1604
- this.listeners.add.forEach((cb) => cb(obj));
1605
- }
1606
- get(id) {
1607
- return this.objects.get(id);
1608
- }
1609
- list() {
1610
- return this.order.map((id) => this.objects.get(id)).filter((obj) => obj !== void 0);
1611
- }
1612
- referencedIds(obj) {
1613
- switch (obj.kind) {
1614
- case "point": {
1615
- const c = obj.constraint;
1616
- if (c.kind === "onPlane") return [c.planeId];
1617
- if (c.kind === "onLine") return [c.lineId];
1618
- if (c.kind === "onPolygon") return [c.polygonId];
1619
- if (c.kind === "onSphere") return [c.sphereId];
1620
- return [];
1621
- }
1622
- case "segment":
1623
- case "line":
1624
- return [obj.p1, obj.p2];
1625
- case "ray":
1626
- return [obj.origin, obj.through];
1627
- case "vector":
1628
- return [obj.from, obj.to];
1629
- case "polygon":
1630
- return obj.vertices;
1631
- case "plane":
1632
- return [obj.p1, obj.p2, obj.p3];
1633
- case "sphere":
1634
- return [obj.center, obj.surfacePoint];
1635
- case "polyhedron":
1636
- return obj.vertices;
1637
- case "cylinder":
1638
- return [obj.baseCenter, obj.topCenter];
1639
- case "cone":
1640
- return [obj.baseCenter, obj.apex];
1641
- }
1642
- }
1643
- collectDependents(targetId) {
1644
- const dependents = /* @__PURE__ */ new Set([targetId]);
1645
- let grew = true;
1646
- while (grew) {
1647
- grew = false;
1648
- for (const obj of this.objects.values()) {
1649
- if (dependents.has(obj.id)) continue;
1650
- const refs = this.referencedIds(obj);
1651
- if (refs.some((r) => dependents.has(r))) {
1652
- dependents.add(obj.id);
1653
- grew = true;
1654
- }
1655
- }
1656
- }
1657
- return dependents;
1658
- }
1659
- delete(id) {
1660
- if (!this.objects.has(id)) return;
1661
- this.capture();
1662
- const toDelete = this.collectDependents(id);
1663
- for (const dependentId of toDelete) {
1664
- this.objects.delete(dependentId);
1665
- this.order = this.order.filter((x) => x !== dependentId);
1666
- this.listeners.delete.forEach((cb) => cb(dependentId));
1667
- }
1668
- }
1669
- reset() {
1670
- this.capture();
1671
- this.objects.clear();
1672
- this.order = [];
1673
- this.counter = 0;
1674
- this.listeners.reset.forEach((cb) => cb());
1675
- }
1676
- reserveId(prefix) {
1677
- return this.nextId(prefix);
1678
- }
1679
- emitChange(id) {
1680
- const obj = this.objects.get(id);
1681
- if (!obj) return;
1682
- this.listeners.change.forEach((cb) => cb(obj));
1683
- }
1684
- snapshot() {
1685
- const cloned = /* @__PURE__ */ new Map();
1686
- for (const [id, obj] of this.objects) {
1687
- cloned.set(id, { ...obj });
1688
- }
1689
- return {
1690
- objects: cloned,
1691
- order: [...this.order],
1692
- counter: this.counter
1693
- };
1694
- }
1695
- restore(snap) {
1696
- this.objects = /* @__PURE__ */ new Map();
1697
- for (const [id, obj] of snap.objects) {
1698
- this.objects.set(id, { ...obj });
1699
- }
1700
- this.order = [...snap.order];
1701
- this.counter = snap.counter;
1702
- this.listeners.reset.forEach((cb) => cb());
1703
- for (const id of this.order) {
1704
- const obj = this.objects.get(id);
1705
- if (obj) this.listeners.add.forEach((cb) => cb(obj));
1706
- }
1707
- }
1708
- capture() {
1709
- if (this.historySuspended) return;
1710
- this.historyPast.push(this.snapshot());
1711
- this.historyFuture = [];
1712
- this.notifyHistoryChange();
1713
- }
1714
- canUndo() {
1715
- return this.historyPast.length > 0;
1716
- }
1717
- canRedo() {
1718
- return this.historyFuture.length > 0;
1719
- }
1720
- undo() {
1721
- const prev = this.historyPast.pop();
1722
- if (!prev) return;
1723
- this.historyFuture.push(this.snapshot());
1724
- this.restore(prev);
1725
- this.notifyHistoryChange();
1726
- }
1727
- redo() {
1728
- const next = this.historyFuture.pop();
1729
- if (!next) return;
1730
- this.historyPast.push(this.snapshot());
1731
- this.restore(next);
1732
- this.notifyHistoryChange();
1733
- }
1734
- withoutHistory(fn) {
1735
- const prev = this.historySuspended;
1736
- this.historySuspended = true;
1737
- try {
1738
- fn();
1739
- } finally {
1740
- this.historySuspended = prev;
1741
- }
1742
- }
1743
- pushUndoCheckpoint(prev) {
1744
- if (this.historySuspended) return;
1745
- this.historyPast.push(prev);
1746
- this.historyFuture = [];
1747
- this.notifyHistoryChange();
1748
- }
1749
- onHistoryChange(cb) {
1750
- this.historyListeners.add(cb);
1751
- return () => {
1752
- this.historyListeners.delete(cb);
1753
- };
1754
- }
1755
- notifyHistoryChange() {
1756
- this.historyListeners.forEach((cb) => cb());
1757
- }
1758
- };
1759
-
1760
- // src/stamps/geometry-3d/editor/scene/persistence.ts
1761
- function sceneToBoard(scene, view, bbox) {
1762
- const elements = [];
1763
- for (const obj of scene.list()) {
1764
- const els = sceneObjectToElements(obj, scene);
1765
- elements.push(...els);
1766
- }
1767
- return { version: 2, bbox, view, showAxes: true, showMesh: true, elements };
1768
- }
1769
- function sceneObjectToElements(obj, scene) {
1770
- const baseAttrs = { label: obj.label, visible: obj.visible, color: obj.color };
1771
- switch (obj.kind) {
1772
- case "point": {
1773
- let w;
1774
- try {
1775
- w = constraintToWorld(obj.constraint, scene);
1776
- } catch {
1777
- w = [0, 0, 0];
1778
- }
1779
- return [{
1780
- type: "point3d",
1781
- parents: [w[0], w[1], w[2]],
1782
- attributes: { id: obj.id, ...baseAttrs },
1783
- id: obj.id,
1784
- label: obj.label,
1785
- constraint: obj.constraint
1786
- }];
1787
- }
1788
- case "segment":
1789
- case "line":
1790
- case "ray":
1791
- case "vector":
1792
- case "plane":
1793
- case "sphere":
1794
- case "polygon":
1795
- case "polyhedron":
1796
- case "cylinder":
1797
- case "cone": {
1798
- return [{
1799
- type: pickJxgType(obj.kind),
1800
- parents: [],
1801
- attributes: { id: obj.id, ...baseAttrs, sceneKind: obj.kind, sceneSpec: encodeSpec(obj) },
1802
- id: obj.id,
1803
- label: obj.label
1804
- }];
1805
- }
1806
- }
1807
- }
1808
- function pickJxgType(kind) {
1809
- switch (kind) {
1810
- case "point":
1811
- return "point3d";
1812
- case "segment":
1813
- case "line":
1814
- case "ray":
1815
- case "vector":
1816
- return "line3d";
1817
- case "plane":
1818
- return "plane3d";
1819
- case "sphere":
1820
- return "sphere3d";
1821
- case "polygon":
1822
- case "polyhedron":
1823
- case "cylinder":
1824
- case "cone":
1825
- return "polygon3d";
1826
- }
1827
- }
1828
- function encodeSpec(obj) {
1829
- const rest = {};
1830
- for (const [k, v] of Object.entries(obj)) {
1831
- if (k === "id" || k === "label" || k === "visible" || k === "color" || k === "kind") continue;
1832
- rest[k] = v;
1833
- }
1834
- return rest;
1835
- }
1836
- function boardToScene(board) {
1837
- const scene = new Scene3D();
1838
- for (const el of board.elements) {
1839
- if (el.type === "point3d") {
1840
- const constraint = el.constraint ?? {
1841
- kind: "free",
1842
- x: Number(el.parents[0] ?? 0),
1843
- y: Number(el.parents[1] ?? 0),
1844
- z: Number(el.parents[2] ?? 0)
1845
- };
1846
- const color2 = el.attributes["color"];
1847
- const visible2 = el.attributes["visible"] !== false;
1848
- try {
1849
- scene.insert({
1850
- kind: "point",
1851
- id: el.id,
1852
- label: el.label ?? el.id,
1853
- visible: visible2,
1854
- color: color2,
1855
- constraint
1856
- });
1857
- } catch {
1858
- }
1859
- continue;
1860
- }
1861
- const sceneKind = el.attributes["sceneKind"];
1862
- const sceneSpec = el.attributes["sceneSpec"];
1863
- if (!sceneKind || !sceneSpec) continue;
1864
- const color = el.attributes["color"];
1865
- const visible = el.attributes["visible"] !== false;
1866
- const obj = {
1867
- id: el.id,
1868
- label: el.label ?? el.id,
1869
- visible,
1870
- color,
1871
- kind: sceneKind,
1872
- ...sceneSpec
1873
- };
1874
- try {
1875
- scene.insert(obj);
1876
- } catch {
1877
- }
1878
- }
1879
- return scene;
1880
- }
1881
- var EditorPanel = React2.forwardRef(
1882
- function EditorPanel2(props, ref) {
1883
- const {
1884
- isDark: isDarkProp,
1885
- initialState,
1886
- scene,
1887
- selectedTool,
1888
- onSelectedToolChange,
1889
- showAxis,
1890
- showGrid,
1891
- onReadyChange,
1892
- onHistoryChange
1893
- } = props;
1894
- const isDark = isDarkProp ?? false;
1895
- const controllerRef = React2.useRef(null);
1896
- if (!controllerRef.current) controllerRef.current = new ToolController(scene);
1897
- const [hint, setHint] = React2.useState("Ch\u1ECDn c\xF4ng c\u1EE5 trong b\u1EA3ng b\xEAn tr\xE1i");
1898
- const [hoverLabel, setHoverLabel] = React2.useState(null);
1899
- const boardRef = React2.useRef(null);
1900
- const rendererRef = React2.useRef(null);
1901
- const onSelectedToolChangeRef = React2.useRef(onSelectedToolChange);
1902
- onSelectedToolChangeRef.current = onSelectedToolChange;
1903
- const onHistoryChangeRef = React2.useRef(onHistoryChange);
1904
- onHistoryChangeRef.current = onHistoryChange;
1905
- const selectedToolRef = React2.useRef(selectedTool);
1906
- selectedToolRef.current = selectedTool;
1907
- const draggedPointRef = React2.useRef(null);
1908
- const dragStartRef = React2.useRef(null);
1909
- const dragSnapshotRef = React2.useRef(null);
1910
- React2.useEffect(() => {
1911
- if (initialState) {
1912
- const loaded = boardToScene(initialState);
1913
- scene.withoutHistory(() => {
1914
- scene.reset();
1915
- for (const obj of loaded.list()) {
1916
- scene.insert(obj);
1917
- }
1918
- });
1919
- }
1920
- }, []);
1921
- React2.useEffect(() => {
1922
- const ctrl = controllerRef.current;
1923
- const unsub = ctrl.on((state) => {
1924
- setHint(state.hint);
1925
- onSelectedToolChangeRef.current(state.tool?.key ?? "move");
1926
- });
1927
- return unsub;
1928
- }, []);
1929
- React2.useEffect(() => {
1930
- onHistoryChangeRef.current?.(scene.canUndo(), scene.canRedo());
1931
- const unsub = scene.onHistoryChange(() => {
1932
- onHistoryChangeRef.current?.(scene.canUndo(), scene.canRedo());
1933
- });
1934
- return unsub;
1935
- }, [scene]);
1936
- React2.useEffect(() => {
1937
- controllerRef.current?.selectTool(selectedTool);
1938
- }, [selectedTool]);
1939
- React2.useEffect(() => {
1940
- const onKey = (e) => {
1941
- const ae = document.activeElement;
1942
- const inField = !!(ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable));
1943
- if (inField) return;
1944
- if (!(e.metaKey || e.ctrlKey)) return;
1945
- const key = e.key.toLowerCase();
1946
- if (key === "z" && !e.shiftKey) {
1947
- e.preventDefault();
1948
- e.stopPropagation();
1949
- scene.undo();
1950
- } else if (key === "z" && e.shiftKey || key === "y" && !e.shiftKey) {
1951
- e.preventDefault();
1952
- e.stopPropagation();
1953
- scene.redo();
1954
- }
1955
- };
1956
- window.addEventListener("keydown", onKey, { capture: true });
1957
- return () => window.removeEventListener("keydown", onKey, { capture: true });
1958
- }, [scene]);
1959
- React2.useEffect(() => {
1960
- return () => {
1961
- rendererRef.current?.dispose();
1962
- rendererRef.current = null;
1963
- };
1964
- }, []);
1965
- React2.useEffect(() => {
1966
- const view = boardRef.current?.getView3D();
1967
- const v = view;
1968
- if (!v || typeof v.setAttribute !== "function") return;
1969
- try {
1970
- v.setAttribute({
1971
- xAxis: { visible: showAxis },
1972
- yAxis: { visible: showAxis },
1973
- zAxis: { visible: showAxis },
1974
- // GeoGebra-style: only the XY ground plane is shown; side walls stay hidden.
1975
- xPlaneRear: { visible: false, mesh3d: { visible: false } },
1976
- yPlaneRear: { visible: false, mesh3d: { visible: false } },
1977
- zPlaneRear: { visible: showGrid, mesh3d: { visible: false } }
1978
- });
1979
- v.board?.update?.();
1980
- } catch {
1981
- }
1982
- }, [showAxis, showGrid]);
1983
- const handleView3DReady = React2.useCallback((view) => {
1984
- rendererRef.current = new JxgRenderer(scene, view);
1985
- if (initialState) {
1986
- try {
1987
- const v = view;
1988
- v?.az_slide?.setValue?.(initialState.view.azimuth);
1989
- v?.el_slide?.setValue?.(initialState.view.elevation);
1990
- v?.board?.update?.();
1991
- } catch {
1992
- }
1993
- }
1994
- onReadyChange?.(true);
1995
- }, [onReadyChange, scene, initialState]);
1996
- const handleClick = React2.useCallback((screen) => {
1997
- const board = boardRef.current;
1998
- if (!board) return;
1999
- const view = board.getView3D();
2000
- if (!view) return;
2001
- try {
2002
- const hit = hitTest(screen, view, scene);
2003
- controllerRef.current.consumeHit(hit);
2004
- } catch {
2005
- }
2006
- }, [scene]);
2007
- const handleMove = React2.useCallback((screen) => {
2008
- const board = boardRef.current;
2009
- if (!board) return;
2010
- const view = board.getView3D();
2011
- if (!view) return;
2012
- if (draggedPointRef.current) return;
2013
- let hit;
2014
- try {
2015
- hit = hitTest(screen, view, scene);
2016
- } catch {
2017
- setHoverLabel(null);
2018
- return;
2019
- }
2020
- if (hit.kind === "empty") setHoverLabel(null);
2021
- else if (hit.kind === "existingPoint") {
2022
- const obj = scene.get(hit.pointId);
2023
- setHoverLabel(obj?.label ?? null);
2024
- } else if (hit.kind === "onGround") setHoverLabel("m\u1EB7t n\u1EC1n");
2025
- else if (hit.kind === "onAxis") setHoverLabel(`tr\u1EE5c ${hit.axis.toUpperCase()}`);
2026
- else if (hit.kind === "onPlane") setHoverLabel(`m\u1EB7t ph\u1EB3ng ${hit.planeId}`);
2027
- else if (hit.kind === "onSphere") setHoverLabel(`m\u1EB7t c\u1EA7u ${hit.sphereId}`);
2028
- else setHoverLabel(null);
2029
- }, [scene]);
2030
- const shouldStartPointDrag = React2.useCallback((screen) => {
2031
- const view = boardRef.current?.getView3D();
2032
- if (!view) return false;
2033
- const tool = selectedToolRef.current;
2034
- if (tool !== "point" && tool !== "move") return false;
2035
- let hit;
2036
- try {
2037
- hit = hitTest(screen, view, scene);
2038
- } catch {
2039
- return false;
2040
- }
2041
- if (hit.kind === "existingPoint") {
2042
- const pt = scene.get(hit.pointId);
2043
- if (!pt || pt.kind !== "point") return false;
2044
- dragSnapshotRef.current = scene.snapshot();
2045
- draggedPointRef.current = hit.pointId;
2046
- dragStartRef.current = {
2047
- screen,
2048
- world: constraintToWorld(pt.constraint, scene)
2049
- };
2050
- return true;
2051
- }
2052
- if (tool === "point" && (hit.kind === "onGround" || hit.kind === "onAxis")) {
2053
- dragSnapshotRef.current = scene.snapshot();
2054
- const constraint = hitToConstraint(hit);
2055
- if (!constraint) {
2056
- dragSnapshotRef.current = null;
2057
- return false;
2058
- }
2059
- let id = null;
2060
- scene.withoutHistory(() => {
2061
- id = scene.addPoint(constraint);
2062
- });
2063
- if (!id) {
2064
- dragSnapshotRef.current = null;
2065
- return false;
2066
- }
2067
- draggedPointRef.current = id;
2068
- dragStartRef.current = {
2069
- screen,
2070
- world: [hit.world[0], hit.world[1], hit.world[2]]
2071
- };
2072
- return true;
2073
- }
2074
- if (tool === "point") {
2075
- dragSnapshotRef.current = null;
2076
- draggedPointRef.current = null;
2077
- dragStartRef.current = null;
2078
- return true;
2079
- }
2080
- return false;
2081
- }, [scene]);
2082
- const onPointerDrag = React2.useCallback((screen) => {
2083
- const pointId = draggedPointRef.current;
2084
- const start = dragStartRef.current;
2085
- if (!pointId || !start) return;
2086
- const view = boardRef.current?.getView3D();
2087
- if (!view) return;
2088
- const tool = selectedToolRef.current;
2089
- let nextWorld;
2090
- if (tool === "point") {
2091
- const dz = screen.y - start.screen.y;
2092
- nextWorld = [start.world[0], start.world[1], start.world[2] + dz];
2093
- } else if (tool === "move") {
2094
- try {
2095
- const ray = screenToRay(screen, view);
2096
- const hit = rayPlane(ray, { point: [0, 0, start.world[2]], normal: [0, 0, 1] });
2097
- if (!hit) return;
2098
- nextWorld = [hit.point[0], hit.point[1], start.world[2]];
2099
- } catch {
2100
- return;
2101
- }
2102
- } else {
2103
- return;
2104
- }
2105
- const obj = scene.get(pointId);
2106
- if (!obj || obj.kind !== "point") return;
2107
- const free = { kind: "free", x: nextWorld[0], y: nextWorld[1], z: nextWorld[2] };
2108
- obj.constraint = free;
2109
- scene.emitChange(pointId);
2110
- }, [scene]);
2111
- const onPointerDragEnd = React2.useCallback(() => {
2112
- const snap = dragSnapshotRef.current;
2113
- dragSnapshotRef.current = null;
2114
- draggedPointRef.current = null;
2115
- dragStartRef.current = null;
2116
- if (snap) {
2117
- scene.pushUndoCheckpoint(snap);
2118
- }
2119
- }, [scene]);
2120
- React2.useImperativeHandle(
2121
- ref,
2122
- () => ({
2123
- hasContent: () => scene.list().length > 0,
2124
- serialize: () => {
2125
- const view = boardRef.current?.getView3D();
2126
- const v = view;
2127
- const azSlider = v?.az_slide ?? v?.az;
2128
- const elSlider = v?.el_slide ?? v?.el;
2129
- const azimuth = typeof azSlider?.Value === "function" ? azSlider.Value() : 0;
2130
- const elevation = typeof elSlider?.Value === "function" ? elSlider.Value() : 0;
2131
- return sceneToBoard(
2132
- scene,
2133
- { azimuth, elevation, bbox3D: [...DEFAULT_VIEW3D.bbox3D] },
2134
- // JSXGraph boundingbox order: [xmin, ymax, xmax, ymin]. Must match
2135
- // MiniBoard3D.initBoard so render reproduces the editor's view.
2136
- [-6, 6, 6, -6]
2137
- );
2138
- },
2139
- setTool: (k) => controllerRef.current.selectTool(k),
2140
- undo: () => scene.undo(),
2141
- redo: () => scene.redo()
2142
- }),
2143
- [scene]
2144
- );
2145
- return /* @__PURE__ */ jsxs(
2146
- "div",
2147
- {
2148
- "data-testid": "editor-panel-3d",
2149
- className: [
2150
- isDark ? "theme--dark " : "",
2151
- "flex h-full w-full min-w-0 flex-col overflow-hidden bg-white"
2152
- ].join(""),
2153
- children: [
2154
- /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsx(
2155
- MiniBoard3D,
2156
- {
2157
- ref: boardRef,
2158
- isDark,
2159
- onView3DReady: handleView3DReady,
2160
- onPointerClick: handleClick,
2161
- onPointerMove: handleMove,
2162
- onPointerLeave: () => setHoverLabel(null),
2163
- shouldStartPointDrag,
2164
- onPointerDrag,
2165
- onPointerDragEnd
2166
- }
2167
- ) }),
2168
- /* @__PURE__ */ jsx(StatusHint, { hint, hoverLabel })
2169
- ]
2170
- }
2171
- );
2172
- }
2173
- );
2174
- function ToolButton(props) {
2175
- const {
2176
- toolKey,
2177
- label,
2178
- selected,
2179
- onClick,
2180
- icon,
2181
- chordNum,
2182
- chordActiveGroup,
2183
- onMouseEnter,
2184
- onMouseLeave
2185
- } = props;
2186
- return /* @__PURE__ */ jsxs(
2187
- "button",
2188
- {
2189
- type: "button",
2190
- "data-tool-key": toolKey,
2191
- "data-testid": `tool-${toolKey}`,
2192
- "aria-label": label,
2193
- "aria-pressed": selected,
2194
- onClick: () => onClick(toolKey),
2195
- onMouseEnter,
2196
- onMouseLeave,
2197
- className: [
2198
- "relative flex aspect-square items-center justify-center rounded-md transition",
2199
- selected ? "bg-emerald-600 text-white shadow-sm" : "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
2200
- ].join(" "),
2201
- children: [
2202
- /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "inline-flex", children: icon ?? null }),
2203
- chordNum != null && /* @__PURE__ */ jsx(
2204
- "span",
2205
- {
2206
- "data-testid": `chord-num-${toolKey}`,
2207
- className: [
2208
- "pointer-events-none absolute bottom-0 right-0.5 font-mono text-[9px] leading-none transition",
2209
- selected ? "text-white/70" : chordActiveGroup ? "text-emerald-700" : "text-slate-300"
2210
- ].join(" "),
2211
- children: chordNum
2212
- }
2213
- )
2214
- ]
2215
- }
2216
- );
2217
- }
2218
- var wrap = (children) => /* @__PURE__ */ jsx(
2219
- "svg",
2220
- {
2221
- width: "18",
2222
- height: "18",
2223
- viewBox: "0 0 24 24",
2224
- fill: "none",
2225
- stroke: "currentColor",
2226
- strokeWidth: "1.6",
2227
- strokeLinecap: "round",
2228
- strokeLinejoin: "round",
2229
- "aria-hidden": "true",
2230
- children
2231
- }
2232
- );
2233
- var dot4 = (cx, cy, r = 1.4) => /* @__PURE__ */ jsx("circle", { cx, cy, r, fill: "currentColor", stroke: "none" });
2234
- var ToolIcons = {
2235
- move: wrap(
2236
- /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("path", { d: "M5 4 L5 14 L8 11 L10 16 L13 15 L11 10 L15 10 Z" }) })
2237
- ),
2238
- point: wrap(
2239
- /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "2.4", fill: "currentColor", stroke: "none" }) })
2240
- ),
2241
- pointOnObject: wrap(
2242
- /* @__PURE__ */ jsxs(Fragment, { children: [
2243
- /* @__PURE__ */ jsx("path", { d: "M3 16 L21 12" }),
2244
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "13.5", r: "2.4", fill: "currentColor", stroke: "none" })
2245
- ] })
2246
- ),
2247
- segment: wrap(
2248
- /* @__PURE__ */ jsxs(Fragment, { children: [
2249
- /* @__PURE__ */ jsx("line", { x1: "4", y1: "18", x2: "20", y2: "6" }),
2250
- dot4(4, 18, 1.6),
2251
- dot4(20, 6, 1.6)
2252
- ] })
2253
- ),
2254
- line: wrap(
2255
- /* @__PURE__ */ jsxs(Fragment, { children: [
2256
- /* @__PURE__ */ jsx("line", { x1: "3", y1: "18", x2: "21", y2: "6" }),
2257
- dot4(8, 14.5, 1.4),
2258
- dot4(16, 9.5, 1.4)
2259
- ] })
2260
- ),
2261
- ray: wrap(
2262
- /* @__PURE__ */ jsxs(Fragment, { children: [
2263
- /* @__PURE__ */ jsx("line", { x1: "5", y1: "18", x2: "19", y2: "7" }),
2264
- /* @__PURE__ */ jsx("path", { d: "M19 7 L15 6 M19 7 L18 11" }),
2265
- dot4(5, 18, 1.6)
2266
- ] })
2267
- ),
2268
- vector: wrap(
2269
- /* @__PURE__ */ jsxs(Fragment, { children: [
2270
- /* @__PURE__ */ jsx("line", { x1: "5", y1: "18", x2: "18", y2: "7" }),
2271
- /* @__PURE__ */ jsx("path", { d: "M18 7 L13 7 M18 7 L18 12" }),
2272
- dot4(5, 18, 1.6)
2273
- ] })
2274
- ),
2275
- polygon: wrap(
2276
- /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("polygon", { points: "12,4 20,10 17,19 7,19 4,10" }) })
2277
- ),
2278
- plane: wrap(
2279
- /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("polygon", { points: "3,9 14,5 21,11 10,15" }) })
2280
- ),
2281
- pyramid: wrap(
2282
- /* @__PURE__ */ jsxs(Fragment, { children: [
2283
- /* @__PURE__ */ jsx("path", { d: "M4 19 L20 19 L12 4 Z" }),
2284
- /* @__PURE__ */ jsx("path", { d: "M4 19 L12 16 L20 19" }),
2285
- /* @__PURE__ */ jsx("path", { d: "M12 4 L12 16", strokeDasharray: "2 2" })
2286
- ] })
2287
- ),
2288
- prism: wrap(
2289
- /* @__PURE__ */ jsxs(Fragment, { children: [
2290
- /* @__PURE__ */ jsx("path", { d: "M4 8 L4 19 L14 19 L14 8 Z" }),
2291
- /* @__PURE__ */ jsx("path", { d: "M4 8 L10 4 L20 4 L14 8" }),
2292
- /* @__PURE__ */ jsx("path", { d: "M14 8 L14 19 L20 15 L20 4" }),
2293
- /* @__PURE__ */ jsx("path", { d: "M4 8 L14 8" })
2294
- ] })
2295
- ),
2296
- tetrahedron: wrap(
2297
- /* @__PURE__ */ jsxs(Fragment, { children: [
2298
- /* @__PURE__ */ jsx("path", { d: "M4 19 L20 19 L12 5 Z" }),
2299
- /* @__PURE__ */ jsx("path", { d: "M4 19 L15 12 L20 19" }),
2300
- /* @__PURE__ */ jsx("path", { d: "M15 12 L12 5" })
2301
- ] })
2302
- ),
2303
- cube: wrap(
2304
- /* @__PURE__ */ jsxs(Fragment, { children: [
2305
- /* @__PURE__ */ jsx("path", { d: "M4 8 L4 19 L14 19 L14 8 Z" }),
2306
- /* @__PURE__ */ jsx("path", { d: "M4 8 L10 4 L20 4 L14 8" }),
2307
- /* @__PURE__ */ jsx("path", { d: "M14 8 L14 19 L20 15 L20 4" })
2308
- ] })
2309
- ),
2310
- sphere: wrap(
2311
- /* @__PURE__ */ jsxs(Fragment, { children: [
2312
- /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "8" }),
2313
- /* @__PURE__ */ jsx("ellipse", { cx: "12", cy: "12", rx: "8", ry: "3" }),
2314
- dot4(12, 12, 1.2)
2315
- ] })
2316
- ),
2317
- cylinder: wrap(
2318
- /* @__PURE__ */ jsxs(Fragment, { children: [
2319
- /* @__PURE__ */ jsx("ellipse", { cx: "12", cy: "6", rx: "6", ry: "2" }),
2320
- /* @__PURE__ */ jsx("path", { d: "M6 6 L6 18" }),
2321
- /* @__PURE__ */ jsx("path", { d: "M18 6 L18 18" }),
2322
- /* @__PURE__ */ jsx("ellipse", { cx: "12", cy: "18", rx: "6", ry: "2" })
2323
- ] })
2324
- ),
2325
- cone: wrap(
2326
- /* @__PURE__ */ jsxs(Fragment, { children: [
2327
- /* @__PURE__ */ jsx("line", { x1: "5", y1: "18", x2: "12", y2: "4" }),
2328
- /* @__PURE__ */ jsx("line", { x1: "19", y1: "18", x2: "12", y2: "4" }),
2329
- /* @__PURE__ */ jsx("ellipse", { cx: "12", cy: "18", rx: "7", ry: "2" })
2330
- ] })
2331
- )
2332
- };
2333
-
2334
- // src/stamps/geometry-3d/editor/toolPanel/groups.ts
2335
- var GROUP_ORDER = [
2336
- "basic",
2337
- "point",
2338
- "line",
2339
- "plane",
2340
- "polyhedron",
2341
- "curve"
2342
- ];
2343
- var GROUP_LABELS = {
2344
- basic: "C\u01A1 b\u1EA3n",
2345
- point: "\u0110i\u1EC3m",
2346
- line: "\u0110\u01B0\u1EDDng th\u1EB3ng",
2347
- plane: "M\u1EB7t ph\u1EB3ng",
2348
- polyhedron: "Kh\u1ED1i \u0111a di\u1EC7n",
2349
- curve: "Kh\u1ED1i cong"
2350
- };
2351
- var TOOLS_BY_GROUP = {
2352
- basic: ["move"],
2353
- point: ["point", "pointOnObject"],
2354
- line: ["segment", "line", "ray", "vector", "polygon"],
2355
- plane: ["plane"],
2356
- polyhedron: ["pyramid", "prism", "tetrahedron", "cube"],
2357
- curve: ["sphere", "cylinder", "cone"]
2358
- };
2359
- var SPEC_BY_KEY = TOOLS.reduce(
2360
- (acc, t) => {
2361
- acc[t.key] = t;
2362
- return acc;
2363
- },
2364
- {}
2365
- );
2366
- var TOOLS_FLAT = GROUP_ORDER.flatMap(
2367
- (group) => TOOLS_BY_GROUP[group].map((key) => {
2368
- const spec = SPEC_BY_KEY[key];
2369
- return {
2370
- key,
2371
- label: spec?.label ?? key,
2372
- hint: spec?.hintIdle ?? "",
2373
- group
2374
- };
2375
- })
2376
- );
2377
- var A_CODE = "A".charCodeAt(0);
2378
- function letterForGroup(g) {
2379
- const idx = GROUP_ORDER.indexOf(g);
2380
- return idx >= 0 ? String.fromCharCode(A_CODE + idx) : "";
2381
- }
2382
- function ToolPalette(props) {
2383
- const { selected, onSelect, chordGroup = null, onHoverTool } = props;
2384
- return /* @__PURE__ */ jsx("div", { "data-testid": "tool-palette", className: "flex flex-col gap-3", children: GROUP_ORDER.map((group) => {
2385
- const keys = TOOLS_BY_GROUP[group];
2386
- const isChordActive = chordGroup === group;
2387
- const dimmed = chordGroup !== null && !isChordActive;
2388
- return /* @__PURE__ */ jsxs(
2389
- "section",
2390
- {
2391
- "data-chord-group": group,
2392
- "data-chord-active": isChordActive ? "true" : "false",
2393
- className: [
2394
- "rounded-md transition",
2395
- isChordActive ? "bg-emerald-50 ring-1 ring-emerald-400 p-1" : "p-0",
2396
- dimmed ? "opacity-55" : "opacity-100"
2397
- ].join(" "),
2398
- children: [
2399
- /* @__PURE__ */ jsxs("h4", { className: "mb-1.5 flex items-center justify-between text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: [
2400
- /* @__PURE__ */ jsx("span", { children: GROUP_LABELS[group] }),
2401
- /* @__PURE__ */ jsx(
2402
- "span",
2403
- {
2404
- "data-testid": `chord-letter-${group}`,
2405
- className: [
2406
- "font-mono text-[10px] leading-none transition",
2407
- isChordActive ? "text-emerald-700 font-bold" : "text-slate-400"
2408
- ].join(" "),
2409
- children: letterForGroup(group)
2410
- }
2411
- )
2412
- ] }),
2413
- /* @__PURE__ */ jsx("div", { className: "grid grid-cols-4 gap-1", children: keys.map((k, i) => {
2414
- const tool = TOOLS.find((t) => t.key === k);
2415
- return /* @__PURE__ */ jsx(
2416
- ToolButton,
2417
- {
2418
- toolKey: k,
2419
- label: tool.label,
2420
- selected: selected === k,
2421
- onClick: onSelect,
2422
- icon: ToolIcons[k],
2423
- chordNum: i + 1,
2424
- chordActiveGroup: isChordActive,
2425
- onMouseEnter: (e) => onHoverTool?.({
2426
- label: tool.label,
2427
- hint: tool.hintIdle,
2428
- x: e.clientX,
2429
- y: e.clientY
2430
- }),
2431
- onMouseLeave: () => onHoverTool?.(null)
2432
- },
2433
- k
2434
- );
2435
- }) })
2436
- ]
2437
- },
2438
- group
2439
- );
2440
- }) });
2441
- }
2442
-
2443
- // src/stamps/geometry-3d/editor/algebraPanel/symbolic.ts
2444
- function symbolicFor(obj, scene) {
2445
- const n = (id) => scene.get(id)?.label ?? id;
2446
- switch (obj.kind) {
2447
- case "point": {
2448
- const c = obj.constraint;
2449
- switch (c.kind) {
2450
- case "free":
2451
- return "Point";
2452
- case "onGround":
2453
- return "Point(xyPlane)";
2454
- case "onAxis":
2455
- return `Point(${c.axis}Axis)`;
2456
- case "onPlane":
2457
- return `Point(${n(c.planeId)})`;
2458
- case "onLine":
2459
- return `Point(${n(c.lineId)})`;
2460
- case "onPolygon":
2461
- return `Point(${n(c.polygonId)})`;
2462
- case "onSphere":
2463
- return `Point(${n(c.sphereId)})`;
2464
- }
2465
- return "Point";
2466
- }
2467
- case "segment":
2468
- return `Segment(${n(obj.p1)}, ${n(obj.p2)})`;
2469
- case "line":
2470
- return `Line(${n(obj.p1)}, ${n(obj.p2)})`;
2471
- case "ray":
2472
- return `Ray(${n(obj.origin)}, ${n(obj.through)})`;
2473
- case "vector":
2474
- return `Vector(${n(obj.from)}, ${n(obj.to)})`;
2475
- case "polygon":
2476
- return `Polygon(${obj.vertices.map(n).join(", ")})`;
2477
- case "plane":
2478
- return `Plane(${n(obj.p1)}, ${n(obj.p2)}, ${n(obj.p3)})`;
2479
- case "sphere":
2480
- return `Sphere(${n(obj.center)}, ${n(obj.surfacePoint)})`;
2481
- case "polyhedron": {
2482
- const flavorVn = {
2483
- pyramid: "Ch\xF3p",
2484
- prism: "L\u0103ng tr\u1EE5",
2485
- tetrahedron: "T\u1EE9 di\u1EC7n",
2486
- cube: "L\u1EADp ph\u01B0\u01A1ng"
2487
- };
2488
- return `${flavorVn[obj.flavor]}(${obj.vertices.length} \u0111\u1EC9nh)`;
2489
- }
2490
- case "cylinder":
2491
- return `Cylinder(${n(obj.baseCenter)}, ${n(obj.topCenter)}, r=${obj.radius})`;
2492
- case "cone":
2493
- return `Cone(${n(obj.baseCenter)}, ${n(obj.apex)}, r=${obj.radius})`;
2494
- }
2495
- }
2496
- function numericFor(obj, scene) {
2497
- if (obj.kind === "point") {
2498
- const w = constraintToWorld(obj.constraint, scene);
2499
- return `(${round(w[0])}, ${round(w[1])}, ${round(w[2])})`;
2500
- }
2501
- return "";
2502
- }
2503
- function round(x) {
2504
- return Math.abs(x) < 1e-9 ? "0" : (Math.round(x * 100) / 100).toString();
2505
- }
2506
- function RowMenu(props) {
2507
- const [open, setOpen] = React2.useState(false);
2508
- return /* @__PURE__ */ jsxs("div", { className: "relative inline-block", children: [
2509
- /* @__PURE__ */ jsx(
2510
- "button",
2511
- {
2512
- type: "button",
2513
- "aria-label": "Row menu",
2514
- onClick: () => setOpen((v) => !v),
2515
- className: "rounded px-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-800",
2516
- children: "\u22EE"
2517
- }
2518
- ),
2519
- open ? /* @__PURE__ */ jsxs(
2520
- "div",
2521
- {
2522
- role: "menu",
2523
- className: "absolute right-0 z-10 mt-1 w-40 rounded-md border border-zinc-200 bg-white py-1 text-xs shadow-lg dark:border-zinc-700 dark:bg-zinc-900",
2524
- children: [
2525
- /* @__PURE__ */ jsx(MenuItem, { onClick: () => {
2526
- setOpen(false);
2527
- props.onRename();
2528
- }, children: "\u0110\u1ED5i t\xEAn" }),
2529
- /* @__PURE__ */ jsx(MenuItem, { onClick: () => {
2530
- setOpen(false);
2531
- props.onChangeColor();
2532
- }, children: "\u0110\u1ED5i m\xE0u" }),
2533
- /* @__PURE__ */ jsx(MenuItem, { onClick: () => {
2534
- setOpen(false);
2535
- props.onToggleVisibility();
2536
- }, children: props.visible ? "\u1EA8n" : "Hi\u1EC7n" }),
2537
- /* @__PURE__ */ jsx(MenuItem, { onClick: () => {
2538
- setOpen(false);
2539
- props.onDelete();
2540
- }, className: "text-red-600", children: "Xo\xE1" })
2541
- ]
2542
- }
2543
- ) : null
2544
- ] });
2545
- }
2546
- function MenuItem({ children, onClick, className }) {
2547
- return /* @__PURE__ */ jsx(
2548
- "button",
2549
- {
2550
- type: "button",
2551
- role: "menuitem",
2552
- onClick,
2553
- className: `block w-full px-3 py-1 text-left hover:bg-zinc-100 dark:hover:bg-zinc-800 ${className ?? ""}`,
2554
- children
2555
- }
2556
- );
2557
- }
2558
- function AlgebraRow(props) {
2559
- const { obj, scene, onDelete } = props;
2560
- const symbolic = symbolicFor(obj, scene);
2561
- const numeric = numericFor(obj, scene);
2562
- return /* @__PURE__ */ jsxs(
2563
- "li",
2564
- {
2565
- "data-testid": `algebra-row-${obj.id}`,
2566
- className: "flex items-center gap-2 border-b border-zinc-100 px-3 py-1.5 text-xs dark:border-zinc-800",
2567
- children: [
2568
- /* @__PURE__ */ jsx(
2569
- "span",
2570
- {
2571
- "aria-hidden": true,
2572
- className: "inline-block size-3 rounded-full border",
2573
- style: { backgroundColor: obj.color ?? "#0066cc" }
2574
- }
2575
- ),
2576
- /* @__PURE__ */ jsx("span", { className: "min-w-[3ch] font-semibold", children: obj.label }),
2577
- /* @__PURE__ */ jsx("span", { className: "text-zinc-500", children: "=" }),
2578
- /* @__PURE__ */ jsx("span", { className: "flex-1 truncate font-mono", children: symbolic }),
2579
- numeric ? /* @__PURE__ */ jsx("span", { className: "truncate text-zinc-500", children: numeric }) : null,
2580
- /* @__PURE__ */ jsx(
2581
- RowMenu,
2582
- {
2583
- visible: obj.visible,
2584
- onRename: () => {
2585
- },
2586
- onChangeColor: () => {
2587
- },
2588
- onToggleVisibility: () => {
2589
- },
2590
- onDelete: () => onDelete(obj.id)
2591
- }
2592
- )
2593
- ]
2594
- }
2595
- );
2596
- }
2597
- function AlgebraList(props) {
2598
- const { scene } = props;
2599
- const [, forceUpdate] = React2.useReducer((x) => x + 1, 0);
2600
- React2.useEffect(() => {
2601
- const unsubAdd = scene.on("add", () => forceUpdate());
2602
- const unsubChange = scene.on("change", () => forceUpdate());
2603
- const unsubDelete = scene.on("delete", () => forceUpdate());
2604
- const unsubReset = scene.on("reset", () => forceUpdate());
2605
- return () => {
2606
- unsubAdd();
2607
- unsubChange();
2608
- unsubDelete();
2609
- unsubReset();
2610
- };
2611
- }, [scene]);
2612
- const objects = scene.list();
2613
- return /* @__PURE__ */ jsx(
2614
- "ul",
2615
- {
2616
- "data-testid": "algebra-list",
2617
- className: "flex max-h-[calc(100vh-200px)] flex-col overflow-y-auto",
2618
- children: objects.length === 0 ? /* @__PURE__ */ jsx("li", { className: "px-3 py-4 text-center text-xs text-zinc-500", children: "Ch\u01B0a c\xF3 \u0111\u1ED1i t\u01B0\u1EE3ng n\xE0o" }) : objects.map((o) => /* @__PURE__ */ jsx(AlgebraRow, { obj: o, scene, onDelete: (id) => scene.delete(id) }, o.id))
2619
- }
2620
- );
2621
- }
2622
- var TOOLTIP_DELAY_MS = 400;
2623
- var Geom3DIconHeader = /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
2624
- /* @__PURE__ */ jsx("path", { d: "M4 9 L4 20 L14 20 L14 9 Z" }),
2625
- /* @__PURE__ */ jsx("path", { d: "M4 9 L10 4 L20 4 L14 9 Z" }),
2626
- /* @__PURE__ */ jsx("path", { d: "M14 9 L20 4 L20 15 L14 20 Z" })
2627
- ] });
2628
- function AxisIcon() {
2629
- return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
2630
- /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "20", y2: "20" }),
2631
- /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "4", y2: "4" }),
2632
- /* @__PURE__ */ jsx("line", { x1: "4", y1: "20", x2: "16", y2: "8" })
2633
- ] });
2634
- }
2635
- function GridIcon() {
2636
- return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
2637
- /* @__PURE__ */ jsx("path", { d: "M4 8 L20 4" }),
2638
- /* @__PURE__ */ jsx("path", { d: "M4 14 L20 10" }),
2639
- /* @__PURE__ */ jsx("path", { d: "M4 20 L20 16" }),
2640
- /* @__PURE__ */ jsx("path", { d: "M4 8 L4 20" }),
2641
- /* @__PURE__ */ jsx("path", { d: "M12 6 L12 18" }),
2642
- /* @__PURE__ */ jsx("path", { d: "M20 4 L20 16" })
2643
- ] });
2644
- }
2645
- function UndoIcon() {
2646
- return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
2647
- /* @__PURE__ */ jsx("path", { d: "M3 10 L8 5 L8 8 L15 8 A5 5 0 0 1 20 13 L20 16" }),
2648
- /* @__PURE__ */ jsx("path", { d: "M3 10 L8 15 L8 12" })
2649
- ] });
2650
- }
2651
- function RedoIcon() {
2652
- return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
2653
- /* @__PURE__ */ jsx("path", { d: "M21 10 L16 5 L16 8 L9 8 A5 5 0 0 0 4 13 L4 16" }),
2654
- /* @__PURE__ */ jsx("path", { d: "M21 10 L16 15 L16 12" })
2655
- ] });
2656
- }
2657
- function CloseIcon() {
2658
- return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
2659
- /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
2660
- /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
2661
- ] });
2662
- }
2663
- function Shell({ title, icon, onClose, children, isDark }) {
2664
- return /* @__PURE__ */ jsxs(
2665
- "aside",
2666
- {
2667
- role: "complementary",
2668
- "aria-label": title,
2669
- "data-testid": "left-panel",
2670
- "data-stamp-area": "true",
2671
- className: [
2672
- isDark ? "theme--dark " : "",
2673
- "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"
2674
- ].join(""),
2675
- children: [
2676
- /* @__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: [
2677
- /* @__PURE__ */ jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
2678
- /* @__PURE__ */ jsx("span", { className: "text-base leading-none", children: icon }),
2679
- title
2680
- ] }),
2681
- /* @__PURE__ */ jsx(
2682
- "button",
2683
- {
2684
- onClick: onClose,
2685
- "aria-label": "\u0110\xF3ng",
2686
- className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
2687
- children: /* @__PURE__ */ jsx(CloseIcon, {})
2688
- }
2689
- )
2690
- ] }),
2691
- /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-3", children })
2692
- ]
2693
- }
2694
- );
2695
- }
2696
- function Section({ label, children }) {
2697
- return /* @__PURE__ */ jsxs("section", { children: [
2698
- /* @__PURE__ */ jsx("h4", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: label }),
2699
- children
2700
- ] });
2701
- }
2702
- function useToolHoverTooltip() {
2703
- const [hover, setHover] = React2.useState(null);
2704
- const [portalReady, setPortalReady] = React2.useState(false);
2705
- const hoverTimerRef = React2.useRef(null);
2706
- React2.useEffect(() => {
2707
- setPortalReady(true);
2708
- return () => {
2709
- if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
2710
- };
2711
- }, []);
2712
- const showHover = React2.useCallback((next) => {
2713
- if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
2714
- hoverTimerRef.current = setTimeout(() => setHover(next), TOOLTIP_DELAY_MS);
2715
- }, []);
2716
- const hideHover = React2.useCallback(() => {
2717
- if (hoverTimerRef.current) {
2718
- clearTimeout(hoverTimerRef.current);
2719
- hoverTimerRef.current = null;
2720
- }
2721
- setHover(null);
2722
- }, []);
2723
- return { hover, portalReady, showHover, hideHover };
2724
- }
2725
- function DesktopPanel(props) {
2726
- const {
2727
- scene,
2728
- selectedTool,
2729
- onSelectTool,
2730
- showAxis,
2731
- showGrid,
2732
- onShowAxisChange,
2733
- onShowGridChange,
2734
- onUndo,
2735
- canUndo,
2736
- onRedo,
2737
- canRedo,
2738
- onClose,
2739
- isDark,
2740
- chordGroup
2741
- } = props;
2742
- const [tab, setTab] = React2.useState("tools");
2743
- const { hover, portalReady, showHover, hideHover } = useToolHoverTooltip();
2744
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2745
- /* @__PURE__ */ jsxs(Shell, { title: "H\xECnh h\u1ECDc 3D", icon: Geom3DIconHeader, onClose, isDark, children: [
2746
- /* @__PURE__ */ jsxs("div", { className: "flex gap-1 rounded-md bg-slate-100 p-0.5", children: [
2747
- /* @__PURE__ */ jsx(TabPill, { active: tab === "tools", onClick: () => setTab("tools"), testId: "tab-tools", children: "\u{1F9F0} C\xF4ng c\u1EE5" }),
2748
- /* @__PURE__ */ jsx(TabPill, { active: tab === "algebra", onClick: () => setTab("algebra"), testId: "tab-algebra", children: "\u{1F4D0} \u0110\u1ED1i t\u01B0\u1EE3ng" })
2749
- ] }),
2750
- tab === "tools" ? /* @__PURE__ */ jsxs(Fragment, { children: [
2751
- /* @__PURE__ */ jsx(Section, { label: "G\xF3c nh\xECn", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 text-[11px] text-slate-700", children: [
2752
- /* @__PURE__ */ jsxs("label", { className: "inline-flex select-none items-center gap-1.5", children: [
2753
- /* @__PURE__ */ jsx(
2754
- "input",
2755
- {
2756
- type: "checkbox",
2757
- checked: showAxis,
2758
- onChange: (e) => onShowAxisChange(e.target.checked),
2759
- "data-testid": "toggle-axis"
2760
- }
2761
- ),
2762
- "Tr\u1EE5c"
2763
- ] }),
2764
- /* @__PURE__ */ jsxs("label", { className: "inline-flex select-none items-center gap-1.5", children: [
2765
- /* @__PURE__ */ jsx(
2766
- "input",
2767
- {
2768
- type: "checkbox",
2769
- checked: showGrid,
2770
- onChange: (e) => onShowGridChange(e.target.checked),
2771
- "data-testid": "toggle-grid"
2772
- }
2773
- ),
2774
- "L\u01B0\u1EDBi"
2775
- ] }),
2776
- /* @__PURE__ */ jsxs("div", { className: "ml-auto flex items-center gap-0.5", children: [
2777
- /* @__PURE__ */ jsx(
2778
- "button",
2779
- {
2780
- type: "button",
2781
- onClick: onUndo,
2782
- disabled: !canUndo,
2783
- title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
2784
- "aria-label": "Ho\xE0n t\xE1c",
2785
- "data-testid": "undo-btn",
2786
- 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",
2787
- children: /* @__PURE__ */ jsx(UndoIcon, {})
2788
- }
2789
- ),
2790
- /* @__PURE__ */ jsx(
2791
- "button",
2792
- {
2793
- type: "button",
2794
- onClick: onRedo,
2795
- disabled: !canRedo,
2796
- title: "L\xE0m l\u1EA1i (Ctrl/Cmd+Shift+Z)",
2797
- "aria-label": "L\xE0m l\u1EA1i",
2798
- "data-testid": "redo-btn",
2799
- 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",
2800
- children: /* @__PURE__ */ jsx(RedoIcon, {})
2801
- }
2802
- )
2803
- ] })
2804
- ] }) }),
2805
- /* @__PURE__ */ jsx(
2806
- ToolPalette,
2807
- {
2808
- selected: selectedTool,
2809
- onSelect: onSelectTool,
2810
- chordGroup: chordGroup ?? null,
2811
- onHoverTool: (info) => info ? showHover(info) : hideHover()
2812
- }
2813
- ),
2814
- chordGroup && /* @__PURE__ */ jsxs(
2815
- "div",
2816
- {
2817
- "data-testid": "chord-hint",
2818
- className: "rounded border border-emerald-200 bg-emerald-50/60 px-2 py-1 text-[11px] leading-snug text-slate-600",
2819
- children: [
2820
- /* @__PURE__ */ jsx("span", { className: "font-mono font-semibold text-emerald-700", children: letterForGroup(chordGroup) }),
2821
- /* @__PURE__ */ jsxs("span", { className: "ml-1.5", children: [
2822
- "\u2192 ",
2823
- GROUP_LABELS[chordGroup],
2824
- ". B\u1EA5m s\u1ED1 1-9 \u0111\u1EC3 ch\u1ECDn c\xF4ng c\u1EE5, Esc hu\u1EF7."
2825
- ] })
2826
- ]
2827
- }
2828
- )
2829
- ] }) : /* @__PURE__ */ jsx("section", { "data-testid": "algebra-panel", children: /* @__PURE__ */ jsx(AlgebraList, { scene }) })
2830
- ] }),
2831
- portalReady && hover && typeof document !== "undefined" ? createPortal(
2832
- /* @__PURE__ */ jsxs(
2833
- "div",
2834
- {
2835
- role: "tooltip",
2836
- 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",
2837
- style: {
2838
- left: hover.x + 8,
2839
- top: hover.y,
2840
- transform: "translate(0, -50%)",
2841
- zIndex: 2147483600
2842
- },
2843
- children: [
2844
- /* @__PURE__ */ jsx("span", { className: "block font-medium", children: hover.label }),
2845
- hover.hint && /* @__PURE__ */ jsx("span", { className: "mt-0.5 block text-slate-300", children: hover.hint })
2846
- ]
2847
- }
2848
- ),
2849
- document.body
2850
- ) : null
2851
- ] });
2852
- }
2853
- function TabPill({
2854
- active,
2855
- onClick,
2856
- testId,
2857
- children
2858
- }) {
2859
- return /* @__PURE__ */ jsx(
2860
- "button",
2861
- {
2862
- type: "button",
2863
- onClick,
2864
- "aria-pressed": active,
2865
- "data-testid": testId,
2866
- className: [
2867
- "flex-1 rounded px-2 py-1 text-[11px] font-medium transition",
2868
- active ? "bg-white text-slate-900 shadow-sm ring-1 ring-slate-200" : "text-slate-500 hover:text-slate-800"
2869
- ].join(" "),
2870
- children
2871
- }
2872
- );
2873
- }
2874
- function MobilePanel(props) {
2875
- const {
2876
- selectedTool,
2877
- onSelectTool,
2878
- showAxis,
2879
- showGrid,
2880
- onShowAxisChange,
2881
- onShowGridChange,
2882
- onUndo,
2883
- canUndo,
2884
- onRedo,
2885
- canRedo,
2886
- isDark,
2887
- drawerOpen,
2888
- onDrawerClose
2889
- } = props;
2890
- const groups = React2.useMemo(
2891
- () => GROUP_ORDER.map((group) => {
2892
- const keys = TOOLS_BY_GROUP[group];
2893
- return {
2894
- group,
2895
- groupLabel: GROUP_LABELS[group],
2896
- tools: keys.map((k) => {
2897
- const tool = TOOLS.find((t) => t.key === k);
2898
- return { key: k, label: tool.label, icon: ToolIcons[k] };
2899
- })
2900
- };
2901
- }),
2902
- []
2903
- );
2904
- return /* @__PURE__ */ jsx(
2905
- MobileToolDrawer,
2906
- {
2907
- title: "H\xECnh h\u1ECDc 3D",
2908
- headerIcon: Geom3DIconHeader,
2909
- testId: "left-panel",
2910
- isDark,
2911
- drawerOpen: !!drawerOpen,
2912
- onDrawerClose: () => onDrawerClose?.(),
2913
- chips: [
2914
- {
2915
- label: "Tr\u1EE5c",
2916
- icon: /* @__PURE__ */ jsx(AxisIcon, {}),
2917
- pressed: showAxis,
2918
- onToggle: onShowAxisChange,
2919
- testId: "toggle-axis"
2920
- },
2921
- {
2922
- label: "L\u01B0\u1EDBi",
2923
- icon: /* @__PURE__ */ jsx(GridIcon, {}),
2924
- pressed: showGrid,
2925
- onToggle: onShowGridChange,
2926
- testId: "toggle-grid"
2927
- }
2928
- ],
2929
- actions: [
2930
- {
2931
- label: "Ho\xE0n t\xE1c",
2932
- title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
2933
- icon: /* @__PURE__ */ jsx(UndoIcon, {}),
2934
- onClick: onUndo,
2935
- disabled: !canUndo,
2936
- testId: "undo-btn"
2937
- },
2938
- {
2939
- label: "L\xE0m l\u1EA1i",
2940
- title: "L\xE0m l\u1EA1i (Ctrl/Cmd+Shift+Z)",
2941
- icon: /* @__PURE__ */ jsx(RedoIcon, {}),
2942
- onClick: onRedo,
2943
- disabled: !canRedo,
2944
- testId: "redo-btn"
2945
- }
2946
- ],
2947
- groups,
2948
- activeTool: selectedTool,
2949
- onToolSelect: onSelectTool
2950
- }
2951
- );
2952
- }
2953
- function LeftPanel(props) {
2954
- if (props.isMobile) return /* @__PURE__ */ jsx(MobilePanel, { ...props });
2955
- return /* @__PURE__ */ jsx(DesktopPanel, { ...props });
2956
- }
2957
- function parseInitial(editingElement) {
2958
- if (!editingElement) return null;
2959
- if (!isGeometry3DCustomData(editingElement.customData)) return null;
2960
- try {
2961
- return parseSerializedBoard3D(editingElement.customData.jsonState);
2962
- } catch {
2963
- return null;
2964
- }
2965
- }
2966
- var Geometry3DStampHost = forwardRef(
2967
- function Geometry3DStampHost2({ api, editingElement, onClose, isDark }, ref) {
2968
- const editorRef = useRef(null);
2969
- const sceneRef = useRef(null);
2970
- if (!sceneRef.current) sceneRef.current = new Scene3D();
2971
- const { isMobile } = useIsMobile();
2972
- const [drawerOpen, setDrawerOpen] = useState(false);
2973
- const [ready, setReady] = useState(false);
2974
- const [selectedTool, setSelectedTool] = useState("move");
2975
- const [showAxis, setShowAxis] = useState(true);
2976
- const [showGrid, setShowGrid] = useState(true);
2977
- const [canUndo, setCanUndo] = useState(false);
2978
- const [canRedo, setCanRedo] = useState(false);
2979
- const [hasContent, setHasContent] = useState(false);
2980
- const handleHistoryChange = useCallback((u, r) => {
2981
- setCanUndo(u);
2982
- setCanRedo(r);
2983
- }, []);
2984
- useEffect(() => {
2985
- const scene = sceneRef.current;
2986
- if (!scene) return;
2987
- const sync = () => setHasContent(scene.list().length > 0);
2988
- sync();
2989
- const unsubs = [
2990
- scene.on("add", sync),
2991
- scene.on("delete", sync),
2992
- scene.on("reset", sync)
2993
- ];
2994
- return () => {
2995
- for (const u of unsubs) u();
2996
- };
2997
- }, []);
2998
- const handleUndo = useCallback(() => {
2999
- editorRef.current?.undo();
3000
- }, []);
3001
- const handleRedo = useCallback(() => {
3002
- editorRef.current?.redo();
3003
- }, []);
3004
- const initial = useMemo(
3005
- () => parseInitial(editingElement),
3006
- [editingElement]
3007
- );
3008
- const { chordGroup } = useChordShortcut({
3009
- groupOrder: GROUP_ORDER,
3010
- tools: TOOLS_FLAT,
3011
- onSelect: (key) => {
3012
- setSelectedTool(key);
3013
- editorRef.current?.setTool(key);
3014
- },
3015
- enabled: !isMobile
3016
- });
3017
- const handleSelectTool = useCallback((k) => {
3018
- setSelectedTool(k);
3019
- editorRef.current?.setTool(k);
3020
- }, []);
3021
- const performInsert = useCallback(
3022
- async (board, width, height, svgString) => {
3023
- if (!api) return;
3024
- const jsonState = serializeBoard3D(board);
3025
- await insertStampImage(api, {
3026
- svgString,
3027
- makeCustomData: () => ({
3028
- kind: "geometry3d",
3029
- version: 1,
3030
- jsonState,
3031
- svgWidth: width,
3032
- svgHeight: height
3033
- }),
3034
- editingElementId: editingElement?.id ?? null
3035
- });
3036
- onClose();
3037
- },
3038
- [api, editingElement, onClose]
3039
- );
3040
- const tryInsert = useCallback(() => {
3041
- if (!editorRef.current) return false;
3042
- if (!editorRef.current.hasContent()) return false;
3043
- const board = editorRef.current.serialize();
3044
- if (board.elements.length === 0) return false;
3045
- void (async () => {
3046
- try {
3047
- const jsonState = serializeBoard3D(board);
3048
- const { svgString, width, height } = await renderGeometry3DSvgFromState(jsonState);
3049
- await performInsert(board, width, height, svgString);
3050
- } catch (err) {
3051
- console.error("Geometry3D insert failed:", err);
3052
- }
3053
- })();
3054
- return true;
3055
- }, [performInsert]);
3056
- useImperativeHandle(
3057
- ref,
3058
- () => ({
3059
- tryInsert,
3060
- hasContent: () => editorRef.current?.hasContent() ?? false
3061
- }),
3062
- [tryInsert]
3063
- );
3064
- const handleEditorInsert = useCallback(
3065
- (board, width, height, svgString) => {
3066
- void performInsert(board, width, height, svgString);
3067
- },
3068
- [performInsert]
3069
- );
3070
- const dialogStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 40 } : {
3071
- position: "absolute",
3072
- top: "50%",
3073
- left: "calc(50% + 120px)",
3074
- transform: "translate(-50%, -50%)",
3075
- zIndex: 40
3076
- };
3077
- return /* @__PURE__ */ jsxs(Fragment, { children: [
3078
- !isMobile && /* @__PURE__ */ jsx(
3079
- LeftPanel,
3080
- {
3081
- scene: sceneRef.current,
3082
- selectedTool,
3083
- onSelectTool: handleSelectTool,
3084
- showAxis,
3085
- showGrid,
3086
- onShowAxisChange: setShowAxis,
3087
- onShowGridChange: setShowGrid,
3088
- onUndo: handleUndo,
3089
- canUndo,
3090
- onRedo: handleRedo,
3091
- canRedo,
3092
- onClose,
3093
- isDark,
3094
- chordGroup
3095
- }
3096
- ),
3097
- /* @__PURE__ */ jsxs(
3098
- "div",
3099
- {
3100
- role: "dialog",
3101
- "aria-label": "D\u1EF1ng h\xECnh h\u1ECDc 3D",
3102
- "data-testid": "geom3d-host",
3103
- "data-stamp-area": "true",
3104
- style: dialogStyle,
3105
- className: [
3106
- isDark ? "theme--dark " : "",
3107
- "flex flex-col overflow-hidden bg-white",
3108
- isMobile ? "h-full w-full" : "h-[600px] max-h-[85vh] w-[800px] max-w-[calc(100vw-320px)] rounded-lg border border-slate-300 shadow-2xl ring-1 ring-black/5"
3109
- ].join(" "),
3110
- children: [
3111
- /* @__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: [
3112
- isMobile && /* @__PURE__ */ jsx(
3113
- "button",
3114
- {
3115
- type: "button",
3116
- onClick: () => setDrawerOpen(true),
3117
- "aria-label": "M\u1EDF ng\u0103n c\xF4ng c\u1EE5",
3118
- className: "-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15",
3119
- children: /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3120
- /* @__PURE__ */ jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
3121
- /* @__PURE__ */ jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
3122
- /* @__PURE__ */ jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
3123
- ] })
3124
- }
3125
- ),
3126
- /* @__PURE__ */ jsxs("h3", { className: "flex flex-1 items-center gap-2 text-sm font-semibold", children: [
3127
- /* @__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 9 L4 20 L14 20 L14 9 Z M4 9 L10 4 L20 4 L14 9 Z M14 9 L20 4 L20 15 L14 20 Z" }) }),
3128
- "D\u1EF1ng h\xECnh h\u1ECDc kh\xF4ng gian"
3129
- ] }),
3130
- isMobile && /* @__PURE__ */ jsx(
3131
- "button",
3132
- {
3133
- type: "button",
3134
- onClick: tryInsert,
3135
- disabled: !ready || !hasContent,
3136
- title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
3137
- "data-testid": "geom3d-insert-btn-mobile",
3138
- className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
3139
- children: "Ch\xE8n"
3140
- }
3141
- ),
3142
- /* @__PURE__ */ jsx(
3143
- "button",
3144
- {
3145
- onClick: onClose,
3146
- "aria-label": "\u0110\xF3ng",
3147
- className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15",
3148
- children: /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3149
- /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
3150
- /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
3151
- ] })
3152
- }
3153
- )
3154
- ] }),
3155
- /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsx(
3156
- EditorPanel,
3157
- {
3158
- ref: editorRef,
3159
- isDark,
3160
- initialState: initial,
3161
- onInsert: handleEditorInsert,
3162
- scene: sceneRef.current,
3163
- selectedTool,
3164
- onSelectedToolChange: setSelectedTool,
3165
- showAxis,
3166
- showGrid,
3167
- onReadyChange: setReady,
3168
- onHistoryChange: handleHistoryChange
3169
- }
3170
- ) }),
3171
- !isMobile && /* @__PURE__ */ jsxs("footer", { className: "flex items-center justify-between border-t border-slate-200 bg-slate-50 px-3 py-2", children: [
3172
- /* @__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." }),
3173
- /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
3174
- /* @__PURE__ */ jsx(
3175
- "button",
3176
- {
3177
- onClick: onClose,
3178
- className: "rounded border border-slate-300 bg-white px-3 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100",
3179
- children: "Hu\u1EF7"
3180
- }
3181
- ),
3182
- /* @__PURE__ */ jsx(
3183
- "button",
3184
- {
3185
- onClick: tryInsert,
3186
- disabled: !ready || !hasContent,
3187
- title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
3188
- "data-testid": "geom3d-insert-btn",
3189
- className: "rounded bg-emerald-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-emerald-700 disabled:opacity-50",
3190
- children: "Ch\xE8n"
3191
- }
3192
- )
3193
- ] })
3194
- ] })
3195
- ]
3196
- }
3197
- ),
3198
- isMobile && /* @__PURE__ */ jsx(
3199
- LeftPanel,
3200
- {
3201
- scene: sceneRef.current,
3202
- selectedTool,
3203
- onSelectTool: handleSelectTool,
3204
- showAxis,
3205
- showGrid,
3206
- onShowAxisChange: setShowAxis,
3207
- onShowGridChange: setShowGrid,
3208
- onUndo: handleUndo,
3209
- canUndo,
3210
- onRedo: handleRedo,
3211
- canRedo,
3212
- onClose,
3213
- isDark,
3214
- isMobile: true,
3215
- drawerOpen,
3216
- onDrawerClose: () => setDrawerOpen(false),
3217
- chordGroup
3218
- }
3219
- )
3220
- ] });
3221
- }
3222
- );
3223
-
3224
- export { Geometry3DStampHost };
3225
- //# sourceMappingURL=host-N6ACNJKI.mjs.map
3226
- //# sourceMappingURL=host-N6ACNJKI.mjs.map