@xom11/whiteboard 0.11.0 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -0
- package/dist/{ExcalidrawWithMenus-EAVPOPJZ.mjs → ExcalidrawWithMenus-WENZRYYE.mjs} +2 -3
- package/dist/ExcalidrawWithMenus-WENZRYYE.mjs.map +1 -0
- package/dist/catalog.json +57 -0
- package/dist/chunk-4D5CSIJO.mjs +1167 -0
- package/dist/chunk-4D5CSIJO.mjs.map +1 -0
- package/dist/chunk-5UTGXHLJ.mjs +57 -0
- package/dist/chunk-5UTGXHLJ.mjs.map +1 -0
- package/dist/chunk-6V4SH4JJ.mjs +1801 -0
- package/dist/chunk-6V4SH4JJ.mjs.map +1 -0
- package/dist/chunk-AZIARTGX.mjs +23 -0
- package/dist/chunk-AZIARTGX.mjs.map +1 -0
- package/dist/chunk-BKSXPNPQ.mjs +348 -0
- package/dist/chunk-BKSXPNPQ.mjs.map +1 -0
- package/dist/{chunk-YVJP7NRG.mjs → chunk-CRAPWQKJ.mjs} +7 -9
- package/dist/chunk-CRAPWQKJ.mjs.map +1 -0
- package/dist/chunk-CSCF3YFZ.mjs +388 -0
- package/dist/chunk-CSCF3YFZ.mjs.map +1 -0
- package/dist/chunk-HNQLZIEP.mjs +78 -0
- package/dist/chunk-HNQLZIEP.mjs.map +1 -0
- package/dist/chunk-IBTRMWD6.mjs +28 -0
- package/dist/chunk-IBTRMWD6.mjs.map +1 -0
- package/dist/chunk-ICR4CVOE.mjs +57 -0
- package/dist/chunk-ICR4CVOE.mjs.map +1 -0
- package/dist/chunk-LVNCYP4J.mjs +57 -0
- package/dist/chunk-LVNCYP4J.mjs.map +1 -0
- package/dist/chunk-MFOGFFIL.mjs +95 -0
- package/dist/chunk-MFOGFFIL.mjs.map +1 -0
- package/dist/chunk-NVJ7K3DK.mjs +29 -0
- package/dist/chunk-NVJ7K3DK.mjs.map +1 -0
- package/dist/chunk-O4WIZFRQ.mjs +11 -0
- package/dist/chunk-O4WIZFRQ.mjs.map +1 -0
- package/dist/{chunk-C6SCVOMC.mjs → chunk-QGNU34T7.mjs} +5 -41
- package/dist/chunk-QGNU34T7.mjs.map +1 -0
- package/dist/chunk-R5FL6S7L.mjs +22 -0
- package/dist/chunk-R5FL6S7L.mjs.map +1 -0
- package/dist/{chunk-7P7SQFOW.mjs → chunk-SGFJLHHG.mjs} +3 -3
- package/dist/chunk-SGFJLHHG.mjs.map +1 -0
- package/dist/{chunk-PWIMZIB6.mjs → chunk-WWMQ2VHZ.mjs} +7 -8
- package/dist/chunk-WWMQ2VHZ.mjs.map +1 -0
- package/dist/chunk-YIPI3WUL.mjs +61 -0
- package/dist/chunk-YIPI3WUL.mjs.map +1 -0
- package/dist/chunk-ZBJBQKJ2.mjs +330 -0
- package/dist/chunk-ZBJBQKJ2.mjs.map +1 -0
- package/dist/geometry-2d.d.mts +3 -6
- package/dist/geometry-2d.d.ts +3 -6
- package/dist/geometry-2d.js +7007 -2633
- package/dist/geometry-2d.js.map +1 -1
- package/dist/geometry-2d.mjs +8 -4
- package/dist/geometry-3d.d.mts +4 -7
- package/dist/geometry-3d.d.ts +4 -7
- package/dist/geometry-3d.js +5446 -2507
- package/dist/geometry-3d.js.map +1 -1
- package/dist/geometry-3d.mjs +7 -4
- package/dist/graph-2d.d.mts +4 -7
- package/dist/graph-2d.d.ts +4 -7
- package/dist/graph-2d.js +5300 -1677
- package/dist/graph-2d.js.map +1 -1
- package/dist/graph-2d.mjs +10 -3
- package/dist/host-DOAYVL35.mjs +3199 -0
- package/dist/host-DOAYVL35.mjs.map +1 -0
- package/dist/host-GKNQBBUE.mjs +1142 -0
- package/dist/host-GKNQBBUE.mjs.map +1 -0
- package/dist/{host-Z3TEJKZA.mjs → host-QS2EOTRJ.mjs} +4 -4
- package/dist/{host-Z3TEJKZA.mjs.map → host-QS2EOTRJ.mjs.map} +1 -1
- package/dist/host-TLIXN4CF.mjs +2374 -0
- package/dist/host-TLIXN4CF.mjs.map +1 -0
- package/dist/index.css +4 -1
- package/dist/index.css.map +1 -1
- package/dist/index.d.mts +659 -19
- package/dist/index.d.ts +659 -19
- package/dist/index.js +13736 -9491
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1465 -342
- package/dist/index.mjs.map +1 -1
- package/dist/latex.d.mts +3 -4
- package/dist/latex.d.ts +3 -4
- package/dist/latex.js +33 -18
- package/dist/latex.js.map +1 -1
- package/dist/latex.mjs +2 -3
- package/dist/render-SA4JTOW3.mjs +8 -0
- package/dist/render-SA4JTOW3.mjs.map +1 -0
- package/dist/serialize-3NZS6A6Q.mjs +6 -0
- package/dist/serialize-3NZS6A6Q.mjs.map +1 -0
- package/dist/{types-CinstD7T.d.mts → types-rA4slL08.d.mts} +69 -4
- package/dist/{types-CinstD7T.d.ts → types-rA4slL08.d.ts} +69 -4
- package/package.json +34 -6
- package/dist/ExcalidrawWithMenus-EAVPOPJZ.mjs.map +0 -1
- package/dist/chunk-74VEEZBV.mjs +0 -619
- package/dist/chunk-74VEEZBV.mjs.map +0 -1
- package/dist/chunk-7P7SQFOW.mjs.map +0 -1
- package/dist/chunk-BJTO5JO5.mjs +0 -11
- package/dist/chunk-BJTO5JO5.mjs.map +0 -1
- package/dist/chunk-C6SCVOMC.mjs.map +0 -1
- package/dist/chunk-D257NCQW.mjs +0 -58
- package/dist/chunk-D257NCQW.mjs.map +0 -1
- package/dist/chunk-G7FR3AIV.mjs +0 -193
- package/dist/chunk-G7FR3AIV.mjs.map +0 -1
- package/dist/chunk-HTBLO5JO.mjs +0 -41
- package/dist/chunk-HTBLO5JO.mjs.map +0 -1
- package/dist/chunk-PWIMZIB6.mjs.map +0 -1
- package/dist/chunk-SBDMF4NQ.mjs +0 -212
- package/dist/chunk-SBDMF4NQ.mjs.map +0 -1
- package/dist/chunk-WQOABS6N.mjs +0 -197
- package/dist/chunk-WQOABS6N.mjs.map +0 -1
- package/dist/chunk-YVJP7NRG.mjs.map +0 -1
- package/dist/host-N6ACNJKI.mjs +0 -3226
- package/dist/host-N6ACNJKI.mjs.map +0 -1
- package/dist/host-NKGV6RF2.mjs +0 -1134
- package/dist/host-NKGV6RF2.mjs.map +0 -1
- package/dist/host-XVK7UCRE.mjs +0 -2908
- package/dist/host-XVK7UCRE.mjs.map +0 -1
|
@@ -0,0 +1,2374 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { VIEW3D_ATTRS, DEFAULT_VIEW3D, GROUND_PLANE_RANGE, GROUND_PLANE_ATTRS, paletteFor, JxgRenderer3D, serializeBoard3D, renderGeometry3DSvgFromState, isGeometry3DCustomData, deserializeBoard3D } from './chunk-CSCF3YFZ.mjs';
|
|
3
|
+
import { useChordShortcut } from './chunk-HNQLZIEP.mjs';
|
|
4
|
+
import { initJxgBoard, attachJxgWheelZoom, ToastHost, STAMP_PANEL_DESKTOP, ToastProvider, useStampStore, StampLeftPanel } from './chunk-4D5CSIJO.mjs';
|
|
5
|
+
import './chunk-R5FL6S7L.mjs';
|
|
6
|
+
import './chunk-ICR4CVOE.mjs';
|
|
7
|
+
import { useEditorState, nextLabel, listObjects } from './chunk-6V4SH4JJ.mjs';
|
|
8
|
+
import './chunk-ZBJBQKJ2.mjs';
|
|
9
|
+
import { useIsMobile } from './chunk-P2AOIF7S.mjs';
|
|
10
|
+
import { insertStampImage } from './chunk-QGNU34T7.mjs';
|
|
11
|
+
import './chunk-5UTGXHLJ.mjs';
|
|
12
|
+
import * as React3 from 'react';
|
|
13
|
+
import { forwardRef, useRef, useState, useCallback, useImperativeHandle } from 'react';
|
|
14
|
+
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
15
|
+
|
|
16
|
+
// src/stamps/geometry-3d/editor/tools/handlers/_ensurePoint.ts
|
|
17
|
+
function hitToConstraint(hit) {
|
|
18
|
+
switch (hit.kind) {
|
|
19
|
+
case "onGround":
|
|
20
|
+
return { kind: "onGround", x: hit.world[0], y: hit.world[1] };
|
|
21
|
+
case "onAxis":
|
|
22
|
+
return { kind: "onAxis", axis: hit.axis, t: hit.t };
|
|
23
|
+
case "onPlane":
|
|
24
|
+
return { kind: "onPlane", planeId: hit.planeId, u: hit.u, v: hit.v };
|
|
25
|
+
case "onLine":
|
|
26
|
+
return { kind: "onLine", lineId: hit.lineId, t: hit.t };
|
|
27
|
+
case "onPolygon":
|
|
28
|
+
return { kind: "onPolygon", polygonId: hit.polygonId, u: hit.u, v: hit.v };
|
|
29
|
+
case "onSphere":
|
|
30
|
+
return { kind: "onSphere", sphereId: hit.sphereId, theta: hit.theta, phi: hit.phi };
|
|
31
|
+
default:
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function makePointId(store, offset = 1) {
|
|
36
|
+
return `p${store.getState().counter + offset}`;
|
|
37
|
+
}
|
|
38
|
+
function buildPointObject(store, constraint, options = {}) {
|
|
39
|
+
const id = makePointId(store, options.idOffset ?? 1);
|
|
40
|
+
const state = store.getState();
|
|
41
|
+
const label = options.label ?? nextLabel(state, "point3d");
|
|
42
|
+
return {
|
|
43
|
+
id,
|
|
44
|
+
kind: "point3d",
|
|
45
|
+
label,
|
|
46
|
+
visible: true,
|
|
47
|
+
locked: false,
|
|
48
|
+
layer: "default",
|
|
49
|
+
schemaVersion: 1,
|
|
50
|
+
attrs: { constraint, ...options.color ? { color: options.color } : {} }
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function addPoint(store, constraint, color) {
|
|
54
|
+
const obj = buildPointObject(store, constraint, {});
|
|
55
|
+
store.dispatch({ type: "ADD", payload: { obj } });
|
|
56
|
+
return obj.id;
|
|
57
|
+
}
|
|
58
|
+
function ensurePoint(hit, store) {
|
|
59
|
+
if (hit.kind === "existingPoint") return hit.pointId;
|
|
60
|
+
const c = hitToConstraint(hit);
|
|
61
|
+
if (!c) return null;
|
|
62
|
+
return addPoint(store, c);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/stamps/geometry-3d/editor/tools/handlers/point.ts
|
|
66
|
+
function buildPoint(args, store) {
|
|
67
|
+
const hit = args[0]?.hit;
|
|
68
|
+
if (!hit) return null;
|
|
69
|
+
if (hit.kind === "existingPoint") return hit.pointId;
|
|
70
|
+
const c = hitToConstraint(hit);
|
|
71
|
+
if (!c) return null;
|
|
72
|
+
return addPoint(store, c);
|
|
73
|
+
}
|
|
74
|
+
var buildPointOnObject = buildPoint;
|
|
75
|
+
|
|
76
|
+
// src/stamps/geometry-3d/editor/tools/handlers/segment.ts
|
|
77
|
+
function makeDerivedId(store, prefix) {
|
|
78
|
+
return `${prefix}${store.getState().counter + 1}`;
|
|
79
|
+
}
|
|
80
|
+
function addDerived(store, kind, prefix, attrs) {
|
|
81
|
+
const id = makeDerivedId(store, prefix);
|
|
82
|
+
const label = nextLabel(store.getState(), kind);
|
|
83
|
+
const obj = {
|
|
84
|
+
id,
|
|
85
|
+
kind,
|
|
86
|
+
label,
|
|
87
|
+
visible: true,
|
|
88
|
+
locked: false,
|
|
89
|
+
layer: "default",
|
|
90
|
+
schemaVersion: 1,
|
|
91
|
+
attrs
|
|
92
|
+
};
|
|
93
|
+
store.dispatch({ type: "ADD", payload: { obj } });
|
|
94
|
+
return id;
|
|
95
|
+
}
|
|
96
|
+
function buildSegment(args, store) {
|
|
97
|
+
if (args.length < 2 || !args[0].hit || !args[1].hit) return null;
|
|
98
|
+
const p1 = ensurePoint(args[0].hit, store);
|
|
99
|
+
const p2 = ensurePoint(args[1].hit, store);
|
|
100
|
+
if (!p1 || !p2 || p1 === p2) return null;
|
|
101
|
+
return addDerived(store, "segment3d", "s", { p1, p2 });
|
|
102
|
+
}
|
|
103
|
+
function buildLine(args, store) {
|
|
104
|
+
if (args.length < 2 || !args[0].hit || !args[1].hit) return null;
|
|
105
|
+
const p1 = ensurePoint(args[0].hit, store);
|
|
106
|
+
const p2 = ensurePoint(args[1].hit, store);
|
|
107
|
+
if (!p1 || !p2 || p1 === p2) return null;
|
|
108
|
+
return addDerived(store, "line3d", "l", { p1, p2 });
|
|
109
|
+
}
|
|
110
|
+
function buildRay(args, store) {
|
|
111
|
+
if (args.length < 2 || !args[0].hit || !args[1].hit) return null;
|
|
112
|
+
const origin = ensurePoint(args[0].hit, store);
|
|
113
|
+
const through = ensurePoint(args[1].hit, store);
|
|
114
|
+
if (!origin || !through || origin === through) return null;
|
|
115
|
+
return addDerived(store, "ray3d", "r", { origin, through });
|
|
116
|
+
}
|
|
117
|
+
function buildVector(args, store) {
|
|
118
|
+
if (args.length < 2 || !args[0].hit || !args[1].hit) return null;
|
|
119
|
+
const from = ensurePoint(args[0].hit, store);
|
|
120
|
+
const to = ensurePoint(args[1].hit, store);
|
|
121
|
+
if (!from || !to || from === to) return null;
|
|
122
|
+
return addDerived(store, "vector3d", "v", { from, to });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/stamps/geometry-3d/editor/tools/handlers/polygon.ts
|
|
126
|
+
function buildPolygon(args, store) {
|
|
127
|
+
const vertexArgs = args.filter((a) => a.step.type === "point");
|
|
128
|
+
const vertexIds = vertexArgs.map((a) => a.hit ? ensurePoint(a.hit, store) : null).filter((x) => !!x);
|
|
129
|
+
if (vertexIds.length < 3) return null;
|
|
130
|
+
const id = `pg${store.getState().counter + 1}`;
|
|
131
|
+
const label = nextLabel(store.getState(), "polygon3d");
|
|
132
|
+
const obj = {
|
|
133
|
+
id,
|
|
134
|
+
kind: "polygon3d",
|
|
135
|
+
label,
|
|
136
|
+
visible: true,
|
|
137
|
+
locked: false,
|
|
138
|
+
layer: "default",
|
|
139
|
+
schemaVersion: 1,
|
|
140
|
+
attrs: { vertices: vertexIds }
|
|
141
|
+
};
|
|
142
|
+
store.dispatch({ type: "ADD", payload: { obj } });
|
|
143
|
+
return id;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/stamps/geometry-3d/editor/scene/constraintMath.ts
|
|
147
|
+
function sub(a, b) {
|
|
148
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
149
|
+
}
|
|
150
|
+
function add(a, b) {
|
|
151
|
+
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
152
|
+
}
|
|
153
|
+
function scale(a, k) {
|
|
154
|
+
return [a[0] * k, a[1] * k, a[2] * k];
|
|
155
|
+
}
|
|
156
|
+
function dot(a, b) {
|
|
157
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
158
|
+
}
|
|
159
|
+
function cross(a, b) {
|
|
160
|
+
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]];
|
|
161
|
+
}
|
|
162
|
+
function norm(a) {
|
|
163
|
+
return Math.sqrt(dot(a, a));
|
|
164
|
+
}
|
|
165
|
+
function normalize(a) {
|
|
166
|
+
const n = norm(a);
|
|
167
|
+
return n === 0 ? a : scale(a, 1 / n);
|
|
168
|
+
}
|
|
169
|
+
function getPointWorld(id, state) {
|
|
170
|
+
const obj = state.objects[id];
|
|
171
|
+
if (!obj || obj.kind !== "point3d") {
|
|
172
|
+
throw new Error(`constraintMath: point ${id} not found`);
|
|
173
|
+
}
|
|
174
|
+
const attrs = obj.attrs;
|
|
175
|
+
return constraintToWorld(attrs.constraint, state);
|
|
176
|
+
}
|
|
177
|
+
function getPlaneBasis(planeObj, state) {
|
|
178
|
+
const p1 = getPointWorld(planeObj.attrs.p1, state);
|
|
179
|
+
const p2 = getPointWorld(planeObj.attrs.p2, state);
|
|
180
|
+
const p3 = getPointWorld(planeObj.attrs.p3, state);
|
|
181
|
+
const basis1 = sub(p2, p1);
|
|
182
|
+
const tmp = sub(p3, p1);
|
|
183
|
+
const normal = normalize(cross(basis1, tmp));
|
|
184
|
+
const basis2 = cross(normal, basis1);
|
|
185
|
+
return { origin: p1, basis1, basis2, normal };
|
|
186
|
+
}
|
|
187
|
+
function constraintToWorld(c, state) {
|
|
188
|
+
switch (c.kind) {
|
|
189
|
+
case "free":
|
|
190
|
+
return [c.x, c.y, c.z];
|
|
191
|
+
case "onGround":
|
|
192
|
+
return [c.x, c.y, 0];
|
|
193
|
+
case "onAxis": {
|
|
194
|
+
if (c.axis === "x") return [c.t, 0, 0];
|
|
195
|
+
if (c.axis === "y") return [0, c.t, 0];
|
|
196
|
+
return [0, 0, c.t];
|
|
197
|
+
}
|
|
198
|
+
case "onPlane": {
|
|
199
|
+
const plane = state.objects[c.planeId];
|
|
200
|
+
if (!plane || plane.kind !== "plane3d") throw new Error("onPlane: plane missing");
|
|
201
|
+
const { origin, basis1, basis2 } = getPlaneBasis(plane, state);
|
|
202
|
+
return add(add(origin, scale(basis1, c.u)), scale(basis2, c.v));
|
|
203
|
+
}
|
|
204
|
+
case "onLine": {
|
|
205
|
+
const line = state.objects[c.lineId];
|
|
206
|
+
if (!line) throw new Error("onLine: parent missing");
|
|
207
|
+
let p1Id;
|
|
208
|
+
let p2Id;
|
|
209
|
+
if (line.kind === "line3d" || line.kind === "segment3d") {
|
|
210
|
+
const a = line.attrs;
|
|
211
|
+
p1Id = a.p1;
|
|
212
|
+
p2Id = a.p2;
|
|
213
|
+
} else if (line.kind === "ray3d") {
|
|
214
|
+
const a = line.attrs;
|
|
215
|
+
p1Id = a.origin;
|
|
216
|
+
p2Id = a.through;
|
|
217
|
+
} else if (line.kind === "vector3d") {
|
|
218
|
+
const a = line.attrs;
|
|
219
|
+
p1Id = a.from;
|
|
220
|
+
p2Id = a.to;
|
|
221
|
+
} else {
|
|
222
|
+
throw new Error("onLine: parent kind not supported");
|
|
223
|
+
}
|
|
224
|
+
const p1 = getPointWorld(p1Id, state);
|
|
225
|
+
const p2 = getPointWorld(p2Id, state);
|
|
226
|
+
const dir = sub(p2, p1);
|
|
227
|
+
return add(p1, scale(dir, c.t));
|
|
228
|
+
}
|
|
229
|
+
case "onPolygon": {
|
|
230
|
+
const pg = state.objects[c.polygonId];
|
|
231
|
+
if (!pg || pg.kind !== "polygon3d") throw new Error("onPolygon: parent missing");
|
|
232
|
+
const v = pg.attrs.vertices;
|
|
233
|
+
if (v.length < 3) throw new Error("onPolygon: < 3 vertices");
|
|
234
|
+
const p1 = getPointWorld(v[0], state);
|
|
235
|
+
const p2 = getPointWorld(v[1], state);
|
|
236
|
+
const p3 = getPointWorld(v[2], state);
|
|
237
|
+
const basis1 = sub(p2, p1);
|
|
238
|
+
const tmp = sub(p3, p1);
|
|
239
|
+
const normal = normalize(cross(basis1, tmp));
|
|
240
|
+
const basis2 = cross(normal, basis1);
|
|
241
|
+
return add(add(p1, scale(basis1, c.u)), scale(basis2, c.v));
|
|
242
|
+
}
|
|
243
|
+
case "onSphere": {
|
|
244
|
+
const sph = state.objects[c.sphereId];
|
|
245
|
+
if (!sph || sph.kind !== "sphere3d") throw new Error("onSphere: parent missing");
|
|
246
|
+
const a = sph.attrs;
|
|
247
|
+
const center = getPointWorld(a.center, state);
|
|
248
|
+
const surface = getPointWorld(a.surfacePoint, state);
|
|
249
|
+
const radius = norm(sub(surface, center));
|
|
250
|
+
const x = center[0] + radius * Math.sin(c.phi) * Math.cos(c.theta);
|
|
251
|
+
const y = center[1] + radius * Math.sin(c.phi) * Math.sin(c.theta);
|
|
252
|
+
const z = center[2] + radius * Math.cos(c.phi);
|
|
253
|
+
return [x, y, z];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/stamps/geometry-3d/editor/scene/geometryChecks.ts
|
|
259
|
+
var EPS = 1e-6;
|
|
260
|
+
function getWorld(id, state) {
|
|
261
|
+
const obj = state.objects[id];
|
|
262
|
+
if (!obj || obj.kind !== "point3d") return null;
|
|
263
|
+
const attrs = obj.attrs;
|
|
264
|
+
return constraintToWorld(attrs.constraint, state);
|
|
265
|
+
}
|
|
266
|
+
function areCollinear3(p1Id, p2Id, p3Id, state) {
|
|
267
|
+
const p1 = getWorld(p1Id, state);
|
|
268
|
+
const p2 = getWorld(p2Id, state);
|
|
269
|
+
const p3 = getWorld(p3Id, state);
|
|
270
|
+
if (!p1 || !p2 || !p3) return true;
|
|
271
|
+
const a = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];
|
|
272
|
+
const b = [p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2]];
|
|
273
|
+
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]];
|
|
274
|
+
return Math.hypot(c[0], c[1], c[2]) < EPS;
|
|
275
|
+
}
|
|
276
|
+
function apexCoplanarWithBase(baseIds, apexId, state) {
|
|
277
|
+
if (baseIds.length < 3) return false;
|
|
278
|
+
const p1 = getWorld(baseIds[0], state);
|
|
279
|
+
const p2 = getWorld(baseIds[1], state);
|
|
280
|
+
const p3 = getWorld(baseIds[2], state);
|
|
281
|
+
const apex = getWorld(apexId, state);
|
|
282
|
+
if (!p1 || !p2 || !p3 || !apex) return false;
|
|
283
|
+
const a = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];
|
|
284
|
+
const b = [p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2]];
|
|
285
|
+
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]];
|
|
286
|
+
const d = [apex[0] - p1[0], apex[1] - p1[1], apex[2] - p1[2]];
|
|
287
|
+
const dotND = n[0] * d[0] + n[1] * d[1] + n[2] * d[2];
|
|
288
|
+
return Math.abs(dotND) < EPS;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/stamps/geometry-3d/editor/tools/handlers/plane.ts
|
|
292
|
+
function buildPlane(args, store) {
|
|
293
|
+
if (args.length < 3 || !args[0].hit || !args[1].hit || !args[2].hit) return null;
|
|
294
|
+
const p1 = ensurePoint(args[0].hit, store);
|
|
295
|
+
const p2 = ensurePoint(args[1].hit, store);
|
|
296
|
+
const p3 = ensurePoint(args[2].hit, store);
|
|
297
|
+
if (!p1 || !p2 || !p3) return null;
|
|
298
|
+
if (p1 === p2 || p2 === p3 || p1 === p3) return null;
|
|
299
|
+
if (areCollinear3(p1, p2, p3, store.getState())) return null;
|
|
300
|
+
const id = `pl${store.getState().counter + 1}`;
|
|
301
|
+
const label = nextLabel(store.getState(), "plane3d");
|
|
302
|
+
const obj = {
|
|
303
|
+
id,
|
|
304
|
+
kind: "plane3d",
|
|
305
|
+
label,
|
|
306
|
+
visible: true,
|
|
307
|
+
locked: false,
|
|
308
|
+
layer: "default",
|
|
309
|
+
schemaVersion: 1,
|
|
310
|
+
attrs: { p1, p2, p3 }
|
|
311
|
+
};
|
|
312
|
+
store.dispatch({ type: "ADD", payload: { obj } });
|
|
313
|
+
return id;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// src/stamps/geometry-3d/editor/tools/handlers/pyramid.ts
|
|
317
|
+
function buildPyramid(args, store) {
|
|
318
|
+
const pointArgs = args.filter((a) => a.step.type === "point");
|
|
319
|
+
const baseArgs = pointArgs.slice(0, -1);
|
|
320
|
+
const apexArg = pointArgs.slice(-1)[0];
|
|
321
|
+
if (baseArgs.length < 3 || !apexArg?.hit) return null;
|
|
322
|
+
const baseIds = baseArgs.map((a) => a.hit ? ensurePoint(a.hit, store) : null).filter((x) => !!x);
|
|
323
|
+
const apexId = ensurePoint(apexArg.hit, store);
|
|
324
|
+
if (!apexId || baseIds.length < 3) return null;
|
|
325
|
+
if (apexCoplanarWithBase(baseIds, apexId, store.getState())) return null;
|
|
326
|
+
const vertices = [...baseIds, apexId];
|
|
327
|
+
const apexIdx = vertices.length - 1;
|
|
328
|
+
const faces = [baseIds.map((_, i) => i)];
|
|
329
|
+
for (let i = 0; i < baseIds.length; i++) {
|
|
330
|
+
faces.push([i, (i + 1) % baseIds.length, apexIdx]);
|
|
331
|
+
}
|
|
332
|
+
const id = `ph${store.getState().counter + 1}`;
|
|
333
|
+
const label = nextLabel(store.getState(), "polyhedron3d");
|
|
334
|
+
const obj = {
|
|
335
|
+
id,
|
|
336
|
+
kind: "polyhedron3d",
|
|
337
|
+
label,
|
|
338
|
+
visible: true,
|
|
339
|
+
locked: false,
|
|
340
|
+
layer: "default",
|
|
341
|
+
schemaVersion: 1,
|
|
342
|
+
attrs: { flavor: "pyramid", vertices, faces }
|
|
343
|
+
};
|
|
344
|
+
store.dispatch({ type: "ADD", payload: { obj } });
|
|
345
|
+
return id;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/stamps/geometry-3d/editor/tools/handlers/prism.ts
|
|
349
|
+
function buildPrism(args, store) {
|
|
350
|
+
const baseArgs = args.filter((a) => a.step.type === "point");
|
|
351
|
+
const numberArg = args.find((a) => a.step.type === "number");
|
|
352
|
+
if (baseArgs.length < 3 || !numberArg || typeof numberArg.value !== "number") return null;
|
|
353
|
+
const height = numberArg.value;
|
|
354
|
+
if (height <= 0) return null;
|
|
355
|
+
const baseIds = baseArgs.map((a) => a.hit ? ensurePoint(a.hit, store) : null).filter((x) => !!x);
|
|
356
|
+
if (baseIds.length < 3) return null;
|
|
357
|
+
const topIds = [];
|
|
358
|
+
for (const id2 of baseIds) {
|
|
359
|
+
const state = store.getState();
|
|
360
|
+
const p = state.objects[id2];
|
|
361
|
+
if (!p || p.kind !== "point3d") return null;
|
|
362
|
+
const attrs = p.attrs;
|
|
363
|
+
const w = constraintToWorld(attrs.constraint, state);
|
|
364
|
+
topIds.push(addPoint(store, { kind: "free", x: w[0], y: w[1], z: w[2] + height }));
|
|
365
|
+
}
|
|
366
|
+
const n = baseIds.length;
|
|
367
|
+
const vertices = [...baseIds, ...topIds];
|
|
368
|
+
const faces = [
|
|
369
|
+
baseIds.map((_, i) => i),
|
|
370
|
+
topIds.map((_, i) => n + i)
|
|
371
|
+
];
|
|
372
|
+
for (let i = 0; i < n; i++) {
|
|
373
|
+
faces.push([i, (i + 1) % n, n + (i + 1) % n, n + i]);
|
|
374
|
+
}
|
|
375
|
+
const id = `ph${store.getState().counter + 1}`;
|
|
376
|
+
const label = nextLabel(store.getState(), "polyhedron3d");
|
|
377
|
+
const obj = {
|
|
378
|
+
id,
|
|
379
|
+
kind: "polyhedron3d",
|
|
380
|
+
label,
|
|
381
|
+
visible: true,
|
|
382
|
+
locked: false,
|
|
383
|
+
layer: "default",
|
|
384
|
+
schemaVersion: 1,
|
|
385
|
+
attrs: { flavor: "prism", vertices, faces }
|
|
386
|
+
};
|
|
387
|
+
store.dispatch({ type: "ADD", payload: { obj } });
|
|
388
|
+
return id;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/stamps/geometry-3d/editor/tools/handlers/tetrahedron.ts
|
|
392
|
+
function buildTetrahedron(args, store) {
|
|
393
|
+
if (args.length < 2 || !args[0].hit || !args[1].hit) return null;
|
|
394
|
+
const p1Id = ensurePoint(args[0].hit, store);
|
|
395
|
+
const p2Id = ensurePoint(args[1].hit, store);
|
|
396
|
+
if (!p1Id || !p2Id || p1Id === p2Id) return null;
|
|
397
|
+
const state0 = store.getState();
|
|
398
|
+
const p1Obj = state0.objects[p1Id];
|
|
399
|
+
const p2Obj = state0.objects[p2Id];
|
|
400
|
+
if (!p1Obj || p1Obj.kind !== "point3d" || !p2Obj || p2Obj.kind !== "point3d") return null;
|
|
401
|
+
const p1 = constraintToWorld(p1Obj.attrs.constraint, state0);
|
|
402
|
+
const p2 = constraintToWorld(p2Obj.attrs.constraint, state0);
|
|
403
|
+
const z0 = Math.min(p1[2], p2[2]);
|
|
404
|
+
const baseA = [p1[0], p1[1], z0];
|
|
405
|
+
const baseB = [p2[0], p2[1], z0];
|
|
406
|
+
const dx = baseB[0] - baseA[0];
|
|
407
|
+
const dy = baseB[1] - baseA[1];
|
|
408
|
+
const edge = Math.hypot(dx, dy);
|
|
409
|
+
if (edge < 1e-9) return null;
|
|
410
|
+
const mid = [(baseA[0] + baseB[0]) / 2, (baseA[1] + baseB[1]) / 2, z0];
|
|
411
|
+
const perpX = -dy;
|
|
412
|
+
const perpY = dx;
|
|
413
|
+
const perpLen = Math.hypot(perpX, perpY);
|
|
414
|
+
const height = edge * Math.sqrt(3) / 2;
|
|
415
|
+
const baseC = [mid[0] + perpX / perpLen * height, mid[1] + perpY / perpLen * height, z0];
|
|
416
|
+
const centroid = [
|
|
417
|
+
(baseA[0] + baseB[0] + baseC[0]) / 3,
|
|
418
|
+
(baseA[1] + baseB[1] + baseC[1]) / 3,
|
|
419
|
+
z0
|
|
420
|
+
];
|
|
421
|
+
const apexHeight = edge * Math.sqrt(2 / 3);
|
|
422
|
+
const apex = [centroid[0], centroid[1], z0 + apexHeight];
|
|
423
|
+
const cId = addPoint(store, { kind: "free", x: baseC[0], y: baseC[1], z: baseC[2] });
|
|
424
|
+
const apexId = addPoint(store, { kind: "free", x: apex[0], y: apex[1], z: apex[2] });
|
|
425
|
+
const vertices = [p1Id, p2Id, cId, apexId];
|
|
426
|
+
const faces = [
|
|
427
|
+
[0, 1, 2],
|
|
428
|
+
// base
|
|
429
|
+
[0, 1, 3],
|
|
430
|
+
// face p1-p2-apex
|
|
431
|
+
[1, 2, 3],
|
|
432
|
+
// face p2-c-apex
|
|
433
|
+
[2, 0, 3]
|
|
434
|
+
// face c-p1-apex
|
|
435
|
+
];
|
|
436
|
+
const id = `ph${store.getState().counter + 1}`;
|
|
437
|
+
const label = nextLabel(store.getState(), "polyhedron3d");
|
|
438
|
+
const obj = {
|
|
439
|
+
id,
|
|
440
|
+
kind: "polyhedron3d",
|
|
441
|
+
label,
|
|
442
|
+
visible: true,
|
|
443
|
+
locked: false,
|
|
444
|
+
layer: "default",
|
|
445
|
+
schemaVersion: 1,
|
|
446
|
+
attrs: { flavor: "tetrahedron", vertices, faces }
|
|
447
|
+
};
|
|
448
|
+
store.dispatch({ type: "ADD", payload: { obj } });
|
|
449
|
+
return id;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/stamps/geometry-3d/editor/tools/handlers/cube.ts
|
|
453
|
+
function buildCube(args, store) {
|
|
454
|
+
if (args.length < 2 || !args[0].hit || !args[1].hit) return null;
|
|
455
|
+
const p1Id = ensurePoint(args[0].hit, store);
|
|
456
|
+
const p2Id = ensurePoint(args[1].hit, store);
|
|
457
|
+
if (!p1Id || !p2Id || p1Id === p2Id) return null;
|
|
458
|
+
const state0 = store.getState();
|
|
459
|
+
const p1Obj = state0.objects[p1Id];
|
|
460
|
+
const p2Obj = state0.objects[p2Id];
|
|
461
|
+
if (!p1Obj || p1Obj.kind !== "point3d" || !p2Obj || p2Obj.kind !== "point3d") return null;
|
|
462
|
+
const p1 = constraintToWorld(p1Obj.attrs.constraint, state0);
|
|
463
|
+
const p2 = constraintToWorld(p2Obj.attrs.constraint, state0);
|
|
464
|
+
if (Math.abs(p1[2]) > 1e-6 || Math.abs(p2[2]) > 1e-6) return null;
|
|
465
|
+
const dx = p2[0] - p1[0];
|
|
466
|
+
const dy = p2[1] - p1[1];
|
|
467
|
+
const edge = Math.hypot(dx, dy);
|
|
468
|
+
if (edge < 1e-9) return null;
|
|
469
|
+
const perpX = -dy;
|
|
470
|
+
const perpY = dx;
|
|
471
|
+
const p3 = [p2[0] + perpX, p2[1] + perpY, 0];
|
|
472
|
+
const p4 = [p1[0] + perpX, p1[1] + perpY, 0];
|
|
473
|
+
const t1 = [p1[0], p1[1], edge];
|
|
474
|
+
const t2 = [p2[0], p2[1], edge];
|
|
475
|
+
const t3 = [p3[0], p3[1], edge];
|
|
476
|
+
const t4 = [p4[0], p4[1], edge];
|
|
477
|
+
const p3Id = addPoint(store, { kind: "onGround", x: p3[0], y: p3[1] });
|
|
478
|
+
const p4Id = addPoint(store, { kind: "onGround", x: p4[0], y: p4[1] });
|
|
479
|
+
const t1Id = addPoint(store, { kind: "free", x: t1[0], y: t1[1], z: t1[2] });
|
|
480
|
+
const t2Id = addPoint(store, { kind: "free", x: t2[0], y: t2[1], z: t2[2] });
|
|
481
|
+
const t3Id = addPoint(store, { kind: "free", x: t3[0], y: t3[1], z: t3[2] });
|
|
482
|
+
const t4Id = addPoint(store, { kind: "free", x: t4[0], y: t4[1], z: t4[2] });
|
|
483
|
+
const vertices = [p1Id, p2Id, p3Id, p4Id, t1Id, t2Id, t3Id, t4Id];
|
|
484
|
+
const faces = [
|
|
485
|
+
[0, 1, 2, 3],
|
|
486
|
+
// bottom
|
|
487
|
+
[4, 5, 6, 7],
|
|
488
|
+
// top
|
|
489
|
+
[0, 1, 5, 4],
|
|
490
|
+
// front
|
|
491
|
+
[1, 2, 6, 5],
|
|
492
|
+
// right
|
|
493
|
+
[2, 3, 7, 6],
|
|
494
|
+
// back
|
|
495
|
+
[3, 0, 4, 7]
|
|
496
|
+
// left
|
|
497
|
+
];
|
|
498
|
+
const id = `ph${store.getState().counter + 1}`;
|
|
499
|
+
const label = nextLabel(store.getState(), "polyhedron3d");
|
|
500
|
+
const obj = {
|
|
501
|
+
id,
|
|
502
|
+
kind: "polyhedron3d",
|
|
503
|
+
label,
|
|
504
|
+
visible: true,
|
|
505
|
+
locked: false,
|
|
506
|
+
layer: "default",
|
|
507
|
+
schemaVersion: 1,
|
|
508
|
+
attrs: { flavor: "cube", vertices, faces }
|
|
509
|
+
};
|
|
510
|
+
store.dispatch({ type: "ADD", payload: { obj } });
|
|
511
|
+
return id;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// src/stamps/geometry-3d/editor/tools/handlers/sphere.ts
|
|
515
|
+
function buildSphere(args, store) {
|
|
516
|
+
if (args.length < 2 || !args[0].hit || !args[1].hit) return null;
|
|
517
|
+
const center = ensurePoint(args[0].hit, store);
|
|
518
|
+
const surface = ensurePoint(args[1].hit, store);
|
|
519
|
+
if (!center || !surface || center === surface) return null;
|
|
520
|
+
const id = `sp${store.getState().counter + 1}`;
|
|
521
|
+
const label = nextLabel(store.getState(), "sphere3d");
|
|
522
|
+
const obj = {
|
|
523
|
+
id,
|
|
524
|
+
kind: "sphere3d",
|
|
525
|
+
label,
|
|
526
|
+
visible: true,
|
|
527
|
+
locked: false,
|
|
528
|
+
layer: "default",
|
|
529
|
+
schemaVersion: 1,
|
|
530
|
+
attrs: { center, surfacePoint: surface }
|
|
531
|
+
};
|
|
532
|
+
store.dispatch({ type: "ADD", payload: { obj } });
|
|
533
|
+
return id;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/stamps/geometry-3d/editor/tools/handlers/cylinder.ts
|
|
537
|
+
function buildCylinder(args, store) {
|
|
538
|
+
const points = args.filter((a) => a.step.type === "point");
|
|
539
|
+
const numberArg = args.find((a) => a.step.type === "number");
|
|
540
|
+
if (points.length < 2 || !points[0].hit || !points[1].hit || !numberArg || typeof numberArg.value !== "number") return null;
|
|
541
|
+
const radius = numberArg.value;
|
|
542
|
+
if (radius <= 0) return null;
|
|
543
|
+
const baseCenter = ensurePoint(points[0].hit, store);
|
|
544
|
+
const topCenter = ensurePoint(points[1].hit, store);
|
|
545
|
+
if (!baseCenter || !topCenter || baseCenter === topCenter) return null;
|
|
546
|
+
const id = `cy${store.getState().counter + 1}`;
|
|
547
|
+
const label = nextLabel(store.getState(), "cylinder3d");
|
|
548
|
+
const obj = {
|
|
549
|
+
id,
|
|
550
|
+
kind: "cylinder3d",
|
|
551
|
+
label,
|
|
552
|
+
visible: true,
|
|
553
|
+
locked: false,
|
|
554
|
+
layer: "default",
|
|
555
|
+
schemaVersion: 1,
|
|
556
|
+
attrs: { baseCenter, topCenter, radius }
|
|
557
|
+
};
|
|
558
|
+
store.dispatch({ type: "ADD", payload: { obj } });
|
|
559
|
+
return id;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// src/stamps/geometry-3d/editor/tools/handlers/cone.ts
|
|
563
|
+
function buildCone(args, store) {
|
|
564
|
+
const points = args.filter((a) => a.step.type === "point");
|
|
565
|
+
const numberArg = args.find((a) => a.step.type === "number");
|
|
566
|
+
if (points.length < 2 || !points[0].hit || !points[1].hit || !numberArg || typeof numberArg.value !== "number") return null;
|
|
567
|
+
const radius = numberArg.value;
|
|
568
|
+
if (radius <= 0) return null;
|
|
569
|
+
const baseCenter = ensurePoint(points[0].hit, store);
|
|
570
|
+
const apex = ensurePoint(points[1].hit, store);
|
|
571
|
+
if (!baseCenter || !apex || baseCenter === apex) return null;
|
|
572
|
+
const id = `co${store.getState().counter + 1}`;
|
|
573
|
+
const label = nextLabel(store.getState(), "cone3d");
|
|
574
|
+
const obj = {
|
|
575
|
+
id,
|
|
576
|
+
kind: "cone3d",
|
|
577
|
+
label,
|
|
578
|
+
visible: true,
|
|
579
|
+
locked: false,
|
|
580
|
+
layer: "default",
|
|
581
|
+
schemaVersion: 1,
|
|
582
|
+
attrs: { baseCenter, apex, radius }
|
|
583
|
+
};
|
|
584
|
+
store.dispatch({ type: "ADD", payload: { obj } });
|
|
585
|
+
return id;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/stamps/geometry-3d/editor/tools/spec.ts
|
|
589
|
+
var stubBuild = () => null;
|
|
590
|
+
var ALL_SURFACES = ["ground", "axis", "plane", "line", "polygon", "sphere"];
|
|
591
|
+
var OBJECT_ONLY = ["plane", "line", "polygon", "sphere"];
|
|
592
|
+
var NO_SURFACE = ["ground", "axis", "plane"];
|
|
593
|
+
var TOOLS = [
|
|
594
|
+
{
|
|
595
|
+
key: "move",
|
|
596
|
+
label: "Di chuy\u1EC3n",
|
|
597
|
+
hintIdle: "K\xE9o \u0111i\u1EC3m ho\u1EB7c xoay khung",
|
|
598
|
+
steps: [],
|
|
599
|
+
build: stubBuild
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
key: "point",
|
|
603
|
+
label: "\u0110i\u1EC3m",
|
|
604
|
+
hintIdle: "Click tr\xEAn m\u1EB7t ph\u1EB3ng Oxy ho\u1EB7c tr\xEAn tr\u1EE5c \u0111\u1EC3 \u0111\u1EB7t \u0111i\u1EC3m",
|
|
605
|
+
steps: [
|
|
606
|
+
{
|
|
607
|
+
type: "point",
|
|
608
|
+
allowExisting: false,
|
|
609
|
+
// GeoGebra-style: a new point must lie on the XY ground plane or on
|
|
610
|
+
// one of the coordinate axes (Oz lets you place points off the plane).
|
|
611
|
+
allowNewOn: ["ground", "axis"],
|
|
612
|
+
hint: "Click tr\xEAn m\u1EB7t ph\u1EB3ng Oxy ho\u1EB7c tr\u1EE5c Ox/Oy/Oz"
|
|
613
|
+
}
|
|
614
|
+
],
|
|
615
|
+
build: buildPoint,
|
|
616
|
+
repeatAfterBuild: true
|
|
617
|
+
},
|
|
618
|
+
{
|
|
619
|
+
key: "pointOnObject",
|
|
620
|
+
label: "\u0110i\u1EC3m tr\xEAn \u0111\u1ED1i t\u01B0\u1EE3ng",
|
|
621
|
+
hintIdle: "Ch\u1ECDn m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng \u0111\u1EC3 \u0111\u1EB7t \u0111i\u1EC3m",
|
|
622
|
+
steps: [{ type: "point", allowExisting: false, allowNewOn: OBJECT_ONLY, hint: "Click l\xEAn m\u1EB7t / \u0111\u01B0\u1EDDng \u0111\u1EC3 \u0111\u1EB7t \u0111i\u1EC3m" }],
|
|
623
|
+
build: buildPointOnObject
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
key: "segment",
|
|
627
|
+
label: "\u0110o\u1EA1n th\u1EB3ng",
|
|
628
|
+
hintIdle: "Ch\u1ECDn 2 \u0111i\u1EC3m \u0111\u1EC3 v\u1EBD \u0111o\u1EA1n th\u1EB3ng",
|
|
629
|
+
steps: [
|
|
630
|
+
{ type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m th\u1EE9 nh\u1EA5t" },
|
|
631
|
+
{ type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m th\u1EE9 hai" }
|
|
632
|
+
],
|
|
633
|
+
build: buildSegment
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
key: "line",
|
|
637
|
+
label: "\u0110\u01B0\u1EDDng th\u1EB3ng",
|
|
638
|
+
hintIdle: "Ch\u1ECDn 2 \u0111i\u1EC3m \u0111\u1EC3 v\u1EBD \u0111\u01B0\u1EDDng th\u1EB3ng",
|
|
639
|
+
steps: [
|
|
640
|
+
{ type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m th\u1EE9 nh\u1EA5t" },
|
|
641
|
+
{ type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m th\u1EE9 hai" }
|
|
642
|
+
],
|
|
643
|
+
build: buildLine
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
key: "ray",
|
|
647
|
+
label: "Tia",
|
|
648
|
+
hintIdle: "Ch\u1ECDn \u0111i\u1EC3m g\u1ED1c r\u1ED3i \u0111i\u1EC3m tr\xEAn tia",
|
|
649
|
+
steps: [
|
|
650
|
+
{ type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m g\u1ED1c" },
|
|
651
|
+
{ type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m tr\xEAn tia" }
|
|
652
|
+
],
|
|
653
|
+
build: buildRay
|
|
654
|
+
},
|
|
655
|
+
{
|
|
656
|
+
key: "vector",
|
|
657
|
+
label: "Vector",
|
|
658
|
+
hintIdle: "Ch\u1ECDn 2 \u0111i\u1EC3m \u0111\u1EC3 v\u1EBD vector",
|
|
659
|
+
steps: [
|
|
660
|
+
{ type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m \u0111\u1EA7u" },
|
|
661
|
+
{ type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m cu\u1ED1i" }
|
|
662
|
+
],
|
|
663
|
+
build: buildVector
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
key: "polygon",
|
|
667
|
+
label: "\u0110a gi\xE1c",
|
|
668
|
+
hintIdle: "Ch\u1ECDn c\xE1c \u0111\u1EC9nh; click \u0111i\u1EC3m \u0111\u1EA7u \u0111\u1EC3 \u0111\xF3ng",
|
|
669
|
+
steps: [
|
|
670
|
+
{ type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111\u1EC9nh th\u1EE9 1" },
|
|
671
|
+
{ type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111\u1EC9nh th\u1EE9 2" },
|
|
672
|
+
{ type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111\u1EC9nh th\u1EE9 3" },
|
|
673
|
+
{ type: "closingPoint", hint: "Click \u0111i\u1EC3m \u0111\u1EA7u \u0111\u1EC3 \u0111\xF3ng (ho\u1EB7c ch\u1ECDn th\xEAm \u0111\u1EC9nh)" }
|
|
674
|
+
],
|
|
675
|
+
build: buildPolygon
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
key: "plane",
|
|
679
|
+
label: "M\u1EB7t ph\u1EB3ng (3 \u0111i\u1EC3m)",
|
|
680
|
+
hintIdle: "Ch\u1ECDn 3 \u0111i\u1EC3m \u0111\u1EC3 x\xE1c \u0111\u1ECBnh m\u1EB7t ph\u1EB3ng",
|
|
681
|
+
steps: [
|
|
682
|
+
{ type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m th\u1EE9 1 c\u1EE7a m\u1EB7t ph\u1EB3ng" },
|
|
683
|
+
{ type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m th\u1EE9 2" },
|
|
684
|
+
{ type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m th\u1EE9 3" }
|
|
685
|
+
],
|
|
686
|
+
build: buildPlane
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
key: "pyramid",
|
|
690
|
+
label: "H\xECnh ch\xF3p",
|
|
691
|
+
hintIdle: "Ch\u1ECDn \u0111\xE1y \u0111a gi\xE1c r\u1ED3i ch\u1ECDn \u0111\u1EC9nh ch\xF3p",
|
|
692
|
+
steps: [
|
|
693
|
+
{ type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111\u1EC9nh \u0111\xE1y 1" },
|
|
694
|
+
{ type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111\u1EC9nh \u0111\xE1y 2" },
|
|
695
|
+
{ type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111\u1EC9nh \u0111\xE1y 3" },
|
|
696
|
+
{ type: "closingPoint", hint: "Click \u0111\u1EC9nh \u0111\xE1y \u0111\u1EA7u ti\xEAn \u0111\u1EC3 \u0111\xF3ng (ho\u1EB7c ch\u1ECDn th\xEAm \u0111\u1EC9nh)" },
|
|
697
|
+
{ type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111\u1EC9nh ch\xF3p" }
|
|
698
|
+
],
|
|
699
|
+
build: buildPyramid
|
|
700
|
+
},
|
|
701
|
+
{
|
|
702
|
+
key: "prism",
|
|
703
|
+
label: "L\u0103ng tr\u1EE5",
|
|
704
|
+
hintIdle: "Ch\u1ECDn \u0111\xE1y \u0111a gi\xE1c r\u1ED3i nh\u1EADp chi\u1EC1u cao",
|
|
705
|
+
steps: [
|
|
706
|
+
{ type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111\u1EC9nh \u0111\xE1y 1" },
|
|
707
|
+
{ type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111\u1EC9nh \u0111\xE1y 2" },
|
|
708
|
+
{ type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111\u1EC9nh \u0111\xE1y 3" },
|
|
709
|
+
{ type: "closingPoint", hint: "Click \u0111\u1EC9nh \u0111\u1EA7u \u0111\u1EC3 \u0111\xF3ng \u0111\xE1y" },
|
|
710
|
+
{ type: "number", prompt: "Chi\u1EC1u cao (theo tr\u1EE5c z)", min: 1e-4 }
|
|
711
|
+
],
|
|
712
|
+
build: buildPrism
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
key: "tetrahedron",
|
|
716
|
+
label: "T\u1EE9 di\u1EC7n \u0111\u1EC1u",
|
|
717
|
+
hintIdle: "Ch\u1ECDn 2 \u0111i\u1EC3m x\xE1c \u0111\u1ECBnh c\u1EA1nh c\u1EE7a t\u1EE9 di\u1EC7n",
|
|
718
|
+
steps: [
|
|
719
|
+
{ type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111i\u1EC3m 1" },
|
|
720
|
+
{ type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111i\u1EC3m 2" }
|
|
721
|
+
],
|
|
722
|
+
build: buildTetrahedron
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
key: "cube",
|
|
726
|
+
label: "L\u1EADp ph\u01B0\u01A1ng",
|
|
727
|
+
hintIdle: "Ch\u1ECDn 2 \u0111i\u1EC3m tr\xEAn n\u1EC1n x\xE1c \u0111\u1ECBnh c\u1EA1nh",
|
|
728
|
+
steps: [
|
|
729
|
+
{ type: "point", allowExisting: true, allowNewOn: ["ground"], hint: "Ch\u1ECDn \u0111i\u1EC3m 1 (tr\xEAn n\u1EC1n)" },
|
|
730
|
+
{ type: "point", allowExisting: true, allowNewOn: ["ground"], hint: "Ch\u1ECDn \u0111i\u1EC3m 2 (tr\xEAn n\u1EC1n)" }
|
|
731
|
+
],
|
|
732
|
+
build: buildCube
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
key: "sphere",
|
|
736
|
+
label: "M\u1EB7t c\u1EA7u",
|
|
737
|
+
hintIdle: "Ch\u1ECDn t\xE2m r\u1ED3i \u0111i\u1EC3m tr\xEAn m\u1EB7t c\u1EA7u",
|
|
738
|
+
steps: [
|
|
739
|
+
{ type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn t\xE2m m\u1EB7t c\u1EA7u" },
|
|
740
|
+
{ type: "point", allowExisting: true, allowNewOn: ALL_SURFACES, hint: "Ch\u1ECDn \u0111i\u1EC3m tr\xEAn m\u1EB7t c\u1EA7u" }
|
|
741
|
+
],
|
|
742
|
+
build: buildSphere
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
key: "cylinder",
|
|
746
|
+
label: "H\xECnh tr\u1EE5",
|
|
747
|
+
hintIdle: "Ch\u1ECDn t\xE2m \u0111\xE1y, t\xE2m tr\xEAn, r\u1ED3i nh\u1EADp b\xE1n k\xEDnh",
|
|
748
|
+
steps: [
|
|
749
|
+
{ type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn t\xE2m \u0111\xE1y" },
|
|
750
|
+
{ type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn t\xE2m tr\xEAn" },
|
|
751
|
+
{ type: "number", prompt: "B\xE1n k\xEDnh", min: 1e-4 }
|
|
752
|
+
],
|
|
753
|
+
build: buildCylinder
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
key: "cone",
|
|
757
|
+
label: "H\xECnh n\xF3n",
|
|
758
|
+
hintIdle: "Ch\u1ECDn t\xE2m \u0111\xE1y, \u0111\u1EC9nh, r\u1ED3i nh\u1EADp b\xE1n k\xEDnh",
|
|
759
|
+
steps: [
|
|
760
|
+
{ type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn t\xE2m \u0111\xE1y" },
|
|
761
|
+
{ type: "point", allowExisting: true, allowNewOn: NO_SURFACE, hint: "Ch\u1ECDn \u0111\u1EC9nh" },
|
|
762
|
+
{ type: "number", prompt: "B\xE1n k\xEDnh", min: 1e-4 }
|
|
763
|
+
],
|
|
764
|
+
build: buildCone
|
|
765
|
+
}
|
|
766
|
+
];
|
|
767
|
+
|
|
768
|
+
// src/stamps/geometry-3d/editor/tools/controller.ts
|
|
769
|
+
function stepHint(step) {
|
|
770
|
+
return step.type === "number" ? step.prompt : step.hint;
|
|
771
|
+
}
|
|
772
|
+
var ToolController = class {
|
|
773
|
+
constructor(store) {
|
|
774
|
+
this.store = store;
|
|
775
|
+
this.state = { tool: null, stepIndex: 0, collected: [], hint: "" };
|
|
776
|
+
this.listeners = /* @__PURE__ */ new Set();
|
|
777
|
+
this.selectTool("move");
|
|
778
|
+
}
|
|
779
|
+
getState() {
|
|
780
|
+
return this.state;
|
|
781
|
+
}
|
|
782
|
+
on(cb) {
|
|
783
|
+
this.listeners.add(cb);
|
|
784
|
+
return () => {
|
|
785
|
+
this.listeners.delete(cb);
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
selectTool(key) {
|
|
789
|
+
const tool = TOOLS.find((t) => t.key === key) ?? TOOLS.find((t) => t.key === "move");
|
|
790
|
+
const firstStep = tool.steps[0];
|
|
791
|
+
this.state = {
|
|
792
|
+
tool,
|
|
793
|
+
stepIndex: 0,
|
|
794
|
+
collected: [],
|
|
795
|
+
hint: firstStep ? stepHint(firstStep) : tool.hintIdle
|
|
796
|
+
};
|
|
797
|
+
this.notify();
|
|
798
|
+
}
|
|
799
|
+
cancel() {
|
|
800
|
+
this.selectTool("move");
|
|
801
|
+
}
|
|
802
|
+
consumeHit(hit) {
|
|
803
|
+
const tool = this.state.tool;
|
|
804
|
+
if (!tool) return false;
|
|
805
|
+
const step = tool.steps[this.state.stepIndex];
|
|
806
|
+
if (!step) return false;
|
|
807
|
+
if (step.type === "closingPoint") {
|
|
808
|
+
if (hit.kind === "empty") return false;
|
|
809
|
+
if (hit.kind === "existingPoint") {
|
|
810
|
+
this.state.collected.push({ step, hit });
|
|
811
|
+
this.state.stepIndex++;
|
|
812
|
+
this.advance();
|
|
813
|
+
return true;
|
|
814
|
+
}
|
|
815
|
+
const prevStep = tool.steps[this.state.stepIndex - 1];
|
|
816
|
+
if (!prevStep || prevStep.type !== "point") return false;
|
|
817
|
+
if (!this.hitMatchesStep(hit, prevStep)) return false;
|
|
818
|
+
this.state.collected.push({ step: prevStep, hit });
|
|
819
|
+
this.notify();
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
822
|
+
if (!this.hitMatchesStep(hit, step)) return false;
|
|
823
|
+
this.state.collected.push({ step, hit });
|
|
824
|
+
this.state.stepIndex++;
|
|
825
|
+
this.advance();
|
|
826
|
+
return true;
|
|
827
|
+
}
|
|
828
|
+
consumeNumber(value) {
|
|
829
|
+
const tool = this.state.tool;
|
|
830
|
+
if (!tool) return false;
|
|
831
|
+
const step = tool.steps[this.state.stepIndex];
|
|
832
|
+
if (!step || step.type !== "number") return false;
|
|
833
|
+
if (step.min != null && value < step.min) return false;
|
|
834
|
+
if (step.max != null && value > step.max) return false;
|
|
835
|
+
this.state.collected.push({ step, value });
|
|
836
|
+
this.state.stepIndex++;
|
|
837
|
+
this.advance();
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
hitMatchesStep(hit, step) {
|
|
841
|
+
if (step.type !== "point" && step.type !== "closingPoint") return false;
|
|
842
|
+
if (hit.kind === "empty") return false;
|
|
843
|
+
if (step.type === "closingPoint") return hit.kind === "existingPoint";
|
|
844
|
+
if (hit.kind === "existingPoint") return step.allowExisting;
|
|
845
|
+
const surfaceMap = {
|
|
846
|
+
onGround: "ground",
|
|
847
|
+
onAxis: "axis",
|
|
848
|
+
onPlane: "plane",
|
|
849
|
+
onLine: "line",
|
|
850
|
+
onPolygon: "polygon",
|
|
851
|
+
onSphere: "sphere"
|
|
852
|
+
};
|
|
853
|
+
const k = surfaceMap[hit.kind];
|
|
854
|
+
return k != null && step.type === "point" && step.allowNewOn.includes(k);
|
|
855
|
+
}
|
|
856
|
+
advance() {
|
|
857
|
+
const tool = this.state.tool;
|
|
858
|
+
if (this.state.stepIndex >= tool.steps.length) {
|
|
859
|
+
tool.build(this.state.collected, this.store);
|
|
860
|
+
if (tool.repeatAfterBuild) {
|
|
861
|
+
this.state.stepIndex = 0;
|
|
862
|
+
this.state.collected = [];
|
|
863
|
+
this.state.hint = stepHint(tool.steps[0]);
|
|
864
|
+
this.notify();
|
|
865
|
+
} else {
|
|
866
|
+
this.selectTool("move");
|
|
867
|
+
}
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
this.state.hint = stepHint(tool.steps[this.state.stepIndex]);
|
|
871
|
+
this.notify();
|
|
872
|
+
}
|
|
873
|
+
notify() {
|
|
874
|
+
for (const cb of this.listeners) cb(this.state);
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
// src/stamps/geometry-3d/editor/hitTest/rayCast.ts
|
|
879
|
+
function screenToRay(screen, view) {
|
|
880
|
+
const near = unproject(screen, view, 20);
|
|
881
|
+
const far = unproject(screen, view, -20);
|
|
882
|
+
const dir = [far[0] - near[0], far[1] - near[1], far[2] - near[2]];
|
|
883
|
+
const n = Math.sqrt(dir[0] ** 2 + dir[1] ** 2 + dir[2] ** 2);
|
|
884
|
+
const norm3 = n === 0 ? [0, 0, -1] : [dir[0] / n, dir[1] / n, dir[2] / n];
|
|
885
|
+
return { origin: near, dir: norm3 };
|
|
886
|
+
}
|
|
887
|
+
function unproject(screen, view, depth) {
|
|
888
|
+
if (typeof view.unprojectScreen === "function") {
|
|
889
|
+
const v = view.unprojectScreen(screen.x, screen.y, depth);
|
|
890
|
+
return [v[0], v[1], v[2]];
|
|
891
|
+
}
|
|
892
|
+
if (typeof view.project3DTo2D === "function") {
|
|
893
|
+
const p0 = view.project3DTo2D(0, 0, 0);
|
|
894
|
+
const px = view.project3DTo2D(1, 0, 0);
|
|
895
|
+
const py = view.project3DTo2D(0, 1, 0);
|
|
896
|
+
const pz = view.project3DTo2D(0, 0, 1);
|
|
897
|
+
const ox = p0[1], oy = p0[2];
|
|
898
|
+
const a = px[1] - ox, b = py[1] - ox, c = pz[1] - ox;
|
|
899
|
+
const d = px[2] - oy, e = py[2] - oy, f = pz[2] - oy;
|
|
900
|
+
const rhsX = screen.x - ox - c * depth;
|
|
901
|
+
const rhsY = screen.y - oy - f * depth;
|
|
902
|
+
const det = a * e - b * d;
|
|
903
|
+
if (Math.abs(det) < 1e-9) return [0, 0, depth];
|
|
904
|
+
const x = (e * rhsX - b * rhsY) / det;
|
|
905
|
+
const y = (-d * rhsX + a * rhsY) / det;
|
|
906
|
+
return [x, y, depth];
|
|
907
|
+
}
|
|
908
|
+
throw new Error("rayCast: view has neither unprojectScreen nor project3DTo2D");
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// src/stamps/geometry-3d/editor/hitTest/intersect.ts
|
|
912
|
+
var EPS2 = 1e-9;
|
|
913
|
+
function dot2(a, b) {
|
|
914
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
915
|
+
}
|
|
916
|
+
function sub2(a, b) {
|
|
917
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
918
|
+
}
|
|
919
|
+
function add2(a, b) {
|
|
920
|
+
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
921
|
+
}
|
|
922
|
+
function scale2(a, k) {
|
|
923
|
+
return [a[0] * k, a[1] * k, a[2] * k];
|
|
924
|
+
}
|
|
925
|
+
function norm2(a) {
|
|
926
|
+
return dot2(a, a);
|
|
927
|
+
}
|
|
928
|
+
function rayPlane(ray, plane) {
|
|
929
|
+
const denom = dot2(ray.dir, plane.normal);
|
|
930
|
+
if (Math.abs(denom) < EPS2) return null;
|
|
931
|
+
const t = dot2(sub2(plane.point, ray.origin), plane.normal) / denom;
|
|
932
|
+
if (t < 0) return null;
|
|
933
|
+
return { point: add2(ray.origin, scale2(ray.dir, t)), t };
|
|
934
|
+
}
|
|
935
|
+
function rayGround(ray) {
|
|
936
|
+
return rayPlane(ray, { point: [0, 0, 0], normal: [0, 0, 1] });
|
|
937
|
+
}
|
|
938
|
+
function raySphere(ray, sphere) {
|
|
939
|
+
const oc = sub2(ray.origin, sphere.center);
|
|
940
|
+
const b = dot2(oc, ray.dir);
|
|
941
|
+
const c = dot2(oc, oc) - sphere.radius * sphere.radius;
|
|
942
|
+
const disc = b * b - c;
|
|
943
|
+
if (disc < 0) return null;
|
|
944
|
+
const sqrtD = Math.sqrt(disc);
|
|
945
|
+
const t1 = -b - sqrtD;
|
|
946
|
+
const t2 = -b + sqrtD;
|
|
947
|
+
const t = t1 >= 0 ? t1 : t2;
|
|
948
|
+
if (t < 0) return null;
|
|
949
|
+
return { point: add2(ray.origin, scale2(ray.dir, t)), t };
|
|
950
|
+
}
|
|
951
|
+
function rayLineSegment(ray, seg, maxDistance) {
|
|
952
|
+
const u = ray.dir;
|
|
953
|
+
const v = sub2(seg.b, seg.a);
|
|
954
|
+
const w0 = sub2(ray.origin, seg.a);
|
|
955
|
+
const a = dot2(u, u);
|
|
956
|
+
const bb = dot2(u, v);
|
|
957
|
+
const cc = dot2(v, v);
|
|
958
|
+
const d = dot2(u, w0);
|
|
959
|
+
const e = dot2(v, w0);
|
|
960
|
+
const denom = a * cc - bb * bb;
|
|
961
|
+
if (Math.abs(denom) < EPS2) return null;
|
|
962
|
+
const sc = (bb * e - cc * d) / denom;
|
|
963
|
+
const tc = (a * e - bb * d) / denom;
|
|
964
|
+
if (sc < 0 || tc < 0 || tc > 1) return null;
|
|
965
|
+
const pRay = add2(ray.origin, scale2(u, sc));
|
|
966
|
+
const pSeg = add2(seg.a, scale2(v, tc));
|
|
967
|
+
const dist2 = norm2(sub2(pRay, pSeg));
|
|
968
|
+
if (dist2 > maxDistance * maxDistance) return null;
|
|
969
|
+
return { point: pSeg, t: sc, tOnSegment: tc };
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// src/stamps/geometry-3d/editor/hitTest/snapping.ts
|
|
973
|
+
function findSnapPoint(screen, view, state, pixelRadius = 8) {
|
|
974
|
+
const board = view?.board;
|
|
975
|
+
const ux = typeof board?.unitX === "number" && board.unitX > 0 ? board.unitX : 1;
|
|
976
|
+
const uy = typeof board?.unitY === "number" && board.unitY > 0 ? board.unitY : ux;
|
|
977
|
+
const rxUser = pixelRadius / ux;
|
|
978
|
+
const ryUser = pixelRadius / uy;
|
|
979
|
+
let best = null;
|
|
980
|
+
for (const obj of listObjects(state)) {
|
|
981
|
+
if (obj.kind !== "point3d") continue;
|
|
982
|
+
if (!obj.visible) continue;
|
|
983
|
+
const attrs = obj.attrs;
|
|
984
|
+
const world = constraintToWorld(attrs.constraint, state);
|
|
985
|
+
const proj = view.project3DTo2D?.(world[0], world[1], world[2]);
|
|
986
|
+
if (!proj) continue;
|
|
987
|
+
const dxN = (proj[1] - screen.x) / rxUser;
|
|
988
|
+
const dyN = (proj[2] - screen.y) / ryUser;
|
|
989
|
+
const d2 = dxN * dxN + dyN * dyN;
|
|
990
|
+
if (d2 <= 1 && (best === null || d2 < best.d2)) {
|
|
991
|
+
best = { id: obj.id, d2 };
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return best?.id ?? null;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// src/stamps/geometry-3d/editor/hitTest/hitTest.ts
|
|
998
|
+
var AXIS_PIXEL_THRESHOLD = 12;
|
|
999
|
+
function hitTest(screen, view, state) {
|
|
1000
|
+
const board = view?.board;
|
|
1001
|
+
const ux = typeof board?.unitX === "number" && board.unitX > 0 ? board.unitX : 1;
|
|
1002
|
+
const axisThresholdUser = AXIS_PIXEL_THRESHOLD / ux;
|
|
1003
|
+
const snap = findSnapPoint(screen, view, state);
|
|
1004
|
+
if (snap) return { kind: "existingPoint", pointId: snap };
|
|
1005
|
+
const ray = screenToRay(screen, view);
|
|
1006
|
+
let bestSphere = null;
|
|
1007
|
+
for (const obj of listObjects(state)) {
|
|
1008
|
+
if (obj.kind !== "sphere3d" || !obj.visible) continue;
|
|
1009
|
+
const attrs = obj.attrs;
|
|
1010
|
+
const centerPoint = state.objects[attrs.center];
|
|
1011
|
+
const surfacePoint = state.objects[attrs.surfacePoint];
|
|
1012
|
+
if (!centerPoint || centerPoint.kind !== "point3d") continue;
|
|
1013
|
+
if (!surfacePoint || surfacePoint.kind !== "point3d") continue;
|
|
1014
|
+
const center = constraintToWorld(centerPoint.attrs.constraint, state);
|
|
1015
|
+
const surface = constraintToWorld(surfacePoint.attrs.constraint, state);
|
|
1016
|
+
const radius = Math.hypot(
|
|
1017
|
+
surface[0] - center[0],
|
|
1018
|
+
surface[1] - center[1],
|
|
1019
|
+
surface[2] - center[2]
|
|
1020
|
+
);
|
|
1021
|
+
const sh = raySphere(ray, { center, radius });
|
|
1022
|
+
if (sh && (bestSphere === null || sh.t < bestSphere.t)) {
|
|
1023
|
+
bestSphere = { id: obj.id, t: sh.t, world: sh.point };
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
if (view.project3DTo2D) {
|
|
1027
|
+
const axes = [
|
|
1028
|
+
{ axis: "x", a: [-10, 0, 0], b: [10, 0, 0] },
|
|
1029
|
+
{ axis: "y", a: [0, -10, 0], b: [0, 10, 0] },
|
|
1030
|
+
{ axis: "z", a: [0, 0, -10], b: [0, 0, 10] }
|
|
1031
|
+
];
|
|
1032
|
+
for (const ax of axes) {
|
|
1033
|
+
const pa = view.project3DTo2D(ax.a[0], ax.a[1], ax.a[2]);
|
|
1034
|
+
const pb = view.project3DTo2D(ax.b[0], ax.b[1], ax.b[2]);
|
|
1035
|
+
const d = distScreenPointToSegment(screen, [pa[1], pa[2]], [pb[1], pb[2]]);
|
|
1036
|
+
if (d <= axisThresholdUser) {
|
|
1037
|
+
const hit = rayLineSegment(ray, { a: ax.a, b: ax.b }, 1e3);
|
|
1038
|
+
if (hit) {
|
|
1039
|
+
const t = ax.axis === "x" ? hit.point[0] : ax.axis === "y" ? hit.point[1] : hit.point[2];
|
|
1040
|
+
return { kind: "onAxis", axis: ax.axis, t, world: hit.point };
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
let bestPlane = null;
|
|
1046
|
+
for (const obj of listObjects(state)) {
|
|
1047
|
+
if (obj.kind !== "plane3d" || !obj.visible) continue;
|
|
1048
|
+
const basis = planeBasis(obj.attrs, state);
|
|
1049
|
+
if (!basis) continue;
|
|
1050
|
+
const ph = rayPlane(ray, { point: basis.origin, normal: basis.normal });
|
|
1051
|
+
if (ph && (bestPlane === null || ph.t < bestPlane.t)) {
|
|
1052
|
+
bestPlane = { id: obj.id, t: ph.t, world: ph.point, basis };
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
if (bestPlane && (!bestSphere || bestPlane.t < bestSphere.t)) {
|
|
1056
|
+
const rel = [
|
|
1057
|
+
bestPlane.world[0] - bestPlane.basis.origin[0],
|
|
1058
|
+
bestPlane.world[1] - bestPlane.basis.origin[1],
|
|
1059
|
+
bestPlane.world[2] - bestPlane.basis.origin[2]
|
|
1060
|
+
];
|
|
1061
|
+
const b1n = dot3(bestPlane.basis.basis1, bestPlane.basis.basis1);
|
|
1062
|
+
const b2n = dot3(bestPlane.basis.basis2, bestPlane.basis.basis2);
|
|
1063
|
+
const u = b1n === 0 ? 0 : dot3(rel, bestPlane.basis.basis1) / b1n;
|
|
1064
|
+
const v = b2n === 0 ? 0 : dot3(rel, bestPlane.basis.basis2) / b2n;
|
|
1065
|
+
return { kind: "onPlane", planeId: bestPlane.id, u, v, world: bestPlane.world };
|
|
1066
|
+
}
|
|
1067
|
+
if (bestSphere) {
|
|
1068
|
+
const sph = state.objects[bestSphere.id];
|
|
1069
|
+
if (sph && sph.kind === "sphere3d") {
|
|
1070
|
+
const centerPt = state.objects[sph.attrs.center];
|
|
1071
|
+
if (centerPt && centerPt.kind === "point3d") {
|
|
1072
|
+
const center = constraintToWorld(centerPt.attrs.constraint, state);
|
|
1073
|
+
const relX = bestSphere.world[0] - center[0];
|
|
1074
|
+
const relY = bestSphere.world[1] - center[1];
|
|
1075
|
+
const relZ = bestSphere.world[2] - center[2];
|
|
1076
|
+
const r = Math.hypot(relX, relY, relZ);
|
|
1077
|
+
const phi = r === 0 ? 0 : Math.acos(relZ / r);
|
|
1078
|
+
const theta = Math.atan2(relY, relX);
|
|
1079
|
+
return { kind: "onSphere", sphereId: bestSphere.id, theta, phi, world: bestSphere.world };
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
const g = rayGround(ray);
|
|
1084
|
+
if (g) return { kind: "onGround", world: g.point };
|
|
1085
|
+
return { kind: "empty" };
|
|
1086
|
+
}
|
|
1087
|
+
function distScreenPointToSegment(p, a, b) {
|
|
1088
|
+
const vx = b[0] - a[0];
|
|
1089
|
+
const vy = b[1] - a[1];
|
|
1090
|
+
const wx = p.x - a[0];
|
|
1091
|
+
const wy = p.y - a[1];
|
|
1092
|
+
const c1 = vx * wx + vy * wy;
|
|
1093
|
+
if (c1 <= 0) return Math.hypot(wx, wy);
|
|
1094
|
+
const c2 = vx * vx + vy * vy;
|
|
1095
|
+
if (c2 <= c1) return Math.hypot(p.x - b[0], p.y - b[1]);
|
|
1096
|
+
const t = c1 / c2;
|
|
1097
|
+
const px = a[0] + t * vx;
|
|
1098
|
+
const py = a[1] + t * vy;
|
|
1099
|
+
return Math.hypot(p.x - px, p.y - py);
|
|
1100
|
+
}
|
|
1101
|
+
function planeBasis(plane, state) {
|
|
1102
|
+
const p1Obj = state.objects[plane.p1];
|
|
1103
|
+
const p2Obj = state.objects[plane.p2];
|
|
1104
|
+
const p3Obj = state.objects[plane.p3];
|
|
1105
|
+
if (!p1Obj || p1Obj.kind !== "point3d") return null;
|
|
1106
|
+
if (!p2Obj || p2Obj.kind !== "point3d") return null;
|
|
1107
|
+
if (!p3Obj || p3Obj.kind !== "point3d") return null;
|
|
1108
|
+
const p1 = constraintToWorld(p1Obj.attrs.constraint, state);
|
|
1109
|
+
const p2 = constraintToWorld(p2Obj.attrs.constraint, state);
|
|
1110
|
+
const p3 = constraintToWorld(p3Obj.attrs.constraint, state);
|
|
1111
|
+
const basis1 = [p2[0] - p1[0], p2[1] - p1[1], p2[2] - p1[2]];
|
|
1112
|
+
const tmp = [p3[0] - p1[0], p3[1] - p1[1], p3[2] - p1[2]];
|
|
1113
|
+
const cx = basis1[1] * tmp[2] - basis1[2] * tmp[1];
|
|
1114
|
+
const cy = basis1[2] * tmp[0] - basis1[0] * tmp[2];
|
|
1115
|
+
const cz = basis1[0] * tmp[1] - basis1[1] * tmp[0];
|
|
1116
|
+
const cLen = Math.hypot(cx, cy, cz);
|
|
1117
|
+
if (cLen === 0) return null;
|
|
1118
|
+
const normal = [cx / cLen, cy / cLen, cz / cLen];
|
|
1119
|
+
const basis2 = [
|
|
1120
|
+
normal[1] * basis1[2] - normal[2] * basis1[1],
|
|
1121
|
+
normal[2] * basis1[0] - normal[0] * basis1[2],
|
|
1122
|
+
normal[0] * basis1[1] - normal[1] * basis1[0]
|
|
1123
|
+
];
|
|
1124
|
+
return { origin: p1, basis1, basis2, normal };
|
|
1125
|
+
}
|
|
1126
|
+
function dot3(a, b) {
|
|
1127
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
1128
|
+
}
|
|
1129
|
+
var MiniBoard3D = React3.forwardRef(
|
|
1130
|
+
function MiniBoard3D2(props, ref) {
|
|
1131
|
+
const containerRef = React3.useRef(null);
|
|
1132
|
+
const boardRef = React3.useRef(null);
|
|
1133
|
+
const viewRef = React3.useRef(null);
|
|
1134
|
+
const {
|
|
1135
|
+
isDark,
|
|
1136
|
+
onView3DReady,
|
|
1137
|
+
onPointerClick,
|
|
1138
|
+
onPointerMove,
|
|
1139
|
+
onPointerLeave,
|
|
1140
|
+
shouldStartPointDrag,
|
|
1141
|
+
onPointerDrag,
|
|
1142
|
+
onPointerDragEnd,
|
|
1143
|
+
isHoveringObject
|
|
1144
|
+
} = props;
|
|
1145
|
+
const onView3DReadyRef = React3.useRef(onView3DReady);
|
|
1146
|
+
const onPointerClickRef = React3.useRef(onPointerClick);
|
|
1147
|
+
const onPointerMoveRef = React3.useRef(onPointerMove);
|
|
1148
|
+
const onPointerLeaveRef = React3.useRef(onPointerLeave);
|
|
1149
|
+
const shouldStartPointDragRef = React3.useRef(shouldStartPointDrag);
|
|
1150
|
+
const onPointerDragRef = React3.useRef(onPointerDrag);
|
|
1151
|
+
const onPointerDragEndRef = React3.useRef(onPointerDragEnd);
|
|
1152
|
+
const isHoveringObjectRef = React3.useRef(isHoveringObject);
|
|
1153
|
+
onView3DReadyRef.current = onView3DReady;
|
|
1154
|
+
onPointerClickRef.current = onPointerClick;
|
|
1155
|
+
onPointerMoveRef.current = onPointerMove;
|
|
1156
|
+
onPointerLeaveRef.current = onPointerLeave;
|
|
1157
|
+
shouldStartPointDragRef.current = shouldStartPointDrag;
|
|
1158
|
+
onPointerDragRef.current = onPointerDrag;
|
|
1159
|
+
onPointerDragEndRef.current = onPointerDragEnd;
|
|
1160
|
+
isHoveringObjectRef.current = isHoveringObject;
|
|
1161
|
+
React3.useImperativeHandle(
|
|
1162
|
+
ref,
|
|
1163
|
+
() => ({
|
|
1164
|
+
getBoard: () => boardRef.current,
|
|
1165
|
+
getView3D: () => viewRef.current,
|
|
1166
|
+
getSvgElement: () => containerRef.current?.querySelector("svg") ?? null
|
|
1167
|
+
}),
|
|
1168
|
+
[]
|
|
1169
|
+
);
|
|
1170
|
+
React3.useEffect(() => {
|
|
1171
|
+
const div = containerRef.current;
|
|
1172
|
+
if (!div) return;
|
|
1173
|
+
let cancelled = false;
|
|
1174
|
+
let board = null;
|
|
1175
|
+
let svgEl = null;
|
|
1176
|
+
let handlePointerDown = null;
|
|
1177
|
+
let handlePointerMove = null;
|
|
1178
|
+
let handlePointerUp = null;
|
|
1179
|
+
let handlePointerLeave = null;
|
|
1180
|
+
let wheelCleanup = null;
|
|
1181
|
+
let freeBoard = null;
|
|
1182
|
+
void (async () => {
|
|
1183
|
+
let initResult;
|
|
1184
|
+
try {
|
|
1185
|
+
initResult = await initJxgBoard(div, {
|
|
1186
|
+
label: "MiniBoard.3d",
|
|
1187
|
+
defaults: { disableElementHighlight: true },
|
|
1188
|
+
boardOptions: {
|
|
1189
|
+
boundingbox: [-6, 6, 6, -6],
|
|
1190
|
+
keepaspectratio: true,
|
|
1191
|
+
axis: false,
|
|
1192
|
+
showCopyright: false,
|
|
1193
|
+
showNavigation: false,
|
|
1194
|
+
renderer: "svg",
|
|
1195
|
+
// Wheel zoom được tự xử lý bằng Ctrl/Cmd + wheel ở dưới (Excalidraw-style).
|
|
1196
|
+
zoom: { wheel: false }
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
} catch {
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
if (cancelled || !containerRef.current) {
|
|
1203
|
+
initResult.cleanup();
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
board = initResult.board;
|
|
1207
|
+
freeBoard = initResult.cleanup;
|
|
1208
|
+
if (cancelled || !board) return;
|
|
1209
|
+
boardRef.current = board;
|
|
1210
|
+
wheelCleanup = attachJxgWheelZoom(div, board, "MiniBoard.3d");
|
|
1211
|
+
let view = null;
|
|
1212
|
+
try {
|
|
1213
|
+
const baseAttrs = VIEW3D_ATTRS(isDark);
|
|
1214
|
+
view = board.create(
|
|
1215
|
+
"view3d",
|
|
1216
|
+
[
|
|
1217
|
+
[-5, -5],
|
|
1218
|
+
[10, 10],
|
|
1219
|
+
[
|
|
1220
|
+
[DEFAULT_VIEW3D.bbox3D[0], DEFAULT_VIEW3D.bbox3D[3]],
|
|
1221
|
+
[DEFAULT_VIEW3D.bbox3D[1], DEFAULT_VIEW3D.bbox3D[4]],
|
|
1222
|
+
[DEFAULT_VIEW3D.bbox3D[2], DEFAULT_VIEW3D.bbox3D[5]]
|
|
1223
|
+
]
|
|
1224
|
+
],
|
|
1225
|
+
{
|
|
1226
|
+
...baseAttrs,
|
|
1227
|
+
// JSXGraph view3d đọc giá trị khởi tạo từ az.slider.start (không
|
|
1228
|
+
// phải az.value). Pass nhầm `value` → JSXGraph dùng default
|
|
1229
|
+
// 1.0/0.3, khiến DEFAULT_VIEW3D bị bỏ qua.
|
|
1230
|
+
az: { ...baseAttrs.az, slider: { ...baseAttrs.az.slider, start: DEFAULT_VIEW3D.azimuth } },
|
|
1231
|
+
el: { ...baseAttrs.el, slider: { ...baseAttrs.el.slider, start: DEFAULT_VIEW3D.elevation } }
|
|
1232
|
+
}
|
|
1233
|
+
);
|
|
1234
|
+
} catch {
|
|
1235
|
+
}
|
|
1236
|
+
viewRef.current = view;
|
|
1237
|
+
if (view) {
|
|
1238
|
+
try {
|
|
1239
|
+
view.create(
|
|
1240
|
+
"plane3d",
|
|
1241
|
+
[
|
|
1242
|
+
[0, 0, 0],
|
|
1243
|
+
[1, 0, 0],
|
|
1244
|
+
[0, 1, 0],
|
|
1245
|
+
GROUND_PLANE_RANGE,
|
|
1246
|
+
GROUND_PLANE_RANGE
|
|
1247
|
+
],
|
|
1248
|
+
GROUND_PLANE_ATTRS(isDark)
|
|
1249
|
+
);
|
|
1250
|
+
} catch {
|
|
1251
|
+
}
|
|
1252
|
+
onView3DReadyRef.current?.(view, board);
|
|
1253
|
+
}
|
|
1254
|
+
svgEl = containerRef.current?.querySelector("svg") ?? null;
|
|
1255
|
+
if (svgEl) {
|
|
1256
|
+
const p2 = paletteFor(isDark);
|
|
1257
|
+
svgEl.style.background = p2.view3dBg;
|
|
1258
|
+
const pixelToUser = (e) => {
|
|
1259
|
+
const rect = svgEl.getBoundingClientRect();
|
|
1260
|
+
const px = e.clientX - rect.left;
|
|
1261
|
+
const py = e.clientY - rect.top;
|
|
1262
|
+
const b = board;
|
|
1263
|
+
if (!b || !b.origin || !b.origin.scrCoords) {
|
|
1264
|
+
return { x: px, y: py };
|
|
1265
|
+
}
|
|
1266
|
+
const ox = b.origin.scrCoords[1];
|
|
1267
|
+
const oy = b.origin.scrCoords[2];
|
|
1268
|
+
const ux = b.unitX || 1;
|
|
1269
|
+
const uy = b.unitY || 1;
|
|
1270
|
+
return { x: (px - ox) / ux, y: (oy - py) / uy };
|
|
1271
|
+
};
|
|
1272
|
+
const DRAG_THRESHOLD = 4;
|
|
1273
|
+
const AZ_PER_PX = 0.01;
|
|
1274
|
+
const EL_PER_PX = 0.01;
|
|
1275
|
+
const EL_LIMIT = Math.PI / 2 - 0.05;
|
|
1276
|
+
let dragStart = null;
|
|
1277
|
+
let dragging = false;
|
|
1278
|
+
let pointDragMode = false;
|
|
1279
|
+
let startAz = 0;
|
|
1280
|
+
let startEl = 0;
|
|
1281
|
+
const readAng = (s) => {
|
|
1282
|
+
if (!s) return 0;
|
|
1283
|
+
if (typeof s.Value === "function") {
|
|
1284
|
+
try {
|
|
1285
|
+
return s.Value();
|
|
1286
|
+
} catch {
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
return typeof s.value === "number" ? s.value : 0;
|
|
1290
|
+
};
|
|
1291
|
+
const setAng = (s, v) => {
|
|
1292
|
+
if (!s) return;
|
|
1293
|
+
if (typeof s.setValue === "function") {
|
|
1294
|
+
try {
|
|
1295
|
+
s.setValue(v);
|
|
1296
|
+
return;
|
|
1297
|
+
} catch {
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
s.value = v;
|
|
1301
|
+
};
|
|
1302
|
+
handlePointerDown = (e) => {
|
|
1303
|
+
if (!svgEl) return;
|
|
1304
|
+
dragStart = { x: e.clientX, y: e.clientY };
|
|
1305
|
+
dragging = false;
|
|
1306
|
+
pointDragMode = false;
|
|
1307
|
+
const screen = pixelToUser(e);
|
|
1308
|
+
try {
|
|
1309
|
+
pointDragMode = shouldStartPointDragRef.current?.(screen) ?? false;
|
|
1310
|
+
} catch {
|
|
1311
|
+
pointDragMode = false;
|
|
1312
|
+
}
|
|
1313
|
+
if (!pointDragMode) {
|
|
1314
|
+
const v = viewRef.current;
|
|
1315
|
+
startAz = readAng(v?.az_slide ?? v?.az);
|
|
1316
|
+
startEl = readAng(v?.el_slide ?? v?.el);
|
|
1317
|
+
}
|
|
1318
|
+
try {
|
|
1319
|
+
svgEl.setPointerCapture?.(e.pointerId);
|
|
1320
|
+
} catch {
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
handlePointerMove = (e) => {
|
|
1324
|
+
if (!svgEl) return;
|
|
1325
|
+
if (dragStart) {
|
|
1326
|
+
const dx = e.clientX - dragStart.x;
|
|
1327
|
+
const dy = e.clientY - dragStart.y;
|
|
1328
|
+
if (!dragging && Math.hypot(dx, dy) > DRAG_THRESHOLD) dragging = true;
|
|
1329
|
+
if (dragging) {
|
|
1330
|
+
if (pointDragMode) {
|
|
1331
|
+
onPointerDragRef.current?.(pixelToUser(e));
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
const v = viewRef.current;
|
|
1335
|
+
const newAz = startAz + dx * AZ_PER_PX;
|
|
1336
|
+
let newEl = startEl - dy * EL_PER_PX;
|
|
1337
|
+
if (newEl > EL_LIMIT) newEl = EL_LIMIT;
|
|
1338
|
+
if (newEl < -EL_LIMIT) newEl = -EL_LIMIT;
|
|
1339
|
+
setAng(v?.az_slide ?? v?.az, newAz);
|
|
1340
|
+
setAng(v?.el_slide ?? v?.el, newEl);
|
|
1341
|
+
try {
|
|
1342
|
+
v?.board?.update?.();
|
|
1343
|
+
} catch {
|
|
1344
|
+
}
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
const screen = pixelToUser(e);
|
|
1349
|
+
onPointerMoveRef.current?.(screen);
|
|
1350
|
+
const hovering = isHoveringObjectRef.current?.(screen) ?? false;
|
|
1351
|
+
svgEl.style.cursor = hovering ? "pointer" : "";
|
|
1352
|
+
};
|
|
1353
|
+
handlePointerUp = (e) => {
|
|
1354
|
+
if (!svgEl) return;
|
|
1355
|
+
const wasDrag = dragging;
|
|
1356
|
+
const hadDown = dragStart !== null;
|
|
1357
|
+
const wasPointDrag = pointDragMode;
|
|
1358
|
+
dragStart = null;
|
|
1359
|
+
dragging = false;
|
|
1360
|
+
pointDragMode = false;
|
|
1361
|
+
try {
|
|
1362
|
+
svgEl.releasePointerCapture?.(e.pointerId);
|
|
1363
|
+
} catch {
|
|
1364
|
+
}
|
|
1365
|
+
if (hadDown && wasPointDrag) {
|
|
1366
|
+
onPointerDragEndRef.current?.(pixelToUser(e));
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
if (hadDown && !wasDrag) {
|
|
1370
|
+
onPointerClickRef.current?.(pixelToUser(e));
|
|
1371
|
+
}
|
|
1372
|
+
};
|
|
1373
|
+
handlePointerLeave = () => {
|
|
1374
|
+
if (pointDragMode) {
|
|
1375
|
+
try {
|
|
1376
|
+
onPointerDragEndRef.current?.({ x: 0, y: 0 });
|
|
1377
|
+
} catch {
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
dragStart = null;
|
|
1381
|
+
dragging = false;
|
|
1382
|
+
pointDragMode = false;
|
|
1383
|
+
onPointerLeaveRef.current?.();
|
|
1384
|
+
};
|
|
1385
|
+
svgEl.addEventListener("pointerdown", handlePointerDown);
|
|
1386
|
+
svgEl.addEventListener("pointermove", handlePointerMove);
|
|
1387
|
+
svgEl.addEventListener("pointerup", handlePointerUp);
|
|
1388
|
+
svgEl.addEventListener("pointercancel", handlePointerUp);
|
|
1389
|
+
svgEl.addEventListener("pointerleave", handlePointerLeave);
|
|
1390
|
+
}
|
|
1391
|
+
})();
|
|
1392
|
+
return () => {
|
|
1393
|
+
cancelled = true;
|
|
1394
|
+
if (wheelCleanup) {
|
|
1395
|
+
wheelCleanup();
|
|
1396
|
+
wheelCleanup = null;
|
|
1397
|
+
}
|
|
1398
|
+
if (svgEl) {
|
|
1399
|
+
if (handlePointerDown) svgEl.removeEventListener("pointerdown", handlePointerDown);
|
|
1400
|
+
if (handlePointerMove) svgEl.removeEventListener("pointermove", handlePointerMove);
|
|
1401
|
+
if (handlePointerUp) {
|
|
1402
|
+
svgEl.removeEventListener("pointerup", handlePointerUp);
|
|
1403
|
+
svgEl.removeEventListener("pointercancel", handlePointerUp);
|
|
1404
|
+
}
|
|
1405
|
+
if (handlePointerLeave) svgEl.removeEventListener("pointerleave", handlePointerLeave);
|
|
1406
|
+
}
|
|
1407
|
+
if (freeBoard) {
|
|
1408
|
+
freeBoard();
|
|
1409
|
+
freeBoard = null;
|
|
1410
|
+
}
|
|
1411
|
+
boardRef.current = null;
|
|
1412
|
+
viewRef.current = null;
|
|
1413
|
+
};
|
|
1414
|
+
}, [isDark]);
|
|
1415
|
+
const p = paletteFor(isDark);
|
|
1416
|
+
return /* @__PURE__ */ jsx(
|
|
1417
|
+
"div",
|
|
1418
|
+
{
|
|
1419
|
+
"data-testid": "mini-board-3d",
|
|
1420
|
+
ref: containerRef,
|
|
1421
|
+
style: {
|
|
1422
|
+
width: "100%",
|
|
1423
|
+
height: "100%",
|
|
1424
|
+
minHeight: 400,
|
|
1425
|
+
background: p.view3dBg,
|
|
1426
|
+
position: "relative",
|
|
1427
|
+
// Clip JSXGraph mesh3d paths projecting outside the container.
|
|
1428
|
+
overflow: "hidden"
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
);
|
|
1432
|
+
}
|
|
1433
|
+
);
|
|
1434
|
+
|
|
1435
|
+
// src/stamps/geometry-3d/editor/preview3d.ts
|
|
1436
|
+
var PREVIEW_STYLE = {
|
|
1437
|
+
strokeColor: "#3b82f6",
|
|
1438
|
+
strokeWidth: 1.5,
|
|
1439
|
+
strokeOpacity: 0.7,
|
|
1440
|
+
dash: 2,
|
|
1441
|
+
fixed: true,
|
|
1442
|
+
highlight: false,
|
|
1443
|
+
withLabel: false
|
|
1444
|
+
};
|
|
1445
|
+
var Preview3DManager = class {
|
|
1446
|
+
constructor(view, store) {
|
|
1447
|
+
this.phantom = null;
|
|
1448
|
+
this.pickPts = [];
|
|
1449
|
+
this.shapes = [];
|
|
1450
|
+
this.disposed = false;
|
|
1451
|
+
this.view = view;
|
|
1452
|
+
this.store = store;
|
|
1453
|
+
}
|
|
1454
|
+
clear() {
|
|
1455
|
+
const v = this.view;
|
|
1456
|
+
for (const s of this.shapes) {
|
|
1457
|
+
try {
|
|
1458
|
+
v.removeObject?.(s);
|
|
1459
|
+
} catch {
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
this.shapes = [];
|
|
1463
|
+
for (const p of this.pickPts) {
|
|
1464
|
+
try {
|
|
1465
|
+
v.removeObject?.(p);
|
|
1466
|
+
} catch {
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
this.pickPts = [];
|
|
1470
|
+
if (this.phantom) {
|
|
1471
|
+
try {
|
|
1472
|
+
v.removeObject?.(this.phantom);
|
|
1473
|
+
} catch {
|
|
1474
|
+
}
|
|
1475
|
+
this.phantom = null;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
dispose() {
|
|
1479
|
+
if (this.disposed) return;
|
|
1480
|
+
this.clear();
|
|
1481
|
+
this.disposed = true;
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Rebuild the preview from scratch using the controller's current `tool` +
|
|
1485
|
+
* `collected` args + the live `hoverHit` from the most recent pointer move.
|
|
1486
|
+
*
|
|
1487
|
+
* Returns early (and clears) when there's nothing to preview:
|
|
1488
|
+
* - no tool selected
|
|
1489
|
+
* - no points collected yet
|
|
1490
|
+
* - hover is empty / off-surface
|
|
1491
|
+
* - any collected arg lacks a hit (number steps)
|
|
1492
|
+
*/
|
|
1493
|
+
update(tool, collected, hoverHit) {
|
|
1494
|
+
if (this.disposed) return;
|
|
1495
|
+
this.clear();
|
|
1496
|
+
if (!tool || tool === "move") return;
|
|
1497
|
+
if (collected.length === 0) return;
|
|
1498
|
+
if (hoverHit.kind === "empty") return;
|
|
1499
|
+
const phantomCoords = this.hitToCoords(hoverHit);
|
|
1500
|
+
if (!phantomCoords) return;
|
|
1501
|
+
const pickCoords = [];
|
|
1502
|
+
for (const c of collected) {
|
|
1503
|
+
if (!c.hit) return;
|
|
1504
|
+
const coords = this.hitToCoords(c.hit);
|
|
1505
|
+
if (!coords) return;
|
|
1506
|
+
pickCoords.push(coords);
|
|
1507
|
+
}
|
|
1508
|
+
try {
|
|
1509
|
+
this.phantom = this.view.create("point3d", phantomCoords, {
|
|
1510
|
+
visible: false,
|
|
1511
|
+
fixed: true,
|
|
1512
|
+
withLabel: false,
|
|
1513
|
+
name: ""
|
|
1514
|
+
});
|
|
1515
|
+
} catch {
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
for (const coords of pickCoords) {
|
|
1519
|
+
try {
|
|
1520
|
+
const p = this.view.create("point3d", coords, {
|
|
1521
|
+
visible: false,
|
|
1522
|
+
fixed: true,
|
|
1523
|
+
withLabel: false,
|
|
1524
|
+
name: ""
|
|
1525
|
+
});
|
|
1526
|
+
this.pickPts.push(p);
|
|
1527
|
+
} catch {
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
this.buildShape(tool);
|
|
1532
|
+
}
|
|
1533
|
+
buildShape(tool) {
|
|
1534
|
+
const phantom = this.phantom;
|
|
1535
|
+
const picks = this.pickPts;
|
|
1536
|
+
if (!phantom || picks.length === 0) return;
|
|
1537
|
+
const last = picks[picks.length - 1];
|
|
1538
|
+
try {
|
|
1539
|
+
switch (tool) {
|
|
1540
|
+
case "segment":
|
|
1541
|
+
this.shapes.push(this.makeLine3d(picks[0], phantom, false, false));
|
|
1542
|
+
return;
|
|
1543
|
+
case "line":
|
|
1544
|
+
this.shapes.push(this.makeLine3d(picks[0], phantom, true, true));
|
|
1545
|
+
return;
|
|
1546
|
+
case "ray":
|
|
1547
|
+
this.shapes.push(this.makeLine3d(picks[0], phantom, false, true));
|
|
1548
|
+
return;
|
|
1549
|
+
case "vector":
|
|
1550
|
+
this.shapes.push(this.view.create("line3d", [picks[0], phantom], {
|
|
1551
|
+
...PREVIEW_STYLE,
|
|
1552
|
+
straightFirst: false,
|
|
1553
|
+
straightLast: false,
|
|
1554
|
+
lastArrow: { type: 1 }
|
|
1555
|
+
}));
|
|
1556
|
+
return;
|
|
1557
|
+
case "polygon":
|
|
1558
|
+
case "pyramid":
|
|
1559
|
+
case "prism": {
|
|
1560
|
+
for (let i = 0; i < picks.length - 1; i += 1) {
|
|
1561
|
+
this.shapes.push(this.makeLine3d(picks[i], picks[i + 1], false, false));
|
|
1562
|
+
}
|
|
1563
|
+
this.shapes.push(this.makeLine3d(last, phantom, false, false));
|
|
1564
|
+
if (picks.length >= 2) {
|
|
1565
|
+
this.shapes.push(this.makeLine3d(picks[0], phantom, false, false));
|
|
1566
|
+
}
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
case "plane": {
|
|
1570
|
+
if (picks.length === 1) {
|
|
1571
|
+
this.shapes.push(this.makeLine3d(picks[0], phantom, false, false));
|
|
1572
|
+
} else if (picks.length === 2) {
|
|
1573
|
+
this.shapes.push(this.makeLine3d(picks[0], picks[1], false, false));
|
|
1574
|
+
this.shapes.push(this.makeLine3d(picks[1], phantom, false, false));
|
|
1575
|
+
this.shapes.push(this.makeLine3d(picks[0], phantom, false, false));
|
|
1576
|
+
}
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
case "sphere": {
|
|
1580
|
+
this.shapes.push(this.view.create("sphere3d", [picks[0], phantom], {
|
|
1581
|
+
...PREVIEW_STYLE,
|
|
1582
|
+
fillColor: "none",
|
|
1583
|
+
fillOpacity: 0
|
|
1584
|
+
}));
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
case "tetrahedron":
|
|
1588
|
+
case "cube":
|
|
1589
|
+
case "cylinder":
|
|
1590
|
+
case "cone":
|
|
1591
|
+
this.shapes.push(this.makeLine3d(picks[0], phantom, false, false));
|
|
1592
|
+
return;
|
|
1593
|
+
default:
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
} catch {
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
makeLine3d(a, b, straightFirst, straightLast) {
|
|
1600
|
+
return this.view.create("line3d", [a, b], {
|
|
1601
|
+
...PREVIEW_STYLE,
|
|
1602
|
+
straightFirst,
|
|
1603
|
+
straightLast
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
hitToCoords(hit) {
|
|
1607
|
+
if (hit.kind === "existingPoint") {
|
|
1608
|
+
const obj = this.store.getState().objects[hit.pointId];
|
|
1609
|
+
if (!obj || obj.kind !== "point3d") return null;
|
|
1610
|
+
return constraintToWorld(obj.attrs.constraint, this.store.getState());
|
|
1611
|
+
}
|
|
1612
|
+
if ("world" in hit) return hit.world;
|
|
1613
|
+
return null;
|
|
1614
|
+
}
|
|
1615
|
+
};
|
|
1616
|
+
function StatusHint(props) {
|
|
1617
|
+
const { hint, hoverLabel } = props;
|
|
1618
|
+
return /* @__PURE__ */ jsxs(
|
|
1619
|
+
"div",
|
|
1620
|
+
{
|
|
1621
|
+
"data-testid": "status-hint",
|
|
1622
|
+
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",
|
|
1623
|
+
children: [
|
|
1624
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
1625
|
+
"\u{1F4D0} ",
|
|
1626
|
+
hint || "Ch\u1ECDn c\xF4ng c\u1EE5 trong b\u1EA3ng b\xEAn tr\xE1i"
|
|
1627
|
+
] }),
|
|
1628
|
+
hoverLabel ? /* @__PURE__ */ jsxs("span", { className: "ml-3 text-zinc-500", children: [
|
|
1629
|
+
"\u2014 \u0111ang tr\xEAn: ",
|
|
1630
|
+
hoverLabel
|
|
1631
|
+
] }) : null
|
|
1632
|
+
]
|
|
1633
|
+
}
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
function usePointDrag(opts) {
|
|
1637
|
+
const { store, boardRef, selectedToolRef } = opts;
|
|
1638
|
+
const draggedPointRef = React3.useRef(null);
|
|
1639
|
+
const dragStartRef = React3.useRef(null);
|
|
1640
|
+
const dragSnapshotRef = React3.useRef(null);
|
|
1641
|
+
const dragMutatedRef = React3.useRef(false);
|
|
1642
|
+
const shouldStartPointDrag = React3.useCallback(
|
|
1643
|
+
(screen) => {
|
|
1644
|
+
const view = boardRef.current?.getView3D();
|
|
1645
|
+
if (!view) return false;
|
|
1646
|
+
const tool = selectedToolRef.current;
|
|
1647
|
+
if (tool !== "point" && tool !== "move") return false;
|
|
1648
|
+
let hit;
|
|
1649
|
+
try {
|
|
1650
|
+
hit = hitTest(screen, view, store.getState());
|
|
1651
|
+
} catch {
|
|
1652
|
+
return false;
|
|
1653
|
+
}
|
|
1654
|
+
if (hit.kind === "existingPoint") {
|
|
1655
|
+
const pt = store.getState().objects[hit.pointId];
|
|
1656
|
+
if (!pt || pt.kind !== "point3d") return false;
|
|
1657
|
+
dragSnapshotRef.current = store.getState();
|
|
1658
|
+
dragMutatedRef.current = false;
|
|
1659
|
+
draggedPointRef.current = hit.pointId;
|
|
1660
|
+
dragStartRef.current = {
|
|
1661
|
+
screen,
|
|
1662
|
+
world: constraintToWorld(pt.attrs.constraint, store.getState())
|
|
1663
|
+
};
|
|
1664
|
+
return true;
|
|
1665
|
+
}
|
|
1666
|
+
if (tool === "point" && (hit.kind === "onGround" || hit.kind === "onAxis")) {
|
|
1667
|
+
dragSnapshotRef.current = store.getState();
|
|
1668
|
+
dragMutatedRef.current = false;
|
|
1669
|
+
const constraint = hitToConstraint(hit);
|
|
1670
|
+
if (!constraint) {
|
|
1671
|
+
dragSnapshotRef.current = null;
|
|
1672
|
+
return false;
|
|
1673
|
+
}
|
|
1674
|
+
let id = null;
|
|
1675
|
+
store.withoutHistory(() => {
|
|
1676
|
+
const stateBefore = store.getState();
|
|
1677
|
+
const newId = `p${stateBefore.counter + 1}`;
|
|
1678
|
+
const label = nextLabel(stateBefore, "point3d");
|
|
1679
|
+
store.dispatch({
|
|
1680
|
+
type: "ADD",
|
|
1681
|
+
payload: {
|
|
1682
|
+
obj: {
|
|
1683
|
+
id: newId,
|
|
1684
|
+
kind: "point3d",
|
|
1685
|
+
label,
|
|
1686
|
+
visible: true,
|
|
1687
|
+
locked: false,
|
|
1688
|
+
layer: "default",
|
|
1689
|
+
schemaVersion: 1,
|
|
1690
|
+
attrs: { constraint }
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
});
|
|
1694
|
+
id = newId;
|
|
1695
|
+
});
|
|
1696
|
+
if (!id) {
|
|
1697
|
+
dragSnapshotRef.current = null;
|
|
1698
|
+
return false;
|
|
1699
|
+
}
|
|
1700
|
+
draggedPointRef.current = id;
|
|
1701
|
+
dragStartRef.current = {
|
|
1702
|
+
screen,
|
|
1703
|
+
world: [hit.world[0], hit.world[1], hit.world[2]]
|
|
1704
|
+
};
|
|
1705
|
+
return true;
|
|
1706
|
+
}
|
|
1707
|
+
if (tool === "point") {
|
|
1708
|
+
dragSnapshotRef.current = null;
|
|
1709
|
+
draggedPointRef.current = null;
|
|
1710
|
+
dragStartRef.current = null;
|
|
1711
|
+
return true;
|
|
1712
|
+
}
|
|
1713
|
+
return false;
|
|
1714
|
+
},
|
|
1715
|
+
[store, boardRef, selectedToolRef]
|
|
1716
|
+
);
|
|
1717
|
+
const onPointerDrag = React3.useCallback(
|
|
1718
|
+
(screen) => {
|
|
1719
|
+
const pointId = draggedPointRef.current;
|
|
1720
|
+
const start = dragStartRef.current;
|
|
1721
|
+
if (!pointId || !start) return;
|
|
1722
|
+
const view = boardRef.current?.getView3D();
|
|
1723
|
+
if (!view) return;
|
|
1724
|
+
const tool = selectedToolRef.current;
|
|
1725
|
+
let nextWorld;
|
|
1726
|
+
if (tool === "point") {
|
|
1727
|
+
const dz = screen.y - start.screen.y;
|
|
1728
|
+
nextWorld = [start.world[0], start.world[1], start.world[2] + dz];
|
|
1729
|
+
} else if (tool === "move") {
|
|
1730
|
+
try {
|
|
1731
|
+
const ray = screenToRay(screen, view);
|
|
1732
|
+
const hit = rayPlane(ray, { point: [0, 0, start.world[2]], normal: [0, 0, 1] });
|
|
1733
|
+
if (!hit) return;
|
|
1734
|
+
nextWorld = [hit.point[0], hit.point[1], start.world[2]];
|
|
1735
|
+
} catch {
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
} else {
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
const obj = store.getState().objects[pointId];
|
|
1742
|
+
if (!obj || obj.kind !== "point3d") return;
|
|
1743
|
+
const free = { kind: "free", x: nextWorld[0], y: nextWorld[1], z: nextWorld[2] };
|
|
1744
|
+
store.withoutHistory(() => {
|
|
1745
|
+
store.dispatch({ type: "UPDATE_ATTRS", payload: { id: pointId, patch: { constraint: free } } });
|
|
1746
|
+
});
|
|
1747
|
+
dragMutatedRef.current = true;
|
|
1748
|
+
},
|
|
1749
|
+
[store, boardRef, selectedToolRef]
|
|
1750
|
+
);
|
|
1751
|
+
const onPointerDragEnd = React3.useCallback(() => {
|
|
1752
|
+
const snap = dragSnapshotRef.current;
|
|
1753
|
+
dragSnapshotRef.current = null;
|
|
1754
|
+
draggedPointRef.current = null;
|
|
1755
|
+
dragStartRef.current = null;
|
|
1756
|
+
dragMutatedRef.current = false;
|
|
1757
|
+
if (snap) {
|
|
1758
|
+
const current = store.getState();
|
|
1759
|
+
store.withoutHistory(() => {
|
|
1760
|
+
store.dispatch({ type: "LOAD", payload: { state: snap } });
|
|
1761
|
+
});
|
|
1762
|
+
store.dispatch({ type: "LOAD", payload: { state: current } });
|
|
1763
|
+
}
|
|
1764
|
+
}, [store]);
|
|
1765
|
+
const isDragging = React3.useCallback(() => draggedPointRef.current !== null, []);
|
|
1766
|
+
return { shouldStartPointDrag, onPointerDrag, onPointerDragEnd, isDragging };
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// src/stamps/geometry-3d/editor/editorHelpers.ts
|
|
1770
|
+
function hitToHoverLabel(hit, state) {
|
|
1771
|
+
if (hit.kind === "empty") return null;
|
|
1772
|
+
if (hit.kind === "existingPoint") return state.objects[hit.pointId]?.label ?? null;
|
|
1773
|
+
if (hit.kind === "onGround") return "m\u1EB7t n\u1EC1n";
|
|
1774
|
+
if (hit.kind === "onAxis") return `tr\u1EE5c ${hit.axis.toUpperCase()}`;
|
|
1775
|
+
if (hit.kind === "onPlane") return `m\u1EB7t ph\u1EB3ng ${hit.planeId}`;
|
|
1776
|
+
if (hit.kind === "onSphere") return `m\u1EB7t c\u1EA7u ${hit.sphereId}`;
|
|
1777
|
+
return null;
|
|
1778
|
+
}
|
|
1779
|
+
function getView3DInfo(view) {
|
|
1780
|
+
const v = view;
|
|
1781
|
+
const azSlider = v?.az_slide ?? v?.az;
|
|
1782
|
+
const elSlider = v?.el_slide ?? v?.el;
|
|
1783
|
+
const azimuth = typeof azSlider?.Value === "function" ? azSlider.Value() : 0;
|
|
1784
|
+
const elevation = typeof elSlider?.Value === "function" ? elSlider.Value() : 0;
|
|
1785
|
+
return {
|
|
1786
|
+
azimuth,
|
|
1787
|
+
elevation,
|
|
1788
|
+
bbox3D: [...DEFAULT_VIEW3D.bbox3D]
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
var EditorPanelInner = React3.forwardRef(
|
|
1792
|
+
function EditorPanel(props, ref) {
|
|
1793
|
+
const {
|
|
1794
|
+
isDark: isDarkProp,
|
|
1795
|
+
onInsert,
|
|
1796
|
+
onClose,
|
|
1797
|
+
store,
|
|
1798
|
+
selectedTool,
|
|
1799
|
+
onSelectedToolChange,
|
|
1800
|
+
showAxis,
|
|
1801
|
+
showGrid,
|
|
1802
|
+
onHistoryChange,
|
|
1803
|
+
isMobile = false,
|
|
1804
|
+
onOpenDrawer,
|
|
1805
|
+
withLeftPanel = false
|
|
1806
|
+
} = props;
|
|
1807
|
+
const isDark = isDarkProp ?? false;
|
|
1808
|
+
const controllerRef = React3.useRef(null);
|
|
1809
|
+
if (!controllerRef.current) controllerRef.current = new ToolController(store);
|
|
1810
|
+
const [hint, setHint] = React3.useState("Ch\u1ECDn c\xF4ng c\u1EE5 trong b\u1EA3ng b\xEAn tr\xE1i");
|
|
1811
|
+
const [hoverLabel, setHoverLabel] = React3.useState(null);
|
|
1812
|
+
const [ready, setReady] = React3.useState(false);
|
|
1813
|
+
const [hasContent, setHasContent] = React3.useState(false);
|
|
1814
|
+
const boardRef = React3.useRef(null);
|
|
1815
|
+
const rendererRef = React3.useRef(null);
|
|
1816
|
+
const previewRef = React3.useRef(null);
|
|
1817
|
+
const lastHoverHitRef = React3.useRef({ kind: "empty" });
|
|
1818
|
+
const onSelectedToolChangeRef = React3.useRef(onSelectedToolChange);
|
|
1819
|
+
onSelectedToolChangeRef.current = onSelectedToolChange;
|
|
1820
|
+
const selectedToolRef = React3.useRef(selectedTool);
|
|
1821
|
+
selectedToolRef.current = selectedTool;
|
|
1822
|
+
const onInsertRef = React3.useRef(onInsert);
|
|
1823
|
+
onInsertRef.current = onInsert;
|
|
1824
|
+
useEditorState({ store, onHistoryChange });
|
|
1825
|
+
const { shouldStartPointDrag, onPointerDrag, onPointerDragEnd, isDragging } = usePointDrag({
|
|
1826
|
+
store,
|
|
1827
|
+
boardRef,
|
|
1828
|
+
selectedToolRef
|
|
1829
|
+
});
|
|
1830
|
+
React3.useEffect(() => {
|
|
1831
|
+
const ctrl = controllerRef.current;
|
|
1832
|
+
return ctrl.on((state) => {
|
|
1833
|
+
setHint(state.hint);
|
|
1834
|
+
onSelectedToolChangeRef.current(state.tool?.key ?? "move");
|
|
1835
|
+
});
|
|
1836
|
+
}, []);
|
|
1837
|
+
React3.useEffect(() => {
|
|
1838
|
+
controllerRef.current?.selectTool(selectedTool);
|
|
1839
|
+
}, [selectedTool]);
|
|
1840
|
+
React3.useEffect(() => {
|
|
1841
|
+
return () => {
|
|
1842
|
+
rendererRef.current?.dispose();
|
|
1843
|
+
rendererRef.current = null;
|
|
1844
|
+
previewRef.current?.dispose();
|
|
1845
|
+
previewRef.current = null;
|
|
1846
|
+
};
|
|
1847
|
+
}, []);
|
|
1848
|
+
React3.useEffect(() => {
|
|
1849
|
+
const sync = () => setHasContent(Object.keys(store.getState().objects).length > 0);
|
|
1850
|
+
sync();
|
|
1851
|
+
return store.subscribe(sync);
|
|
1852
|
+
}, [store]);
|
|
1853
|
+
React3.useEffect(() => {
|
|
1854
|
+
const controller = controllerRef.current;
|
|
1855
|
+
if (!controller) return;
|
|
1856
|
+
return controller.on((state) => {
|
|
1857
|
+
if (!previewRef.current) return;
|
|
1858
|
+
if (state.collected.length === 0) previewRef.current.clear();
|
|
1859
|
+
else previewRef.current.update(state.tool?.key ?? null, state.collected, lastHoverHitRef.current);
|
|
1860
|
+
});
|
|
1861
|
+
}, []);
|
|
1862
|
+
React3.useEffect(() => {
|
|
1863
|
+
const view = boardRef.current?.getView3D();
|
|
1864
|
+
const v = view;
|
|
1865
|
+
if (!v || typeof v.setAttribute !== "function") return;
|
|
1866
|
+
try {
|
|
1867
|
+
v.setAttribute({
|
|
1868
|
+
xAxis: { visible: showAxis },
|
|
1869
|
+
yAxis: { visible: showAxis },
|
|
1870
|
+
zAxis: { visible: showAxis },
|
|
1871
|
+
xPlaneRear: { visible: false, mesh3d: { visible: false } },
|
|
1872
|
+
yPlaneRear: { visible: false, mesh3d: { visible: false } },
|
|
1873
|
+
zPlaneRear: { visible: showGrid, mesh3d: { visible: false } }
|
|
1874
|
+
});
|
|
1875
|
+
v.board?.update?.();
|
|
1876
|
+
} catch {
|
|
1877
|
+
}
|
|
1878
|
+
}, [showAxis, showGrid]);
|
|
1879
|
+
const handleView3DReady = React3.useCallback((view) => {
|
|
1880
|
+
rendererRef.current = new JxgRenderer3D(store, view);
|
|
1881
|
+
previewRef.current = new Preview3DManager(view, store);
|
|
1882
|
+
const meta = store.getState().meta;
|
|
1883
|
+
const savedView = meta.domain === "3d" ? meta.view : null;
|
|
1884
|
+
if (savedView) {
|
|
1885
|
+
try {
|
|
1886
|
+
const v = view;
|
|
1887
|
+
v?.az_slide?.setValue?.(savedView.azimuth);
|
|
1888
|
+
v?.el_slide?.setValue?.(savedView.elevation);
|
|
1889
|
+
v?.board?.update?.();
|
|
1890
|
+
} catch {
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
setReady(true);
|
|
1894
|
+
}, [store]);
|
|
1895
|
+
const handleClick = React3.useCallback((screen) => {
|
|
1896
|
+
const view = boardRef.current?.getView3D();
|
|
1897
|
+
if (!view) return;
|
|
1898
|
+
try {
|
|
1899
|
+
const hit = hitTest(screen, view, store.getState());
|
|
1900
|
+
controllerRef.current.consumeHit(hit);
|
|
1901
|
+
} catch {
|
|
1902
|
+
}
|
|
1903
|
+
}, [store]);
|
|
1904
|
+
const handleMove = React3.useCallback((screen) => {
|
|
1905
|
+
const view = boardRef.current?.getView3D();
|
|
1906
|
+
if (!view) return;
|
|
1907
|
+
if (isDragging()) return;
|
|
1908
|
+
let hit;
|
|
1909
|
+
try {
|
|
1910
|
+
hit = hitTest(screen, view, store.getState());
|
|
1911
|
+
} catch {
|
|
1912
|
+
setHoverLabel(null);
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
lastHoverHitRef.current = hit;
|
|
1916
|
+
const ctrl = controllerRef.current;
|
|
1917
|
+
if (previewRef.current && ctrl) {
|
|
1918
|
+
const cs = ctrl.getState();
|
|
1919
|
+
previewRef.current.update(cs.tool?.key ?? null, cs.collected, hit);
|
|
1920
|
+
}
|
|
1921
|
+
setHoverLabel(hitToHoverLabel(hit, store.getState()));
|
|
1922
|
+
}, [store, isDragging]);
|
|
1923
|
+
const isHoveringObject = React3.useCallback((screen) => {
|
|
1924
|
+
const view = boardRef.current?.getView3D();
|
|
1925
|
+
if (!view) return false;
|
|
1926
|
+
try {
|
|
1927
|
+
return hitTest(screen, view, store.getState()).kind !== "empty";
|
|
1928
|
+
} catch {
|
|
1929
|
+
return false;
|
|
1930
|
+
}
|
|
1931
|
+
}, [store]);
|
|
1932
|
+
const tryInsert = React3.useCallback(() => {
|
|
1933
|
+
const state = store.getState();
|
|
1934
|
+
if (Object.keys(state.objects).length === 0) return false;
|
|
1935
|
+
const view = getView3DInfo(boardRef.current?.getView3D());
|
|
1936
|
+
const jsonState = serializeBoard3D(state, view);
|
|
1937
|
+
void (async () => {
|
|
1938
|
+
try {
|
|
1939
|
+
const { svgString } = await renderGeometry3DSvgFromState(jsonState);
|
|
1940
|
+
onInsertRef.current?.(jsonState, svgString);
|
|
1941
|
+
} catch (err) {
|
|
1942
|
+
console.error("Geometry3D insert failed:", err);
|
|
1943
|
+
}
|
|
1944
|
+
})();
|
|
1945
|
+
return true;
|
|
1946
|
+
}, [store]);
|
|
1947
|
+
React3.useImperativeHandle(
|
|
1948
|
+
ref,
|
|
1949
|
+
() => ({
|
|
1950
|
+
hasContent: () => Object.keys(store.getState().objects).length > 0,
|
|
1951
|
+
tryInsert,
|
|
1952
|
+
setTool: (k) => controllerRef.current.selectTool(k),
|
|
1953
|
+
undo: () => store.undo(),
|
|
1954
|
+
redo: () => store.redo(),
|
|
1955
|
+
highlight: (id) => rendererRef.current?.highlight(id)
|
|
1956
|
+
}),
|
|
1957
|
+
[store, tryInsert]
|
|
1958
|
+
);
|
|
1959
|
+
const dialogStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 40 } : {
|
|
1960
|
+
position: "absolute",
|
|
1961
|
+
top: "50%",
|
|
1962
|
+
left: withLeftPanel ? "calc(50% + 120px)" : "50%",
|
|
1963
|
+
transform: "translate(-50%, -50%)",
|
|
1964
|
+
zIndex: 40
|
|
1965
|
+
};
|
|
1966
|
+
return /* @__PURE__ */ jsxs(
|
|
1967
|
+
"div",
|
|
1968
|
+
{
|
|
1969
|
+
role: "dialog",
|
|
1970
|
+
"aria-label": "D\u1EF1ng h\xECnh h\u1ECDc 3D",
|
|
1971
|
+
"data-testid": "geom3d-host",
|
|
1972
|
+
"data-stamp-area": "true",
|
|
1973
|
+
style: dialogStyle,
|
|
1974
|
+
className: [
|
|
1975
|
+
isDark ? "theme--dark " : "",
|
|
1976
|
+
"relative flex flex-col overflow-hidden bg-white",
|
|
1977
|
+
isMobile ? "h-full w-full" : `${STAMP_PANEL_DESKTOP} rounded-lg border border-slate-300 shadow-2xl ring-1 ring-black/5`
|
|
1978
|
+
].join(" "),
|
|
1979
|
+
children: [
|
|
1980
|
+
/* @__PURE__ */ jsxs("header", { className: "flex items-center gap-2 border-b border-slate-200 bg-gradient-to-r from-emerald-600 to-teal-600 px-3 py-2 text-white", children: [
|
|
1981
|
+
isMobile && /* @__PURE__ */ jsx(
|
|
1982
|
+
"button",
|
|
1983
|
+
{
|
|
1984
|
+
type: "button",
|
|
1985
|
+
onClick: onOpenDrawer,
|
|
1986
|
+
"aria-label": "M\u1EDF ng\u0103n c\xF4ng c\u1EE5",
|
|
1987
|
+
className: "-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15",
|
|
1988
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
1989
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
|
|
1990
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
|
|
1991
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
|
|
1992
|
+
] })
|
|
1993
|
+
}
|
|
1994
|
+
),
|
|
1995
|
+
/* @__PURE__ */ jsxs("h3", { className: "flex flex-1 items-center gap-2 text-sm font-semibold", children: [
|
|
1996
|
+
/* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsx("path", { d: "M4 9 L4 20 L14 20 L14 9 Z M4 9 L10 4 L20 4 L14 9 Z M14 9 L20 4 L20 15 L14 20 Z" }) }),
|
|
1997
|
+
"D\u1EF1ng h\xECnh h\u1ECDc kh\xF4ng gian"
|
|
1998
|
+
] }),
|
|
1999
|
+
isMobile && /* @__PURE__ */ jsx(
|
|
2000
|
+
"button",
|
|
2001
|
+
{
|
|
2002
|
+
type: "button",
|
|
2003
|
+
onClick: tryInsert,
|
|
2004
|
+
disabled: !ready || !hasContent,
|
|
2005
|
+
title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
|
|
2006
|
+
"data-testid": "geom3d-insert-btn-mobile",
|
|
2007
|
+
className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
|
|
2008
|
+
children: "Ch\xE8n"
|
|
2009
|
+
}
|
|
2010
|
+
),
|
|
2011
|
+
/* @__PURE__ */ jsx(
|
|
2012
|
+
"button",
|
|
2013
|
+
{
|
|
2014
|
+
onClick: onClose,
|
|
2015
|
+
"aria-label": "\u0110\xF3ng",
|
|
2016
|
+
className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15",
|
|
2017
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
2018
|
+
/* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
2019
|
+
/* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
2020
|
+
] })
|
|
2021
|
+
}
|
|
2022
|
+
)
|
|
2023
|
+
] }),
|
|
2024
|
+
/* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsx(
|
|
2025
|
+
MiniBoard3D,
|
|
2026
|
+
{
|
|
2027
|
+
ref: boardRef,
|
|
2028
|
+
isDark,
|
|
2029
|
+
onView3DReady: handleView3DReady,
|
|
2030
|
+
onPointerClick: handleClick,
|
|
2031
|
+
onPointerMove: handleMove,
|
|
2032
|
+
onPointerLeave: () => {
|
|
2033
|
+
setHoverLabel(null);
|
|
2034
|
+
lastHoverHitRef.current = { kind: "empty" };
|
|
2035
|
+
previewRef.current?.clear();
|
|
2036
|
+
},
|
|
2037
|
+
shouldStartPointDrag,
|
|
2038
|
+
onPointerDrag,
|
|
2039
|
+
onPointerDragEnd,
|
|
2040
|
+
isHoveringObject
|
|
2041
|
+
}
|
|
2042
|
+
) }),
|
|
2043
|
+
/* @__PURE__ */ jsx(StatusHint, { hint, hoverLabel }),
|
|
2044
|
+
!isMobile && /* @__PURE__ */ jsxs("footer", { className: "flex items-center justify-between border-t border-slate-200 bg-slate-50 px-3 py-2", children: [
|
|
2045
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-slate-500", children: "Ch\u1ECDn c\xF4ng c\u1EE5 b\xEAn tr\xE1i, click tr\xEAn b\u1EA3ng \u0111\u1EC3 d\u1EF1ng h\xECnh." }),
|
|
2046
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
|
|
2047
|
+
/* @__PURE__ */ jsx(
|
|
2048
|
+
"button",
|
|
2049
|
+
{
|
|
2050
|
+
onClick: onClose,
|
|
2051
|
+
className: "rounded border border-slate-300 bg-white px-3 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100",
|
|
2052
|
+
children: "Hu\u1EF7"
|
|
2053
|
+
}
|
|
2054
|
+
),
|
|
2055
|
+
/* @__PURE__ */ jsx(
|
|
2056
|
+
"button",
|
|
2057
|
+
{
|
|
2058
|
+
onClick: tryInsert,
|
|
2059
|
+
disabled: !ready || !hasContent,
|
|
2060
|
+
title: !hasContent ? "V\u1EBD \xEDt nh\u1EA5t m\u1ED9t \u0111\u1ED1i t\u01B0\u1EE3ng tr\u01B0\u1EDBc khi ch\xE8n" : void 0,
|
|
2061
|
+
"data-testid": "geom3d-insert-btn",
|
|
2062
|
+
className: "rounded bg-emerald-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-emerald-700 disabled:opacity-50",
|
|
2063
|
+
children: "Ch\xE8n"
|
|
2064
|
+
}
|
|
2065
|
+
)
|
|
2066
|
+
] })
|
|
2067
|
+
] }),
|
|
2068
|
+
/* @__PURE__ */ jsx(ToastHost, {})
|
|
2069
|
+
]
|
|
2070
|
+
}
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
2073
|
+
);
|
|
2074
|
+
var EditorPanel2 = React3.forwardRef(
|
|
2075
|
+
function EditorPanel3(props, ref) {
|
|
2076
|
+
return /* @__PURE__ */ jsx(ToastProvider, { children: /* @__PURE__ */ jsx(EditorPanelInner, { ...props, ref }) });
|
|
2077
|
+
}
|
|
2078
|
+
);
|
|
2079
|
+
var wrap = (children) => /* @__PURE__ */ jsx(
|
|
2080
|
+
"svg",
|
|
2081
|
+
{
|
|
2082
|
+
width: "22",
|
|
2083
|
+
height: "22",
|
|
2084
|
+
viewBox: "0 0 24 24",
|
|
2085
|
+
fill: "none",
|
|
2086
|
+
stroke: "currentColor",
|
|
2087
|
+
strokeWidth: "1.6",
|
|
2088
|
+
strokeLinecap: "round",
|
|
2089
|
+
strokeLinejoin: "round",
|
|
2090
|
+
"aria-hidden": "true",
|
|
2091
|
+
children
|
|
2092
|
+
}
|
|
2093
|
+
);
|
|
2094
|
+
var dot4 = (cx, cy, r = 1.4) => /* @__PURE__ */ jsx("circle", { cx, cy, r, fill: "currentColor", stroke: "none" });
|
|
2095
|
+
var ToolIcons = {
|
|
2096
|
+
move: wrap(
|
|
2097
|
+
/* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("path", { d: "M5 4 L5 14 L8 11 L10 16 L13 15 L11 10 L15 10 Z" }) })
|
|
2098
|
+
),
|
|
2099
|
+
point: wrap(
|
|
2100
|
+
/* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "2.4", fill: "currentColor", stroke: "none" }) })
|
|
2101
|
+
),
|
|
2102
|
+
pointOnObject: wrap(
|
|
2103
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2104
|
+
/* @__PURE__ */ jsx("path", { d: "M3 16 L21 12" }),
|
|
2105
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "13.5", r: "2.4", fill: "currentColor", stroke: "none" })
|
|
2106
|
+
] })
|
|
2107
|
+
),
|
|
2108
|
+
segment: wrap(
|
|
2109
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2110
|
+
/* @__PURE__ */ jsx("line", { x1: "4", y1: "18", x2: "20", y2: "6" }),
|
|
2111
|
+
dot4(4, 18, 1.6),
|
|
2112
|
+
dot4(20, 6, 1.6)
|
|
2113
|
+
] })
|
|
2114
|
+
),
|
|
2115
|
+
line: wrap(
|
|
2116
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2117
|
+
/* @__PURE__ */ jsx("line", { x1: "3", y1: "18", x2: "21", y2: "6" }),
|
|
2118
|
+
dot4(8, 14.5, 1.4),
|
|
2119
|
+
dot4(16, 9.5, 1.4)
|
|
2120
|
+
] })
|
|
2121
|
+
),
|
|
2122
|
+
ray: wrap(
|
|
2123
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2124
|
+
/* @__PURE__ */ jsx("line", { x1: "5", y1: "18", x2: "19", y2: "7" }),
|
|
2125
|
+
/* @__PURE__ */ jsx("path", { d: "M19 7 L15 6 M19 7 L18 11" }),
|
|
2126
|
+
dot4(5, 18, 1.6)
|
|
2127
|
+
] })
|
|
2128
|
+
),
|
|
2129
|
+
vector: wrap(
|
|
2130
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2131
|
+
/* @__PURE__ */ jsx("line", { x1: "5", y1: "18", x2: "18", y2: "7" }),
|
|
2132
|
+
/* @__PURE__ */ jsx("path", { d: "M18 7 L13 7 M18 7 L18 12" }),
|
|
2133
|
+
dot4(5, 18, 1.6)
|
|
2134
|
+
] })
|
|
2135
|
+
),
|
|
2136
|
+
polygon: wrap(
|
|
2137
|
+
/* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("polygon", { points: "12,4 20,10 17,19 7,19 4,10" }) })
|
|
2138
|
+
),
|
|
2139
|
+
plane: wrap(
|
|
2140
|
+
/* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("polygon", { points: "3,9 14,5 21,11 10,15" }) })
|
|
2141
|
+
),
|
|
2142
|
+
pyramid: wrap(
|
|
2143
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2144
|
+
/* @__PURE__ */ jsx("path", { d: "M4 19 L20 19 L12 4 Z" }),
|
|
2145
|
+
/* @__PURE__ */ jsx("path", { d: "M4 19 L12 16 L20 19" }),
|
|
2146
|
+
/* @__PURE__ */ jsx("path", { d: "M12 4 L12 16", strokeDasharray: "2 2" })
|
|
2147
|
+
] })
|
|
2148
|
+
),
|
|
2149
|
+
prism: wrap(
|
|
2150
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2151
|
+
/* @__PURE__ */ jsx("path", { d: "M4 8 L4 19 L14 19 L14 8 Z" }),
|
|
2152
|
+
/* @__PURE__ */ jsx("path", { d: "M4 8 L10 4 L20 4 L14 8" }),
|
|
2153
|
+
/* @__PURE__ */ jsx("path", { d: "M14 8 L14 19 L20 15 L20 4" }),
|
|
2154
|
+
/* @__PURE__ */ jsx("path", { d: "M4 8 L14 8" })
|
|
2155
|
+
] })
|
|
2156
|
+
),
|
|
2157
|
+
tetrahedron: wrap(
|
|
2158
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2159
|
+
/* @__PURE__ */ jsx("path", { d: "M4 19 L20 19 L12 5 Z" }),
|
|
2160
|
+
/* @__PURE__ */ jsx("path", { d: "M4 19 L15 12 L20 19" }),
|
|
2161
|
+
/* @__PURE__ */ jsx("path", { d: "M15 12 L12 5" })
|
|
2162
|
+
] })
|
|
2163
|
+
),
|
|
2164
|
+
cube: wrap(
|
|
2165
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2166
|
+
/* @__PURE__ */ jsx("path", { d: "M4 8 L4 19 L14 19 L14 8 Z" }),
|
|
2167
|
+
/* @__PURE__ */ jsx("path", { d: "M4 8 L10 4 L20 4 L14 8" }),
|
|
2168
|
+
/* @__PURE__ */ jsx("path", { d: "M14 8 L14 19 L20 15 L20 4" })
|
|
2169
|
+
] })
|
|
2170
|
+
),
|
|
2171
|
+
sphere: wrap(
|
|
2172
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2173
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "8" }),
|
|
2174
|
+
/* @__PURE__ */ jsx("ellipse", { cx: "12", cy: "12", rx: "8", ry: "3" }),
|
|
2175
|
+
dot4(12, 12, 1.2)
|
|
2176
|
+
] })
|
|
2177
|
+
),
|
|
2178
|
+
cylinder: wrap(
|
|
2179
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2180
|
+
/* @__PURE__ */ jsx("ellipse", { cx: "12", cy: "6", rx: "6", ry: "2" }),
|
|
2181
|
+
/* @__PURE__ */ jsx("path", { d: "M6 6 L6 18" }),
|
|
2182
|
+
/* @__PURE__ */ jsx("path", { d: "M18 6 L18 18" }),
|
|
2183
|
+
/* @__PURE__ */ jsx("ellipse", { cx: "12", cy: "18", rx: "6", ry: "2" })
|
|
2184
|
+
] })
|
|
2185
|
+
),
|
|
2186
|
+
cone: wrap(
|
|
2187
|
+
/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2188
|
+
/* @__PURE__ */ jsx("line", { x1: "5", y1: "18", x2: "12", y2: "4" }),
|
|
2189
|
+
/* @__PURE__ */ jsx("line", { x1: "19", y1: "18", x2: "12", y2: "4" }),
|
|
2190
|
+
/* @__PURE__ */ jsx("ellipse", { cx: "12", cy: "18", rx: "7", ry: "2" })
|
|
2191
|
+
] })
|
|
2192
|
+
)
|
|
2193
|
+
};
|
|
2194
|
+
|
|
2195
|
+
// src/stamps/geometry-3d/editor/toolPanel/groups.ts
|
|
2196
|
+
var GROUP_ORDER = [
|
|
2197
|
+
"basic",
|
|
2198
|
+
"point",
|
|
2199
|
+
"line",
|
|
2200
|
+
"plane",
|
|
2201
|
+
"polyhedron",
|
|
2202
|
+
"curve"
|
|
2203
|
+
];
|
|
2204
|
+
var GROUP_LABELS = {
|
|
2205
|
+
basic: "C\u01A1 b\u1EA3n",
|
|
2206
|
+
point: "\u0110i\u1EC3m",
|
|
2207
|
+
line: "\u0110\u01B0\u1EDDng th\u1EB3ng",
|
|
2208
|
+
plane: "M\u1EB7t ph\u1EB3ng",
|
|
2209
|
+
polyhedron: "Kh\u1ED1i \u0111a di\u1EC7n",
|
|
2210
|
+
curve: "Kh\u1ED1i cong"
|
|
2211
|
+
};
|
|
2212
|
+
var TOOLS_BY_GROUP = {
|
|
2213
|
+
basic: ["move"],
|
|
2214
|
+
point: ["point", "pointOnObject"],
|
|
2215
|
+
line: ["segment", "line", "ray", "vector", "polygon"],
|
|
2216
|
+
plane: ["plane"],
|
|
2217
|
+
polyhedron: ["pyramid", "prism", "tetrahedron", "cube"],
|
|
2218
|
+
curve: ["sphere", "cylinder", "cone"]
|
|
2219
|
+
};
|
|
2220
|
+
var SPEC_BY_KEY = TOOLS.reduce(
|
|
2221
|
+
(acc, t) => {
|
|
2222
|
+
acc[t.key] = t;
|
|
2223
|
+
return acc;
|
|
2224
|
+
},
|
|
2225
|
+
{}
|
|
2226
|
+
);
|
|
2227
|
+
var TOOLS_FLAT = GROUP_ORDER.flatMap(
|
|
2228
|
+
(group) => TOOLS_BY_GROUP[group].map((key) => {
|
|
2229
|
+
const spec = SPEC_BY_KEY[key];
|
|
2230
|
+
return {
|
|
2231
|
+
key,
|
|
2232
|
+
label: spec?.label ?? key,
|
|
2233
|
+
hint: spec?.hintIdle ?? "",
|
|
2234
|
+
icon: ToolIcons[key],
|
|
2235
|
+
group
|
|
2236
|
+
};
|
|
2237
|
+
})
|
|
2238
|
+
);
|
|
2239
|
+
var A_CODE = "A".charCodeAt(0);
|
|
2240
|
+
function letterForGroup(g) {
|
|
2241
|
+
const idx = GROUP_ORDER.indexOf(g);
|
|
2242
|
+
return idx >= 0 ? String.fromCharCode(A_CODE + idx) : "";
|
|
2243
|
+
}
|
|
2244
|
+
var Geom3DIconHeader = /* @__PURE__ */ jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
|
|
2245
|
+
/* @__PURE__ */ jsx("path", { d: "M4 9 L4 20 L14 20 L14 9 Z" }),
|
|
2246
|
+
/* @__PURE__ */ jsx("path", { d: "M4 9 L10 4 L20 4 L14 9 Z" }),
|
|
2247
|
+
/* @__PURE__ */ jsx("path", { d: "M14 9 L20 4 L20 15 L14 20 Z" })
|
|
2248
|
+
] });
|
|
2249
|
+
function parseInitialState(data) {
|
|
2250
|
+
if (!isGeometry3DCustomData(data)) return null;
|
|
2251
|
+
return deserializeBoard3D(data.jsonState);
|
|
2252
|
+
}
|
|
2253
|
+
var Geometry3DStampHost = forwardRef(
|
|
2254
|
+
function Geometry3DStampHost2({ api, editingElement, onClose, isDark }, ref) {
|
|
2255
|
+
const editorRef = useRef(null);
|
|
2256
|
+
const sceneStore = useStampStore("3d", editingElement, parseInitialState);
|
|
2257
|
+
const { isMobile } = useIsMobile();
|
|
2258
|
+
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
2259
|
+
const [selectedTool, setSelectedTool] = useState("move");
|
|
2260
|
+
const [showAxis, setShowAxis] = useState(true);
|
|
2261
|
+
const [showGrid, setShowGrid] = useState(true);
|
|
2262
|
+
const [canUndo, setCanUndo] = useState(false);
|
|
2263
|
+
const [canRedo, setCanRedo] = useState(false);
|
|
2264
|
+
const [selectedObjectId, setSelectedObjectId] = useState(void 0);
|
|
2265
|
+
const handleHistoryChange = useCallback((u, r) => {
|
|
2266
|
+
setCanUndo(u);
|
|
2267
|
+
setCanRedo(r);
|
|
2268
|
+
}, []);
|
|
2269
|
+
const handleObjectSelect = useCallback((id) => {
|
|
2270
|
+
setSelectedObjectId(id ?? void 0);
|
|
2271
|
+
editorRef.current?.highlight(id);
|
|
2272
|
+
}, []);
|
|
2273
|
+
const handleUndo = useCallback(() => editorRef.current?.undo(), []);
|
|
2274
|
+
const handleRedo = useCallback(() => editorRef.current?.redo(), []);
|
|
2275
|
+
const { chordGroup } = useChordShortcut({
|
|
2276
|
+
groupOrder: GROUP_ORDER,
|
|
2277
|
+
tools: TOOLS_FLAT,
|
|
2278
|
+
onSelect: (key) => {
|
|
2279
|
+
setSelectedTool(key);
|
|
2280
|
+
editorRef.current?.setTool(key);
|
|
2281
|
+
},
|
|
2282
|
+
enabled: !isMobile
|
|
2283
|
+
});
|
|
2284
|
+
const handleSelectTool = useCallback((k) => {
|
|
2285
|
+
setSelectedTool(k);
|
|
2286
|
+
editorRef.current?.setTool(k);
|
|
2287
|
+
}, []);
|
|
2288
|
+
const handleEditorInsert = useCallback(
|
|
2289
|
+
async (jsonState, svgString) => {
|
|
2290
|
+
if (!api) return;
|
|
2291
|
+
await insertStampImage(api, {
|
|
2292
|
+
svgString,
|
|
2293
|
+
makeCustomData: () => ({
|
|
2294
|
+
kind: "geometry3d",
|
|
2295
|
+
version: 2,
|
|
2296
|
+
jsonState
|
|
2297
|
+
}),
|
|
2298
|
+
editingElementId: editingElement?.id ?? null
|
|
2299
|
+
});
|
|
2300
|
+
onClose();
|
|
2301
|
+
},
|
|
2302
|
+
[api, editingElement, onClose]
|
|
2303
|
+
);
|
|
2304
|
+
useImperativeHandle(
|
|
2305
|
+
ref,
|
|
2306
|
+
() => ({
|
|
2307
|
+
tryInsert: () => editorRef.current?.tryInsert() ?? false,
|
|
2308
|
+
hasContent: () => editorRef.current?.hasContent() ?? false
|
|
2309
|
+
}),
|
|
2310
|
+
[]
|
|
2311
|
+
);
|
|
2312
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2313
|
+
/* @__PURE__ */ jsx(
|
|
2314
|
+
StampLeftPanel,
|
|
2315
|
+
{
|
|
2316
|
+
title: "H\xECnh h\u1ECDc 3D",
|
|
2317
|
+
icon: Geom3DIconHeader,
|
|
2318
|
+
onClose,
|
|
2319
|
+
isDark,
|
|
2320
|
+
testId: "stamp-left-panel",
|
|
2321
|
+
tools: TOOLS_FLAT,
|
|
2322
|
+
groupOrder: GROUP_ORDER,
|
|
2323
|
+
groupLabels: GROUP_LABELS,
|
|
2324
|
+
activeTool: selectedTool,
|
|
2325
|
+
onToolChange: handleSelectTool,
|
|
2326
|
+
view: {
|
|
2327
|
+
sectionLabel: "G\xF3c nh\xECn",
|
|
2328
|
+
showAxis,
|
|
2329
|
+
showGrid,
|
|
2330
|
+
onShowAxisChange: setShowAxis,
|
|
2331
|
+
onShowGridChange: setShowGrid
|
|
2332
|
+
},
|
|
2333
|
+
history: {
|
|
2334
|
+
onUndo: handleUndo,
|
|
2335
|
+
canUndo,
|
|
2336
|
+
onRedo: handleRedo,
|
|
2337
|
+
canRedo
|
|
2338
|
+
},
|
|
2339
|
+
chord: { activeGroup: chordGroup, letterForGroup },
|
|
2340
|
+
objects: {
|
|
2341
|
+
store: sceneStore,
|
|
2342
|
+
selectedObjectId,
|
|
2343
|
+
onObjectSelect: handleObjectSelect
|
|
2344
|
+
},
|
|
2345
|
+
isMobile,
|
|
2346
|
+
drawerOpen,
|
|
2347
|
+
onDrawerClose: () => setDrawerOpen(false)
|
|
2348
|
+
}
|
|
2349
|
+
),
|
|
2350
|
+
/* @__PURE__ */ jsx(
|
|
2351
|
+
EditorPanel2,
|
|
2352
|
+
{
|
|
2353
|
+
ref: editorRef,
|
|
2354
|
+
isDark,
|
|
2355
|
+
onInsert: handleEditorInsert,
|
|
2356
|
+
onClose,
|
|
2357
|
+
store: sceneStore,
|
|
2358
|
+
selectedTool,
|
|
2359
|
+
onSelectedToolChange: setSelectedTool,
|
|
2360
|
+
showAxis,
|
|
2361
|
+
showGrid,
|
|
2362
|
+
onHistoryChange: handleHistoryChange,
|
|
2363
|
+
isMobile,
|
|
2364
|
+
onOpenDrawer: () => setDrawerOpen(true),
|
|
2365
|
+
withLeftPanel: !isMobile
|
|
2366
|
+
}
|
|
2367
|
+
)
|
|
2368
|
+
] });
|
|
2369
|
+
}
|
|
2370
|
+
);
|
|
2371
|
+
|
|
2372
|
+
export { Geometry3DStampHost };
|
|
2373
|
+
//# sourceMappingURL=host-TLIXN4CF.mjs.map
|
|
2374
|
+
//# sourceMappingURL=host-TLIXN4CF.mjs.map
|