@xom11/whiteboard 0.6.5 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +87 -1
  2. package/dist/chunk-74VEEZBV.mjs +619 -0
  3. package/dist/chunk-74VEEZBV.mjs.map +1 -0
  4. package/dist/chunk-7P7SQFOW.mjs +39 -0
  5. package/dist/chunk-7P7SQFOW.mjs.map +1 -0
  6. package/dist/chunk-C6SCVOMC.mjs +111 -0
  7. package/dist/chunk-C6SCVOMC.mjs.map +1 -0
  8. package/dist/chunk-DU2NFHRR.mjs +103 -0
  9. package/dist/chunk-DU2NFHRR.mjs.map +1 -0
  10. package/dist/chunk-DU3RHKT5.mjs +44 -0
  11. package/dist/chunk-DU3RHKT5.mjs.map +1 -0
  12. package/dist/chunk-HTBLO5JO.mjs +41 -0
  13. package/dist/chunk-HTBLO5JO.mjs.map +1 -0
  14. package/dist/chunk-IUVV52HO.mjs +144 -0
  15. package/dist/chunk-IUVV52HO.mjs.map +1 -0
  16. package/dist/chunk-KEYZ5EZT.mjs +154 -0
  17. package/dist/chunk-KEYZ5EZT.mjs.map +1 -0
  18. package/dist/chunk-P2AOIF7S.mjs +40 -0
  19. package/dist/chunk-P2AOIF7S.mjs.map +1 -0
  20. package/dist/chunk-SBDMF4NQ.mjs +212 -0
  21. package/dist/chunk-SBDMF4NQ.mjs.map +1 -0
  22. package/dist/chunk-X5R72SSJ.mjs +52 -0
  23. package/dist/chunk-X5R72SSJ.mjs.map +1 -0
  24. package/dist/chunk-ZVN356JZ.mjs +58 -0
  25. package/dist/chunk-ZVN356JZ.mjs.map +1 -0
  26. package/dist/geometry-2d.d.mts +16 -0
  27. package/dist/geometry-2d.d.ts +16 -0
  28. package/dist/geometry-2d.js +3581 -0
  29. package/dist/geometry-2d.js.map +1 -0
  30. package/dist/geometry-2d.mjs +7 -0
  31. package/dist/geometry-2d.mjs.map +1 -0
  32. package/dist/geometry-3d.d.mts +16 -0
  33. package/dist/geometry-3d.d.ts +16 -0
  34. package/dist/geometry-3d.js +4105 -0
  35. package/dist/geometry-3d.js.map +1 -0
  36. package/dist/geometry-3d.mjs +7 -0
  37. package/dist/geometry-3d.mjs.map +1 -0
  38. package/dist/graph-2d.d.mts +16 -0
  39. package/dist/graph-2d.d.ts +16 -0
  40. package/dist/graph-2d.js +2019 -0
  41. package/dist/graph-2d.js.map +1 -0
  42. package/dist/graph-2d.mjs +6 -0
  43. package/dist/graph-2d.mjs.map +1 -0
  44. package/dist/host-LZH2FZ2N.mjs +1066 -0
  45. package/dist/host-LZH2FZ2N.mjs.map +1 -0
  46. package/dist/host-PIIDSMVE.mjs +3187 -0
  47. package/dist/host-PIIDSMVE.mjs.map +1 -0
  48. package/dist/host-VDNAJMLC.mjs +2864 -0
  49. package/dist/host-VDNAJMLC.mjs.map +1 -0
  50. package/dist/host-Z3TEJKZA.mjs +466 -0
  51. package/dist/host-Z3TEJKZA.mjs.map +1 -0
  52. package/dist/index.d.mts +30 -148
  53. package/dist/index.d.ts +30 -148
  54. package/dist/index.js +8370 -5614
  55. package/dist/index.js.map +1 -1
  56. package/dist/index.mjs +395 -7294
  57. package/dist/index.mjs.map +1 -1
  58. package/dist/latex.d.mts +15 -0
  59. package/dist/latex.d.ts +15 -0
  60. package/dist/latex.js +750 -0
  61. package/dist/latex.js.map +1 -0
  62. package/dist/latex.mjs +6 -0
  63. package/dist/latex.mjs.map +1 -0
  64. package/dist/types-CinstD7T.d.mts +110 -0
  65. package/dist/types-CinstD7T.d.ts +110 -0
  66. package/package.json +26 -7
@@ -0,0 +1,4105 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ var React2 = require('react');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+ var reactDom = require('react-dom');
7
+
8
+ function _interopNamespace(e) {
9
+ if (e && e.__esModule) return e;
10
+ var n = Object.create(null);
11
+ if (e) {
12
+ Object.keys(e).forEach(function (k) {
13
+ if (k !== 'default') {
14
+ var d = Object.getOwnPropertyDescriptor(e, k);
15
+ Object.defineProperty(n, k, d.get ? d : {
16
+ enumerable: true,
17
+ get: function () { return e[k]; }
18
+ });
19
+ }
20
+ });
21
+ }
22
+ n.default = e;
23
+ return Object.freeze(n);
24
+ }
25
+
26
+ var React2__namespace = /*#__PURE__*/_interopNamespace(React2);
27
+
28
+ var __defProp = Object.defineProperty;
29
+ var __getOwnPropNames = Object.getOwnPropertyNames;
30
+ var __esm = (fn, res) => function __init() {
31
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
32
+ };
33
+ var __export = (target, all) => {
34
+ for (var name in all)
35
+ __defProp(target, name, { get: all[name], enumerable: true });
36
+ };
37
+
38
+ // src/stamps/geometry-3d/serialize.ts
39
+ function isGeometry3DCustomData(data) {
40
+ if (!data || typeof data !== "object") return false;
41
+ const d = data;
42
+ return d.kind === "geometry3d" && (d.version === 1 || d.version === 2) && typeof d.jsonState === "string";
43
+ }
44
+ function serializeBoard3D(state) {
45
+ return JSON.stringify(state);
46
+ }
47
+ function parseSerializedBoard3D(json) {
48
+ const parsed = JSON.parse(json);
49
+ if (!parsed || typeof parsed !== "object") {
50
+ throw new Error("parseSerializedBoard3D: not an object");
51
+ }
52
+ const p = parsed;
53
+ if (p.version !== 1 && p.version !== 2) {
54
+ throw new Error(`parseSerializedBoard3D: unsupported version ${String(p.version)}`);
55
+ }
56
+ if (!Array.isArray(p.elements)) {
57
+ throw new Error("parseSerializedBoard3D: elements missing");
58
+ }
59
+ return parsed;
60
+ }
61
+ var init_serialize = __esm({
62
+ "src/stamps/geometry-3d/serialize.ts"() {
63
+ }
64
+ });
65
+
66
+ // src/stamps/geometry-2d/editor/theme.ts
67
+ function paletteFor(isDark) {
68
+ return {
69
+ stroke: themeStroke(isDark),
70
+ axis: themeAxis(isDark),
71
+ grid: themeGrid(isDark),
72
+ label: themeLabel(isDark)
73
+ };
74
+ }
75
+ var themeStroke, themeAxis, themeGrid, themeLabel;
76
+ var init_theme = __esm({
77
+ "src/stamps/geometry-2d/editor/theme.ts"() {
78
+ themeStroke = (dark) => dark ? "#e2e8f0" : "#0f172a";
79
+ themeAxis = (dark) => dark ? "#cbd5e1" : "#94a3b8";
80
+ themeGrid = (dark) => dark ? "#475569" : "#e2e8f0";
81
+ themeLabel = (dark) => dark ? "#e2e8f0" : "#000000";
82
+ }
83
+ });
84
+
85
+ // src/stamps/geometry-3d/editor/theme.ts
86
+ function paletteFor2(isDark) {
87
+ const base = paletteFor(isDark);
88
+ return {
89
+ ...base,
90
+ view3dBg: isDark ? "#1a1a1a" : "#ffffff",
91
+ axisX: "#d63b3b",
92
+ axisY: "#2d8a2d",
93
+ axisZ: "#2d6dd6"
94
+ };
95
+ }
96
+ var DEFAULT_VIEW3D, VIEW3D_ATTRS, GROUND_PLANE_ATTRS, GROUND_PLANE_RANGE;
97
+ var init_theme2 = __esm({
98
+ "src/stamps/geometry-3d/editor/theme.ts"() {
99
+ init_theme();
100
+ DEFAULT_VIEW3D = {
101
+ azimuth: 0.7,
102
+ elevation: 0.4,
103
+ bbox3D: [-3, -3, -3, 3, 3, 3]
104
+ };
105
+ VIEW3D_ATTRS = (isDark) => {
106
+ const p = paletteFor2(isDark);
107
+ const axisLabel = (color) => ({
108
+ strokeColor: color,
109
+ fontSize: 14,
110
+ offset: [10, 0]
111
+ });
112
+ return {
113
+ az: { slider: { visible: false }, point2: { visible: false } },
114
+ el: { slider: { visible: false } },
115
+ projection: "central",
116
+ // GeoGebra-style: axes pass through origin (0,0,0) instead of bbox border.
117
+ axesPosition: "center",
118
+ xAxis: {
119
+ strokeColor: p.axisX,
120
+ strokeWidth: 2,
121
+ lastArrow: { type: 2, size: 8 },
122
+ name: "x",
123
+ withLabel: true,
124
+ label: axisLabel(p.axisX)
125
+ },
126
+ yAxis: {
127
+ strokeColor: p.axisY,
128
+ strokeWidth: 2,
129
+ lastArrow: { type: 2, size: 8 },
130
+ name: "y",
131
+ withLabel: true,
132
+ label: axisLabel(p.axisY)
133
+ },
134
+ zAxis: {
135
+ strokeColor: p.axisZ,
136
+ strokeWidth: 2,
137
+ lastArrow: { type: 2, size: 8 },
138
+ name: "z",
139
+ withLabel: true,
140
+ label: axisLabel(p.axisZ)
141
+ },
142
+ // GeoGebra-style: hide ALL bbox wall planes; the XY ground plane is drawn
143
+ // explicitly at z=0 via the helper below (so it coincides with Ox/Oy).
144
+ xPlaneRear: { visible: false, mesh3d: { visible: false } },
145
+ yPlaneRear: { visible: false, mesh3d: { visible: false } },
146
+ zPlaneRear: { visible: false, mesh3d: { visible: false } }
147
+ };
148
+ };
149
+ GROUND_PLANE_ATTRS = (isDark) => ({
150
+ fillColor: isDark ? "#2a2a2a" : "#e6e6e6",
151
+ fillOpacity: isDark ? 0.5 : 0.55,
152
+ strokeColor: isDark ? "#3a3a3a" : "#cfcfcf",
153
+ strokeOpacity: 0.7,
154
+ strokeWidth: 1,
155
+ fixed: true,
156
+ highlight: false,
157
+ withLabel: false,
158
+ layer: 0
159
+ });
160
+ GROUND_PLANE_RANGE = [-3, 3];
161
+ }
162
+ });
163
+
164
+ // src/stamps/geometry-3d/editor/tools/handlers/_ensurePoint.ts
165
+ function hitToConstraint(hit) {
166
+ switch (hit.kind) {
167
+ case "onGround":
168
+ return { kind: "onGround", x: hit.world[0], y: hit.world[1] };
169
+ case "onAxis":
170
+ return { kind: "onAxis", axis: hit.axis, t: hit.t };
171
+ case "onPlane":
172
+ return { kind: "onPlane", planeId: hit.planeId, u: hit.u, v: hit.v };
173
+ case "onLine":
174
+ return { kind: "onLine", lineId: hit.lineId, t: hit.t };
175
+ case "onPolygon":
176
+ return { kind: "onPolygon", polygonId: hit.polygonId, u: hit.u, v: hit.v };
177
+ case "onSphere":
178
+ return { kind: "onSphere", sphereId: hit.sphereId, theta: hit.theta, phi: hit.phi };
179
+ default:
180
+ return null;
181
+ }
182
+ }
183
+ function ensurePoint(hit, scene) {
184
+ if (hit.kind === "existingPoint") return hit.pointId;
185
+ const c = hitToConstraint(hit);
186
+ if (!c) return null;
187
+ return scene.addPoint(c);
188
+ }
189
+ var init_ensurePoint = __esm({
190
+ "src/stamps/geometry-3d/editor/tools/handlers/_ensurePoint.ts"() {
191
+ }
192
+ });
193
+
194
+ // src/stamps/geometry-3d/editor/tools/handlers/point.ts
195
+ function buildPoint(args, scene) {
196
+ const hit = args[0]?.hit;
197
+ if (!hit) return null;
198
+ if (hit.kind === "existingPoint") return hit.pointId;
199
+ const c = hitToConstraint(hit);
200
+ if (!c) return null;
201
+ return scene.addPoint(c);
202
+ }
203
+ var buildPointOnObject;
204
+ var init_point = __esm({
205
+ "src/stamps/geometry-3d/editor/tools/handlers/point.ts"() {
206
+ init_ensurePoint();
207
+ buildPointOnObject = buildPoint;
208
+ }
209
+ });
210
+
211
+ // src/stamps/geometry-3d/editor/tools/handlers/segment.ts
212
+ function buildSegment(args, scene) {
213
+ if (args.length < 2 || !args[0].hit || !args[1].hit) return null;
214
+ const p1 = ensurePoint(args[0].hit, scene);
215
+ const p2 = ensurePoint(args[1].hit, scene);
216
+ if (!p1 || !p2 || p1 === p2) return null;
217
+ return scene.addObject("segment", { p1, p2 });
218
+ }
219
+ function buildLine(args, scene) {
220
+ if (args.length < 2 || !args[0].hit || !args[1].hit) return null;
221
+ const p1 = ensurePoint(args[0].hit, scene);
222
+ const p2 = ensurePoint(args[1].hit, scene);
223
+ if (!p1 || !p2 || p1 === p2) return null;
224
+ return scene.addObject("line", { p1, p2 });
225
+ }
226
+ function buildRay(args, scene) {
227
+ if (args.length < 2 || !args[0].hit || !args[1].hit) return null;
228
+ const origin = ensurePoint(args[0].hit, scene);
229
+ const through = ensurePoint(args[1].hit, scene);
230
+ if (!origin || !through || origin === through) return null;
231
+ return scene.addObject("ray", { origin, through });
232
+ }
233
+ function buildVector(args, scene) {
234
+ if (args.length < 2 || !args[0].hit || !args[1].hit) return null;
235
+ const from = ensurePoint(args[0].hit, scene);
236
+ const to = ensurePoint(args[1].hit, scene);
237
+ if (!from || !to || from === to) return null;
238
+ return scene.addObject("vector", { from, to });
239
+ }
240
+ var init_segment = __esm({
241
+ "src/stamps/geometry-3d/editor/tools/handlers/segment.ts"() {
242
+ init_ensurePoint();
243
+ }
244
+ });
245
+
246
+ // src/stamps/geometry-3d/editor/tools/handlers/polygon.ts
247
+ function buildPolygon(args, scene) {
248
+ const vertexArgs = args.filter((a) => a.step.type === "point");
249
+ const vertexIds = vertexArgs.map((a) => a.hit ? ensurePoint(a.hit, scene) : null).filter((x) => !!x);
250
+ if (vertexIds.length < 3) return null;
251
+ return scene.addObject("polygon", { vertices: vertexIds });
252
+ }
253
+ var init_polygon = __esm({
254
+ "src/stamps/geometry-3d/editor/tools/handlers/polygon.ts"() {
255
+ init_ensurePoint();
256
+ }
257
+ });
258
+
259
+ // src/stamps/geometry-3d/editor/scene/constraintMath.ts
260
+ function sub(a, b) {
261
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
262
+ }
263
+ function add(a, b) {
264
+ return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
265
+ }
266
+ function scale(a, k) {
267
+ return [a[0] * k, a[1] * k, a[2] * k];
268
+ }
269
+ function dot(a, b) {
270
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
271
+ }
272
+ function cross(a, b) {
273
+ 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]];
274
+ }
275
+ function norm(a) {
276
+ return Math.sqrt(dot(a, a));
277
+ }
278
+ function normalize(a) {
279
+ const n = norm(a);
280
+ return n === 0 ? a : scale(a, 1 / n);
281
+ }
282
+ function getPointWorld(id, scene) {
283
+ const obj = scene.get(id);
284
+ if (!obj || obj.kind !== "point") {
285
+ throw new Error(`constraintMath: point ${id} not found`);
286
+ }
287
+ return constraintToWorld(obj.constraint, scene);
288
+ }
289
+ function getPlaneBasis(planeObj, scene) {
290
+ const p1 = getPointWorld(planeObj.p1, scene);
291
+ const p2 = getPointWorld(planeObj.p2, scene);
292
+ const p3 = getPointWorld(planeObj.p3, scene);
293
+ const basis1 = sub(p2, p1);
294
+ const tmp = sub(p3, p1);
295
+ const normal = normalize(cross(basis1, tmp));
296
+ const basis2 = cross(normal, basis1);
297
+ return { origin: p1, basis1, basis2, normal };
298
+ }
299
+ function constraintToWorld(c, scene) {
300
+ switch (c.kind) {
301
+ case "free":
302
+ return [c.x, c.y, c.z];
303
+ case "onGround":
304
+ return [c.x, c.y, 0];
305
+ case "onAxis": {
306
+ if (c.axis === "x") return [c.t, 0, 0];
307
+ if (c.axis === "y") return [0, c.t, 0];
308
+ return [0, 0, c.t];
309
+ }
310
+ case "onPlane": {
311
+ const plane = scene.get(c.planeId);
312
+ if (!plane || plane.kind !== "plane") throw new Error("onPlane: plane missing");
313
+ const { origin, basis1, basis2 } = getPlaneBasis(plane, scene);
314
+ return add(add(origin, scale(basis1, c.u)), scale(basis2, c.v));
315
+ }
316
+ case "onLine": {
317
+ const line = scene.get(c.lineId);
318
+ if (!line || line.kind !== "line" && line.kind !== "segment" && line.kind !== "ray") {
319
+ throw new Error("onLine: parent missing");
320
+ }
321
+ const p1Id = line.kind === "ray" ? line.origin : line.p1;
322
+ const p2Id = line.kind === "ray" ? line.through : line.p2;
323
+ const p1 = getPointWorld(p1Id, scene);
324
+ const p2 = getPointWorld(p2Id, scene);
325
+ const dir = sub(p2, p1);
326
+ return add(p1, scale(dir, c.t));
327
+ }
328
+ case "onPolygon": {
329
+ const pg = scene.get(c.polygonId);
330
+ if (!pg || pg.kind !== "polygon") throw new Error("onPolygon: parent missing");
331
+ const v = pg.vertices;
332
+ if (v.length < 3) throw new Error("onPolygon: < 3 vertices");
333
+ const p1 = getPointWorld(v[0], scene);
334
+ const p2 = getPointWorld(v[1], scene);
335
+ const p3 = getPointWorld(v[2], scene);
336
+ const basis1 = sub(p2, p1);
337
+ const tmp = sub(p3, p1);
338
+ const normal = normalize(cross(basis1, tmp));
339
+ const basis2 = cross(normal, basis1);
340
+ return add(add(p1, scale(basis1, c.u)), scale(basis2, c.v));
341
+ }
342
+ case "onSphere": {
343
+ const sph = scene.get(c.sphereId);
344
+ if (!sph || sph.kind !== "sphere") throw new Error("onSphere: parent missing");
345
+ const center = getPointWorld(sph.center, scene);
346
+ const surface = getPointWorld(sph.surfacePoint, scene);
347
+ const radius = norm(sub(surface, center));
348
+ const x = center[0] + radius * Math.sin(c.phi) * Math.cos(c.theta);
349
+ const y = center[1] + radius * Math.sin(c.phi) * Math.sin(c.theta);
350
+ const z = center[2] + radius * Math.cos(c.phi);
351
+ return [x, y, z];
352
+ }
353
+ }
354
+ }
355
+ var init_constraintMath = __esm({
356
+ "src/stamps/geometry-3d/editor/scene/constraintMath.ts"() {
357
+ }
358
+ });
359
+
360
+ // src/stamps/geometry-3d/editor/scene/geometryChecks.ts
361
+ function getWorld(id, scene) {
362
+ const obj = scene.get(id);
363
+ if (!obj || obj.kind !== "point") return null;
364
+ return constraintToWorld(obj.constraint, scene);
365
+ }
366
+ function areCollinear3(p1Id, p2Id, p3Id, scene) {
367
+ const p1 = getWorld(p1Id, scene);
368
+ const p2 = getWorld(p2Id, scene);
369
+ const p3 = getWorld(p3Id, scene);
370
+ if (!p1 || !p2 || !p3) return true;
371
+ const a = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];
372
+ const b = [p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2]];
373
+ 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]];
374
+ return Math.hypot(c[0], c[1], c[2]) < EPS;
375
+ }
376
+ function apexCoplanarWithBase(baseIds, apexId, scene) {
377
+ if (baseIds.length < 3) return false;
378
+ const p1 = getWorld(baseIds[0], scene);
379
+ const p2 = getWorld(baseIds[1], scene);
380
+ const p3 = getWorld(baseIds[2], scene);
381
+ const apex = getWorld(apexId, scene);
382
+ if (!p1 || !p2 || !p3 || !apex) return false;
383
+ const a = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];
384
+ const b = [p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2]];
385
+ 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]];
386
+ const d = [apex[0] - p1[0], apex[1] - p1[1], apex[2] - p1[2]];
387
+ const dotND = n[0] * d[0] + n[1] * d[1] + n[2] * d[2];
388
+ return Math.abs(dotND) < EPS;
389
+ }
390
+ var EPS;
391
+ var init_geometryChecks = __esm({
392
+ "src/stamps/geometry-3d/editor/scene/geometryChecks.ts"() {
393
+ init_constraintMath();
394
+ EPS = 1e-6;
395
+ }
396
+ });
397
+
398
+ // src/stamps/geometry-3d/editor/tools/handlers/plane.ts
399
+ function buildPlane(args, scene) {
400
+ if (args.length < 3 || !args[0].hit || !args[1].hit || !args[2].hit) return null;
401
+ const p1 = ensurePoint(args[0].hit, scene);
402
+ const p2 = ensurePoint(args[1].hit, scene);
403
+ const p3 = ensurePoint(args[2].hit, scene);
404
+ if (!p1 || !p2 || !p3) return null;
405
+ if (p1 === p2 || p2 === p3 || p1 === p3) return null;
406
+ if (areCollinear3(p1, p2, p3, scene)) return null;
407
+ return scene.addObject("plane", { p1, p2, p3 });
408
+ }
409
+ var init_plane = __esm({
410
+ "src/stamps/geometry-3d/editor/tools/handlers/plane.ts"() {
411
+ init_ensurePoint();
412
+ init_geometryChecks();
413
+ }
414
+ });
415
+
416
+ // src/stamps/geometry-3d/editor/tools/handlers/pyramid.ts
417
+ function buildPyramid(args, scene) {
418
+ const pointArgs = args.filter((a) => a.step.type === "point");
419
+ const baseArgs = pointArgs.slice(0, -1);
420
+ const apexArg = pointArgs.slice(-1)[0];
421
+ if (baseArgs.length < 3 || !apexArg?.hit) return null;
422
+ const baseIds = baseArgs.map((a) => a.hit ? ensurePoint(a.hit, scene) : null).filter((x) => !!x);
423
+ const apexId = ensurePoint(apexArg.hit, scene);
424
+ if (!apexId || baseIds.length < 3) return null;
425
+ if (apexCoplanarWithBase(baseIds, apexId, scene)) return null;
426
+ const vertices = [...baseIds, apexId];
427
+ const apexIdx = vertices.length - 1;
428
+ const faces = [baseIds.map((_, i) => i)];
429
+ for (let i = 0; i < baseIds.length; i++) {
430
+ faces.push([i, (i + 1) % baseIds.length, apexIdx]);
431
+ }
432
+ return scene.addObject("polyhedron", { flavor: "pyramid", vertices, faces });
433
+ }
434
+ var init_pyramid = __esm({
435
+ "src/stamps/geometry-3d/editor/tools/handlers/pyramid.ts"() {
436
+ init_ensurePoint();
437
+ init_geometryChecks();
438
+ }
439
+ });
440
+
441
+ // src/stamps/geometry-3d/editor/tools/handlers/prism.ts
442
+ function buildPrism(args, scene) {
443
+ const baseArgs = args.filter((a) => a.step.type === "point");
444
+ const numberArg = args.find((a) => a.step.type === "number");
445
+ if (baseArgs.length < 3 || !numberArg || typeof numberArg.value !== "number") return null;
446
+ const height = numberArg.value;
447
+ if (height <= 0) return null;
448
+ const baseIds = baseArgs.map((a) => a.hit ? ensurePoint(a.hit, scene) : null).filter((x) => !!x);
449
+ if (baseIds.length < 3) return null;
450
+ const topIds = [];
451
+ for (const id of baseIds) {
452
+ const p = scene.get(id);
453
+ if (!p || p.kind !== "point") return null;
454
+ const w = constraintToWorld(p.constraint, scene);
455
+ topIds.push(scene.addPoint({ kind: "free", x: w[0], y: w[1], z: w[2] + height }));
456
+ }
457
+ const n = baseIds.length;
458
+ const vertices = [...baseIds, ...topIds];
459
+ const faces = [
460
+ baseIds.map((_, i) => i),
461
+ topIds.map((_, i) => n + i)
462
+ ];
463
+ for (let i = 0; i < n; i++) {
464
+ faces.push([i, (i + 1) % n, n + (i + 1) % n, n + i]);
465
+ }
466
+ return scene.addObject("polyhedron", { flavor: "prism", vertices, faces });
467
+ }
468
+ var init_prism = __esm({
469
+ "src/stamps/geometry-3d/editor/tools/handlers/prism.ts"() {
470
+ init_ensurePoint();
471
+ init_constraintMath();
472
+ }
473
+ });
474
+
475
+ // src/stamps/geometry-3d/editor/tools/handlers/tetrahedron.ts
476
+ function buildTetrahedron(args, scene) {
477
+ if (args.length < 2 || !args[0].hit || !args[1].hit) return null;
478
+ const p1Id = ensurePoint(args[0].hit, scene);
479
+ const p2Id = ensurePoint(args[1].hit, scene);
480
+ if (!p1Id || !p2Id || p1Id === p2Id) return null;
481
+ const p1Obj = scene.get(p1Id);
482
+ const p2Obj = scene.get(p2Id);
483
+ if (!p1Obj || p1Obj.kind !== "point" || !p2Obj || p2Obj.kind !== "point") return null;
484
+ const p1 = constraintToWorld(p1Obj.constraint, scene);
485
+ const p2 = constraintToWorld(p2Obj.constraint, scene);
486
+ const z0 = Math.min(p1[2], p2[2]);
487
+ const baseA = [p1[0], p1[1], z0];
488
+ const baseB = [p2[0], p2[1], z0];
489
+ const dx = baseB[0] - baseA[0];
490
+ const dy = baseB[1] - baseA[1];
491
+ const edge = Math.hypot(dx, dy);
492
+ if (edge < 1e-9) return null;
493
+ const mid = [(baseA[0] + baseB[0]) / 2, (baseA[1] + baseB[1]) / 2, z0];
494
+ const perpX = -dy;
495
+ const perpY = dx;
496
+ const perpLen = Math.hypot(perpX, perpY);
497
+ const height = edge * Math.sqrt(3) / 2;
498
+ const baseC = [mid[0] + perpX / perpLen * height, mid[1] + perpY / perpLen * height, z0];
499
+ const centroid = [
500
+ (baseA[0] + baseB[0] + baseC[0]) / 3,
501
+ (baseA[1] + baseB[1] + baseC[1]) / 3,
502
+ z0
503
+ ];
504
+ const apexHeight = edge * Math.sqrt(2 / 3);
505
+ const apex = [centroid[0], centroid[1], z0 + apexHeight];
506
+ const cId = scene.addPoint({ kind: "free", x: baseC[0], y: baseC[1], z: baseC[2] });
507
+ const apexId = scene.addPoint({ kind: "free", x: apex[0], y: apex[1], z: apex[2] });
508
+ const vertices = [p1Id, p2Id, cId, apexId];
509
+ const faces = [
510
+ [0, 1, 2],
511
+ // base
512
+ [0, 1, 3],
513
+ // face p1-p2-apex
514
+ [1, 2, 3],
515
+ // face p2-c-apex
516
+ [2, 0, 3]
517
+ // face c-p1-apex
518
+ ];
519
+ return scene.addObject("polyhedron", { flavor: "tetrahedron", vertices, faces });
520
+ }
521
+ var init_tetrahedron = __esm({
522
+ "src/stamps/geometry-3d/editor/tools/handlers/tetrahedron.ts"() {
523
+ init_ensurePoint();
524
+ init_constraintMath();
525
+ }
526
+ });
527
+
528
+ // src/stamps/geometry-3d/editor/tools/handlers/cube.ts
529
+ function buildCube(args, scene) {
530
+ if (args.length < 2 || !args[0].hit || !args[1].hit) return null;
531
+ const p1Id = ensurePoint(args[0].hit, scene);
532
+ const p2Id = ensurePoint(args[1].hit, scene);
533
+ if (!p1Id || !p2Id || p1Id === p2Id) return null;
534
+ const p1Obj = scene.get(p1Id);
535
+ const p2Obj = scene.get(p2Id);
536
+ if (!p1Obj || p1Obj.kind !== "point" || !p2Obj || p2Obj.kind !== "point") return null;
537
+ const p1 = constraintToWorld(p1Obj.constraint, scene);
538
+ const p2 = constraintToWorld(p2Obj.constraint, scene);
539
+ if (Math.abs(p1[2]) > 1e-6 || Math.abs(p2[2]) > 1e-6) return null;
540
+ const dx = p2[0] - p1[0];
541
+ const dy = p2[1] - p1[1];
542
+ const edge = Math.hypot(dx, dy);
543
+ if (edge < 1e-9) return null;
544
+ const perpX = -dy;
545
+ const perpY = dx;
546
+ const p3 = [p2[0] + perpX, p2[1] + perpY, 0];
547
+ const p4 = [p1[0] + perpX, p1[1] + perpY, 0];
548
+ const t1 = [p1[0], p1[1], edge];
549
+ const t2 = [p2[0], p2[1], edge];
550
+ const t3 = [p3[0], p3[1], edge];
551
+ const t4 = [p4[0], p4[1], edge];
552
+ const p3Id = scene.addPoint({ kind: "onGround", x: p3[0], y: p3[1] });
553
+ const p4Id = scene.addPoint({ kind: "onGround", x: p4[0], y: p4[1] });
554
+ const t1Id = scene.addPoint({ kind: "free", x: t1[0], y: t1[1], z: t1[2] });
555
+ const t2Id = scene.addPoint({ kind: "free", x: t2[0], y: t2[1], z: t2[2] });
556
+ const t3Id = scene.addPoint({ kind: "free", x: t3[0], y: t3[1], z: t3[2] });
557
+ const t4Id = scene.addPoint({ kind: "free", x: t4[0], y: t4[1], z: t4[2] });
558
+ const vertices = [p1Id, p2Id, p3Id, p4Id, t1Id, t2Id, t3Id, t4Id];
559
+ const faces = [
560
+ [0, 1, 2, 3],
561
+ // bottom
562
+ [4, 5, 6, 7],
563
+ // top
564
+ [0, 1, 5, 4],
565
+ // front
566
+ [1, 2, 6, 5],
567
+ // right
568
+ [2, 3, 7, 6],
569
+ // back
570
+ [3, 0, 4, 7]
571
+ // left
572
+ ];
573
+ return scene.addObject("polyhedron", { flavor: "cube", vertices, faces });
574
+ }
575
+ var init_cube = __esm({
576
+ "src/stamps/geometry-3d/editor/tools/handlers/cube.ts"() {
577
+ init_ensurePoint();
578
+ init_constraintMath();
579
+ }
580
+ });
581
+
582
+ // src/stamps/geometry-3d/editor/tools/handlers/sphere.ts
583
+ function buildSphere(args, scene) {
584
+ if (args.length < 2 || !args[0].hit || !args[1].hit) return null;
585
+ const center = ensurePoint(args[0].hit, scene);
586
+ const surface = ensurePoint(args[1].hit, scene);
587
+ if (!center || !surface || center === surface) return null;
588
+ return scene.addObject("sphere", { center, surfacePoint: surface });
589
+ }
590
+ var init_sphere = __esm({
591
+ "src/stamps/geometry-3d/editor/tools/handlers/sphere.ts"() {
592
+ init_ensurePoint();
593
+ }
594
+ });
595
+
596
+ // src/stamps/geometry-3d/editor/tools/handlers/cylinder.ts
597
+ function buildCylinder(args, scene) {
598
+ const points = args.filter((a) => a.step.type === "point");
599
+ const numberArg = args.find((a) => a.step.type === "number");
600
+ if (points.length < 2 || !points[0].hit || !points[1].hit || !numberArg || typeof numberArg.value !== "number") return null;
601
+ const radius = numberArg.value;
602
+ if (radius <= 0) return null;
603
+ const baseCenter = ensurePoint(points[0].hit, scene);
604
+ const topCenter = ensurePoint(points[1].hit, scene);
605
+ if (!baseCenter || !topCenter || baseCenter === topCenter) return null;
606
+ return scene.addObject("cylinder", { baseCenter, topCenter, radius });
607
+ }
608
+ var init_cylinder = __esm({
609
+ "src/stamps/geometry-3d/editor/tools/handlers/cylinder.ts"() {
610
+ init_ensurePoint();
611
+ }
612
+ });
613
+
614
+ // src/stamps/geometry-3d/editor/tools/handlers/cone.ts
615
+ function buildCone(args, scene) {
616
+ const points = args.filter((a) => a.step.type === "point");
617
+ const numberArg = args.find((a) => a.step.type === "number");
618
+ if (points.length < 2 || !points[0].hit || !points[1].hit || !numberArg || typeof numberArg.value !== "number") return null;
619
+ const radius = numberArg.value;
620
+ if (radius <= 0) return null;
621
+ const baseCenter = ensurePoint(points[0].hit, scene);
622
+ const apex = ensurePoint(points[1].hit, scene);
623
+ if (!baseCenter || !apex || baseCenter === apex) return null;
624
+ return scene.addObject("cone", { baseCenter, apex, radius });
625
+ }
626
+ var init_cone = __esm({
627
+ "src/stamps/geometry-3d/editor/tools/handlers/cone.ts"() {
628
+ init_ensurePoint();
629
+ }
630
+ });
631
+
632
+ // src/stamps/geometry-3d/editor/tools/spec.ts
633
+ var stubBuild, ALL_SURFACES, OBJECT_ONLY, NO_SURFACE, TOOLS;
634
+ var init_spec = __esm({
635
+ "src/stamps/geometry-3d/editor/tools/spec.ts"() {
636
+ init_point();
637
+ init_segment();
638
+ init_polygon();
639
+ init_plane();
640
+ init_pyramid();
641
+ init_prism();
642
+ init_tetrahedron();
643
+ init_cube();
644
+ init_sphere();
645
+ init_cylinder();
646
+ init_cone();
647
+ stubBuild = () => null;
648
+ ALL_SURFACES = ["ground", "axis", "plane", "line", "polygon", "sphere"];
649
+ OBJECT_ONLY = ["plane", "line", "polygon", "sphere"];
650
+ NO_SURFACE = ["ground", "axis", "plane"];
651
+ TOOLS = [
652
+ {
653
+ key: "move",
654
+ label: "Di chuy\u1EC3n",
655
+ hintIdle: "K\xE9o \u0111i\u1EC3m ho\u1EB7c xoay khung",
656
+ steps: [],
657
+ build: stubBuild
658
+ },
659
+ {
660
+ key: "point",
661
+ label: "\u0110i\u1EC3m",
662
+ hintIdle: "Click tr\xEAn m\u1EB7t ph\u1EB3ng Oxy ho\u1EB7c tr\xEAn tr\u1EE5c \u0111\u1EC3 \u0111\u1EB7t \u0111i\u1EC3m",
663
+ steps: [
664
+ {
665
+ type: "point",
666
+ allowExisting: false,
667
+ // GeoGebra-style: a new point must lie on the XY ground plane or on
668
+ // one of the coordinate axes (Oz lets you place points off the plane).
669
+ allowNewOn: ["ground", "axis"],
670
+ hint: "Click tr\xEAn m\u1EB7t ph\u1EB3ng Oxy ho\u1EB7c tr\u1EE5c Ox/Oy/Oz"
671
+ }
672
+ ],
673
+ build: buildPoint,
674
+ repeatAfterBuild: true
675
+ },
676
+ {
677
+ key: "pointOnObject",
678
+ label: "\u0110i\u1EC3m tr\xEAn \u0111\u1ED1i t\u01B0\u1EE3ng",
679
+ hintIdle: "Ch\u1ECDn m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng \u0111\u1EC3 \u0111\u1EB7t \u0111i\u1EC3m",
680
+ steps: [{ type: "point", allowExisting: false, allowNewOn: OBJECT_ONLY, hint: "Click l\xEAn m\u1EB7t / \u0111\u01B0\u1EDDng \u0111\u1EC3 \u0111\u1EB7t \u0111i\u1EC3m" }],
681
+ build: buildPointOnObject
682
+ },
683
+ {
684
+ key: "segment",
685
+ label: "\u0110o\u1EA1n th\u1EB3ng",
686
+ hintIdle: "Ch\u1ECDn 2 \u0111i\u1EC3m \u0111\u1EC3 v\u1EBD \u0111o\u1EA1n th\u1EB3ng",
687
+ steps: [
688
+ { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m th\u1EE9 nh\u1EA5t" },
689
+ { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m th\u1EE9 hai" }
690
+ ],
691
+ build: buildSegment
692
+ },
693
+ {
694
+ key: "line",
695
+ label: "\u0110\u01B0\u1EDDng th\u1EB3ng",
696
+ hintIdle: "Ch\u1ECDn 2 \u0111i\u1EC3m \u0111\u1EC3 v\u1EBD \u0111\u01B0\u1EDDng th\u1EB3ng",
697
+ steps: [
698
+ { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m th\u1EE9 nh\u1EA5t" },
699
+ { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m th\u1EE9 hai" }
700
+ ],
701
+ build: buildLine
702
+ },
703
+ {
704
+ key: "ray",
705
+ label: "Tia",
706
+ hintIdle: "Ch\u1ECDn \u0111i\u1EC3m g\u1ED1c r\u1ED3i \u0111i\u1EC3m tr\xEAn tia",
707
+ steps: [
708
+ { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m g\u1ED1c" },
709
+ { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m tr\xEAn tia" }
710
+ ],
711
+ build: buildRay
712
+ },
713
+ {
714
+ key: "vector",
715
+ label: "Vector",
716
+ hintIdle: "Ch\u1ECDn 2 \u0111i\u1EC3m \u0111\u1EC3 v\u1EBD vector",
717
+ steps: [
718
+ { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m \u0111\u1EA7u" },
719
+ { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m cu\u1ED1i" }
720
+ ],
721
+ build: buildVector
722
+ },
723
+ {
724
+ key: "polygon",
725
+ label: "\u0110a gi\xE1c",
726
+ hintIdle: "Ch\u1ECDn c\xE1c \u0111\u1EC9nh; click \u0111i\u1EC3m \u0111\u1EA7u \u0111\u1EC3 \u0111\xF3ng",
727
+ steps: [
728
+ { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111\u1EC9nh th\u1EE9 1" },
729
+ { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111\u1EC9nh th\u1EE9 2" },
730
+ { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111\u1EC9nh th\u1EE9 3" },
731
+ { type: "closingPoint", hint: "Click \u0111i\u1EC3m \u0111\u1EA7u \u0111\u1EC3 \u0111\xF3ng (ho\u1EB7c ch\u1ECDn th\xEAm \u0111\u1EC9nh)" }
732
+ ],
733
+ build: buildPolygon
734
+ },
735
+ {
736
+ key: "plane",
737
+ label: "M\u1EB7t ph\u1EB3ng (3 \u0111i\u1EC3m)",
738
+ hintIdle: "Ch\u1ECDn 3 \u0111i\u1EC3m \u0111\u1EC3 x\xE1c \u0111\u1ECBnh m\u1EB7t ph\u1EB3ng",
739
+ steps: [
740
+ { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m th\u1EE9 1 c\u1EE7a m\u1EB7t ph\u1EB3ng" },
741
+ { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m th\u1EE9 2" },
742
+ { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m th\u1EE9 3" }
743
+ ],
744
+ build: buildPlane
745
+ },
746
+ {
747
+ key: "pyramid",
748
+ label: "H\xECnh ch\xF3p",
749
+ hintIdle: "Ch\u1ECDn \u0111\xE1y \u0111a gi\xE1c r\u1ED3i ch\u1ECDn \u0111\u1EC9nh ch\xF3p",
750
+ steps: [
751
+ { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111\u1EC9nh \u0111\xE1y 1" },
752
+ { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111\u1EC9nh \u0111\xE1y 2" },
753
+ { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111\u1EC9nh \u0111\xE1y 3" },
754
+ { type: "closingPoint", hint: "Click \u0111\u1EC9nh \u0111\xE1y \u0111\u1EA7u ti\xEAn \u0111\u1EC3 \u0111\xF3ng (ho\u1EB7c ch\u1ECDn th\xEAm \u0111\u1EC9nh)" },
755
+ { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111\u1EC9nh ch\xF3p" }
756
+ ],
757
+ build: buildPyramid
758
+ },
759
+ {
760
+ key: "prism",
761
+ label: "L\u0103ng tr\u1EE5",
762
+ hintIdle: "Ch\u1ECDn \u0111\xE1y \u0111a gi\xE1c r\u1ED3i nh\u1EADp chi\u1EC1u cao",
763
+ steps: [
764
+ { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111\u1EC9nh \u0111\xE1y 1" },
765
+ { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111\u1EC9nh \u0111\xE1y 2" },
766
+ { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111\u1EC9nh \u0111\xE1y 3" },
767
+ { type: "closingPoint", hint: "Click \u0111\u1EC9nh \u0111\u1EA7u \u0111\u1EC3 \u0111\xF3ng \u0111\xE1y" },
768
+ { type: "number", prompt: "Chi\u1EC1u cao (theo tr\u1EE5c z)", min: 1e-4 }
769
+ ],
770
+ build: buildPrism
771
+ },
772
+ {
773
+ key: "tetrahedron",
774
+ label: "T\u1EE9 di\u1EC7n \u0111\u1EC1u",
775
+ hintIdle: "Ch\u1ECDn 2 \u0111i\u1EC3m x\xE1c \u0111\u1ECBnh c\u1EA1nh c\u1EE7a t\u1EE9 di\u1EC7n",
776
+ steps: [
777
+ { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111i\u1EC3m 1" },
778
+ { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111i\u1EC3m 2" }
779
+ ],
780
+ build: buildTetrahedron
781
+ },
782
+ {
783
+ key: "cube",
784
+ label: "L\u1EADp ph\u01B0\u01A1ng",
785
+ hintIdle: "Ch\u1ECDn 2 \u0111i\u1EC3m tr\xEAn n\u1EC1n x\xE1c \u0111\u1ECBnh c\u1EA1nh",
786
+ steps: [
787
+ { type: "point", allowExisting: true, allowNewOn: ["ground"], hint: "Ch\u1ECDn \u0111i\u1EC3m 1 (tr\xEAn n\u1EC1n)" },
788
+ { type: "point", allowExisting: true, allowNewOn: ["ground"], hint: "Ch\u1ECDn \u0111i\u1EC3m 2 (tr\xEAn n\u1EC1n)" }
789
+ ],
790
+ build: buildCube
791
+ },
792
+ {
793
+ key: "sphere",
794
+ label: "M\u1EB7t c\u1EA7u",
795
+ hintIdle: "Ch\u1ECDn t\xE2m r\u1ED3i \u0111i\u1EC3m tr\xEAn m\u1EB7t c\u1EA7u",
796
+ steps: [
797
+ { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn t\xE2m m\u1EB7t c\u1EA7u" },
798
+ { type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m tr\xEAn m\u1EB7t c\u1EA7u" }
799
+ ],
800
+ build: buildSphere
801
+ },
802
+ {
803
+ key: "cylinder",
804
+ label: "H\xECnh tr\u1EE5",
805
+ hintIdle: "Ch\u1ECDn t\xE2m \u0111\xE1y, t\xE2m tr\xEAn, r\u1ED3i nh\u1EADp b\xE1n k\xEDnh",
806
+ steps: [
807
+ { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn t\xE2m \u0111\xE1y" },
808
+ { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn t\xE2m tr\xEAn" },
809
+ { type: "number", prompt: "B\xE1n k\xEDnh", min: 1e-4 }
810
+ ],
811
+ build: buildCylinder
812
+ },
813
+ {
814
+ key: "cone",
815
+ label: "H\xECnh n\xF3n",
816
+ hintIdle: "Ch\u1ECDn t\xE2m \u0111\xE1y, \u0111\u1EC9nh, r\u1ED3i nh\u1EADp b\xE1n k\xEDnh",
817
+ steps: [
818
+ { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn t\xE2m \u0111\xE1y" },
819
+ { type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111\u1EC9nh" },
820
+ { type: "number", prompt: "B\xE1n k\xEDnh", min: 1e-4 }
821
+ ],
822
+ build: buildCone
823
+ }
824
+ ];
825
+ }
826
+ });
827
+
828
+ // src/stamps/geometry-3d/editor/tools/controller.ts
829
+ function stepHint(step) {
830
+ return step.type === "number" ? step.prompt : step.hint;
831
+ }
832
+ var ToolController;
833
+ var init_controller = __esm({
834
+ "src/stamps/geometry-3d/editor/tools/controller.ts"() {
835
+ init_spec();
836
+ ToolController = class {
837
+ constructor(scene) {
838
+ this.scene = scene;
839
+ this.state = { tool: null, stepIndex: 0, collected: [], hint: "" };
840
+ this.listeners = /* @__PURE__ */ new Set();
841
+ this.selectTool("move");
842
+ }
843
+ getState() {
844
+ return this.state;
845
+ }
846
+ on(cb) {
847
+ this.listeners.add(cb);
848
+ return () => {
849
+ this.listeners.delete(cb);
850
+ };
851
+ }
852
+ selectTool(key) {
853
+ const tool = TOOLS.find((t) => t.key === key) ?? TOOLS.find((t) => t.key === "move");
854
+ const firstStep = tool.steps[0];
855
+ this.state = {
856
+ tool,
857
+ stepIndex: 0,
858
+ collected: [],
859
+ hint: firstStep ? stepHint(firstStep) : tool.hintIdle
860
+ };
861
+ this.notify();
862
+ }
863
+ cancel() {
864
+ this.selectTool("move");
865
+ }
866
+ consumeHit(hit) {
867
+ const tool = this.state.tool;
868
+ if (!tool) return false;
869
+ const step = tool.steps[this.state.stepIndex];
870
+ if (!step) return false;
871
+ if (step.type === "closingPoint") {
872
+ if (hit.kind === "empty") return false;
873
+ if (hit.kind === "existingPoint") {
874
+ this.state.collected.push({ step, hit });
875
+ this.state.stepIndex++;
876
+ this.advance();
877
+ return true;
878
+ }
879
+ const prevStep = tool.steps[this.state.stepIndex - 1];
880
+ if (!prevStep || prevStep.type !== "point") return false;
881
+ if (!this.hitMatchesStep(hit, prevStep)) return false;
882
+ this.state.collected.push({ step: prevStep, hit });
883
+ this.notify();
884
+ return true;
885
+ }
886
+ if (!this.hitMatchesStep(hit, step)) return false;
887
+ this.state.collected.push({ step, hit });
888
+ this.state.stepIndex++;
889
+ this.advance();
890
+ return true;
891
+ }
892
+ consumeNumber(value) {
893
+ const tool = this.state.tool;
894
+ if (!tool) return false;
895
+ const step = tool.steps[this.state.stepIndex];
896
+ if (!step || step.type !== "number") return false;
897
+ if (step.min != null && value < step.min) return false;
898
+ if (step.max != null && value > step.max) return false;
899
+ this.state.collected.push({ step, value });
900
+ this.state.stepIndex++;
901
+ this.advance();
902
+ return true;
903
+ }
904
+ hitMatchesStep(hit, step) {
905
+ if (step.type !== "point" && step.type !== "closingPoint") return false;
906
+ if (hit.kind === "empty") return false;
907
+ if (step.type === "closingPoint") return hit.kind === "existingPoint";
908
+ if (hit.kind === "existingPoint") return step.allowExisting;
909
+ const surfaceMap = {
910
+ onGround: "ground",
911
+ onAxis: "axis",
912
+ onPlane: "plane",
913
+ onLine: "line",
914
+ onPolygon: "polygon",
915
+ onSphere: "sphere"
916
+ };
917
+ const k = surfaceMap[hit.kind];
918
+ return k != null && step.type === "point" && step.allowNewOn.includes(k);
919
+ }
920
+ advance() {
921
+ const tool = this.state.tool;
922
+ if (this.state.stepIndex >= tool.steps.length) {
923
+ tool.build(this.state.collected, this.scene);
924
+ if (tool.repeatAfterBuild) {
925
+ this.state.stepIndex = 0;
926
+ this.state.collected = [];
927
+ this.state.hint = stepHint(tool.steps[0]);
928
+ this.notify();
929
+ } else {
930
+ this.selectTool("move");
931
+ }
932
+ return;
933
+ }
934
+ this.state.hint = stepHint(tool.steps[this.state.stepIndex]);
935
+ this.notify();
936
+ }
937
+ notify() {
938
+ for (const cb of this.listeners) cb(this.state);
939
+ }
940
+ };
941
+ }
942
+ });
943
+
944
+ // src/stamps/geometry-3d/editor/renderer/faceted.ts
945
+ function cylinderFaces(center, top, radius) {
946
+ const baseRing = [];
947
+ const topRing = [];
948
+ for (let i = 0; i < CURVED_SEGMENTS; i++) {
949
+ const theta = i / CURVED_SEGMENTS * Math.PI * 2;
950
+ const dx = radius * Math.cos(theta);
951
+ const dy = radius * Math.sin(theta);
952
+ baseRing.push([center[0] + dx, center[1] + dy, center[2]]);
953
+ topRing.push([top[0] + dx, top[1] + dy, top[2]]);
954
+ }
955
+ const vertices = [...baseRing, ...topRing];
956
+ const faces = [];
957
+ faces.push(baseRing.map((_, i) => i));
958
+ faces.push(topRing.map((_, i) => CURVED_SEGMENTS + i));
959
+ for (let i = 0; i < CURVED_SEGMENTS; i++) {
960
+ const next = (i + 1) % CURVED_SEGMENTS;
961
+ faces.push([i, next, CURVED_SEGMENTS + next, CURVED_SEGMENTS + i]);
962
+ }
963
+ return { vertices, faces };
964
+ }
965
+ function coneFaces(baseCenter, apex, radius) {
966
+ const baseRing = [];
967
+ for (let i = 0; i < CURVED_SEGMENTS; i++) {
968
+ const theta = i / CURVED_SEGMENTS * Math.PI * 2;
969
+ baseRing.push([
970
+ baseCenter[0] + radius * Math.cos(theta),
971
+ baseCenter[1] + radius * Math.sin(theta),
972
+ baseCenter[2]
973
+ ]);
974
+ }
975
+ const apexIdx = baseRing.length;
976
+ const vertices = [...baseRing, apex];
977
+ const faces = [baseRing.map((_, i) => i)];
978
+ for (let i = 0; i < CURVED_SEGMENTS; i++) {
979
+ faces.push([i, (i + 1) % CURVED_SEGMENTS, apexIdx]);
980
+ }
981
+ return { vertices, faces };
982
+ }
983
+ var CURVED_SEGMENTS;
984
+ var init_faceted = __esm({
985
+ "src/stamps/geometry-3d/editor/renderer/faceted.ts"() {
986
+ CURVED_SEGMENTS = 16;
987
+ }
988
+ });
989
+
990
+ // src/stamps/geometry-3d/editor/renderer/JxgRenderer.ts
991
+ var JxgRenderer;
992
+ var init_JxgRenderer = __esm({
993
+ "src/stamps/geometry-3d/editor/renderer/JxgRenderer.ts"() {
994
+ init_constraintMath();
995
+ init_faceted();
996
+ JxgRenderer = class {
997
+ constructor(scene, view) {
998
+ this.scene = scene;
999
+ this.view = view;
1000
+ this.map = /* @__PURE__ */ new Map();
1001
+ this.unsubAdd = scene.on("add", (o) => this.handleAdd(o));
1002
+ this.unsubChange = scene.on("change", (o) => this.handleChange(o));
1003
+ this.unsubDelete = scene.on("delete", (id) => this.handleDelete(id));
1004
+ this.unsubReset = scene.on("reset", () => this.handleReset());
1005
+ for (const obj of scene.list()) this.handleAdd(obj);
1006
+ }
1007
+ dispose() {
1008
+ this.unsubAdd();
1009
+ this.unsubChange();
1010
+ this.unsubDelete();
1011
+ this.unsubReset();
1012
+ for (const [id, j] of this.map) {
1013
+ try {
1014
+ j.remove?.();
1015
+ } catch {
1016
+ }
1017
+ this.map.delete(id);
1018
+ }
1019
+ }
1020
+ handleAdd(obj) {
1021
+ if (this.map.has(obj.id)) return;
1022
+ if (obj.kind === "point") {
1023
+ const world = constraintToWorld(obj.constraint, this.scene);
1024
+ const attrs = { id: obj.id, name: obj.label, size: 4, visible: obj.visible, fixed: true };
1025
+ const jxg = this.view.create("point3d", world, attrs);
1026
+ this.map.set(obj.id, jxg);
1027
+ return;
1028
+ }
1029
+ if (obj.kind === "segment") {
1030
+ const a = this.map.get(obj.p1);
1031
+ const b = this.map.get(obj.p2);
1032
+ const attrs = {
1033
+ id: obj.id,
1034
+ straightFirst: false,
1035
+ straightLast: false,
1036
+ visible: obj.visible,
1037
+ strokeColor: obj.color ?? "#0066cc",
1038
+ strokeWidth: 2
1039
+ };
1040
+ this.map.set(obj.id, this.view.create("line3d", [a, b], attrs));
1041
+ return;
1042
+ }
1043
+ if (obj.kind === "line") {
1044
+ const attrs = {
1045
+ id: obj.id,
1046
+ visible: obj.visible,
1047
+ strokeColor: obj.color ?? "#0066cc",
1048
+ strokeWidth: 2
1049
+ };
1050
+ this.map.set(
1051
+ obj.id,
1052
+ this.view.create("line3d", [this.map.get(obj.p1), this.map.get(obj.p2)], attrs)
1053
+ );
1054
+ return;
1055
+ }
1056
+ if (obj.kind === "ray") {
1057
+ const attrs = { id: obj.id, straightFirst: false, visible: obj.visible };
1058
+ this.map.set(
1059
+ obj.id,
1060
+ this.view.create("line3d", [this.map.get(obj.origin), this.map.get(obj.through)], attrs)
1061
+ );
1062
+ return;
1063
+ }
1064
+ if (obj.kind === "vector") {
1065
+ const attrs = {
1066
+ id: obj.id,
1067
+ lastArrow: true,
1068
+ straightFirst: false,
1069
+ straightLast: false,
1070
+ visible: obj.visible
1071
+ };
1072
+ this.map.set(
1073
+ obj.id,
1074
+ this.view.create("line3d", [this.map.get(obj.from), this.map.get(obj.to)], attrs)
1075
+ );
1076
+ return;
1077
+ }
1078
+ if (obj.kind === "plane") {
1079
+ const attrs = { id: obj.id, fillOpacity: 0.2, visible: obj.visible };
1080
+ this.map.set(
1081
+ obj.id,
1082
+ this.view.create(
1083
+ "plane3d",
1084
+ [this.map.get(obj.p1), this.map.get(obj.p2), this.map.get(obj.p3)],
1085
+ attrs
1086
+ )
1087
+ );
1088
+ return;
1089
+ }
1090
+ if (obj.kind === "polygon") {
1091
+ const refs = obj.vertices.map((v) => this.map.get(v));
1092
+ const attrs = { id: obj.id, fillOpacity: 0.3, visible: obj.visible };
1093
+ this.map.set(obj.id, this.view.create("polygon3d", [refs], attrs));
1094
+ return;
1095
+ }
1096
+ if (obj.kind === "sphere") {
1097
+ const attrs = { id: obj.id, fillOpacity: 0.25, visible: obj.visible };
1098
+ this.map.set(
1099
+ obj.id,
1100
+ this.view.create("sphere3d", [this.map.get(obj.center), this.map.get(obj.surfacePoint)], attrs)
1101
+ );
1102
+ return;
1103
+ }
1104
+ if (obj.kind === "polyhedron") {
1105
+ const verts = obj.vertices.map((id) => this.map.get(id));
1106
+ const faceJxgs = obj.faces.map(
1107
+ (face) => this.view.create("polygon3d", [face.map((idx) => verts[idx])], {
1108
+ id: `${obj.id}.face${face.join("-")}`,
1109
+ fillOpacity: 0.25,
1110
+ strokeColor: "#0066cc",
1111
+ strokeWidth: 1.5,
1112
+ visible: obj.visible
1113
+ })
1114
+ );
1115
+ this.map.set(obj.id, {
1116
+ _faces: faceJxgs,
1117
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1118
+ remove: () => faceJxgs.forEach((f) => f.remove?.())
1119
+ });
1120
+ return;
1121
+ }
1122
+ if (obj.kind === "cylinder" || obj.kind === "cone") {
1123
+ const baseCenterPt = this.scene.get(obj.baseCenter);
1124
+ if (!baseCenterPt || baseCenterPt.kind !== "point") return;
1125
+ const base = constraintToWorld(baseCenterPt.constraint, this.scene);
1126
+ let secondPt;
1127
+ if (obj.kind === "cylinder") {
1128
+ const topCenterPt = this.scene.get(obj.topCenter);
1129
+ if (!topCenterPt || topCenterPt.kind !== "point") return;
1130
+ secondPt = constraintToWorld(topCenterPt.constraint, this.scene);
1131
+ } else {
1132
+ const apexPt = this.scene.get(obj.apex);
1133
+ if (!apexPt || apexPt.kind !== "point") return;
1134
+ secondPt = constraintToWorld(apexPt.constraint, this.scene);
1135
+ }
1136
+ const geom = obj.kind === "cylinder" ? cylinderFaces(base, secondPt, obj.radius) : coneFaces(base, secondPt, obj.radius);
1137
+ const vertJxgs = geom.vertices.map(
1138
+ (v, i) => this.view.create("point3d", v, {
1139
+ id: `${obj.id}.v${i}`,
1140
+ visible: false,
1141
+ fixed: true,
1142
+ withLabel: false
1143
+ })
1144
+ );
1145
+ const faceJxgs = geom.faces.map(
1146
+ (face) => this.view.create("polygon3d", [face.map((idx) => vertJxgs[idx])], {
1147
+ id: `${obj.id}.face${face.join("-")}`,
1148
+ fillOpacity: 0.25,
1149
+ strokeColor: "#0066cc",
1150
+ strokeWidth: 1.5,
1151
+ visible: obj.visible
1152
+ })
1153
+ );
1154
+ this.map.set(obj.id, {
1155
+ _verts: vertJxgs,
1156
+ _faces: faceJxgs,
1157
+ remove: () => {
1158
+ faceJxgs.forEach((f) => f.remove?.());
1159
+ vertJxgs.forEach((v) => v.remove?.());
1160
+ }
1161
+ });
1162
+ return;
1163
+ }
1164
+ }
1165
+ handleChange(obj) {
1166
+ const j = this.map.get(obj.id);
1167
+ if (!j) return;
1168
+ if (obj.kind === "point" && typeof j.moveTo === "function") {
1169
+ const w = constraintToWorld(obj.constraint, this.scene);
1170
+ try {
1171
+ j.moveTo([w[0], w[1], w[2]], 0);
1172
+ } catch {
1173
+ }
1174
+ }
1175
+ }
1176
+ handleDelete(id) {
1177
+ const j = this.map.get(id);
1178
+ if (!j) return;
1179
+ try {
1180
+ j.remove?.();
1181
+ } catch {
1182
+ }
1183
+ this.map.delete(id);
1184
+ }
1185
+ handleReset() {
1186
+ for (const [, j] of this.map) {
1187
+ try {
1188
+ j.remove?.();
1189
+ } catch {
1190
+ }
1191
+ }
1192
+ this.map.clear();
1193
+ }
1194
+ };
1195
+ }
1196
+ });
1197
+
1198
+ // src/stamps/geometry-3d/editor/hitTest/rayCast.ts
1199
+ function screenToRay(screen, view) {
1200
+ const near = unproject(screen, view, 20);
1201
+ const far = unproject(screen, view, -20);
1202
+ const dir = [far[0] - near[0], far[1] - near[1], far[2] - near[2]];
1203
+ const n = Math.sqrt(dir[0] ** 2 + dir[1] ** 2 + dir[2] ** 2);
1204
+ const norm3 = n === 0 ? [0, 0, -1] : [dir[0] / n, dir[1] / n, dir[2] / n];
1205
+ return { origin: near, dir: norm3 };
1206
+ }
1207
+ function unproject(screen, view, depth) {
1208
+ if (typeof view.unprojectScreen === "function") {
1209
+ const v = view.unprojectScreen(screen.x, screen.y, depth);
1210
+ return [v[0], v[1], v[2]];
1211
+ }
1212
+ if (typeof view.project3DTo2D === "function") {
1213
+ const p0 = view.project3DTo2D(0, 0, 0);
1214
+ const px = view.project3DTo2D(1, 0, 0);
1215
+ const py = view.project3DTo2D(0, 1, 0);
1216
+ const pz = view.project3DTo2D(0, 0, 1);
1217
+ const ox = p0[1], oy = p0[2];
1218
+ const a = px[1] - ox, b = py[1] - ox, c = pz[1] - ox;
1219
+ const d = px[2] - oy, e = py[2] - oy, f = pz[2] - oy;
1220
+ const rhsX = screen.x - ox - c * depth;
1221
+ const rhsY = screen.y - oy - f * depth;
1222
+ const det = a * e - b * d;
1223
+ if (Math.abs(det) < 1e-9) return [0, 0, depth];
1224
+ const x = (e * rhsX - b * rhsY) / det;
1225
+ const y = (-d * rhsX + a * rhsY) / det;
1226
+ return [x, y, depth];
1227
+ }
1228
+ throw new Error("rayCast: view has neither unprojectScreen nor project3DTo2D");
1229
+ }
1230
+ var init_rayCast = __esm({
1231
+ "src/stamps/geometry-3d/editor/hitTest/rayCast.ts"() {
1232
+ }
1233
+ });
1234
+
1235
+ // src/stamps/geometry-3d/editor/hitTest/intersect.ts
1236
+ function dot2(a, b) {
1237
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
1238
+ }
1239
+ function sub2(a, b) {
1240
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
1241
+ }
1242
+ function add2(a, b) {
1243
+ return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
1244
+ }
1245
+ function scale2(a, k) {
1246
+ return [a[0] * k, a[1] * k, a[2] * k];
1247
+ }
1248
+ function norm2(a) {
1249
+ return dot2(a, a);
1250
+ }
1251
+ function rayPlane(ray, plane) {
1252
+ const denom = dot2(ray.dir, plane.normal);
1253
+ if (Math.abs(denom) < EPS2) return null;
1254
+ const t = dot2(sub2(plane.point, ray.origin), plane.normal) / denom;
1255
+ if (t < 0) return null;
1256
+ return { point: add2(ray.origin, scale2(ray.dir, t)), t };
1257
+ }
1258
+ function rayGround(ray) {
1259
+ return rayPlane(ray, { point: [0, 0, 0], normal: [0, 0, 1] });
1260
+ }
1261
+ function raySphere(ray, sphere) {
1262
+ const oc = sub2(ray.origin, sphere.center);
1263
+ const b = dot2(oc, ray.dir);
1264
+ const c = dot2(oc, oc) - sphere.radius * sphere.radius;
1265
+ const disc = b * b - c;
1266
+ if (disc < 0) return null;
1267
+ const sqrtD = Math.sqrt(disc);
1268
+ const t1 = -b - sqrtD;
1269
+ const t2 = -b + sqrtD;
1270
+ const t = t1 >= 0 ? t1 : t2;
1271
+ if (t < 0) return null;
1272
+ return { point: add2(ray.origin, scale2(ray.dir, t)), t };
1273
+ }
1274
+ function rayLineSegment(ray, seg, maxDistance) {
1275
+ const u = ray.dir;
1276
+ const v = sub2(seg.b, seg.a);
1277
+ const w0 = sub2(ray.origin, seg.a);
1278
+ const a = dot2(u, u);
1279
+ const bb = dot2(u, v);
1280
+ const cc = dot2(v, v);
1281
+ const d = dot2(u, w0);
1282
+ const e = dot2(v, w0);
1283
+ const denom = a * cc - bb * bb;
1284
+ if (Math.abs(denom) < EPS2) return null;
1285
+ const sc = (bb * e - cc * d) / denom;
1286
+ const tc = (a * e - bb * d) / denom;
1287
+ if (sc < 0 || tc < 0 || tc > 1) return null;
1288
+ const pRay = add2(ray.origin, scale2(u, sc));
1289
+ const pSeg = add2(seg.a, scale2(v, tc));
1290
+ const dist2 = norm2(sub2(pRay, pSeg));
1291
+ if (dist2 > maxDistance * maxDistance) return null;
1292
+ return { point: pSeg, t: sc, tOnSegment: tc };
1293
+ }
1294
+ var EPS2;
1295
+ var init_intersect = __esm({
1296
+ "src/stamps/geometry-3d/editor/hitTest/intersect.ts"() {
1297
+ EPS2 = 1e-9;
1298
+ }
1299
+ });
1300
+
1301
+ // src/stamps/geometry-3d/editor/hitTest/snapping.ts
1302
+ function findSnapPoint(screen, view, scene, pixelRadius = 8) {
1303
+ const board = view?.board;
1304
+ const ux = typeof board?.unitX === "number" && board.unitX > 0 ? board.unitX : 1;
1305
+ const uy = typeof board?.unitY === "number" && board.unitY > 0 ? board.unitY : ux;
1306
+ const rxUser = pixelRadius / ux;
1307
+ const ryUser = pixelRadius / uy;
1308
+ let best = null;
1309
+ for (const obj of scene.list()) {
1310
+ if (obj.kind !== "point") continue;
1311
+ if (!obj.visible) continue;
1312
+ const world = constraintToWorld(obj.constraint, scene);
1313
+ const proj = view.project3DTo2D?.(world[0], world[1], world[2]);
1314
+ if (!proj) continue;
1315
+ const dxN = (proj[1] - screen.x) / rxUser;
1316
+ const dyN = (proj[2] - screen.y) / ryUser;
1317
+ const d2 = dxN * dxN + dyN * dyN;
1318
+ if (d2 <= 1 && (best === null || d2 < best.d2)) {
1319
+ best = { id: obj.id, d2 };
1320
+ }
1321
+ }
1322
+ return best?.id ?? null;
1323
+ }
1324
+ var init_snapping = __esm({
1325
+ "src/stamps/geometry-3d/editor/hitTest/snapping.ts"() {
1326
+ init_constraintMath();
1327
+ }
1328
+ });
1329
+
1330
+ // src/stamps/geometry-3d/editor/hitTest/hitTest.ts
1331
+ function hitTest(screen, view, scene) {
1332
+ const board = view?.board;
1333
+ const ux = typeof board?.unitX === "number" && board.unitX > 0 ? board.unitX : 1;
1334
+ const axisThresholdUser = AXIS_PIXEL_THRESHOLD / ux;
1335
+ const snap = findSnapPoint(screen, view, scene);
1336
+ if (snap) return { kind: "existingPoint", pointId: snap };
1337
+ const ray = screenToRay(screen, view);
1338
+ let bestSphere = null;
1339
+ for (const obj of scene.list()) {
1340
+ if (obj.kind !== "sphere" || !obj.visible) continue;
1341
+ const centerPoint = scene.get(obj.center);
1342
+ const surfacePoint = scene.get(obj.surfacePoint);
1343
+ if (!centerPoint || centerPoint.kind !== "point") continue;
1344
+ if (!surfacePoint || surfacePoint.kind !== "point") continue;
1345
+ const center = constraintToWorld(centerPoint.constraint, scene);
1346
+ const surface = constraintToWorld(surfacePoint.constraint, scene);
1347
+ const radius = Math.hypot(
1348
+ surface[0] - center[0],
1349
+ surface[1] - center[1],
1350
+ surface[2] - center[2]
1351
+ );
1352
+ const sh = raySphere(ray, { center, radius });
1353
+ if (sh && (bestSphere === null || sh.t < bestSphere.t)) {
1354
+ bestSphere = { id: obj.id, t: sh.t, world: sh.point };
1355
+ }
1356
+ }
1357
+ if (view.project3DTo2D) {
1358
+ const axes = [
1359
+ { axis: "x", a: [-10, 0, 0], b: [10, 0, 0] },
1360
+ { axis: "y", a: [0, -10, 0], b: [0, 10, 0] },
1361
+ { axis: "z", a: [0, 0, -10], b: [0, 0, 10] }
1362
+ ];
1363
+ for (const ax of axes) {
1364
+ const pa = view.project3DTo2D(ax.a[0], ax.a[1], ax.a[2]);
1365
+ const pb = view.project3DTo2D(ax.b[0], ax.b[1], ax.b[2]);
1366
+ const d = distScreenPointToSegment(screen, [pa[1], pa[2]], [pb[1], pb[2]]);
1367
+ if (d <= axisThresholdUser) {
1368
+ const hit = rayLineSegment(ray, { a: ax.a, b: ax.b }, 1e3);
1369
+ if (hit) {
1370
+ const t = ax.axis === "x" ? hit.point[0] : ax.axis === "y" ? hit.point[1] : hit.point[2];
1371
+ return { kind: "onAxis", axis: ax.axis, t, world: hit.point };
1372
+ }
1373
+ }
1374
+ }
1375
+ }
1376
+ let bestPlane = null;
1377
+ for (const obj of scene.list()) {
1378
+ if (obj.kind !== "plane" || !obj.visible) continue;
1379
+ const basis = planeBasis(obj, scene);
1380
+ if (!basis) continue;
1381
+ const ph = rayPlane(ray, { point: basis.origin, normal: basis.normal });
1382
+ if (ph && (bestPlane === null || ph.t < bestPlane.t)) {
1383
+ bestPlane = { id: obj.id, t: ph.t, world: ph.point, basis };
1384
+ }
1385
+ }
1386
+ if (bestPlane && (!bestSphere || bestPlane.t < bestSphere.t)) {
1387
+ const rel = [
1388
+ bestPlane.world[0] - bestPlane.basis.origin[0],
1389
+ bestPlane.world[1] - bestPlane.basis.origin[1],
1390
+ bestPlane.world[2] - bestPlane.basis.origin[2]
1391
+ ];
1392
+ const b1n = dot3(bestPlane.basis.basis1, bestPlane.basis.basis1);
1393
+ const b2n = dot3(bestPlane.basis.basis2, bestPlane.basis.basis2);
1394
+ const u = b1n === 0 ? 0 : dot3(rel, bestPlane.basis.basis1) / b1n;
1395
+ const v = b2n === 0 ? 0 : dot3(rel, bestPlane.basis.basis2) / b2n;
1396
+ return { kind: "onPlane", planeId: bestPlane.id, u, v, world: bestPlane.world };
1397
+ }
1398
+ if (bestSphere) {
1399
+ const sph = scene.get(bestSphere.id);
1400
+ if (sph && sph.kind === "sphere") {
1401
+ const centerPt = scene.get(sph.center);
1402
+ if (centerPt && centerPt.kind === "point") {
1403
+ const center = constraintToWorld(centerPt.constraint, scene);
1404
+ const relX = bestSphere.world[0] - center[0];
1405
+ const relY = bestSphere.world[1] - center[1];
1406
+ const relZ = bestSphere.world[2] - center[2];
1407
+ const r = Math.hypot(relX, relY, relZ);
1408
+ const phi = r === 0 ? 0 : Math.acos(relZ / r);
1409
+ const theta = Math.atan2(relY, relX);
1410
+ return { kind: "onSphere", sphereId: bestSphere.id, theta, phi, world: bestSphere.world };
1411
+ }
1412
+ }
1413
+ }
1414
+ const g = rayGround(ray);
1415
+ if (g) return { kind: "onGround", world: g.point };
1416
+ return { kind: "empty" };
1417
+ }
1418
+ function distScreenPointToSegment(p, a, b) {
1419
+ const vx = b[0] - a[0];
1420
+ const vy = b[1] - a[1];
1421
+ const wx = p.x - a[0];
1422
+ const wy = p.y - a[1];
1423
+ const c1 = vx * wx + vy * wy;
1424
+ if (c1 <= 0) return Math.hypot(wx, wy);
1425
+ const c2 = vx * vx + vy * vy;
1426
+ if (c2 <= c1) return Math.hypot(p.x - b[0], p.y - b[1]);
1427
+ const t = c1 / c2;
1428
+ const px = a[0] + t * vx;
1429
+ const py = a[1] + t * vy;
1430
+ return Math.hypot(p.x - px, p.y - py);
1431
+ }
1432
+ function planeBasis(planeObj, scene) {
1433
+ const p1Obj = scene.get(planeObj.p1);
1434
+ const p2Obj = scene.get(planeObj.p2);
1435
+ const p3Obj = scene.get(planeObj.p3);
1436
+ if (!p1Obj || p1Obj.kind !== "point") return null;
1437
+ if (!p2Obj || p2Obj.kind !== "point") return null;
1438
+ if (!p3Obj || p3Obj.kind !== "point") return null;
1439
+ const p1 = constraintToWorld(p1Obj.constraint, scene);
1440
+ const p2 = constraintToWorld(p2Obj.constraint, scene);
1441
+ const p3 = constraintToWorld(p3Obj.constraint, scene);
1442
+ const basis1 = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];
1443
+ const tmp = [p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2]];
1444
+ const cx = basis1[1] * tmp[2] - basis1[2] * tmp[1];
1445
+ const cy = basis1[2] * tmp[0] - basis1[0] * tmp[2];
1446
+ const cz = basis1[0] * tmp[1] - basis1[1] * tmp[0];
1447
+ const cLen = Math.hypot(cx, cy, cz);
1448
+ if (cLen === 0) return null;
1449
+ const normal = [cx / cLen, cy / cLen, cz / cLen];
1450
+ const basis2 = [
1451
+ normal[1] * basis1[2] - normal[2] * basis1[1],
1452
+ normal[2] * basis1[0] - normal[0] * basis1[2],
1453
+ normal[0] * basis1[1] - normal[1] * basis1[0]
1454
+ ];
1455
+ return { origin: p1, basis1, basis2, normal };
1456
+ }
1457
+ function dot3(a, b) {
1458
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
1459
+ }
1460
+ var AXIS_PIXEL_THRESHOLD;
1461
+ var init_hitTest = __esm({
1462
+ "src/stamps/geometry-3d/editor/hitTest/hitTest.ts"() {
1463
+ init_rayCast();
1464
+ init_intersect();
1465
+ init_snapping();
1466
+ init_constraintMath();
1467
+ AXIS_PIXEL_THRESHOLD = 12;
1468
+ }
1469
+ });
1470
+ var MiniBoard3D;
1471
+ var init_MiniBoard3D = __esm({
1472
+ "src/stamps/geometry-3d/editor/MiniBoard3D.tsx"() {
1473
+ "use client";
1474
+ init_theme2();
1475
+ MiniBoard3D = React2__namespace.forwardRef(
1476
+ function MiniBoard3D2(props, ref) {
1477
+ const containerRef = React2__namespace.useRef(null);
1478
+ const boardRef = React2__namespace.useRef(null);
1479
+ const viewRef = React2__namespace.useRef(null);
1480
+ const {
1481
+ isDark,
1482
+ onView3DReady,
1483
+ onPointerClick,
1484
+ onPointerMove,
1485
+ onPointerLeave,
1486
+ shouldStartPointDrag,
1487
+ onPointerDrag,
1488
+ onPointerDragEnd
1489
+ } = props;
1490
+ const onView3DReadyRef = React2__namespace.useRef(onView3DReady);
1491
+ const onPointerClickRef = React2__namespace.useRef(onPointerClick);
1492
+ const onPointerMoveRef = React2__namespace.useRef(onPointerMove);
1493
+ const onPointerLeaveRef = React2__namespace.useRef(onPointerLeave);
1494
+ const shouldStartPointDragRef = React2__namespace.useRef(shouldStartPointDrag);
1495
+ const onPointerDragRef = React2__namespace.useRef(onPointerDrag);
1496
+ const onPointerDragEndRef = React2__namespace.useRef(onPointerDragEnd);
1497
+ onView3DReadyRef.current = onView3DReady;
1498
+ onPointerClickRef.current = onPointerClick;
1499
+ onPointerMoveRef.current = onPointerMove;
1500
+ onPointerLeaveRef.current = onPointerLeave;
1501
+ shouldStartPointDragRef.current = shouldStartPointDrag;
1502
+ onPointerDragRef.current = onPointerDrag;
1503
+ onPointerDragEndRef.current = onPointerDragEnd;
1504
+ React2__namespace.useImperativeHandle(
1505
+ ref,
1506
+ () => ({
1507
+ getBoard: () => boardRef.current,
1508
+ getView3D: () => viewRef.current,
1509
+ getSvgElement: () => containerRef.current?.querySelector("svg") ?? null
1510
+ }),
1511
+ []
1512
+ );
1513
+ React2__namespace.useEffect(() => {
1514
+ const div = containerRef.current;
1515
+ if (!div) return;
1516
+ let cancelled = false;
1517
+ let JXG = null;
1518
+ let board = null;
1519
+ let svgEl = null;
1520
+ let handlePointerDown = null;
1521
+ let handlePointerMove = null;
1522
+ let handlePointerUp = null;
1523
+ let handlePointerLeave = null;
1524
+ void (async () => {
1525
+ try {
1526
+ JXG = (await import('jsxgraph')).default;
1527
+ } catch {
1528
+ return;
1529
+ }
1530
+ if (cancelled || !containerRef.current) return;
1531
+ try {
1532
+ JXG.Options.text.display = "internal";
1533
+ } catch {
1534
+ }
1535
+ try {
1536
+ board = JXG.JSXGraph.initBoard(div, {
1537
+ boundingbox: [-6, 6, 6, -6],
1538
+ keepaspectratio: true,
1539
+ axis: false,
1540
+ showCopyright: false,
1541
+ showNavigation: false,
1542
+ renderer: "svg"
1543
+ });
1544
+ } catch {
1545
+ return;
1546
+ }
1547
+ if (cancelled || !board) return;
1548
+ boardRef.current = board;
1549
+ let view = null;
1550
+ try {
1551
+ const baseAttrs = VIEW3D_ATTRS(isDark);
1552
+ view = board.create(
1553
+ "view3d",
1554
+ [
1555
+ [-5, -5],
1556
+ [10, 10],
1557
+ [
1558
+ [DEFAULT_VIEW3D.bbox3D[0], DEFAULT_VIEW3D.bbox3D[3]],
1559
+ [DEFAULT_VIEW3D.bbox3D[1], DEFAULT_VIEW3D.bbox3D[4]],
1560
+ [DEFAULT_VIEW3D.bbox3D[2], DEFAULT_VIEW3D.bbox3D[5]]
1561
+ ]
1562
+ ],
1563
+ {
1564
+ ...baseAttrs,
1565
+ az: { ...baseAttrs.az, value: DEFAULT_VIEW3D.azimuth },
1566
+ el: { ...baseAttrs.el, value: DEFAULT_VIEW3D.elevation }
1567
+ }
1568
+ );
1569
+ } catch {
1570
+ }
1571
+ viewRef.current = view;
1572
+ if (view) {
1573
+ try {
1574
+ view.create(
1575
+ "plane3d",
1576
+ [
1577
+ [0, 0, 0],
1578
+ [1, 0, 0],
1579
+ [0, 1, 0],
1580
+ GROUND_PLANE_RANGE,
1581
+ GROUND_PLANE_RANGE
1582
+ ],
1583
+ GROUND_PLANE_ATTRS(isDark)
1584
+ );
1585
+ } catch {
1586
+ }
1587
+ onView3DReadyRef.current?.(view, board);
1588
+ }
1589
+ svgEl = containerRef.current?.querySelector("svg") ?? null;
1590
+ if (svgEl) {
1591
+ const p2 = paletteFor2(isDark);
1592
+ svgEl.style.background = p2.view3dBg;
1593
+ const pixelToUser = (e) => {
1594
+ const rect = svgEl.getBoundingClientRect();
1595
+ const px = e.clientX - rect.left;
1596
+ const py = e.clientY - rect.top;
1597
+ const b = board;
1598
+ if (!b || !b.origin || !b.origin.scrCoords) {
1599
+ return { x: px, y: py };
1600
+ }
1601
+ const ox = b.origin.scrCoords[1];
1602
+ const oy = b.origin.scrCoords[2];
1603
+ const ux = b.unitX || 1;
1604
+ const uy = b.unitY || 1;
1605
+ return { x: (px - ox) / ux, y: (oy - py) / uy };
1606
+ };
1607
+ const DRAG_THRESHOLD = 4;
1608
+ const AZ_PER_PX = 0.01;
1609
+ const EL_PER_PX = 0.01;
1610
+ const EL_LIMIT = Math.PI / 2 - 0.05;
1611
+ let dragStart = null;
1612
+ let dragging = false;
1613
+ let pointDragMode = false;
1614
+ let startAz = 0;
1615
+ let startEl = 0;
1616
+ const readAng = (s) => {
1617
+ if (!s) return 0;
1618
+ if (typeof s.Value === "function") {
1619
+ try {
1620
+ return s.Value();
1621
+ } catch {
1622
+ }
1623
+ }
1624
+ return typeof s.value === "number" ? s.value : 0;
1625
+ };
1626
+ const setAng = (s, v) => {
1627
+ if (!s) return;
1628
+ if (typeof s.setValue === "function") {
1629
+ try {
1630
+ s.setValue(v);
1631
+ return;
1632
+ } catch {
1633
+ }
1634
+ }
1635
+ s.value = v;
1636
+ };
1637
+ handlePointerDown = (e) => {
1638
+ if (!svgEl) return;
1639
+ dragStart = { x: e.clientX, y: e.clientY };
1640
+ dragging = false;
1641
+ pointDragMode = false;
1642
+ const screen = pixelToUser(e);
1643
+ try {
1644
+ pointDragMode = shouldStartPointDragRef.current?.(screen) ?? false;
1645
+ } catch {
1646
+ pointDragMode = false;
1647
+ }
1648
+ if (!pointDragMode) {
1649
+ const v = viewRef.current;
1650
+ startAz = readAng(v?.az_slide ?? v?.az);
1651
+ startEl = readAng(v?.el_slide ?? v?.el);
1652
+ }
1653
+ try {
1654
+ svgEl.setPointerCapture?.(e.pointerId);
1655
+ } catch {
1656
+ }
1657
+ };
1658
+ handlePointerMove = (e) => {
1659
+ if (!svgEl) return;
1660
+ if (dragStart) {
1661
+ const dx = e.clientX - dragStart.x;
1662
+ const dy = e.clientY - dragStart.y;
1663
+ if (!dragging && Math.hypot(dx, dy) > DRAG_THRESHOLD) dragging = true;
1664
+ if (dragging) {
1665
+ if (pointDragMode) {
1666
+ onPointerDragRef.current?.(pixelToUser(e));
1667
+ return;
1668
+ }
1669
+ const v = viewRef.current;
1670
+ const newAz = startAz + dx * AZ_PER_PX;
1671
+ let newEl = startEl - dy * EL_PER_PX;
1672
+ if (newEl > EL_LIMIT) newEl = EL_LIMIT;
1673
+ if (newEl < -EL_LIMIT) newEl = -EL_LIMIT;
1674
+ setAng(v?.az_slide ?? v?.az, newAz);
1675
+ setAng(v?.el_slide ?? v?.el, newEl);
1676
+ try {
1677
+ v?.board?.update?.();
1678
+ } catch {
1679
+ }
1680
+ return;
1681
+ }
1682
+ }
1683
+ onPointerMoveRef.current?.(pixelToUser(e));
1684
+ };
1685
+ handlePointerUp = (e) => {
1686
+ if (!svgEl) return;
1687
+ const wasDrag = dragging;
1688
+ const hadDown = dragStart !== null;
1689
+ const wasPointDrag = pointDragMode;
1690
+ dragStart = null;
1691
+ dragging = false;
1692
+ pointDragMode = false;
1693
+ try {
1694
+ svgEl.releasePointerCapture?.(e.pointerId);
1695
+ } catch {
1696
+ }
1697
+ if (hadDown && wasPointDrag) {
1698
+ onPointerDragEndRef.current?.(pixelToUser(e));
1699
+ return;
1700
+ }
1701
+ if (hadDown && !wasDrag) {
1702
+ onPointerClickRef.current?.(pixelToUser(e));
1703
+ }
1704
+ };
1705
+ handlePointerLeave = () => {
1706
+ if (pointDragMode) {
1707
+ try {
1708
+ onPointerDragEndRef.current?.({ x: 0, y: 0 });
1709
+ } catch {
1710
+ }
1711
+ }
1712
+ dragStart = null;
1713
+ dragging = false;
1714
+ pointDragMode = false;
1715
+ onPointerLeaveRef.current?.();
1716
+ };
1717
+ svgEl.addEventListener("pointerdown", handlePointerDown);
1718
+ svgEl.addEventListener("pointermove", handlePointerMove);
1719
+ svgEl.addEventListener("pointerup", handlePointerUp);
1720
+ svgEl.addEventListener("pointercancel", handlePointerUp);
1721
+ svgEl.addEventListener("pointerleave", handlePointerLeave);
1722
+ }
1723
+ })();
1724
+ return () => {
1725
+ cancelled = true;
1726
+ if (svgEl) {
1727
+ if (handlePointerDown) svgEl.removeEventListener("pointerdown", handlePointerDown);
1728
+ if (handlePointerMove) svgEl.removeEventListener("pointermove", handlePointerMove);
1729
+ if (handlePointerUp) {
1730
+ svgEl.removeEventListener("pointerup", handlePointerUp);
1731
+ svgEl.removeEventListener("pointercancel", handlePointerUp);
1732
+ }
1733
+ if (handlePointerLeave) svgEl.removeEventListener("pointerleave", handlePointerLeave);
1734
+ }
1735
+ try {
1736
+ if (board && JXG) JXG.JSXGraph.freeBoard(board);
1737
+ } catch {
1738
+ }
1739
+ boardRef.current = null;
1740
+ viewRef.current = null;
1741
+ };
1742
+ }, [isDark]);
1743
+ const p = paletteFor2(isDark);
1744
+ return /* @__PURE__ */ jsxRuntime.jsx(
1745
+ "div",
1746
+ {
1747
+ "data-testid": "mini-board-3d",
1748
+ ref: containerRef,
1749
+ style: {
1750
+ width: "100%",
1751
+ height: "100%",
1752
+ minHeight: 400,
1753
+ background: p.view3dBg,
1754
+ position: "relative",
1755
+ // Clip JSXGraph mesh3d paths projecting outside the container.
1756
+ overflow: "hidden"
1757
+ }
1758
+ }
1759
+ );
1760
+ }
1761
+ );
1762
+ }
1763
+ });
1764
+ function StatusHint(props) {
1765
+ const { hint, hoverLabel } = props;
1766
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1767
+ "div",
1768
+ {
1769
+ "data-testid": "status-hint",
1770
+ 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",
1771
+ children: [
1772
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
1773
+ "\u{1F4D0} ",
1774
+ hint || "Ch\u1ECDn c\xF4ng c\u1EE5 trong b\u1EA3ng b\xEAn tr\xE1i"
1775
+ ] }),
1776
+ hoverLabel ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-3 text-zinc-500", children: [
1777
+ "\u2014 \u0111ang tr\xEAn: ",
1778
+ hoverLabel
1779
+ ] }) : null
1780
+ ]
1781
+ }
1782
+ );
1783
+ }
1784
+ var init_StatusHint = __esm({
1785
+ "src/stamps/geometry-3d/editor/StatusHint.tsx"() {
1786
+ "use client";
1787
+ }
1788
+ });
1789
+
1790
+ // src/stamps/geometry-3d/editor/scene/labels.ts
1791
+ function nextPointLabel(existing) {
1792
+ const used = new Set(existing);
1793
+ for (let suffix = 0; suffix < 1e3; suffix++) {
1794
+ for (let i = 0; i < 26; i++) {
1795
+ const letter = String.fromCharCode(A + i);
1796
+ const candidate = suffix === 0 ? letter : `${letter}_${suffix}`;
1797
+ if (!used.has(candidate)) return candidate;
1798
+ }
1799
+ }
1800
+ return `P_${used.size}`;
1801
+ }
1802
+ function nextDerivedLabel(kind, existing) {
1803
+ const used = new Set(existing);
1804
+ if (LOWERCASE_KINDS.includes(kind)) {
1805
+ for (let i = 0; i < 26; i++) {
1806
+ const c = String.fromCharCode("a".charCodeAt(0) + i);
1807
+ if (!used.has(c)) return c;
1808
+ }
1809
+ for (let n = 1; n < 1e3; n++) {
1810
+ const c = `a_${n}`;
1811
+ if (!used.has(c)) return c;
1812
+ }
1813
+ }
1814
+ const prefix = PREFIX[kind] ?? kind[0];
1815
+ for (let n = 1; n < 1e3; n++) {
1816
+ const candidate = `${prefix}_${n}`;
1817
+ if (!used.has(candidate)) return candidate;
1818
+ }
1819
+ return `${prefix}_x`;
1820
+ }
1821
+ var A, LOWERCASE_KINDS, PREFIX;
1822
+ var init_labels = __esm({
1823
+ "src/stamps/geometry-3d/editor/scene/labels.ts"() {
1824
+ A = "A".charCodeAt(0);
1825
+ LOWERCASE_KINDS = ["segment", "line", "ray", "vector"];
1826
+ PREFIX = {
1827
+ sphere: "s",
1828
+ polyhedron: "h",
1829
+ cylinder: "c",
1830
+ cone: "k",
1831
+ polygon: "g",
1832
+ plane: "\u03C0"
1833
+ };
1834
+ }
1835
+ });
1836
+
1837
+ // src/stamps/geometry-3d/editor/scene/Scene3D.ts
1838
+ var Scene3D;
1839
+ var init_Scene3D = __esm({
1840
+ "src/stamps/geometry-3d/editor/scene/Scene3D.ts"() {
1841
+ init_labels();
1842
+ Scene3D = class {
1843
+ constructor() {
1844
+ this.objects = /* @__PURE__ */ new Map();
1845
+ this.order = [];
1846
+ this.counter = 0;
1847
+ this.listeners = {
1848
+ add: /* @__PURE__ */ new Set(),
1849
+ change: /* @__PURE__ */ new Set(),
1850
+ delete: /* @__PURE__ */ new Set(),
1851
+ reset: /* @__PURE__ */ new Set()
1852
+ };
1853
+ this.historyPast = [];
1854
+ this.historyFuture = [];
1855
+ this.historySuspended = false;
1856
+ this.historyListeners = /* @__PURE__ */ new Set();
1857
+ }
1858
+ on(event, cb) {
1859
+ const set = this.listeners[event];
1860
+ set.add(cb);
1861
+ return () => {
1862
+ set.delete(cb);
1863
+ };
1864
+ }
1865
+ nextId(prefix) {
1866
+ this.counter += 1;
1867
+ return `${prefix}${this.counter}`;
1868
+ }
1869
+ addPoint(constraint, label, color) {
1870
+ this.capture();
1871
+ const id = this.nextId("p");
1872
+ const existingLabels = this.list().filter((o) => o.kind === "point").map((o) => o.label);
1873
+ const autoLabel = label ?? nextPointLabel(existingLabels);
1874
+ const obj = {
1875
+ kind: "point",
1876
+ id,
1877
+ label: autoLabel,
1878
+ visible: true,
1879
+ color,
1880
+ constraint
1881
+ };
1882
+ this.objects.set(id, obj);
1883
+ this.order.push(id);
1884
+ this.listeners.add.forEach((cb) => cb(obj));
1885
+ return id;
1886
+ }
1887
+ addObject(kind, spec, label) {
1888
+ this.capture();
1889
+ const id = this.nextId(kind[0]);
1890
+ const existingLabels = this.list().filter((o) => o.kind === kind).map((o) => o.label);
1891
+ const autoLabel = label ?? nextDerivedLabel(kind, existingLabels);
1892
+ const obj = { id, label: autoLabel, visible: true, kind, ...spec };
1893
+ this.objects.set(id, obj);
1894
+ this.order.push(id);
1895
+ this.listeners.add.forEach((cb) => cb(obj));
1896
+ return id;
1897
+ }
1898
+ insert(obj) {
1899
+ this.capture();
1900
+ if (this.objects.has(obj.id)) {
1901
+ throw new Error(`Scene3D.insert: id ${obj.id} already exists`);
1902
+ }
1903
+ this.objects.set(obj.id, obj);
1904
+ this.order.push(obj.id);
1905
+ this.listeners.add.forEach((cb) => cb(obj));
1906
+ }
1907
+ get(id) {
1908
+ return this.objects.get(id);
1909
+ }
1910
+ list() {
1911
+ return this.order.map((id) => this.objects.get(id)).filter((obj) => obj !== void 0);
1912
+ }
1913
+ referencedIds(obj) {
1914
+ switch (obj.kind) {
1915
+ case "point": {
1916
+ const c = obj.constraint;
1917
+ if (c.kind === "onPlane") return [c.planeId];
1918
+ if (c.kind === "onLine") return [c.lineId];
1919
+ if (c.kind === "onPolygon") return [c.polygonId];
1920
+ if (c.kind === "onSphere") return [c.sphereId];
1921
+ return [];
1922
+ }
1923
+ case "segment":
1924
+ case "line":
1925
+ return [obj.p1, obj.p2];
1926
+ case "ray":
1927
+ return [obj.origin, obj.through];
1928
+ case "vector":
1929
+ return [obj.from, obj.to];
1930
+ case "polygon":
1931
+ return obj.vertices;
1932
+ case "plane":
1933
+ return [obj.p1, obj.p2, obj.p3];
1934
+ case "sphere":
1935
+ return [obj.center, obj.surfacePoint];
1936
+ case "polyhedron":
1937
+ return obj.vertices;
1938
+ case "cylinder":
1939
+ return [obj.baseCenter, obj.topCenter];
1940
+ case "cone":
1941
+ return [obj.baseCenter, obj.apex];
1942
+ }
1943
+ }
1944
+ collectDependents(targetId) {
1945
+ const dependents = /* @__PURE__ */ new Set([targetId]);
1946
+ let grew = true;
1947
+ while (grew) {
1948
+ grew = false;
1949
+ for (const obj of this.objects.values()) {
1950
+ if (dependents.has(obj.id)) continue;
1951
+ const refs = this.referencedIds(obj);
1952
+ if (refs.some((r) => dependents.has(r))) {
1953
+ dependents.add(obj.id);
1954
+ grew = true;
1955
+ }
1956
+ }
1957
+ }
1958
+ return dependents;
1959
+ }
1960
+ delete(id) {
1961
+ if (!this.objects.has(id)) return;
1962
+ this.capture();
1963
+ const toDelete = this.collectDependents(id);
1964
+ for (const dependentId of toDelete) {
1965
+ this.objects.delete(dependentId);
1966
+ this.order = this.order.filter((x) => x !== dependentId);
1967
+ this.listeners.delete.forEach((cb) => cb(dependentId));
1968
+ }
1969
+ }
1970
+ reset() {
1971
+ this.capture();
1972
+ this.objects.clear();
1973
+ this.order = [];
1974
+ this.counter = 0;
1975
+ this.listeners.reset.forEach((cb) => cb());
1976
+ }
1977
+ reserveId(prefix) {
1978
+ return this.nextId(prefix);
1979
+ }
1980
+ emitChange(id) {
1981
+ const obj = this.objects.get(id);
1982
+ if (!obj) return;
1983
+ this.listeners.change.forEach((cb) => cb(obj));
1984
+ }
1985
+ snapshot() {
1986
+ const cloned = /* @__PURE__ */ new Map();
1987
+ for (const [id, obj] of this.objects) {
1988
+ cloned.set(id, { ...obj });
1989
+ }
1990
+ return {
1991
+ objects: cloned,
1992
+ order: [...this.order],
1993
+ counter: this.counter
1994
+ };
1995
+ }
1996
+ restore(snap) {
1997
+ this.objects = /* @__PURE__ */ new Map();
1998
+ for (const [id, obj] of snap.objects) {
1999
+ this.objects.set(id, { ...obj });
2000
+ }
2001
+ this.order = [...snap.order];
2002
+ this.counter = snap.counter;
2003
+ this.listeners.reset.forEach((cb) => cb());
2004
+ for (const id of this.order) {
2005
+ const obj = this.objects.get(id);
2006
+ if (obj) this.listeners.add.forEach((cb) => cb(obj));
2007
+ }
2008
+ }
2009
+ capture() {
2010
+ if (this.historySuspended) return;
2011
+ this.historyPast.push(this.snapshot());
2012
+ this.historyFuture = [];
2013
+ this.notifyHistoryChange();
2014
+ }
2015
+ canUndo() {
2016
+ return this.historyPast.length > 0;
2017
+ }
2018
+ canRedo() {
2019
+ return this.historyFuture.length > 0;
2020
+ }
2021
+ undo() {
2022
+ const prev = this.historyPast.pop();
2023
+ if (!prev) return;
2024
+ this.historyFuture.push(this.snapshot());
2025
+ this.restore(prev);
2026
+ this.notifyHistoryChange();
2027
+ }
2028
+ redo() {
2029
+ const next = this.historyFuture.pop();
2030
+ if (!next) return;
2031
+ this.historyPast.push(this.snapshot());
2032
+ this.restore(next);
2033
+ this.notifyHistoryChange();
2034
+ }
2035
+ withoutHistory(fn) {
2036
+ const prev = this.historySuspended;
2037
+ this.historySuspended = true;
2038
+ try {
2039
+ fn();
2040
+ } finally {
2041
+ this.historySuspended = prev;
2042
+ }
2043
+ }
2044
+ pushUndoCheckpoint(prev) {
2045
+ if (this.historySuspended) return;
2046
+ this.historyPast.push(prev);
2047
+ this.historyFuture = [];
2048
+ this.notifyHistoryChange();
2049
+ }
2050
+ onHistoryChange(cb) {
2051
+ this.historyListeners.add(cb);
2052
+ return () => {
2053
+ this.historyListeners.delete(cb);
2054
+ };
2055
+ }
2056
+ notifyHistoryChange() {
2057
+ this.historyListeners.forEach((cb) => cb());
2058
+ }
2059
+ };
2060
+ }
2061
+ });
2062
+
2063
+ // src/stamps/geometry-3d/editor/scene/persistence.ts
2064
+ function sceneToBoard(scene, view, bbox) {
2065
+ const elements = [];
2066
+ for (const obj of scene.list()) {
2067
+ const els = sceneObjectToElements(obj, scene);
2068
+ elements.push(...els);
2069
+ }
2070
+ return { version: 2, bbox, view, showAxes: true, showMesh: true, elements };
2071
+ }
2072
+ function sceneObjectToElements(obj, scene) {
2073
+ const baseAttrs = { label: obj.label, visible: obj.visible, color: obj.color };
2074
+ switch (obj.kind) {
2075
+ case "point": {
2076
+ let w;
2077
+ try {
2078
+ w = constraintToWorld(obj.constraint, scene);
2079
+ } catch {
2080
+ w = [0, 0, 0];
2081
+ }
2082
+ return [{
2083
+ type: "point3d",
2084
+ parents: [w[0], w[1], w[2]],
2085
+ attributes: { id: obj.id, ...baseAttrs },
2086
+ id: obj.id,
2087
+ label: obj.label,
2088
+ constraint: obj.constraint
2089
+ }];
2090
+ }
2091
+ case "segment":
2092
+ case "line":
2093
+ case "ray":
2094
+ case "vector":
2095
+ case "plane":
2096
+ case "sphere":
2097
+ case "polygon":
2098
+ case "polyhedron":
2099
+ case "cylinder":
2100
+ case "cone": {
2101
+ return [{
2102
+ type: pickJxgType(obj.kind),
2103
+ parents: [],
2104
+ attributes: { id: obj.id, ...baseAttrs, sceneKind: obj.kind, sceneSpec: encodeSpec(obj) },
2105
+ id: obj.id,
2106
+ label: obj.label
2107
+ }];
2108
+ }
2109
+ }
2110
+ }
2111
+ function pickJxgType(kind) {
2112
+ switch (kind) {
2113
+ case "point":
2114
+ return "point3d";
2115
+ case "segment":
2116
+ case "line":
2117
+ case "ray":
2118
+ case "vector":
2119
+ return "line3d";
2120
+ case "plane":
2121
+ return "plane3d";
2122
+ case "sphere":
2123
+ return "sphere3d";
2124
+ case "polygon":
2125
+ case "polyhedron":
2126
+ case "cylinder":
2127
+ case "cone":
2128
+ return "polygon3d";
2129
+ }
2130
+ }
2131
+ function encodeSpec(obj) {
2132
+ const rest = {};
2133
+ for (const [k, v] of Object.entries(obj)) {
2134
+ if (k === "id" || k === "label" || k === "visible" || k === "color" || k === "kind") continue;
2135
+ rest[k] = v;
2136
+ }
2137
+ return rest;
2138
+ }
2139
+ function boardToScene(board) {
2140
+ const scene = new Scene3D();
2141
+ for (const el of board.elements) {
2142
+ if (el.type === "point3d") {
2143
+ const constraint = el.constraint ?? {
2144
+ kind: "free",
2145
+ x: Number(el.parents[0] ?? 0),
2146
+ y: Number(el.parents[1] ?? 0),
2147
+ z: Number(el.parents[2] ?? 0)
2148
+ };
2149
+ const color2 = el.attributes["color"];
2150
+ const visible2 = el.attributes["visible"] !== false;
2151
+ try {
2152
+ scene.insert({
2153
+ kind: "point",
2154
+ id: el.id,
2155
+ label: el.label ?? el.id,
2156
+ visible: visible2,
2157
+ color: color2,
2158
+ constraint
2159
+ });
2160
+ } catch {
2161
+ }
2162
+ continue;
2163
+ }
2164
+ const sceneKind = el.attributes["sceneKind"];
2165
+ const sceneSpec = el.attributes["sceneSpec"];
2166
+ if (!sceneKind || !sceneSpec) continue;
2167
+ const color = el.attributes["color"];
2168
+ const visible = el.attributes["visible"] !== false;
2169
+ const obj = {
2170
+ id: el.id,
2171
+ label: el.label ?? el.id,
2172
+ visible,
2173
+ color,
2174
+ kind: sceneKind,
2175
+ ...sceneSpec
2176
+ };
2177
+ try {
2178
+ scene.insert(obj);
2179
+ } catch {
2180
+ }
2181
+ }
2182
+ return scene;
2183
+ }
2184
+ var init_persistence = __esm({
2185
+ "src/stamps/geometry-3d/editor/scene/persistence.ts"() {
2186
+ init_Scene3D();
2187
+ init_constraintMath();
2188
+ }
2189
+ });
2190
+ var EditorPanel;
2191
+ var init_EditorPanel = __esm({
2192
+ "src/stamps/geometry-3d/editor/EditorPanel.tsx"() {
2193
+ "use client";
2194
+ init_controller();
2195
+ init_JxgRenderer();
2196
+ init_hitTest();
2197
+ init_rayCast();
2198
+ init_intersect();
2199
+ init_constraintMath();
2200
+ init_ensurePoint();
2201
+ init_MiniBoard3D();
2202
+ init_StatusHint();
2203
+ init_persistence();
2204
+ EditorPanel = React2__namespace.forwardRef(
2205
+ function EditorPanel2(props, ref) {
2206
+ const {
2207
+ isDark: isDarkProp,
2208
+ initialState,
2209
+ scene,
2210
+ selectedTool,
2211
+ onSelectedToolChange,
2212
+ showAxis,
2213
+ showGrid,
2214
+ onReadyChange,
2215
+ onHistoryChange
2216
+ } = props;
2217
+ const isDark = isDarkProp ?? false;
2218
+ const controllerRef = React2__namespace.useRef(null);
2219
+ if (!controllerRef.current) controllerRef.current = new ToolController(scene);
2220
+ const [hint, setHint] = React2__namespace.useState("Ch\u1ECDn c\xF4ng c\u1EE5 trong b\u1EA3ng b\xEAn tr\xE1i");
2221
+ const [hoverLabel, setHoverLabel] = React2__namespace.useState(null);
2222
+ const boardRef = React2__namespace.useRef(null);
2223
+ const rendererRef = React2__namespace.useRef(null);
2224
+ const onSelectedToolChangeRef = React2__namespace.useRef(onSelectedToolChange);
2225
+ onSelectedToolChangeRef.current = onSelectedToolChange;
2226
+ const onHistoryChangeRef = React2__namespace.useRef(onHistoryChange);
2227
+ onHistoryChangeRef.current = onHistoryChange;
2228
+ const selectedToolRef = React2__namespace.useRef(selectedTool);
2229
+ selectedToolRef.current = selectedTool;
2230
+ const draggedPointRef = React2__namespace.useRef(null);
2231
+ const dragStartRef = React2__namespace.useRef(null);
2232
+ const dragSnapshotRef = React2__namespace.useRef(null);
2233
+ React2__namespace.useEffect(() => {
2234
+ if (initialState) {
2235
+ const loaded = boardToScene(initialState);
2236
+ scene.withoutHistory(() => {
2237
+ scene.reset();
2238
+ for (const obj of loaded.list()) {
2239
+ scene.insert(obj);
2240
+ }
2241
+ });
2242
+ }
2243
+ }, []);
2244
+ React2__namespace.useEffect(() => {
2245
+ const ctrl = controllerRef.current;
2246
+ const unsub = ctrl.on((state) => {
2247
+ setHint(state.hint);
2248
+ onSelectedToolChangeRef.current(state.tool?.key ?? "move");
2249
+ });
2250
+ return unsub;
2251
+ }, []);
2252
+ React2__namespace.useEffect(() => {
2253
+ onHistoryChangeRef.current?.(scene.canUndo(), scene.canRedo());
2254
+ const unsub = scene.onHistoryChange(() => {
2255
+ onHistoryChangeRef.current?.(scene.canUndo(), scene.canRedo());
2256
+ });
2257
+ return unsub;
2258
+ }, [scene]);
2259
+ React2__namespace.useEffect(() => {
2260
+ controllerRef.current?.selectTool(selectedTool);
2261
+ }, [selectedTool]);
2262
+ React2__namespace.useEffect(() => {
2263
+ const onKey = (e) => {
2264
+ const ae = document.activeElement;
2265
+ const inField = !!(ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable));
2266
+ if (inField) return;
2267
+ if (!(e.metaKey || e.ctrlKey)) return;
2268
+ const key = e.key.toLowerCase();
2269
+ if (key === "z" && !e.shiftKey) {
2270
+ e.preventDefault();
2271
+ e.stopPropagation();
2272
+ scene.undo();
2273
+ } else if (key === "z" && e.shiftKey || key === "y" && !e.shiftKey) {
2274
+ e.preventDefault();
2275
+ e.stopPropagation();
2276
+ scene.redo();
2277
+ }
2278
+ };
2279
+ window.addEventListener("keydown", onKey, { capture: true });
2280
+ return () => window.removeEventListener("keydown", onKey, { capture: true });
2281
+ }, [scene]);
2282
+ React2__namespace.useEffect(() => {
2283
+ return () => {
2284
+ rendererRef.current?.dispose();
2285
+ rendererRef.current = null;
2286
+ };
2287
+ }, []);
2288
+ React2__namespace.useEffect(() => {
2289
+ const view = boardRef.current?.getView3D();
2290
+ const v = view;
2291
+ if (!v || typeof v.setAttribute !== "function") return;
2292
+ try {
2293
+ v.setAttribute({
2294
+ xAxis: { visible: showAxis },
2295
+ yAxis: { visible: showAxis },
2296
+ zAxis: { visible: showAxis },
2297
+ // GeoGebra-style: only the XY ground plane is shown; side walls stay hidden.
2298
+ xPlaneRear: { visible: false, mesh3d: { visible: false } },
2299
+ yPlaneRear: { visible: false, mesh3d: { visible: false } },
2300
+ zPlaneRear: { visible: showGrid, mesh3d: { visible: false } }
2301
+ });
2302
+ v.board?.update?.();
2303
+ } catch {
2304
+ }
2305
+ }, [showAxis, showGrid]);
2306
+ const handleView3DReady = React2__namespace.useCallback((view) => {
2307
+ rendererRef.current = new JxgRenderer(scene, view);
2308
+ onReadyChange?.(true);
2309
+ }, [onReadyChange, scene]);
2310
+ const handleClick = React2__namespace.useCallback((screen) => {
2311
+ const board = boardRef.current;
2312
+ if (!board) return;
2313
+ const view = board.getView3D();
2314
+ if (!view) return;
2315
+ try {
2316
+ const hit = hitTest(screen, view, scene);
2317
+ controllerRef.current.consumeHit(hit);
2318
+ } catch {
2319
+ }
2320
+ }, [scene]);
2321
+ const handleMove = React2__namespace.useCallback((screen) => {
2322
+ const board = boardRef.current;
2323
+ if (!board) return;
2324
+ const view = board.getView3D();
2325
+ if (!view) return;
2326
+ if (draggedPointRef.current) return;
2327
+ let hit;
2328
+ try {
2329
+ hit = hitTest(screen, view, scene);
2330
+ } catch {
2331
+ setHoverLabel(null);
2332
+ return;
2333
+ }
2334
+ if (hit.kind === "empty") setHoverLabel(null);
2335
+ else if (hit.kind === "existingPoint") {
2336
+ const obj = scene.get(hit.pointId);
2337
+ setHoverLabel(obj?.label ?? null);
2338
+ } else if (hit.kind === "onGround") setHoverLabel("m\u1EB7t n\u1EC1n");
2339
+ else if (hit.kind === "onAxis") setHoverLabel(`tr\u1EE5c ${hit.axis.toUpperCase()}`);
2340
+ else if (hit.kind === "onPlane") setHoverLabel(`m\u1EB7t ph\u1EB3ng ${hit.planeId}`);
2341
+ else if (hit.kind === "onSphere") setHoverLabel(`m\u1EB7t c\u1EA7u ${hit.sphereId}`);
2342
+ else setHoverLabel(null);
2343
+ }, [scene]);
2344
+ const shouldStartPointDrag = React2__namespace.useCallback((screen) => {
2345
+ const view = boardRef.current?.getView3D();
2346
+ if (!view) return false;
2347
+ const tool = selectedToolRef.current;
2348
+ if (tool !== "point" && tool !== "move") return false;
2349
+ let hit;
2350
+ try {
2351
+ hit = hitTest(screen, view, scene);
2352
+ } catch {
2353
+ return false;
2354
+ }
2355
+ if (hit.kind === "existingPoint") {
2356
+ const pt = scene.get(hit.pointId);
2357
+ if (!pt || pt.kind !== "point") return false;
2358
+ dragSnapshotRef.current = scene.snapshot();
2359
+ draggedPointRef.current = hit.pointId;
2360
+ dragStartRef.current = {
2361
+ screen,
2362
+ world: constraintToWorld(pt.constraint, scene)
2363
+ };
2364
+ return true;
2365
+ }
2366
+ if (tool === "point" && (hit.kind === "onGround" || hit.kind === "onAxis")) {
2367
+ dragSnapshotRef.current = scene.snapshot();
2368
+ const constraint = hitToConstraint(hit);
2369
+ if (!constraint) {
2370
+ dragSnapshotRef.current = null;
2371
+ return false;
2372
+ }
2373
+ let id = null;
2374
+ scene.withoutHistory(() => {
2375
+ id = scene.addPoint(constraint);
2376
+ });
2377
+ if (!id) {
2378
+ dragSnapshotRef.current = null;
2379
+ return false;
2380
+ }
2381
+ draggedPointRef.current = id;
2382
+ dragStartRef.current = {
2383
+ screen,
2384
+ world: [hit.world[0], hit.world[1], hit.world[2]]
2385
+ };
2386
+ return true;
2387
+ }
2388
+ if (tool === "point") {
2389
+ dragSnapshotRef.current = null;
2390
+ draggedPointRef.current = null;
2391
+ dragStartRef.current = null;
2392
+ return true;
2393
+ }
2394
+ return false;
2395
+ }, [scene]);
2396
+ const onPointerDrag = React2__namespace.useCallback((screen) => {
2397
+ const pointId = draggedPointRef.current;
2398
+ const start = dragStartRef.current;
2399
+ if (!pointId || !start) return;
2400
+ const view = boardRef.current?.getView3D();
2401
+ if (!view) return;
2402
+ const tool = selectedToolRef.current;
2403
+ let nextWorld;
2404
+ if (tool === "point") {
2405
+ const dz = screen.y - start.screen.y;
2406
+ nextWorld = [start.world[0], start.world[1], start.world[2] + dz];
2407
+ } else if (tool === "move") {
2408
+ try {
2409
+ const ray = screenToRay(screen, view);
2410
+ const hit = rayPlane(ray, { point: [0, 0, start.world[2]], normal: [0, 0, 1] });
2411
+ if (!hit) return;
2412
+ nextWorld = [hit.point[0], hit.point[1], start.world[2]];
2413
+ } catch {
2414
+ return;
2415
+ }
2416
+ } else {
2417
+ return;
2418
+ }
2419
+ const obj = scene.get(pointId);
2420
+ if (!obj || obj.kind !== "point") return;
2421
+ const free = { kind: "free", x: nextWorld[0], y: nextWorld[1], z: nextWorld[2] };
2422
+ obj.constraint = free;
2423
+ scene.emitChange(pointId);
2424
+ }, [scene]);
2425
+ const onPointerDragEnd = React2__namespace.useCallback(() => {
2426
+ const snap = dragSnapshotRef.current;
2427
+ dragSnapshotRef.current = null;
2428
+ draggedPointRef.current = null;
2429
+ dragStartRef.current = null;
2430
+ if (snap) {
2431
+ scene.pushUndoCheckpoint(snap);
2432
+ }
2433
+ }, [scene]);
2434
+ React2__namespace.useImperativeHandle(
2435
+ ref,
2436
+ () => ({
2437
+ hasContent: () => scene.list().length > 0,
2438
+ serialize: () => {
2439
+ const view = boardRef.current?.getView3D();
2440
+ const v = view;
2441
+ const azSlider = v?.az_slide ?? v?.az;
2442
+ const elSlider = v?.el_slide ?? v?.el;
2443
+ const azimuth = typeof azSlider?.Value === "function" ? azSlider.Value() : 0;
2444
+ const elevation = typeof elSlider?.Value === "function" ? elSlider.Value() : 0;
2445
+ return sceneToBoard(
2446
+ scene,
2447
+ { azimuth, elevation, bbox3D: [-5, -5, -5, 5, 5, 5] },
2448
+ [-6, -6, 6, 6]
2449
+ );
2450
+ },
2451
+ setTool: (k) => controllerRef.current.selectTool(k),
2452
+ undo: () => scene.undo(),
2453
+ redo: () => scene.redo()
2454
+ }),
2455
+ [scene]
2456
+ );
2457
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2458
+ "div",
2459
+ {
2460
+ "data-testid": "editor-panel-3d",
2461
+ className: [
2462
+ isDark ? "theme--dark " : "",
2463
+ "flex h-full w-full min-w-0 flex-col overflow-hidden bg-white"
2464
+ ].join(""),
2465
+ children: [
2466
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(
2467
+ MiniBoard3D,
2468
+ {
2469
+ ref: boardRef,
2470
+ isDark,
2471
+ onView3DReady: handleView3DReady,
2472
+ onPointerClick: handleClick,
2473
+ onPointerMove: handleMove,
2474
+ onPointerLeave: () => setHoverLabel(null),
2475
+ shouldStartPointDrag,
2476
+ onPointerDrag,
2477
+ onPointerDragEnd
2478
+ }
2479
+ ) }),
2480
+ /* @__PURE__ */ jsxRuntime.jsx(StatusHint, { hint, hoverLabel })
2481
+ ]
2482
+ }
2483
+ );
2484
+ }
2485
+ );
2486
+ }
2487
+ });
2488
+ function ToolButton(props) {
2489
+ const {
2490
+ toolKey,
2491
+ label,
2492
+ selected,
2493
+ onClick,
2494
+ icon,
2495
+ chordNum,
2496
+ chordActiveGroup,
2497
+ onMouseEnter,
2498
+ onMouseLeave
2499
+ } = props;
2500
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2501
+ "button",
2502
+ {
2503
+ type: "button",
2504
+ "data-tool-key": toolKey,
2505
+ "data-testid": `tool-${toolKey}`,
2506
+ "aria-label": label,
2507
+ "aria-pressed": selected,
2508
+ onClick: () => onClick(toolKey),
2509
+ onMouseEnter,
2510
+ onMouseLeave,
2511
+ className: [
2512
+ "relative flex aspect-square items-center justify-center rounded-md transition",
2513
+ selected ? "bg-emerald-600 text-white shadow-sm" : "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
2514
+ ].join(" "),
2515
+ children: [
2516
+ /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className: "inline-flex", children: icon ?? null }),
2517
+ chordNum != null && /* @__PURE__ */ jsxRuntime.jsx(
2518
+ "span",
2519
+ {
2520
+ "data-testid": `chord-num-${toolKey}`,
2521
+ className: [
2522
+ "pointer-events-none absolute bottom-0 right-0.5 font-mono text-[9px] leading-none transition",
2523
+ selected ? "text-white/70" : chordActiveGroup ? "text-emerald-700" : "text-slate-300"
2524
+ ].join(" "),
2525
+ children: chordNum
2526
+ }
2527
+ )
2528
+ ]
2529
+ }
2530
+ );
2531
+ }
2532
+ var init_ToolButton = __esm({
2533
+ "src/stamps/geometry-3d/editor/toolPanel/ToolButton.tsx"() {
2534
+ "use client";
2535
+ }
2536
+ });
2537
+ var wrap, dot4, ToolIcons;
2538
+ var init_icons = __esm({
2539
+ "src/stamps/geometry-3d/editor/toolPanel/icons.tsx"() {
2540
+ wrap = (children) => /* @__PURE__ */ jsxRuntime.jsx(
2541
+ "svg",
2542
+ {
2543
+ width: "18",
2544
+ height: "18",
2545
+ viewBox: "0 0 24 24",
2546
+ fill: "none",
2547
+ stroke: "currentColor",
2548
+ strokeWidth: "1.6",
2549
+ strokeLinecap: "round",
2550
+ strokeLinejoin: "round",
2551
+ "aria-hidden": "true",
2552
+ children
2553
+ }
2554
+ );
2555
+ dot4 = (cx, cy, r = 1.4) => /* @__PURE__ */ jsxRuntime.jsx("circle", { cx, cy, r, fill: "currentColor", stroke: "none" });
2556
+ ToolIcons = {
2557
+ move: wrap(
2558
+ /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 4 L5 14 L8 11 L10 16 L13 15 L11 10 L15 10 Z" }) })
2559
+ ),
2560
+ point: wrap(
2561
+ /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "2.4", fill: "currentColor", stroke: "none" }) })
2562
+ ),
2563
+ pointOnObject: wrap(
2564
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2565
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 16 L21 12" }),
2566
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "13.5", r: "2.4", fill: "currentColor", stroke: "none" })
2567
+ ] })
2568
+ ),
2569
+ segment: wrap(
2570
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2571
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "18", x2: "20", y2: "6" }),
2572
+ dot4(4, 18, 1.6),
2573
+ dot4(20, 6, 1.6)
2574
+ ] })
2575
+ ),
2576
+ line: wrap(
2577
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2578
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "3", y1: "18", x2: "21", y2: "6" }),
2579
+ dot4(8, 14.5, 1.4),
2580
+ dot4(16, 9.5, 1.4)
2581
+ ] })
2582
+ ),
2583
+ ray: wrap(
2584
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2585
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "5", y1: "18", x2: "19", y2: "7" }),
2586
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M19 7 L15 6 M19 7 L18 11" }),
2587
+ dot4(5, 18, 1.6)
2588
+ ] })
2589
+ ),
2590
+ vector: wrap(
2591
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2592
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "5", y1: "18", x2: "18", y2: "7" }),
2593
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M18 7 L13 7 M18 7 L18 12" }),
2594
+ dot4(5, 18, 1.6)
2595
+ ] })
2596
+ ),
2597
+ polygon: wrap(
2598
+ /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "12,4 20,10 17,19 7,19 4,10" }) })
2599
+ ),
2600
+ plane: wrap(
2601
+ /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: /* @__PURE__ */ jsxRuntime.jsx("polygon", { points: "3,9 14,5 21,11 10,15" }) })
2602
+ ),
2603
+ pyramid: wrap(
2604
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2605
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 19 L20 19 L12 4 Z" }),
2606
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 19 L12 16 L20 19" }),
2607
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 4 L12 16", strokeDasharray: "2 2" })
2608
+ ] })
2609
+ ),
2610
+ prism: wrap(
2611
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2612
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 8 L4 19 L14 19 L14 8 Z" }),
2613
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 8 L10 4 L20 4 L14 8" }),
2614
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 8 L14 19 L20 15 L20 4" }),
2615
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 8 L14 8" })
2616
+ ] })
2617
+ ),
2618
+ tetrahedron: wrap(
2619
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2620
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 19 L20 19 L12 5 Z" }),
2621
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 19 L15 12 L20 19" }),
2622
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M15 12 L12 5" })
2623
+ ] })
2624
+ ),
2625
+ cube: wrap(
2626
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2627
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 8 L4 19 L14 19 L14 8 Z" }),
2628
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 8 L10 4 L20 4 L14 8" }),
2629
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 8 L14 19 L20 15 L20 4" })
2630
+ ] })
2631
+ ),
2632
+ sphere: wrap(
2633
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2634
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "8" }),
2635
+ /* @__PURE__ */ jsxRuntime.jsx("ellipse", { cx: "12", cy: "12", rx: "8", ry: "3" }),
2636
+ dot4(12, 12, 1.2)
2637
+ ] })
2638
+ ),
2639
+ cylinder: wrap(
2640
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2641
+ /* @__PURE__ */ jsxRuntime.jsx("ellipse", { cx: "12", cy: "6", rx: "6", ry: "2" }),
2642
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M6 6 L6 18" }),
2643
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M18 6 L18 18" }),
2644
+ /* @__PURE__ */ jsxRuntime.jsx("ellipse", { cx: "12", cy: "18", rx: "6", ry: "2" })
2645
+ ] })
2646
+ ),
2647
+ cone: wrap(
2648
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2649
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "5", y1: "18", x2: "12", y2: "4" }),
2650
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "19", y1: "18", x2: "12", y2: "4" }),
2651
+ /* @__PURE__ */ jsxRuntime.jsx("ellipse", { cx: "12", cy: "18", rx: "7", ry: "2" })
2652
+ ] })
2653
+ )
2654
+ };
2655
+ }
2656
+ });
2657
+
2658
+ // src/stamps/geometry-3d/editor/toolPanel/groups.ts
2659
+ function letterForGroup(g) {
2660
+ const idx = GROUP_ORDER.indexOf(g);
2661
+ return idx >= 0 ? String.fromCharCode(A_CODE + idx) : "";
2662
+ }
2663
+ var GROUP_ORDER, GROUP_LABELS, TOOLS_BY_GROUP, SPEC_BY_KEY, TOOLS_FLAT, A_CODE;
2664
+ var init_groups = __esm({
2665
+ "src/stamps/geometry-3d/editor/toolPanel/groups.ts"() {
2666
+ init_spec();
2667
+ GROUP_ORDER = [
2668
+ "basic",
2669
+ "point",
2670
+ "line",
2671
+ "plane",
2672
+ "polyhedron",
2673
+ "curve"
2674
+ ];
2675
+ GROUP_LABELS = {
2676
+ basic: "C\u01A1 b\u1EA3n",
2677
+ point: "\u0110i\u1EC3m",
2678
+ line: "\u0110\u01B0\u1EDDng th\u1EB3ng",
2679
+ plane: "M\u1EB7t ph\u1EB3ng",
2680
+ polyhedron: "Kh\u1ED1i \u0111a di\u1EC7n",
2681
+ curve: "Kh\u1ED1i cong"
2682
+ };
2683
+ TOOLS_BY_GROUP = {
2684
+ basic: ["move"],
2685
+ point: ["point", "pointOnObject"],
2686
+ line: ["segment", "line", "ray", "vector", "polygon"],
2687
+ plane: ["plane"],
2688
+ polyhedron: ["pyramid", "prism", "tetrahedron", "cube"],
2689
+ curve: ["sphere", "cylinder", "cone"]
2690
+ };
2691
+ SPEC_BY_KEY = TOOLS.reduce(
2692
+ (acc, t) => {
2693
+ acc[t.key] = t;
2694
+ return acc;
2695
+ },
2696
+ {}
2697
+ );
2698
+ TOOLS_FLAT = GROUP_ORDER.flatMap(
2699
+ (group) => TOOLS_BY_GROUP[group].map((key) => {
2700
+ const spec = SPEC_BY_KEY[key];
2701
+ return {
2702
+ key,
2703
+ label: spec?.label ?? key,
2704
+ hint: spec?.hintIdle ?? "",
2705
+ group
2706
+ };
2707
+ })
2708
+ );
2709
+ A_CODE = "A".charCodeAt(0);
2710
+ }
2711
+ });
2712
+ function ToolPalette(props) {
2713
+ const { selected, onSelect, chordGroup = null, onHoverTool } = props;
2714
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-testid": "tool-palette", className: "flex flex-col gap-3", children: GROUP_ORDER.map((group) => {
2715
+ const keys = TOOLS_BY_GROUP[group];
2716
+ const isChordActive = chordGroup === group;
2717
+ const dimmed = chordGroup !== null && !isChordActive;
2718
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2719
+ "section",
2720
+ {
2721
+ "data-chord-group": group,
2722
+ "data-chord-active": isChordActive ? "true" : "false",
2723
+ className: [
2724
+ "rounded-md transition",
2725
+ isChordActive ? "bg-emerald-50 ring-1 ring-emerald-400 p-1" : "p-0",
2726
+ dimmed ? "opacity-55" : "opacity-100"
2727
+ ].join(" "),
2728
+ children: [
2729
+ /* @__PURE__ */ jsxRuntime.jsxs("h4", { className: "mb-1.5 flex items-center justify-between text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: [
2730
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: GROUP_LABELS[group] }),
2731
+ /* @__PURE__ */ jsxRuntime.jsx(
2732
+ "span",
2733
+ {
2734
+ "data-testid": `chord-letter-${group}`,
2735
+ className: [
2736
+ "font-mono text-[10px] leading-none transition",
2737
+ isChordActive ? "text-emerald-700 font-bold" : "text-slate-400"
2738
+ ].join(" "),
2739
+ children: letterForGroup(group)
2740
+ }
2741
+ )
2742
+ ] }),
2743
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-4 gap-1", children: keys.map((k, i) => {
2744
+ const tool = TOOLS.find((t) => t.key === k);
2745
+ return /* @__PURE__ */ jsxRuntime.jsx(
2746
+ ToolButton,
2747
+ {
2748
+ toolKey: k,
2749
+ label: tool.label,
2750
+ selected: selected === k,
2751
+ onClick: onSelect,
2752
+ icon: ToolIcons[k],
2753
+ chordNum: i + 1,
2754
+ chordActiveGroup: isChordActive,
2755
+ onMouseEnter: (e) => onHoverTool?.({
2756
+ label: tool.label,
2757
+ hint: tool.hintIdle,
2758
+ x: e.clientX,
2759
+ y: e.clientY
2760
+ }),
2761
+ onMouseLeave: () => onHoverTool?.(null)
2762
+ },
2763
+ k
2764
+ );
2765
+ }) })
2766
+ ]
2767
+ },
2768
+ group
2769
+ );
2770
+ }) });
2771
+ }
2772
+ var init_ToolPalette = __esm({
2773
+ "src/stamps/geometry-3d/editor/toolPanel/ToolPalette.tsx"() {
2774
+ "use client";
2775
+ init_ToolButton();
2776
+ init_icons();
2777
+ init_groups();
2778
+ init_spec();
2779
+ }
2780
+ });
2781
+
2782
+ // src/stamps/geometry-3d/editor/algebraPanel/symbolic.ts
2783
+ function symbolicFor(obj, scene) {
2784
+ const n = (id) => scene.get(id)?.label ?? id;
2785
+ switch (obj.kind) {
2786
+ case "point": {
2787
+ const c = obj.constraint;
2788
+ switch (c.kind) {
2789
+ case "free":
2790
+ return "Point";
2791
+ case "onGround":
2792
+ return "Point(xyPlane)";
2793
+ case "onAxis":
2794
+ return `Point(${c.axis}Axis)`;
2795
+ case "onPlane":
2796
+ return `Point(${n(c.planeId)})`;
2797
+ case "onLine":
2798
+ return `Point(${n(c.lineId)})`;
2799
+ case "onPolygon":
2800
+ return `Point(${n(c.polygonId)})`;
2801
+ case "onSphere":
2802
+ return `Point(${n(c.sphereId)})`;
2803
+ }
2804
+ return "Point";
2805
+ }
2806
+ case "segment":
2807
+ return `Segment(${n(obj.p1)}, ${n(obj.p2)})`;
2808
+ case "line":
2809
+ return `Line(${n(obj.p1)}, ${n(obj.p2)})`;
2810
+ case "ray":
2811
+ return `Ray(${n(obj.origin)}, ${n(obj.through)})`;
2812
+ case "vector":
2813
+ return `Vector(${n(obj.from)}, ${n(obj.to)})`;
2814
+ case "polygon":
2815
+ return `Polygon(${obj.vertices.map(n).join(", ")})`;
2816
+ case "plane":
2817
+ return `Plane(${n(obj.p1)}, ${n(obj.p2)}, ${n(obj.p3)})`;
2818
+ case "sphere":
2819
+ return `Sphere(${n(obj.center)}, ${n(obj.surfacePoint)})`;
2820
+ case "polyhedron": {
2821
+ const flavorVn = {
2822
+ pyramid: "Ch\xF3p",
2823
+ prism: "L\u0103ng tr\u1EE5",
2824
+ tetrahedron: "T\u1EE9 di\u1EC7n",
2825
+ cube: "L\u1EADp ph\u01B0\u01A1ng"
2826
+ };
2827
+ return `${flavorVn[obj.flavor]}(${obj.vertices.length} \u0111\u1EC9nh)`;
2828
+ }
2829
+ case "cylinder":
2830
+ return `Cylinder(${n(obj.baseCenter)}, ${n(obj.topCenter)}, r=${obj.radius})`;
2831
+ case "cone":
2832
+ return `Cone(${n(obj.baseCenter)}, ${n(obj.apex)}, r=${obj.radius})`;
2833
+ }
2834
+ }
2835
+ function numericFor(obj, scene) {
2836
+ if (obj.kind === "point") {
2837
+ const w = constraintToWorld(obj.constraint, scene);
2838
+ return `(${round(w[0])}, ${round(w[1])}, ${round(w[2])})`;
2839
+ }
2840
+ return "";
2841
+ }
2842
+ function round(x) {
2843
+ return Math.abs(x) < 1e-9 ? "0" : (Math.round(x * 100) / 100).toString();
2844
+ }
2845
+ var init_symbolic = __esm({
2846
+ "src/stamps/geometry-3d/editor/algebraPanel/symbolic.ts"() {
2847
+ init_constraintMath();
2848
+ }
2849
+ });
2850
+ function RowMenu(props) {
2851
+ const [open, setOpen] = React2__namespace.useState(false);
2852
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative inline-block", children: [
2853
+ /* @__PURE__ */ jsxRuntime.jsx(
2854
+ "button",
2855
+ {
2856
+ type: "button",
2857
+ "aria-label": "Row menu",
2858
+ onClick: () => setOpen((v) => !v),
2859
+ className: "rounded px-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-800",
2860
+ children: "\u22EE"
2861
+ }
2862
+ ),
2863
+ open ? /* @__PURE__ */ jsxRuntime.jsxs(
2864
+ "div",
2865
+ {
2866
+ role: "menu",
2867
+ 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",
2868
+ children: [
2869
+ /* @__PURE__ */ jsxRuntime.jsx(MenuItem, { onClick: () => {
2870
+ setOpen(false);
2871
+ props.onRename();
2872
+ }, children: "\u0110\u1ED5i t\xEAn" }),
2873
+ /* @__PURE__ */ jsxRuntime.jsx(MenuItem, { onClick: () => {
2874
+ setOpen(false);
2875
+ props.onChangeColor();
2876
+ }, children: "\u0110\u1ED5i m\xE0u" }),
2877
+ /* @__PURE__ */ jsxRuntime.jsx(MenuItem, { onClick: () => {
2878
+ setOpen(false);
2879
+ props.onToggleVisibility();
2880
+ }, children: props.visible ? "\u1EA8n" : "Hi\u1EC7n" }),
2881
+ /* @__PURE__ */ jsxRuntime.jsx(MenuItem, { onClick: () => {
2882
+ setOpen(false);
2883
+ props.onDelete();
2884
+ }, className: "text-red-600", children: "Xo\xE1" })
2885
+ ]
2886
+ }
2887
+ ) : null
2888
+ ] });
2889
+ }
2890
+ function MenuItem({ children, onClick, className }) {
2891
+ return /* @__PURE__ */ jsxRuntime.jsx(
2892
+ "button",
2893
+ {
2894
+ type: "button",
2895
+ role: "menuitem",
2896
+ onClick,
2897
+ className: `block w-full px-3 py-1 text-left hover:bg-zinc-100 dark:hover:bg-zinc-800 ${className ?? ""}`,
2898
+ children
2899
+ }
2900
+ );
2901
+ }
2902
+ var init_RowMenu = __esm({
2903
+ "src/stamps/geometry-3d/editor/algebraPanel/RowMenu.tsx"() {
2904
+ "use client";
2905
+ }
2906
+ });
2907
+ function AlgebraRow(props) {
2908
+ const { obj, scene, onDelete } = props;
2909
+ const symbolic = symbolicFor(obj, scene);
2910
+ const numeric = numericFor(obj, scene);
2911
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2912
+ "li",
2913
+ {
2914
+ "data-testid": `algebra-row-${obj.id}`,
2915
+ className: "flex items-center gap-2 border-b border-zinc-100 px-3 py-1.5 text-xs dark:border-zinc-800",
2916
+ children: [
2917
+ /* @__PURE__ */ jsxRuntime.jsx(
2918
+ "span",
2919
+ {
2920
+ "aria-hidden": true,
2921
+ className: "inline-block size-3 rounded-full border",
2922
+ style: { backgroundColor: obj.color ?? "#0066cc" }
2923
+ }
2924
+ ),
2925
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-[3ch] font-semibold", children: obj.label }),
2926
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-zinc-500", children: "=" }),
2927
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 truncate font-mono", children: symbolic }),
2928
+ numeric ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate text-zinc-500", children: numeric }) : null,
2929
+ /* @__PURE__ */ jsxRuntime.jsx(
2930
+ RowMenu,
2931
+ {
2932
+ visible: obj.visible,
2933
+ onRename: () => {
2934
+ },
2935
+ onChangeColor: () => {
2936
+ },
2937
+ onToggleVisibility: () => {
2938
+ },
2939
+ onDelete: () => onDelete(obj.id)
2940
+ }
2941
+ )
2942
+ ]
2943
+ }
2944
+ );
2945
+ }
2946
+ var init_AlgebraRow = __esm({
2947
+ "src/stamps/geometry-3d/editor/algebraPanel/AlgebraRow.tsx"() {
2948
+ "use client";
2949
+ init_symbolic();
2950
+ init_RowMenu();
2951
+ }
2952
+ });
2953
+ function AlgebraList(props) {
2954
+ const { scene } = props;
2955
+ const [, forceUpdate] = React2__namespace.useReducer((x) => x + 1, 0);
2956
+ React2__namespace.useEffect(() => {
2957
+ const unsubAdd = scene.on("add", () => forceUpdate());
2958
+ const unsubChange = scene.on("change", () => forceUpdate());
2959
+ const unsubDelete = scene.on("delete", () => forceUpdate());
2960
+ const unsubReset = scene.on("reset", () => forceUpdate());
2961
+ return () => {
2962
+ unsubAdd();
2963
+ unsubChange();
2964
+ unsubDelete();
2965
+ unsubReset();
2966
+ };
2967
+ }, [scene]);
2968
+ const objects = scene.list();
2969
+ return /* @__PURE__ */ jsxRuntime.jsx(
2970
+ "ul",
2971
+ {
2972
+ "data-testid": "algebra-list",
2973
+ className: "flex max-h-[calc(100vh-200px)] flex-col overflow-y-auto",
2974
+ children: objects.length === 0 ? /* @__PURE__ */ jsxRuntime.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__ */ jsxRuntime.jsx(AlgebraRow, { obj: o, scene, onDelete: (id) => scene.delete(id) }, o.id))
2975
+ }
2976
+ );
2977
+ }
2978
+ var init_AlgebraList = __esm({
2979
+ "src/stamps/geometry-3d/editor/algebraPanel/AlgebraList.tsx"() {
2980
+ "use client";
2981
+ init_AlgebraRow();
2982
+ }
2983
+ });
2984
+ function MobileToolDrawer({
2985
+ title,
2986
+ headerIcon,
2987
+ chips,
2988
+ actions,
2989
+ groups,
2990
+ activeTool,
2991
+ onToolSelect,
2992
+ drawerOpen,
2993
+ onDrawerClose,
2994
+ isDark,
2995
+ testId
2996
+ }) {
2997
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2998
+ drawerOpen && /* @__PURE__ */ jsxRuntime.jsx(
2999
+ "div",
3000
+ {
3001
+ className: "stamp-drawer-backdrop",
3002
+ onPointerDown: onDrawerClose,
3003
+ "aria-hidden": "true"
3004
+ }
3005
+ ),
3006
+ /* @__PURE__ */ jsxRuntime.jsxs(
3007
+ "aside",
3008
+ {
3009
+ role: "complementary",
3010
+ "aria-label": title,
3011
+ "aria-hidden": !drawerOpen ? "true" : void 0,
3012
+ "data-testid": testId,
3013
+ "data-stamp-area": "true",
3014
+ "data-mobile-drawer": "true",
3015
+ "data-geo-mobile": "true",
3016
+ "data-drawer-state": drawerOpen ? "open" : "closed",
3017
+ className: [
3018
+ isDark ? "theme--dark " : "",
3019
+ "stamp-drawer-mobile flex flex-col border-r border-slate-200 bg-white shadow-md"
3020
+ ].join(""),
3021
+ children: [
3022
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center justify-between border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white px-4 py-3", children: [
3023
+ /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex items-center gap-2 text-base font-semibold text-slate-800", children: [
3024
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex h-7 w-7 items-center justify-center rounded-lg bg-emerald-50 text-emerald-700", children: headerIcon }),
3025
+ title
3026
+ ] }),
3027
+ /* @__PURE__ */ jsxRuntime.jsx(
3028
+ "button",
3029
+ {
3030
+ type: "button",
3031
+ onClick: onDrawerClose,
3032
+ "aria-label": "\u0110\xF3ng ng\u0103n c\xF4ng c\u1EE5",
3033
+ className: "inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
3034
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3035
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
3036
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
3037
+ ] })
3038
+ }
3039
+ )
3040
+ ] }),
3041
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sticky top-0 z-10 flex items-center gap-2 border-b border-slate-200 bg-white/95 px-3 py-2 backdrop-blur", children: [
3042
+ chips.map((c) => /* @__PURE__ */ jsxRuntime.jsxs(
3043
+ "button",
3044
+ {
3045
+ type: "button",
3046
+ role: "switch",
3047
+ "aria-pressed": c.pressed,
3048
+ "aria-label": c.label,
3049
+ "data-testid": c.testId,
3050
+ onClick: () => c.onToggle(!c.pressed),
3051
+ className: "geo-mobile-chip",
3052
+ children: [
3053
+ c.icon,
3054
+ c.label
3055
+ ]
3056
+ },
3057
+ c.label
3058
+ )),
3059
+ actions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ml-auto flex items-center gap-1", children: actions.map((a) => /* @__PURE__ */ jsxRuntime.jsx(
3060
+ "button",
3061
+ {
3062
+ type: "button",
3063
+ onClick: a.onClick,
3064
+ disabled: a.disabled,
3065
+ "aria-label": a.label,
3066
+ title: a.title ?? a.label,
3067
+ "data-testid": a.testId,
3068
+ className: "inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent",
3069
+ children: a.icon
3070
+ },
3071
+ a.label
3072
+ )) })
3073
+ ] }),
3074
+ /* @__PURE__ */ jsxRuntime.jsx(
3075
+ "div",
3076
+ {
3077
+ className: "min-h-0 flex-1 overflow-y-auto",
3078
+ style: { paddingBottom: "calc(0.75rem + env(safe-area-inset-bottom))" },
3079
+ children: groups.map((g) => /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "px-3 pt-3 pb-1", children: [
3080
+ /* @__PURE__ */ jsxRuntime.jsxs("h4", { className: "mb-2 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500", children: [
3081
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-1 w-1 rounded-full bg-emerald-500" }),
3082
+ g.groupLabel
3083
+ ] }),
3084
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-3 gap-2", children: g.tools.map((t) => {
3085
+ const active = activeTool === t.key;
3086
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3087
+ "button",
3088
+ {
3089
+ type: "button",
3090
+ "aria-label": t.label,
3091
+ "aria-pressed": active,
3092
+ "data-tool": t.key,
3093
+ onClick: () => {
3094
+ onToolSelect(t.key);
3095
+ onDrawerClose();
3096
+ },
3097
+ className: [
3098
+ "flex flex-col items-center justify-center gap-1.5 rounded-2xl px-2 py-3 transition active:scale-95",
3099
+ active ? "geo-mobile-tool-active" : "bg-slate-50 text-slate-700 hover:bg-slate-100"
3100
+ ].join(" "),
3101
+ children: [
3102
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-6 w-6 items-center justify-center", children: t.icon }),
3103
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-center text-[11px] font-medium leading-tight line-clamp-2", children: t.label })
3104
+ ]
3105
+ },
3106
+ t.key
3107
+ );
3108
+ }) })
3109
+ ] }, g.group))
3110
+ }
3111
+ )
3112
+ ]
3113
+ }
3114
+ )
3115
+ ] });
3116
+ }
3117
+ var init_MobileToolDrawer = __esm({
3118
+ "src/stamps/shared/MobileToolDrawer.tsx"() {
3119
+ "use client";
3120
+ }
3121
+ });
3122
+ function AxisIcon() {
3123
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
3124
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "20", x2: "20", y2: "20" }),
3125
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "20", x2: "4", y2: "4" }),
3126
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "20", x2: "16", y2: "8" })
3127
+ ] });
3128
+ }
3129
+ function GridIcon() {
3130
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
3131
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 8 L20 4" }),
3132
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 14 L20 10" }),
3133
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 20 L20 16" }),
3134
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 8 L4 20" }),
3135
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 6 L12 18" }),
3136
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M20 4 L20 16" })
3137
+ ] });
3138
+ }
3139
+ function UndoIcon() {
3140
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
3141
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 10 L8 5 L8 8 L15 8 A5 5 0 0 1 20 13 L20 16" }),
3142
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 10 L8 15 L8 12" })
3143
+ ] });
3144
+ }
3145
+ function RedoIcon() {
3146
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
3147
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 10 L16 5 L16 8 L9 8 A5 5 0 0 0 4 13 L4 16" }),
3148
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M21 10 L16 15 L16 12" })
3149
+ ] });
3150
+ }
3151
+ function CloseIcon() {
3152
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
3153
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
3154
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
3155
+ ] });
3156
+ }
3157
+ function Shell({ title, icon, onClose, children, isDark }) {
3158
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3159
+ "aside",
3160
+ {
3161
+ role: "complementary",
3162
+ "aria-label": title,
3163
+ "data-testid": "left-panel",
3164
+ "data-stamp-area": "true",
3165
+ className: [
3166
+ isDark ? "theme--dark " : "",
3167
+ "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"
3168
+ ].join(""),
3169
+ children: [
3170
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center justify-between border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white px-3 py-2", children: [
3171
+ /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
3172
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-base leading-none", children: icon }),
3173
+ title
3174
+ ] }),
3175
+ /* @__PURE__ */ jsxRuntime.jsx(
3176
+ "button",
3177
+ {
3178
+ onClick: onClose,
3179
+ "aria-label": "\u0110\xF3ng",
3180
+ className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
3181
+ children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, {})
3182
+ }
3183
+ )
3184
+ ] }),
3185
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-3", children })
3186
+ ]
3187
+ }
3188
+ );
3189
+ }
3190
+ function Section({ label, children }) {
3191
+ return /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
3192
+ /* @__PURE__ */ jsxRuntime.jsx("h4", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: label }),
3193
+ children
3194
+ ] });
3195
+ }
3196
+ function useToolHoverTooltip() {
3197
+ const [hover, setHover] = React2__namespace.useState(null);
3198
+ const [portalReady, setPortalReady] = React2__namespace.useState(false);
3199
+ const hoverTimerRef = React2__namespace.useRef(null);
3200
+ React2__namespace.useEffect(() => {
3201
+ setPortalReady(true);
3202
+ return () => {
3203
+ if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
3204
+ };
3205
+ }, []);
3206
+ const showHover = React2__namespace.useCallback((next) => {
3207
+ if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
3208
+ hoverTimerRef.current = setTimeout(() => setHover(next), TOOLTIP_DELAY_MS);
3209
+ }, []);
3210
+ const hideHover = React2__namespace.useCallback(() => {
3211
+ if (hoverTimerRef.current) {
3212
+ clearTimeout(hoverTimerRef.current);
3213
+ hoverTimerRef.current = null;
3214
+ }
3215
+ setHover(null);
3216
+ }, []);
3217
+ return { hover, portalReady, showHover, hideHover };
3218
+ }
3219
+ function DesktopPanel(props) {
3220
+ const {
3221
+ scene,
3222
+ selectedTool,
3223
+ onSelectTool,
3224
+ showAxis,
3225
+ showGrid,
3226
+ onShowAxisChange,
3227
+ onShowGridChange,
3228
+ onUndo,
3229
+ canUndo,
3230
+ onRedo,
3231
+ canRedo,
3232
+ onClose,
3233
+ isDark,
3234
+ chordGroup
3235
+ } = props;
3236
+ const [tab, setTab] = React2__namespace.useState("tools");
3237
+ const { hover, portalReady, showHover, hideHover } = useToolHoverTooltip();
3238
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3239
+ /* @__PURE__ */ jsxRuntime.jsxs(Shell, { title: "H\xECnh h\u1ECDc 3D", icon: Geom3DIconHeader, onClose, isDark, children: [
3240
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-1 rounded-md bg-slate-100 p-0.5", children: [
3241
+ /* @__PURE__ */ jsxRuntime.jsx(TabPill, { active: tab === "tools", onClick: () => setTab("tools"), testId: "tab-tools", children: "\u{1F9F0} C\xF4ng c\u1EE5" }),
3242
+ /* @__PURE__ */ jsxRuntime.jsx(TabPill, { active: tab === "algebra", onClick: () => setTab("algebra"), testId: "tab-algebra", children: "\u{1F4D0} \u0110\u1ED1i t\u01B0\u1EE3ng" })
3243
+ ] }),
3244
+ tab === "tools" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3245
+ /* @__PURE__ */ jsxRuntime.jsx(Section, { label: "G\xF3c nh\xECn", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 text-[11px] text-slate-700", children: [
3246
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "inline-flex select-none items-center gap-1.5", children: [
3247
+ /* @__PURE__ */ jsxRuntime.jsx(
3248
+ "input",
3249
+ {
3250
+ type: "checkbox",
3251
+ checked: showAxis,
3252
+ onChange: (e) => onShowAxisChange(e.target.checked),
3253
+ "data-testid": "toggle-axis"
3254
+ }
3255
+ ),
3256
+ "Tr\u1EE5c"
3257
+ ] }),
3258
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "inline-flex select-none items-center gap-1.5", children: [
3259
+ /* @__PURE__ */ jsxRuntime.jsx(
3260
+ "input",
3261
+ {
3262
+ type: "checkbox",
3263
+ checked: showGrid,
3264
+ onChange: (e) => onShowGridChange(e.target.checked),
3265
+ "data-testid": "toggle-grid"
3266
+ }
3267
+ ),
3268
+ "L\u01B0\u1EDBi"
3269
+ ] }),
3270
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "ml-auto flex items-center gap-0.5", children: [
3271
+ /* @__PURE__ */ jsxRuntime.jsx(
3272
+ "button",
3273
+ {
3274
+ type: "button",
3275
+ onClick: onUndo,
3276
+ disabled: !canUndo,
3277
+ title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
3278
+ "aria-label": "Ho\xE0n t\xE1c",
3279
+ "data-testid": "undo-btn",
3280
+ 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",
3281
+ children: /* @__PURE__ */ jsxRuntime.jsx(UndoIcon, {})
3282
+ }
3283
+ ),
3284
+ /* @__PURE__ */ jsxRuntime.jsx(
3285
+ "button",
3286
+ {
3287
+ type: "button",
3288
+ onClick: onRedo,
3289
+ disabled: !canRedo,
3290
+ title: "L\xE0m l\u1EA1i (Ctrl/Cmd+Shift+Z)",
3291
+ "aria-label": "L\xE0m l\u1EA1i",
3292
+ "data-testid": "redo-btn",
3293
+ 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",
3294
+ children: /* @__PURE__ */ jsxRuntime.jsx(RedoIcon, {})
3295
+ }
3296
+ )
3297
+ ] })
3298
+ ] }) }),
3299
+ /* @__PURE__ */ jsxRuntime.jsx(
3300
+ ToolPalette,
3301
+ {
3302
+ selected: selectedTool,
3303
+ onSelect: onSelectTool,
3304
+ chordGroup: chordGroup ?? null,
3305
+ onHoverTool: (info) => info ? showHover(info) : hideHover()
3306
+ }
3307
+ ),
3308
+ chordGroup && /* @__PURE__ */ jsxRuntime.jsxs(
3309
+ "div",
3310
+ {
3311
+ "data-testid": "chord-hint",
3312
+ className: "rounded border border-emerald-200 bg-emerald-50/60 px-2 py-1 text-[11px] leading-snug text-slate-600",
3313
+ children: [
3314
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono font-semibold text-emerald-700", children: letterForGroup(chordGroup) }),
3315
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-1.5", children: [
3316
+ "\u2192 ",
3317
+ GROUP_LABELS[chordGroup],
3318
+ ". B\u1EA5m s\u1ED1 1-9 \u0111\u1EC3 ch\u1ECDn c\xF4ng c\u1EE5, Esc hu\u1EF7."
3319
+ ] })
3320
+ ]
3321
+ }
3322
+ )
3323
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("section", { "data-testid": "algebra-panel", children: /* @__PURE__ */ jsxRuntime.jsx(AlgebraList, { scene }) })
3324
+ ] }),
3325
+ portalReady && hover && typeof document !== "undefined" ? reactDom.createPortal(
3326
+ /* @__PURE__ */ jsxRuntime.jsxs(
3327
+ "div",
3328
+ {
3329
+ role: "tooltip",
3330
+ 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",
3331
+ style: {
3332
+ left: hover.x + 8,
3333
+ top: hover.y,
3334
+ transform: "translate(0, -50%)",
3335
+ zIndex: 2147483600
3336
+ },
3337
+ children: [
3338
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block font-medium", children: hover.label }),
3339
+ hover.hint && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-0.5 block text-slate-300", children: hover.hint })
3340
+ ]
3341
+ }
3342
+ ),
3343
+ document.body
3344
+ ) : null
3345
+ ] });
3346
+ }
3347
+ function TabPill({
3348
+ active,
3349
+ onClick,
3350
+ testId,
3351
+ children
3352
+ }) {
3353
+ return /* @__PURE__ */ jsxRuntime.jsx(
3354
+ "button",
3355
+ {
3356
+ type: "button",
3357
+ onClick,
3358
+ "aria-pressed": active,
3359
+ "data-testid": testId,
3360
+ className: [
3361
+ "flex-1 rounded px-2 py-1 text-[11px] font-medium transition",
3362
+ active ? "bg-white text-slate-900 shadow-sm ring-1 ring-slate-200" : "text-slate-500 hover:text-slate-800"
3363
+ ].join(" "),
3364
+ children
3365
+ }
3366
+ );
3367
+ }
3368
+ function MobilePanel(props) {
3369
+ const {
3370
+ selectedTool,
3371
+ onSelectTool,
3372
+ showAxis,
3373
+ showGrid,
3374
+ onShowAxisChange,
3375
+ onShowGridChange,
3376
+ onUndo,
3377
+ canUndo,
3378
+ onRedo,
3379
+ canRedo,
3380
+ isDark,
3381
+ drawerOpen,
3382
+ onDrawerClose
3383
+ } = props;
3384
+ const groups = React2__namespace.useMemo(
3385
+ () => GROUP_ORDER.map((group) => {
3386
+ const keys = TOOLS_BY_GROUP[group];
3387
+ return {
3388
+ group,
3389
+ groupLabel: GROUP_LABELS[group],
3390
+ tools: keys.map((k) => {
3391
+ const tool = TOOLS.find((t) => t.key === k);
3392
+ return { key: k, label: tool.label, icon: ToolIcons[k] };
3393
+ })
3394
+ };
3395
+ }),
3396
+ []
3397
+ );
3398
+ return /* @__PURE__ */ jsxRuntime.jsx(
3399
+ MobileToolDrawer,
3400
+ {
3401
+ title: "H\xECnh h\u1ECDc 3D",
3402
+ headerIcon: Geom3DIconHeader,
3403
+ testId: "left-panel",
3404
+ isDark,
3405
+ drawerOpen: !!drawerOpen,
3406
+ onDrawerClose: () => onDrawerClose?.(),
3407
+ chips: [
3408
+ {
3409
+ label: "Tr\u1EE5c",
3410
+ icon: /* @__PURE__ */ jsxRuntime.jsx(AxisIcon, {}),
3411
+ pressed: showAxis,
3412
+ onToggle: onShowAxisChange,
3413
+ testId: "toggle-axis"
3414
+ },
3415
+ {
3416
+ label: "L\u01B0\u1EDBi",
3417
+ icon: /* @__PURE__ */ jsxRuntime.jsx(GridIcon, {}),
3418
+ pressed: showGrid,
3419
+ onToggle: onShowGridChange,
3420
+ testId: "toggle-grid"
3421
+ }
3422
+ ],
3423
+ actions: [
3424
+ {
3425
+ label: "Ho\xE0n t\xE1c",
3426
+ title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
3427
+ icon: /* @__PURE__ */ jsxRuntime.jsx(UndoIcon, {}),
3428
+ onClick: onUndo,
3429
+ disabled: !canUndo,
3430
+ testId: "undo-btn"
3431
+ },
3432
+ {
3433
+ label: "L\xE0m l\u1EA1i",
3434
+ title: "L\xE0m l\u1EA1i (Ctrl/Cmd+Shift+Z)",
3435
+ icon: /* @__PURE__ */ jsxRuntime.jsx(RedoIcon, {}),
3436
+ onClick: onRedo,
3437
+ disabled: !canRedo,
3438
+ testId: "redo-btn"
3439
+ }
3440
+ ],
3441
+ groups,
3442
+ activeTool: selectedTool,
3443
+ onToolSelect: onSelectTool
3444
+ }
3445
+ );
3446
+ }
3447
+ function LeftPanel(props) {
3448
+ if (props.isMobile) return /* @__PURE__ */ jsxRuntime.jsx(MobilePanel, { ...props });
3449
+ return /* @__PURE__ */ jsxRuntime.jsx(DesktopPanel, { ...props });
3450
+ }
3451
+ var TOOLTIP_DELAY_MS, Geom3DIconHeader;
3452
+ var init_LeftPanel = __esm({
3453
+ "src/stamps/geometry-3d/editor/LeftPanel.tsx"() {
3454
+ "use client";
3455
+ init_ToolPalette();
3456
+ init_AlgebraList();
3457
+ init_icons();
3458
+ init_groups();
3459
+ init_spec();
3460
+ init_MobileToolDrawer();
3461
+ TOOLTIP_DELAY_MS = 400;
3462
+ Geom3DIconHeader = /* @__PURE__ */ jsxRuntime.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: [
3463
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 9 L4 20 L14 20 L14 9 Z" }),
3464
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 9 L10 4 L20 4 L14 9 Z" }),
3465
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 9 L20 4 L20 15 L14 20 Z" })
3466
+ ] });
3467
+ }
3468
+ });
3469
+ function isFieldFocused() {
3470
+ const ae = typeof document !== "undefined" ? document.activeElement : null;
3471
+ return !!(ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable));
3472
+ }
3473
+ function useChordShortcut(args) {
3474
+ const { groupOrder, tools, onSelect, enabled } = args;
3475
+ const [chordGroup, setChordGroup] = React2.useState(null);
3476
+ const groupOrderRef = React2.useRef(groupOrder);
3477
+ const toolsRef = React2.useRef(tools);
3478
+ const onSelectRef = React2.useRef(onSelect);
3479
+ const chordGroupRef = React2.useRef(null);
3480
+ groupOrderRef.current = groupOrder;
3481
+ toolsRef.current = tools;
3482
+ onSelectRef.current = onSelect;
3483
+ const cancel = React2.useCallback(() => {
3484
+ chordGroupRef.current = null;
3485
+ setChordGroup(null);
3486
+ }, []);
3487
+ React2.useEffect(() => {
3488
+ if (!enabled) return;
3489
+ const setChord = (next) => {
3490
+ chordGroupRef.current = next;
3491
+ setChordGroup(next);
3492
+ };
3493
+ const onKey = (e) => {
3494
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
3495
+ if (isFieldFocused()) return;
3496
+ const key = e.key;
3497
+ const lower = key.length === 1 ? key.toLowerCase() : key;
3498
+ if (key === "Escape") {
3499
+ if (chordGroupRef.current !== null) {
3500
+ e.preventDefault();
3501
+ e.stopPropagation();
3502
+ setChord(null);
3503
+ }
3504
+ return;
3505
+ }
3506
+ if (lower.length === 1 && lower >= "a" && lower <= "z") {
3507
+ const idx = lower.charCodeAt(0) - A_CODE2;
3508
+ if (idx < groupOrderRef.current.length) {
3509
+ e.preventDefault();
3510
+ e.stopPropagation();
3511
+ setChord(groupOrderRef.current[idx]);
3512
+ }
3513
+ return;
3514
+ }
3515
+ if (key >= "1" && key <= "9") {
3516
+ const active = chordGroupRef.current;
3517
+ if (active === null) return;
3518
+ const n = key.charCodeAt(0) - "1".charCodeAt(0);
3519
+ const toolsInGroup = toolsRef.current.filter(
3520
+ (t) => t.group === active
3521
+ );
3522
+ e.preventDefault();
3523
+ e.stopPropagation();
3524
+ if (n < toolsInGroup.length) {
3525
+ onSelectRef.current(toolsInGroup[n].key);
3526
+ }
3527
+ setChord(null);
3528
+ return;
3529
+ }
3530
+ };
3531
+ window.addEventListener("keydown", onKey, { capture: true });
3532
+ return () => {
3533
+ window.removeEventListener("keydown", onKey, { capture: true });
3534
+ };
3535
+ }, [enabled]);
3536
+ return { chordGroup, cancel };
3537
+ }
3538
+ var A_CODE2;
3539
+ var init_useChordShortcut = __esm({
3540
+ "src/stamps/shared/useChordShortcut.ts"() {
3541
+ A_CODE2 = "a".charCodeAt(0);
3542
+ }
3543
+ });
3544
+
3545
+ // src/stamps/shared/svgToImage.ts
3546
+ async function hashString(input) {
3547
+ if (typeof crypto !== "undefined" && crypto.subtle) {
3548
+ const buf = new TextEncoder().encode(input);
3549
+ const digest = await crypto.subtle.digest("SHA-256", buf);
3550
+ return Array.from(new Uint8Array(digest)).slice(0, 16).map((b) => b.toString(16).padStart(2, "0")).join("");
3551
+ }
3552
+ let h1 = 2166136261;
3553
+ let h2 = 3421674724;
3554
+ for (let i = 0; i < input.length; i++) {
3555
+ const c = input.charCodeAt(i);
3556
+ h1 ^= c;
3557
+ h1 = Math.imul(h1, 16777619);
3558
+ h2 ^= c + i;
3559
+ h2 = Math.imul(h2, 1099511628211 & 4294967295);
3560
+ }
3561
+ return (h1 >>> 0).toString(16).padStart(8, "0") + (h2 >>> 0).toString(16).padStart(8, "0");
3562
+ }
3563
+ function parseSize(svg, attr) {
3564
+ const re = new RegExp(`<svg[^>]*\\s${attr}="(\\d+(?:\\.\\d+)?)`, "i");
3565
+ const m = svg.match(re);
3566
+ if (m) return Math.max(1, Math.round(parseFloat(m[1])));
3567
+ const vb = svg.match(/viewBox="([\d.\s-]+)"/i);
3568
+ if (vb) {
3569
+ const parts = vb[1].trim().split(/\s+/).map(parseFloat);
3570
+ if (parts.length === 4) return Math.max(1, Math.round(attr === "width" ? parts[2] : parts[3]));
3571
+ }
3572
+ return attr === "width" ? 200 : 100;
3573
+ }
3574
+ async function svgToImageElement(svg) {
3575
+ const width = parseSize(svg, "width");
3576
+ const height = parseSize(svg, "height");
3577
+ const utf8 = unescape(encodeURIComponent(svg));
3578
+ const dataURL = "data:image/svg+xml;base64," + btoa(utf8);
3579
+ const fileId = await hashString(dataURL);
3580
+ return { dataURL, fileId, width, height, mimeType: "image/svg+xml" };
3581
+ }
3582
+ var init_svgToImage = __esm({
3583
+ "src/stamps/shared/svgToImage.ts"() {
3584
+ }
3585
+ });
3586
+
3587
+ // src/stamps/shared/insertImage.ts
3588
+ function buildStampImageElement(api, fileId, width, height, customData, x, y) {
3589
+ const appState = api?.getAppState() ?? { scrollX: 0, scrollY: 0, width: 800, height: 600, zoom: { value: 1 } };
3590
+ const cx = x ?? appState.scrollX + (appState.width ?? 800) / 2 / (appState.zoom?.value ?? 1) - width / 2;
3591
+ const cy = y ?? appState.scrollY + (appState.height ?? 600) / 2 / (appState.zoom?.value ?? 1) - height / 2;
3592
+ return {
3593
+ type: "image",
3594
+ id: "stamp_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8),
3595
+ x: cx,
3596
+ y: cy,
3597
+ width,
3598
+ height,
3599
+ fileId,
3600
+ customData,
3601
+ angle: 0,
3602
+ strokeColor: "transparent",
3603
+ backgroundColor: "transparent",
3604
+ fillStyle: "solid",
3605
+ strokeWidth: 1,
3606
+ strokeStyle: "solid",
3607
+ roughness: 0,
3608
+ opacity: 100,
3609
+ groupIds: [],
3610
+ roundness: null,
3611
+ seed: Math.floor(Math.random() * 1e9),
3612
+ versionNonce: 0,
3613
+ version: 1,
3614
+ isDeleted: false,
3615
+ boundElements: null,
3616
+ updated: Date.now(),
3617
+ link: null,
3618
+ locked: false,
3619
+ status: "saved",
3620
+ scale: [1, 1]
3621
+ };
3622
+ }
3623
+ async function insertStampImage(api, opts) {
3624
+ const { dataURL, fileId, width, height, mimeType } = await svgToImageElement(opts.svgString);
3625
+ api.addFiles([{ id: fileId, dataURL, mimeType, created: Date.now() }]);
3626
+ const customData = opts.makeCustomData(width, height);
3627
+ const elements = api.getSceneElements();
3628
+ const editingId = opts.editingElementId ?? null;
3629
+ if (editingId) {
3630
+ const updated = elements.map(
3631
+ (e) => e.id === editingId ? { ...e, fileId, customData, width, height } : e
3632
+ );
3633
+ api.updateScene({ elements: updated, appState: clearAppStateAfterInsert() });
3634
+ return { fileId, width, height, elementId: editingId };
3635
+ }
3636
+ const newElement = buildStampImageElement(
3637
+ api,
3638
+ fileId,
3639
+ width,
3640
+ height,
3641
+ customData,
3642
+ opts.position?.x,
3643
+ opts.position?.y
3644
+ );
3645
+ api.updateScene({
3646
+ elements: [...elements, newElement],
3647
+ appState: clearAppStateAfterInsert()
3648
+ });
3649
+ return { fileId, width, height, elementId: newElement.id };
3650
+ }
3651
+ var clearAppStateAfterInsert;
3652
+ var init_insertImage = __esm({
3653
+ "src/stamps/shared/insertImage.ts"() {
3654
+ init_svgToImage();
3655
+ clearAppStateAfterInsert = () => ({
3656
+ selectedElementIds: {},
3657
+ croppingElementId: null
3658
+ });
3659
+ }
3660
+ });
3661
+ function readMatch(query) {
3662
+ if (typeof window === "undefined" || !window.matchMedia) return false;
3663
+ try {
3664
+ return window.matchMedia(query).matches;
3665
+ } catch {
3666
+ return false;
3667
+ }
3668
+ }
3669
+ function useIsMobile() {
3670
+ const [state, setState] = React2.useState(() => ({
3671
+ isMobile: readMatch(MOBILE_QUERY),
3672
+ isTouchOnly: readMatch(NO_HOVER_QUERY)
3673
+ }));
3674
+ React2.useEffect(() => {
3675
+ if (typeof window === "undefined" || !window.matchMedia) return;
3676
+ const mql = window.matchMedia(MOBILE_QUERY);
3677
+ const tql = window.matchMedia(NO_HOVER_QUERY);
3678
+ const update = () => {
3679
+ setState({ isMobile: mql.matches, isTouchOnly: tql.matches });
3680
+ };
3681
+ update();
3682
+ mql.addEventListener("change", update);
3683
+ tql.addEventListener("change", update);
3684
+ return () => {
3685
+ mql.removeEventListener("change", update);
3686
+ tql.removeEventListener("change", update);
3687
+ };
3688
+ }, []);
3689
+ return state;
3690
+ }
3691
+ var MOBILE_QUERY, NO_HOVER_QUERY;
3692
+ var init_useIsMobile = __esm({
3693
+ "src/stamps/shared/useIsMobile.ts"() {
3694
+ "use client";
3695
+ MOBILE_QUERY = "(max-width: 768px)";
3696
+ NO_HOVER_QUERY = "(hover: none)";
3697
+ }
3698
+ });
3699
+
3700
+ // src/stamps/geometry-3d/host.tsx
3701
+ var host_exports = {};
3702
+ __export(host_exports, {
3703
+ Geometry3DStampHost: () => Geometry3DStampHost
3704
+ });
3705
+ function parseInitial(editingElement) {
3706
+ if (!editingElement) return null;
3707
+ if (!isGeometry3DCustomData(editingElement.customData)) return null;
3708
+ try {
3709
+ return parseSerializedBoard3D(editingElement.customData.jsonState);
3710
+ } catch {
3711
+ return null;
3712
+ }
3713
+ }
3714
+ var Geometry3DStampHost;
3715
+ var init_host = __esm({
3716
+ "src/stamps/geometry-3d/host.tsx"() {
3717
+ "use client";
3718
+ init_EditorPanel();
3719
+ init_LeftPanel();
3720
+ init_Scene3D();
3721
+ init_groups();
3722
+ init_useChordShortcut();
3723
+ init_insertImage();
3724
+ init_useIsMobile();
3725
+ init_serialize();
3726
+ Geometry3DStampHost = React2.forwardRef(
3727
+ function Geometry3DStampHost2({ api, editingElement, onClose, isDark }, ref) {
3728
+ const editorRef = React2.useRef(null);
3729
+ const sceneRef = React2.useRef(null);
3730
+ if (!sceneRef.current) sceneRef.current = new Scene3D();
3731
+ const { isMobile } = useIsMobile();
3732
+ const [drawerOpen, setDrawerOpen] = React2.useState(false);
3733
+ const [ready, setReady] = React2.useState(false);
3734
+ const [selectedTool, setSelectedTool] = React2.useState("move");
3735
+ const [showAxis, setShowAxis] = React2.useState(true);
3736
+ const [showGrid, setShowGrid] = React2.useState(true);
3737
+ const [canUndo, setCanUndo] = React2.useState(false);
3738
+ const [canRedo, setCanRedo] = React2.useState(false);
3739
+ const handleHistoryChange = React2.useCallback((u, r) => {
3740
+ setCanUndo(u);
3741
+ setCanRedo(r);
3742
+ }, []);
3743
+ const handleUndo = React2.useCallback(() => {
3744
+ editorRef.current?.undo();
3745
+ }, []);
3746
+ const handleRedo = React2.useCallback(() => {
3747
+ editorRef.current?.redo();
3748
+ }, []);
3749
+ const initial = React2.useMemo(
3750
+ () => parseInitial(editingElement),
3751
+ [editingElement]
3752
+ );
3753
+ const { chordGroup } = useChordShortcut({
3754
+ groupOrder: GROUP_ORDER,
3755
+ tools: TOOLS_FLAT,
3756
+ onSelect: (key) => {
3757
+ setSelectedTool(key);
3758
+ editorRef.current?.setTool(key);
3759
+ },
3760
+ enabled: !isMobile
3761
+ });
3762
+ const handleSelectTool = React2.useCallback((k) => {
3763
+ setSelectedTool(k);
3764
+ editorRef.current?.setTool(k);
3765
+ }, []);
3766
+ const performInsert = React2.useCallback(
3767
+ async (board, width, height, svgString) => {
3768
+ if (!api) return;
3769
+ const jsonState = serializeBoard3D(board);
3770
+ await insertStampImage(api, {
3771
+ svgString,
3772
+ makeCustomData: () => ({
3773
+ kind: "geometry3d",
3774
+ version: 1,
3775
+ jsonState,
3776
+ svgWidth: width,
3777
+ svgHeight: height
3778
+ }),
3779
+ editingElementId: editingElement?.id ?? null
3780
+ });
3781
+ onClose();
3782
+ },
3783
+ [api, editingElement, onClose]
3784
+ );
3785
+ const tryInsert = React2.useCallback(() => {
3786
+ if (!editorRef.current) return false;
3787
+ if (!editorRef.current.hasContent()) return false;
3788
+ const board = editorRef.current.serialize();
3789
+ if (board.elements.length === 0) return false;
3790
+ void performInsert(board, 0, 0, "");
3791
+ return true;
3792
+ }, [performInsert]);
3793
+ React2.useImperativeHandle(
3794
+ ref,
3795
+ () => ({
3796
+ tryInsert,
3797
+ hasContent: () => editorRef.current?.hasContent() ?? false
3798
+ }),
3799
+ [tryInsert]
3800
+ );
3801
+ const handleEditorInsert = React2.useCallback(
3802
+ (board, width, height, svgString) => {
3803
+ void performInsert(board, width, height, svgString);
3804
+ },
3805
+ [performInsert]
3806
+ );
3807
+ const dialogStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 40 } : {
3808
+ position: "absolute",
3809
+ top: "50%",
3810
+ left: "calc(50% + 120px)",
3811
+ transform: "translate(-50%, -50%)",
3812
+ zIndex: 40
3813
+ };
3814
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3815
+ !isMobile && /* @__PURE__ */ jsxRuntime.jsx(
3816
+ LeftPanel,
3817
+ {
3818
+ scene: sceneRef.current,
3819
+ selectedTool,
3820
+ onSelectTool: handleSelectTool,
3821
+ showAxis,
3822
+ showGrid,
3823
+ onShowAxisChange: setShowAxis,
3824
+ onShowGridChange: setShowGrid,
3825
+ onUndo: handleUndo,
3826
+ canUndo,
3827
+ onRedo: handleRedo,
3828
+ canRedo,
3829
+ onClose,
3830
+ isDark,
3831
+ chordGroup
3832
+ }
3833
+ ),
3834
+ /* @__PURE__ */ jsxRuntime.jsxs(
3835
+ "div",
3836
+ {
3837
+ role: "dialog",
3838
+ "aria-label": "D\u1EF1ng h\xECnh h\u1ECDc 3D",
3839
+ "data-testid": "geom3d-host",
3840
+ "data-stamp-area": "true",
3841
+ style: dialogStyle,
3842
+ className: [
3843
+ isDark ? "theme--dark " : "",
3844
+ "flex flex-col overflow-hidden bg-white",
3845
+ 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"
3846
+ ].join(" "),
3847
+ children: [
3848
+ /* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center gap-2 border-b border-slate-200 bg-gradient-to-r from-emerald-600 to-teal-600 px-3 py-2 text-white", children: [
3849
+ isMobile && /* @__PURE__ */ jsxRuntime.jsx(
3850
+ "button",
3851
+ {
3852
+ type: "button",
3853
+ onClick: () => setDrawerOpen(true),
3854
+ "aria-label": "M\u1EDF ng\u0103n c\xF4ng c\u1EE5",
3855
+ className: "-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15",
3856
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3857
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
3858
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
3859
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
3860
+ ] })
3861
+ }
3862
+ ),
3863
+ /* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex flex-1 items-center gap-2 text-sm font-semibold", children: [
3864
+ /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 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" }) }),
3865
+ "D\u1EF1ng h\xECnh h\u1ECDc kh\xF4ng gian"
3866
+ ] }),
3867
+ isMobile && /* @__PURE__ */ jsxRuntime.jsx(
3868
+ "button",
3869
+ {
3870
+ type: "button",
3871
+ onClick: tryInsert,
3872
+ disabled: !ready,
3873
+ "data-testid": "geom3d-insert-btn-mobile",
3874
+ className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
3875
+ children: "Ch\xE8n"
3876
+ }
3877
+ ),
3878
+ /* @__PURE__ */ jsxRuntime.jsx(
3879
+ "button",
3880
+ {
3881
+ onClick: onClose,
3882
+ "aria-label": "\u0110\xF3ng",
3883
+ className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15",
3884
+ children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
3885
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
3886
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
3887
+ ] })
3888
+ }
3889
+ )
3890
+ ] }),
3891
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(
3892
+ EditorPanel,
3893
+ {
3894
+ ref: editorRef,
3895
+ isDark,
3896
+ initialState: initial,
3897
+ onInsert: handleEditorInsert,
3898
+ scene: sceneRef.current,
3899
+ selectedTool,
3900
+ onSelectedToolChange: setSelectedTool,
3901
+ showAxis,
3902
+ showGrid,
3903
+ onReadyChange: setReady,
3904
+ onHistoryChange: handleHistoryChange
3905
+ }
3906
+ ) }),
3907
+ !isMobile && /* @__PURE__ */ jsxRuntime.jsxs("footer", { className: "flex items-center justify-between border-t border-slate-200 bg-slate-50 px-3 py-2", children: [
3908
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-slate-500", children: "Ch\u1ECDn c\xF4ng c\u1EE5 b\xEAn tr\xE1i, click tr\xEAn b\u1EA3ng \u0111\u1EC3 d\u1EF1ng h\xECnh." }),
3909
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
3910
+ /* @__PURE__ */ jsxRuntime.jsx(
3911
+ "button",
3912
+ {
3913
+ onClick: onClose,
3914
+ className: "rounded border border-slate-300 bg-white px-3 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100",
3915
+ children: "Hu\u1EF7"
3916
+ }
3917
+ ),
3918
+ /* @__PURE__ */ jsxRuntime.jsx(
3919
+ "button",
3920
+ {
3921
+ onClick: tryInsert,
3922
+ disabled: !ready,
3923
+ "data-testid": "geom3d-insert-btn",
3924
+ className: "rounded bg-emerald-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-emerald-700 disabled:opacity-50",
3925
+ children: "Ch\xE8n"
3926
+ }
3927
+ )
3928
+ ] })
3929
+ ] })
3930
+ ]
3931
+ }
3932
+ ),
3933
+ isMobile && /* @__PURE__ */ jsxRuntime.jsx(
3934
+ LeftPanel,
3935
+ {
3936
+ scene: sceneRef.current,
3937
+ selectedTool,
3938
+ onSelectTool: handleSelectTool,
3939
+ showAxis,
3940
+ showGrid,
3941
+ onShowAxisChange: setShowAxis,
3942
+ onShowGridChange: setShowGrid,
3943
+ onUndo: handleUndo,
3944
+ canUndo,
3945
+ onRedo: handleRedo,
3946
+ canRedo,
3947
+ onClose,
3948
+ isDark,
3949
+ isMobile: true,
3950
+ drawerOpen,
3951
+ onDrawerClose: () => setDrawerOpen(false),
3952
+ chordGroup
3953
+ }
3954
+ )
3955
+ ] });
3956
+ }
3957
+ );
3958
+ }
3959
+ });
3960
+
3961
+ // src/stamps/geometry-3d/index.tsx
3962
+ init_serialize();
3963
+
3964
+ // src/stamps/geometry-3d/render.ts
3965
+ init_serialize();
3966
+ init_theme2();
3967
+ var OUTPUT_WIDTH = 1024;
3968
+ var OUTPUT_HEIGHT = 768;
3969
+ async function renderGeometry3DSvgFromState(jsonState) {
3970
+ const state = parseSerializedBoard3D(jsonState);
3971
+ const JXG = (await import('jsxgraph')).default;
3972
+ const div = document.createElement("div");
3973
+ div.style.cssText = `position:absolute;left:-9999px;top:-9999px;width:${OUTPUT_WIDTH}px;height:${OUTPUT_HEIGHT}px;`;
3974
+ document.body.appendChild(div);
3975
+ try {
3976
+ JXG.Options.text.display = "internal";
3977
+ const board = JXG.JSXGraph.initBoard(div, {
3978
+ boundingbox: state.bbox,
3979
+ axis: false,
3980
+ showCopyright: false,
3981
+ showNavigation: false,
3982
+ renderer: "svg"
3983
+ });
3984
+ const baseAttrs = VIEW3D_ATTRS(false);
3985
+ const view = board.create(
3986
+ "view3d",
3987
+ [
3988
+ [-5, -5],
3989
+ [10, 10],
3990
+ [
3991
+ [state.view.bbox3D[0], state.view.bbox3D[3]],
3992
+ [state.view.bbox3D[1], state.view.bbox3D[4]],
3993
+ [state.view.bbox3D[2], state.view.bbox3D[5]]
3994
+ ]
3995
+ ],
3996
+ {
3997
+ ...baseAttrs,
3998
+ az: { ...baseAttrs.az, value: state.view.azimuth },
3999
+ el: { ...baseAttrs.el, value: state.view.elevation }
4000
+ }
4001
+ );
4002
+ if (!state.showAxes) {
4003
+ view.defaultAxes = [];
4004
+ }
4005
+ try {
4006
+ view.create(
4007
+ "plane3d",
4008
+ [
4009
+ [0, 0, 0],
4010
+ [1, 0, 0],
4011
+ [0, 1, 0],
4012
+ GROUND_PLANE_RANGE,
4013
+ GROUND_PLANE_RANGE
4014
+ ],
4015
+ GROUND_PLANE_ATTRS(false)
4016
+ );
4017
+ } catch {
4018
+ }
4019
+ const idMap = /* @__PURE__ */ new Map();
4020
+ for (const el of state.elements) {
4021
+ const parents = el.parents.map(
4022
+ (p) => typeof p === "string" && p.startsWith("@id:") ? idMap.get(p.slice(4)) : p
4023
+ );
4024
+ const obj = view.create(el.type, parents, {
4025
+ ...el.attributes,
4026
+ id: el.id,
4027
+ name: el.label
4028
+ });
4029
+ idMap.set(el.id, obj);
4030
+ }
4031
+ const svg = div.querySelector("svg");
4032
+ if (!svg) {
4033
+ throw new Error("renderGeometry3DSvgFromState: SVG not produced");
4034
+ }
4035
+ const clone = svg.cloneNode(true);
4036
+ clone.setAttribute("width", String(OUTPUT_WIDTH));
4037
+ clone.setAttribute("height", String(OUTPUT_HEIGHT));
4038
+ const svgString = new XMLSerializer().serializeToString(clone);
4039
+ try {
4040
+ JXG.JSXGraph.freeBoard(board);
4041
+ } catch {
4042
+ }
4043
+ return { svgString, width: OUTPUT_WIDTH, height: OUTPUT_HEIGHT };
4044
+ } finally {
4045
+ document.body.removeChild(div);
4046
+ }
4047
+ }
4048
+ var Geometry3DStampHost3 = React2.lazy(
4049
+ () => Promise.resolve().then(() => (init_host(), host_exports)).then((m) => ({ default: m.Geometry3DStampHost }))
4050
+ );
4051
+ var Geometry3DIcon = /* @__PURE__ */ jsxRuntime.jsxs(
4052
+ "svg",
4053
+ {
4054
+ width: "20",
4055
+ height: "20",
4056
+ viewBox: "0 0 24 24",
4057
+ fill: "none",
4058
+ stroke: "currentColor",
4059
+ strokeWidth: "1.6",
4060
+ strokeLinecap: "round",
4061
+ strokeLinejoin: "round",
4062
+ "aria-hidden": "true",
4063
+ children: [
4064
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 9 L4 20 L14 20 L14 9 Z" }),
4065
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 9 L10 4 L20 4 L14 9 Z" }),
4066
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 9 L20 4 L20 15 L14 20 Z" })
4067
+ ]
4068
+ }
4069
+ );
4070
+ var geometry3dStamp = {
4071
+ kind: "geometry3d",
4072
+ experimental: true,
4073
+ shortcutKey: "d",
4074
+ toolbarLabel: "D",
4075
+ toolbarTitle: "H\xECnh 3D (D)",
4076
+ toolbarIcon: Geometry3DIcon,
4077
+ toolbarTestId: "stamp-toolbar-geometry3d",
4078
+ matchesCustomData: isGeometry3DCustomData,
4079
+ async renderSvgFromCustomData(data) {
4080
+ if (!isGeometry3DCustomData(data)) {
4081
+ throw new Error("geometry3dStamp.renderSvgFromCustomData: customData kh\xF4ng ph\u1EA3i geometry3d");
4082
+ }
4083
+ const { svgString } = await renderGeometry3DSvgFromState(data.jsonState);
4084
+ return svgString;
4085
+ },
4086
+ restoreFileFromCustomData: async (element) => {
4087
+ const data = element.customData;
4088
+ const fileId = element.fileId;
4089
+ if (!data || !fileId) return null;
4090
+ if (!isGeometry3DCustomData(data)) return null;
4091
+ try {
4092
+ const { svgString } = await renderGeometry3DSvgFromState(data.jsonState);
4093
+ const dataURL = `data:image/svg+xml;base64,${typeof btoa !== "undefined" ? btoa(unescape(encodeURIComponent(svgString))) : Buffer.from(svgString).toString("base64")}`;
4094
+ return { fileId, dataURL, mimeType: "image/svg+xml" };
4095
+ } catch {
4096
+ return null;
4097
+ }
4098
+ },
4099
+ Host: Geometry3DStampHost3
4100
+ };
4101
+
4102
+ exports.geometry3dStamp = geometry3dStamp;
4103
+ exports.isGeometry3DCustomData = isGeometry3DCustomData;
4104
+ //# sourceMappingURL=geometry-3d.js.map
4105
+ //# sourceMappingURL=geometry-3d.js.map