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