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