@xom11/whiteboard 0.6.4 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/dist/chunk-3SSQKRRO.mjs +58 -0
- package/dist/chunk-3SSQKRRO.mjs.map +1 -0
- package/dist/chunk-7P7SQFOW.mjs +39 -0
- package/dist/chunk-7P7SQFOW.mjs.map +1 -0
- package/dist/chunk-BJX4YNA5.mjs +137 -0
- package/dist/chunk-BJX4YNA5.mjs.map +1 -0
- package/dist/chunk-C6SCVOMC.mjs +111 -0
- package/dist/chunk-C6SCVOMC.mjs.map +1 -0
- package/dist/chunk-DJTBZEAR.mjs +25 -0
- package/dist/chunk-DJTBZEAR.mjs.map +1 -0
- package/dist/chunk-HM7RIXJE.mjs +331 -0
- package/dist/chunk-HM7RIXJE.mjs.map +1 -0
- package/dist/chunk-HTBLO5JO.mjs +41 -0
- package/dist/chunk-HTBLO5JO.mjs.map +1 -0
- package/dist/chunk-HYXFHEDJ.mjs +129 -0
- package/dist/chunk-HYXFHEDJ.mjs.map +1 -0
- package/dist/chunk-LPM4MM45.mjs +211 -0
- package/dist/chunk-LPM4MM45.mjs.map +1 -0
- package/dist/chunk-P2AOIF7S.mjs +40 -0
- package/dist/chunk-P2AOIF7S.mjs.map +1 -0
- package/dist/chunk-SHFOGORM.mjs +44 -0
- package/dist/chunk-SHFOGORM.mjs.map +1 -0
- package/dist/chunk-X5R72SSJ.mjs +52 -0
- package/dist/chunk-X5R72SSJ.mjs.map +1 -0
- package/dist/geometry-2d.d.mts +16 -0
- package/dist/geometry-2d.d.ts +16 -0
- package/dist/geometry-2d.js +3549 -0
- package/dist/geometry-2d.js.map +1 -0
- package/dist/geometry-2d.mjs +7 -0
- package/dist/geometry-2d.mjs.map +1 -0
- package/dist/geometry-3d.d.mts +16 -0
- package/dist/geometry-3d.d.ts +16 -0
- package/dist/geometry-3d.js +2030 -0
- package/dist/geometry-3d.js.map +1 -0
- package/dist/geometry-3d.mjs +6 -0
- package/dist/geometry-3d.mjs.map +1 -0
- package/dist/graph-2d.d.mts +16 -0
- package/dist/graph-2d.d.ts +16 -0
- package/dist/graph-2d.js +1725 -0
- package/dist/graph-2d.js.map +1 -0
- package/dist/graph-2d.mjs +6 -0
- package/dist/graph-2d.mjs.map +1 -0
- package/dist/host-2QGKMGCT.mjs +1066 -0
- package/dist/host-2QGKMGCT.mjs.map +1 -0
- package/dist/host-T2W6R6SO.mjs +2859 -0
- package/dist/host-T2W6R6SO.mjs.map +1 -0
- package/dist/host-XUFON6CQ.mjs +1422 -0
- package/dist/host-XUFON6CQ.mjs.map +1 -0
- package/dist/host-Z3TEJKZA.mjs +466 -0
- package/dist/host-Z3TEJKZA.mjs.map +1 -0
- package/dist/index.d.mts +27 -146
- package/dist/index.d.ts +27 -146
- package/dist/index.js +4694 -4482
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +136 -7179
- package/dist/index.mjs.map +1 -1
- package/dist/latex.d.mts +15 -0
- package/dist/latex.d.ts +15 -0
- package/dist/latex.js +750 -0
- package/dist/latex.js.map +1 -0
- package/dist/latex.mjs +6 -0
- package/dist/latex.mjs.map +1 -0
- package/dist/types-CinstD7T.d.mts +110 -0
- package/dist/types-CinstD7T.d.ts +110 -0
- package/package.json +24 -2
|
@@ -0,0 +1,2030 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
var reactDom = require('react-dom');
|
|
7
|
+
|
|
8
|
+
var __defProp = Object.defineProperty;
|
|
9
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
10
|
+
var __esm = (fn, res) => function __init() {
|
|
11
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
12
|
+
};
|
|
13
|
+
var __export = (target, all) => {
|
|
14
|
+
for (var name in all)
|
|
15
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/stamps/geometry-3d/serialize.ts
|
|
19
|
+
function isGeometry3DCustomData(data) {
|
|
20
|
+
if (!data || typeof data !== "object") return false;
|
|
21
|
+
const d = data;
|
|
22
|
+
return d.kind === "geometry3d" && d.version === 1 && typeof d.jsonState === "string";
|
|
23
|
+
}
|
|
24
|
+
function parseSerializedBoard3D(json) {
|
|
25
|
+
const parsed = JSON.parse(json);
|
|
26
|
+
if (!parsed || typeof parsed !== "object") {
|
|
27
|
+
throw new Error("parseSerializedBoard3D: not an object");
|
|
28
|
+
}
|
|
29
|
+
const p = parsed;
|
|
30
|
+
if (p.version !== 1) {
|
|
31
|
+
throw new Error(`parseSerializedBoard3D: unsupported version ${String(p.version)}`);
|
|
32
|
+
}
|
|
33
|
+
if (!Array.isArray(p.elements)) {
|
|
34
|
+
throw new Error("parseSerializedBoard3D: elements missing");
|
|
35
|
+
}
|
|
36
|
+
return parsed;
|
|
37
|
+
}
|
|
38
|
+
var init_serialize = __esm({
|
|
39
|
+
"src/stamps/geometry-3d/serialize.ts"() {
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// src/stamps/geometry-2d/editor/theme.ts
|
|
44
|
+
function paletteFor(isDark) {
|
|
45
|
+
return {
|
|
46
|
+
stroke: themeStroke(isDark),
|
|
47
|
+
axis: themeAxis(isDark),
|
|
48
|
+
grid: themeGrid(isDark),
|
|
49
|
+
label: themeLabel(isDark)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
var themeStroke, themeAxis, themeGrid, themeLabel;
|
|
53
|
+
var init_theme = __esm({
|
|
54
|
+
"src/stamps/geometry-2d/editor/theme.ts"() {
|
|
55
|
+
themeStroke = (dark) => dark ? "#e2e8f0" : "#0f172a";
|
|
56
|
+
themeAxis = (dark) => dark ? "#cbd5e1" : "#94a3b8";
|
|
57
|
+
themeGrid = (dark) => dark ? "#475569" : "#e2e8f0";
|
|
58
|
+
themeLabel = (dark) => dark ? "#e2e8f0" : "#000000";
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// src/stamps/geometry-3d/editor/theme.ts
|
|
63
|
+
function paletteFor2(isDark) {
|
|
64
|
+
const base = paletteFor(isDark);
|
|
65
|
+
return {
|
|
66
|
+
...base,
|
|
67
|
+
view3dBg: isDark ? "#1a1a1a" : "#ffffff",
|
|
68
|
+
axisX: "#d63b3b",
|
|
69
|
+
axisY: "#2d8a2d",
|
|
70
|
+
axisZ: "#2d6dd6"
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
var DEFAULT_VIEW3D, VIEW3D_ATTRS;
|
|
74
|
+
var init_theme2 = __esm({
|
|
75
|
+
"src/stamps/geometry-3d/editor/theme.ts"() {
|
|
76
|
+
init_theme();
|
|
77
|
+
DEFAULT_VIEW3D = {
|
|
78
|
+
azimuth: 0.7,
|
|
79
|
+
elevation: 0.4,
|
|
80
|
+
bbox3D: [-3, -3, -3, 3, 3, 3]
|
|
81
|
+
};
|
|
82
|
+
VIEW3D_ATTRS = (isDark) => {
|
|
83
|
+
const p = paletteFor2(isDark);
|
|
84
|
+
return {
|
|
85
|
+
az: { slider: { visible: false }, point2: { visible: false } },
|
|
86
|
+
el: { slider: { visible: false } },
|
|
87
|
+
projection: "central",
|
|
88
|
+
axesPosition: "border",
|
|
89
|
+
xAxis: { strokeColor: p.axisX, lastArrow: { type: 2 } },
|
|
90
|
+
yAxis: { strokeColor: p.axisY, lastArrow: { type: 2 } },
|
|
91
|
+
zAxis: { strokeColor: p.axisZ, lastArrow: { type: 2 } }
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// src/stamps/geometry-3d/editor/handlers.ts
|
|
98
|
+
function createHandlerContext(deps) {
|
|
99
|
+
return {
|
|
100
|
+
...deps,
|
|
101
|
+
pendingPoints: [],
|
|
102
|
+
pendingFlags: {},
|
|
103
|
+
pushedPointCoords: /* @__PURE__ */ new Map()
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function refByPlaceholder(id) {
|
|
107
|
+
return `@id:${id}`;
|
|
108
|
+
}
|
|
109
|
+
function createPoint3D(ctx, x, y, z, label) {
|
|
110
|
+
const id = ctx.nextId();
|
|
111
|
+
const attrs = { id, size: 3 };
|
|
112
|
+
const ref = ctx.view.create("point3d", [x, y, z], attrs);
|
|
113
|
+
ctx.objMap.set(id, ref);
|
|
114
|
+
ctx.pushedPointCoords.set(id, [x, y, z]);
|
|
115
|
+
ctx.pushLog({
|
|
116
|
+
type: "point3d",
|
|
117
|
+
parents: [x, y, z],
|
|
118
|
+
attributes: attrs,
|
|
119
|
+
id,
|
|
120
|
+
label
|
|
121
|
+
});
|
|
122
|
+
return { id, ref, coords: [x, y, z] };
|
|
123
|
+
}
|
|
124
|
+
function resolvePoint(ctx, hit) {
|
|
125
|
+
if (hit.existingPointId && ctx.objMap.has(hit.existingPointId)) {
|
|
126
|
+
const stored = ctx.pushedPointCoords.get(hit.existingPointId);
|
|
127
|
+
return {
|
|
128
|
+
id: hit.existingPointId,
|
|
129
|
+
ref: ctx.objMap.get(hit.existingPointId),
|
|
130
|
+
coords: stored ?? [hit.x3, hit.y3, hit.z3]
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return createPoint3D(ctx, hit.x3, hit.y3, hit.z3);
|
|
134
|
+
}
|
|
135
|
+
function finishPolygon(ctx, points, extraAttrs = {}) {
|
|
136
|
+
const id = ctx.nextId();
|
|
137
|
+
const refs = points.map((p) => p.ref);
|
|
138
|
+
const attrs = { id, ...extraAttrs };
|
|
139
|
+
const ref = ctx.view.create("polygon3d", [refs], attrs);
|
|
140
|
+
ctx.objMap.set(id, ref);
|
|
141
|
+
ctx.pushLog({
|
|
142
|
+
type: "polygon3d",
|
|
143
|
+
parents: [points.map((p) => refByPlaceholder(p.id))],
|
|
144
|
+
attributes: attrs,
|
|
145
|
+
id
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function finishLineLike(ctx, elType, points, extraAttrs = {}) {
|
|
149
|
+
const id = ctx.nextId();
|
|
150
|
+
const refs = points.map((p) => p.ref);
|
|
151
|
+
const attrs = { id, ...extraAttrs };
|
|
152
|
+
const ref = ctx.view.create(elType, refs, attrs);
|
|
153
|
+
ctx.objMap.set(id, ref);
|
|
154
|
+
ctx.pushLog({
|
|
155
|
+
type: elType,
|
|
156
|
+
parents: points.map((p) => refByPlaceholder(p.id)),
|
|
157
|
+
attributes: attrs,
|
|
158
|
+
id
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function handleToolStep(ctx, tool, hit) {
|
|
162
|
+
switch (tool) {
|
|
163
|
+
case "move":
|
|
164
|
+
return;
|
|
165
|
+
case "point": {
|
|
166
|
+
const coords = ctx.promptCoords("To\u1EA1 \u0111\u1ED9 \u0111i\u1EC3m (x, y, z)");
|
|
167
|
+
if (!coords) return;
|
|
168
|
+
createPoint3D(ctx, coords.x, coords.y, coords.z);
|
|
169
|
+
ctx.notify();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
case "segment":
|
|
173
|
+
case "line": {
|
|
174
|
+
const p = resolvePoint(ctx, hit);
|
|
175
|
+
ctx.pendingPoints.push(p);
|
|
176
|
+
if (ctx.pendingPoints.length === 2) {
|
|
177
|
+
const lineColor = ctx.isDark ? "#9ecbff" : "#0066cc";
|
|
178
|
+
const baseAttrs = {
|
|
179
|
+
strokeColor: lineColor,
|
|
180
|
+
strokeWidth: 2,
|
|
181
|
+
visible: true,
|
|
182
|
+
fixed: true
|
|
183
|
+
};
|
|
184
|
+
if (tool === "segment") {
|
|
185
|
+
baseAttrs.straightFirst = false;
|
|
186
|
+
baseAttrs.straightLast = false;
|
|
187
|
+
}
|
|
188
|
+
finishLineLike(ctx, "line3d", ctx.pendingPoints, baseAttrs);
|
|
189
|
+
ctx.pendingPoints = [];
|
|
190
|
+
}
|
|
191
|
+
ctx.notify();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
case "plane": {
|
|
195
|
+
const p = resolvePoint(ctx, hit);
|
|
196
|
+
ctx.pendingPoints.push(p);
|
|
197
|
+
if (ctx.pendingPoints.length === 3) {
|
|
198
|
+
finishLineLike(ctx, "plane3d", ctx.pendingPoints);
|
|
199
|
+
ctx.pendingPoints = [];
|
|
200
|
+
}
|
|
201
|
+
ctx.notify();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
case "triangle": {
|
|
205
|
+
const p = resolvePoint(ctx, hit);
|
|
206
|
+
ctx.pendingPoints.push(p);
|
|
207
|
+
if (ctx.pendingPoints.length === 3) {
|
|
208
|
+
finishPolygon(ctx, ctx.pendingPoints);
|
|
209
|
+
ctx.pendingPoints = [];
|
|
210
|
+
}
|
|
211
|
+
ctx.notify();
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
case "polygon": {
|
|
215
|
+
if (ctx.pendingPoints.length >= 3 && hit.existingPointId === ctx.pendingPoints[0].id) {
|
|
216
|
+
finishPolygon(ctx, ctx.pendingPoints);
|
|
217
|
+
ctx.pendingPoints = [];
|
|
218
|
+
ctx.notify();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const p = resolvePoint(ctx, hit);
|
|
222
|
+
ctx.pendingPoints.push(p);
|
|
223
|
+
ctx.notify();
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
case "label": {
|
|
227
|
+
if (!hit.existingPointId) return;
|
|
228
|
+
const text = ctx.promptText("N\u1ED9i dung nh\xE3n");
|
|
229
|
+
if (!text) return;
|
|
230
|
+
const id = ctx.nextId();
|
|
231
|
+
const pointLog = ctx.pushedPointCoords.get(hit.existingPointId);
|
|
232
|
+
if (!pointLog) return;
|
|
233
|
+
const [x, y, z] = pointLog;
|
|
234
|
+
const attrs = {
|
|
235
|
+
id,
|
|
236
|
+
fontSize: 14,
|
|
237
|
+
strokeColor: ctx.isDark ? "#f5f5f5" : "#111111"
|
|
238
|
+
};
|
|
239
|
+
const ref = ctx.view.create("text3d", [x, y, z, text], attrs);
|
|
240
|
+
ctx.objMap.set(id, ref);
|
|
241
|
+
ctx.pushLog({
|
|
242
|
+
type: "text3d",
|
|
243
|
+
parents: [x, y, z, text],
|
|
244
|
+
attributes: attrs,
|
|
245
|
+
id,
|
|
246
|
+
label: text
|
|
247
|
+
});
|
|
248
|
+
ctx.notify();
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
// Solids + curved handled in B8, B9
|
|
252
|
+
default:
|
|
253
|
+
handleSolidStep(ctx, tool, hit);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function handleSolidStep(ctx, tool, hit) {
|
|
258
|
+
switch (tool) {
|
|
259
|
+
case "tetrahedron": {
|
|
260
|
+
const p = resolvePoint(ctx, hit);
|
|
261
|
+
ctx.pendingPoints.push(p);
|
|
262
|
+
if (ctx.pendingPoints.length === 4) {
|
|
263
|
+
const [a, b, c, d] = ctx.pendingPoints;
|
|
264
|
+
finishPolyhedron(ctx, [
|
|
265
|
+
[a, b, c],
|
|
266
|
+
[a, b, d],
|
|
267
|
+
[a, c, d],
|
|
268
|
+
[b, c, d]
|
|
269
|
+
]);
|
|
270
|
+
ctx.pendingPoints = [];
|
|
271
|
+
}
|
|
272
|
+
ctx.notify();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
case "parallelepiped": {
|
|
276
|
+
const origin = resolvePoint(ctx, hit);
|
|
277
|
+
const v1 = ctx.promptCoords("Vector c\u1EA1nh 1 (dx, dy, dz)");
|
|
278
|
+
const v2 = ctx.promptCoords("Vector c\u1EA1nh 2 (dx, dy, dz)");
|
|
279
|
+
const v3 = ctx.promptCoords("Vector c\u1EA1nh 3 (dx, dy, dz)");
|
|
280
|
+
if (!v1 || !v2 || !v3) return;
|
|
281
|
+
const [ox, oy, oz] = origin.coords;
|
|
282
|
+
const c1 = createPoint3D(ctx, ox + v1.x, oy + v1.y, oz + v1.z);
|
|
283
|
+
const c2 = createPoint3D(ctx, ox + v2.x, oy + v2.y, oz + v2.z);
|
|
284
|
+
const c3 = createPoint3D(ctx, ox + v3.x, oy + v3.y, oz + v3.z);
|
|
285
|
+
const c12 = createPoint3D(
|
|
286
|
+
ctx,
|
|
287
|
+
ox + v1.x + v2.x,
|
|
288
|
+
oy + v1.y + v2.y,
|
|
289
|
+
oz + v1.z + v2.z
|
|
290
|
+
);
|
|
291
|
+
const c13 = createPoint3D(
|
|
292
|
+
ctx,
|
|
293
|
+
ox + v1.x + v3.x,
|
|
294
|
+
oy + v1.y + v3.y,
|
|
295
|
+
oz + v1.z + v3.z
|
|
296
|
+
);
|
|
297
|
+
const c23 = createPoint3D(
|
|
298
|
+
ctx,
|
|
299
|
+
ox + v2.x + v3.x,
|
|
300
|
+
oy + v2.y + v3.y,
|
|
301
|
+
oz + v2.z + v3.z
|
|
302
|
+
);
|
|
303
|
+
const c123 = createPoint3D(
|
|
304
|
+
ctx,
|
|
305
|
+
ox + v1.x + v2.x + v3.x,
|
|
306
|
+
oy + v1.y + v2.y + v3.y,
|
|
307
|
+
oz + v1.z + v2.z + v3.z
|
|
308
|
+
);
|
|
309
|
+
finishPolyhedron(ctx, [
|
|
310
|
+
[origin, c1, c12, c2],
|
|
311
|
+
[origin, c1, c13, c3],
|
|
312
|
+
[origin, c2, c23, c3],
|
|
313
|
+
[c123, c12, c1, c13],
|
|
314
|
+
[c123, c12, c2, c23],
|
|
315
|
+
[c123, c13, c3, c23]
|
|
316
|
+
]);
|
|
317
|
+
ctx.pendingPoints = [];
|
|
318
|
+
ctx.notify();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
case "prism": {
|
|
322
|
+
if (ctx.pendingPoints.length >= 3 && hit.existingPointId === ctx.pendingPoints[0].id) {
|
|
323
|
+
const base = ctx.pendingPoints;
|
|
324
|
+
const height = ctx.promptNumber("Chi\u1EC1u cao (theo tr\u1EE5c z)");
|
|
325
|
+
if (!height) return;
|
|
326
|
+
const top = base.map(
|
|
327
|
+
(bp) => createPoint3D(ctx, bp.coords[0], bp.coords[1], bp.coords[2] + height)
|
|
328
|
+
);
|
|
329
|
+
const faces = [base, top];
|
|
330
|
+
for (let i = 0; i < base.length; i++) {
|
|
331
|
+
const next = (i + 1) % base.length;
|
|
332
|
+
faces.push([base[i], base[next], top[next], top[i]]);
|
|
333
|
+
}
|
|
334
|
+
finishPolyhedron(ctx, faces);
|
|
335
|
+
ctx.pendingPoints = [];
|
|
336
|
+
ctx.notify();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const p = resolvePoint(ctx, hit);
|
|
340
|
+
ctx.pendingPoints.push(p);
|
|
341
|
+
ctx.notify();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
case "pyramid": {
|
|
345
|
+
const baseDone = ctx.pendingFlags.pyramidBaseDone === true;
|
|
346
|
+
if (!baseDone && ctx.pendingPoints.length >= 3 && hit.existingPointId === ctx.pendingPoints[0].id) {
|
|
347
|
+
ctx.pendingFlags.pyramidBaseDone = true;
|
|
348
|
+
ctx.notify();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (baseDone) {
|
|
352
|
+
const base = ctx.pendingPoints;
|
|
353
|
+
const apex = createPoint3D(ctx, hit.x3, hit.y3, hit.z3);
|
|
354
|
+
const faces = [base];
|
|
355
|
+
for (let i = 0; i < base.length; i++) {
|
|
356
|
+
const next = (i + 1) % base.length;
|
|
357
|
+
faces.push([base[i], base[next], apex]);
|
|
358
|
+
}
|
|
359
|
+
finishPolyhedron(ctx, faces);
|
|
360
|
+
ctx.pendingPoints = [];
|
|
361
|
+
ctx.pendingFlags.pyramidBaseDone = false;
|
|
362
|
+
ctx.notify();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const p = resolvePoint(ctx, hit);
|
|
366
|
+
ctx.pendingPoints.push(p);
|
|
367
|
+
ctx.notify();
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
// Curved → B9
|
|
371
|
+
default:
|
|
372
|
+
handleCurvedStep(ctx, tool, hit);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function finishPolyhedron(ctx, faces) {
|
|
377
|
+
const faceColor = ctx.isDark ? "rgba(150, 180, 220, 0.35)" : "rgba(60, 120, 200, 0.25)";
|
|
378
|
+
const edgeColor = ctx.isDark ? "#9ecbff" : "#0066cc";
|
|
379
|
+
for (const face of faces) {
|
|
380
|
+
finishPolygon(ctx, face, {
|
|
381
|
+
fillColor: faceColor,
|
|
382
|
+
fillOpacity: 1,
|
|
383
|
+
strokeColor: edgeColor,
|
|
384
|
+
strokeWidth: 1.5,
|
|
385
|
+
visible: true
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function handleCurvedStep(ctx, tool, hit) {
|
|
390
|
+
switch (tool) {
|
|
391
|
+
case "sphere": {
|
|
392
|
+
const radius = ctx.promptNumber("B\xE1n k\xEDnh m\u1EB7t c\u1EA7u");
|
|
393
|
+
if (radius == null) return;
|
|
394
|
+
const center = resolvePoint(ctx, hit);
|
|
395
|
+
const id = ctx.nextId();
|
|
396
|
+
const ref = ctx.view.create("sphere3d", [center.ref, radius], { id });
|
|
397
|
+
ctx.objMap.set(id, ref);
|
|
398
|
+
ctx.pushLog({
|
|
399
|
+
type: "sphere3d",
|
|
400
|
+
parents: [refByPlaceholder(center.id), radius],
|
|
401
|
+
attributes: { id },
|
|
402
|
+
id
|
|
403
|
+
});
|
|
404
|
+
ctx.notify();
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
case "cone": {
|
|
408
|
+
const baseDone = ctx.pendingFlags.coneBaseDone === true;
|
|
409
|
+
if (!baseDone) {
|
|
410
|
+
const radius2 = ctx.promptNumber("B\xE1n k\xEDnh \u0111\xE1y");
|
|
411
|
+
if (radius2 == null) return;
|
|
412
|
+
const center2 = resolvePoint(ctx, hit);
|
|
413
|
+
ctx.pendingFlags.coneCenter = center2;
|
|
414
|
+
ctx.pendingFlags.coneRadius = radius2;
|
|
415
|
+
ctx.pendingFlags.coneBaseDone = true;
|
|
416
|
+
ctx.notify();
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const center = ctx.pendingFlags.coneCenter;
|
|
420
|
+
const radius = ctx.pendingFlags.coneRadius;
|
|
421
|
+
const apex = createPoint3D(ctx, hit.x3, hit.y3, hit.z3);
|
|
422
|
+
const [cx, cy, cz] = center.coords;
|
|
423
|
+
const basePoints = [];
|
|
424
|
+
for (let i = 0; i < CURVED_SEGMENTS; i++) {
|
|
425
|
+
const theta = i / CURVED_SEGMENTS * Math.PI * 2;
|
|
426
|
+
basePoints.push(
|
|
427
|
+
createPoint3D(
|
|
428
|
+
ctx,
|
|
429
|
+
cx + radius * Math.cos(theta),
|
|
430
|
+
cy + radius * Math.sin(theta),
|
|
431
|
+
cz
|
|
432
|
+
)
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
const faces = [basePoints];
|
|
436
|
+
for (let i = 0; i < CURVED_SEGMENTS; i++) {
|
|
437
|
+
faces.push([basePoints[i], basePoints[(i + 1) % CURVED_SEGMENTS], apex]);
|
|
438
|
+
}
|
|
439
|
+
finishPolyhedron(ctx, faces);
|
|
440
|
+
ctx.pendingFlags.coneBaseDone = false;
|
|
441
|
+
ctx.pendingFlags.coneCenter = void 0;
|
|
442
|
+
ctx.pendingFlags.coneRadius = void 0;
|
|
443
|
+
ctx.notify();
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
case "cylinder": {
|
|
447
|
+
const radius = ctx.promptNumber("B\xE1n k\xEDnh \u0111\xE1y");
|
|
448
|
+
if (radius == null) return;
|
|
449
|
+
const height = ctx.promptNumber("Chi\u1EC1u cao (theo tr\u1EE5c z)");
|
|
450
|
+
if (height == null) return;
|
|
451
|
+
const center = resolvePoint(ctx, hit);
|
|
452
|
+
const [cx, cy, cz] = center.coords;
|
|
453
|
+
const basePoints = [];
|
|
454
|
+
const topPoints = [];
|
|
455
|
+
for (let i = 0; i < CURVED_SEGMENTS; i++) {
|
|
456
|
+
const theta = i / CURVED_SEGMENTS * Math.PI * 2;
|
|
457
|
+
basePoints.push(
|
|
458
|
+
createPoint3D(
|
|
459
|
+
ctx,
|
|
460
|
+
cx + radius * Math.cos(theta),
|
|
461
|
+
cy + radius * Math.sin(theta),
|
|
462
|
+
cz
|
|
463
|
+
)
|
|
464
|
+
);
|
|
465
|
+
topPoints.push(
|
|
466
|
+
createPoint3D(
|
|
467
|
+
ctx,
|
|
468
|
+
cx + radius * Math.cos(theta),
|
|
469
|
+
cy + radius * Math.sin(theta),
|
|
470
|
+
cz + height
|
|
471
|
+
)
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
const faces = [basePoints, topPoints];
|
|
475
|
+
for (let i = 0; i < CURVED_SEGMENTS; i++) {
|
|
476
|
+
const next = (i + 1) % CURVED_SEGMENTS;
|
|
477
|
+
faces.push([basePoints[i], basePoints[next], topPoints[next], topPoints[i]]);
|
|
478
|
+
}
|
|
479
|
+
finishPolyhedron(ctx, faces);
|
|
480
|
+
ctx.notify();
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
// 'solidofrevolution' removed in 0.6.1 — `solidofrevolution3d` is not a valid
|
|
484
|
+
// JSXGraph 1.12.2 element. See Bug #8.
|
|
485
|
+
default:
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
var CURVED_SEGMENTS;
|
|
490
|
+
var init_handlers = __esm({
|
|
491
|
+
"src/stamps/geometry-3d/editor/handlers.ts"() {
|
|
492
|
+
CURVED_SEGMENTS = 16;
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
var MiniBoard3D;
|
|
496
|
+
var init_MiniBoard3D = __esm({
|
|
497
|
+
"src/stamps/geometry-3d/editor/MiniBoard3D.tsx"() {
|
|
498
|
+
"use client";
|
|
499
|
+
init_theme2();
|
|
500
|
+
init_handlers();
|
|
501
|
+
MiniBoard3D = react.forwardRef(function MiniBoard3D2({ isDark, initialState }, ref) {
|
|
502
|
+
const reactId = react.useId();
|
|
503
|
+
const containerId = `geom3d_${reactId.replace(/[^a-zA-Z0-9_]/g, "_")}`;
|
|
504
|
+
const containerRef = react.useRef(null);
|
|
505
|
+
const boardRef = react.useRef(null);
|
|
506
|
+
const viewRef = react.useRef(null);
|
|
507
|
+
const toolRef = react.useRef("move");
|
|
508
|
+
const logRef = react.useRef([]);
|
|
509
|
+
const objMapRef = react.useRef(/* @__PURE__ */ new Map());
|
|
510
|
+
const subsRef = react.useRef(/* @__PURE__ */ new Set());
|
|
511
|
+
const initialBbox3D = react.useRef(
|
|
512
|
+
initialState?.view.bbox3D ?? DEFAULT_VIEW3D.bbox3D
|
|
513
|
+
);
|
|
514
|
+
const ctxRef = react.useRef(null);
|
|
515
|
+
const pointerHandlerRef = react.useRef(null);
|
|
516
|
+
const [showAxes, setShowAxes] = react.useState(initialState?.showAxes ?? true);
|
|
517
|
+
const [showMesh, setShowMesh] = react.useState(initialState?.showMesh ?? false);
|
|
518
|
+
const notify = react.useCallback(() => {
|
|
519
|
+
for (const cb of subsRef.current) cb();
|
|
520
|
+
}, []);
|
|
521
|
+
react.useEffect(() => {
|
|
522
|
+
const div = containerRef.current;
|
|
523
|
+
if (!div) return;
|
|
524
|
+
let cancelled = false;
|
|
525
|
+
let JXG = null;
|
|
526
|
+
let board = null;
|
|
527
|
+
void (async () => {
|
|
528
|
+
JXG = (await import('jsxgraph')).default;
|
|
529
|
+
if (cancelled || !containerRef.current) return;
|
|
530
|
+
JXG.Options.text.display = "internal";
|
|
531
|
+
board = JXG.JSXGraph.initBoard(div, {
|
|
532
|
+
boundingbox: [-6, 6, 6, -6],
|
|
533
|
+
axis: false,
|
|
534
|
+
showCopyright: false,
|
|
535
|
+
showNavigation: false,
|
|
536
|
+
renderer: "svg"
|
|
537
|
+
});
|
|
538
|
+
boardRef.current = board;
|
|
539
|
+
const initView = initialState?.view ?? DEFAULT_VIEW3D;
|
|
540
|
+
const baseAttrs = VIEW3D_ATTRS(isDark);
|
|
541
|
+
const view = board.create(
|
|
542
|
+
"view3d",
|
|
543
|
+
[
|
|
544
|
+
[-5, -5],
|
|
545
|
+
[10, 10],
|
|
546
|
+
[
|
|
547
|
+
[initView.bbox3D[0], initView.bbox3D[3]],
|
|
548
|
+
[initView.bbox3D[1], initView.bbox3D[4]],
|
|
549
|
+
[initView.bbox3D[2], initView.bbox3D[5]]
|
|
550
|
+
]
|
|
551
|
+
],
|
|
552
|
+
{
|
|
553
|
+
...baseAttrs,
|
|
554
|
+
az: { ...baseAttrs.az, value: initView.azimuth },
|
|
555
|
+
el: { ...baseAttrs.el, value: initView.elevation }
|
|
556
|
+
}
|
|
557
|
+
);
|
|
558
|
+
viewRef.current = view;
|
|
559
|
+
let idCounter = 1;
|
|
560
|
+
const ctx = createHandlerContext({
|
|
561
|
+
view,
|
|
562
|
+
pushLog: (e) => {
|
|
563
|
+
logRef.current.push(e);
|
|
564
|
+
notify();
|
|
565
|
+
},
|
|
566
|
+
objMap: objMapRef.current,
|
|
567
|
+
nextId: () => `obj_${Date.now().toString(36)}_${(idCounter++).toString(36)}`,
|
|
568
|
+
isDark,
|
|
569
|
+
promptCoords: (label) => {
|
|
570
|
+
const raw = window.prompt(`${label}
|
|
571
|
+
(\u0111\u1ECBnh d\u1EA1ng "x,y,z")`, "0,0,0");
|
|
572
|
+
if (!raw) return null;
|
|
573
|
+
const parts = raw.split(",").map((s) => Number(s.trim()));
|
|
574
|
+
if (parts.length !== 3 || parts.some((n) => !isFinite(n))) return null;
|
|
575
|
+
return { x: parts[0], y: parts[1], z: parts[2] };
|
|
576
|
+
},
|
|
577
|
+
promptNumber: (label) => {
|
|
578
|
+
const raw = window.prompt(label, "1");
|
|
579
|
+
if (raw == null) return null;
|
|
580
|
+
const n = Number(raw);
|
|
581
|
+
return isFinite(n) ? n : null;
|
|
582
|
+
},
|
|
583
|
+
promptText: (label) => {
|
|
584
|
+
const raw = window.prompt(label, "");
|
|
585
|
+
return raw == null ? null : raw;
|
|
586
|
+
},
|
|
587
|
+
notify
|
|
588
|
+
});
|
|
589
|
+
ctxRef.current = ctx;
|
|
590
|
+
function findExistingPointAt(clientX, clientY) {
|
|
591
|
+
const containerRect = div.getBoundingClientRect();
|
|
592
|
+
const localX = clientX - containerRect.left;
|
|
593
|
+
const localY = clientY - containerRect.top;
|
|
594
|
+
const PICK = 18;
|
|
595
|
+
const svg = div.querySelector("svg");
|
|
596
|
+
if (!svg) return void 0;
|
|
597
|
+
for (const [id, obj] of objMapRef.current) {
|
|
598
|
+
const entry = obj;
|
|
599
|
+
if (entry?.elType !== "point3d") continue;
|
|
600
|
+
const sc = entry.element2D?.coords?.scrCoords;
|
|
601
|
+
if (!sc || sc.length < 3) continue;
|
|
602
|
+
const dx = sc[1] - localX;
|
|
603
|
+
const dy = sc[2] - localY;
|
|
604
|
+
if (dx * dx + dy * dy <= PICK * PICK) return id;
|
|
605
|
+
}
|
|
606
|
+
return void 0;
|
|
607
|
+
}
|
|
608
|
+
const handlePointerDown = (e) => {
|
|
609
|
+
const tool = toolRef.current;
|
|
610
|
+
if (tool === "move") return;
|
|
611
|
+
const existingPointId = findExistingPointAt(e.clientX, e.clientY);
|
|
612
|
+
let x3 = 0;
|
|
613
|
+
let y3 = 0;
|
|
614
|
+
const z3 = 0;
|
|
615
|
+
try {
|
|
616
|
+
const board2d = boardRef.current;
|
|
617
|
+
if (board2d?.getUsrCoordsOfMouse) {
|
|
618
|
+
const uc = board2d.getUsrCoordsOfMouse(e);
|
|
619
|
+
if (Array.isArray(uc) && uc.length >= 2) {
|
|
620
|
+
x3 = uc[0];
|
|
621
|
+
y3 = uc[1];
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
} catch {
|
|
625
|
+
}
|
|
626
|
+
const hit = { x3, y3, z3, existingPointId };
|
|
627
|
+
handleToolStep(ctx, tool, hit);
|
|
628
|
+
};
|
|
629
|
+
const svgEl = div.querySelector("svg");
|
|
630
|
+
const targetEl = svgEl ?? div;
|
|
631
|
+
const handlePointerDownEv = (e) => handlePointerDown(e);
|
|
632
|
+
targetEl.addEventListener("pointerdown", handlePointerDownEv);
|
|
633
|
+
pointerHandlerRef.current = { el: targetEl, fn: handlePointerDownEv };
|
|
634
|
+
if (initialState?.elements?.length) {
|
|
635
|
+
const map = objMapRef.current;
|
|
636
|
+
for (const el of initialState.elements) {
|
|
637
|
+
const parents = el.parents.map(
|
|
638
|
+
(p2) => typeof p2 === "string" && p2.startsWith("@id:") ? map.get(p2.slice(4)) : p2
|
|
639
|
+
);
|
|
640
|
+
const obj = view.create(el.type, parents, {
|
|
641
|
+
...el.attributes,
|
|
642
|
+
id: el.id,
|
|
643
|
+
name: el.label
|
|
644
|
+
});
|
|
645
|
+
map.set(el.id, obj);
|
|
646
|
+
logRef.current.push(el);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
})();
|
|
650
|
+
return () => {
|
|
651
|
+
cancelled = true;
|
|
652
|
+
if (pointerHandlerRef.current) {
|
|
653
|
+
pointerHandlerRef.current.el.removeEventListener(
|
|
654
|
+
"pointerdown",
|
|
655
|
+
pointerHandlerRef.current.fn
|
|
656
|
+
);
|
|
657
|
+
pointerHandlerRef.current = null;
|
|
658
|
+
}
|
|
659
|
+
try {
|
|
660
|
+
if (board && JXG) JXG.JSXGraph.freeBoard(board);
|
|
661
|
+
} catch {
|
|
662
|
+
}
|
|
663
|
+
boardRef.current = null;
|
|
664
|
+
viewRef.current = null;
|
|
665
|
+
ctxRef.current = null;
|
|
666
|
+
objMapRef.current.clear();
|
|
667
|
+
};
|
|
668
|
+
}, []);
|
|
669
|
+
const handleRef = react.useRef(null);
|
|
670
|
+
handleRef.current = {
|
|
671
|
+
getContainer: () => containerRef.current,
|
|
672
|
+
getTool: () => toolRef.current,
|
|
673
|
+
setTool: (t) => {
|
|
674
|
+
toolRef.current = t;
|
|
675
|
+
notify();
|
|
676
|
+
},
|
|
677
|
+
// Sync toạ độ live của free point3d về log trước khi trả ra. JSXGraph
|
|
678
|
+
// cho phép drag point3d (parents=[x,y,z] không có ref), việc drag chỉ
|
|
679
|
+
// cập nhật obj.X()/Y()/Z() chứ không đụng log → re-edit + Chèn sẽ
|
|
680
|
+
// serialize toạ độ cũ → SVG không đổi → fileId trùng → user thấy
|
|
681
|
+
// "k thay đổi". Line/plane/polygon/sphere tham chiếu point qua @id nên
|
|
682
|
+
// auto-update theo.
|
|
683
|
+
getCreationLog: () => logRef.current.map((e) => {
|
|
684
|
+
if (e.type !== "point3d") return { ...e };
|
|
685
|
+
const parents = e.parents;
|
|
686
|
+
if (!Array.isArray(parents) || parents.length !== 3) return { ...e };
|
|
687
|
+
if (typeof parents[0] !== "number" || typeof parents[1] !== "number" || typeof parents[2] !== "number") return { ...e };
|
|
688
|
+
const obj = objMapRef.current.get(e.id);
|
|
689
|
+
if (!obj || typeof obj.X !== "function" || typeof obj.Y !== "function" || typeof obj.Z !== "function") return { ...e };
|
|
690
|
+
const x = obj.X();
|
|
691
|
+
const y = obj.Y();
|
|
692
|
+
const z = obj.Z();
|
|
693
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) return { ...e };
|
|
694
|
+
return { ...e, parents: [x, y, z] };
|
|
695
|
+
}),
|
|
696
|
+
pushLog: (e) => {
|
|
697
|
+
logRef.current.push(e);
|
|
698
|
+
notify();
|
|
699
|
+
},
|
|
700
|
+
getViewState: () => {
|
|
701
|
+
const v = viewRef.current;
|
|
702
|
+
return {
|
|
703
|
+
azimuth: v?.az?.Value?.() ?? DEFAULT_VIEW3D.azimuth,
|
|
704
|
+
elevation: v?.el?.Value?.() ?? DEFAULT_VIEW3D.elevation,
|
|
705
|
+
bbox3D: initialBbox3D.current
|
|
706
|
+
};
|
|
707
|
+
},
|
|
708
|
+
getBbox: () => [-6, 6, 6, -6],
|
|
709
|
+
getShowAxes: () => showAxes,
|
|
710
|
+
getShowMesh: () => showMesh,
|
|
711
|
+
setShowAxes: (b) => {
|
|
712
|
+
setShowAxes(b);
|
|
713
|
+
notify();
|
|
714
|
+
},
|
|
715
|
+
setShowMesh: (b) => {
|
|
716
|
+
setShowMesh(b);
|
|
717
|
+
notify();
|
|
718
|
+
},
|
|
719
|
+
resetView: () => {
|
|
720
|
+
notify();
|
|
721
|
+
},
|
|
722
|
+
undo: () => {
|
|
723
|
+
logRef.current.pop();
|
|
724
|
+
notify();
|
|
725
|
+
},
|
|
726
|
+
canUndo: () => logRef.current.length > 0,
|
|
727
|
+
snapshotSVG: () => {
|
|
728
|
+
const div = containerRef.current;
|
|
729
|
+
if (!div) return { svgString: "", width: 0, height: 0 };
|
|
730
|
+
const svg = div.querySelector("svg");
|
|
731
|
+
if (!svg) return { svgString: "", width: 0, height: 0 };
|
|
732
|
+
const clone = svg.cloneNode(true);
|
|
733
|
+
const rect = svg.getBoundingClientRect();
|
|
734
|
+
const width = rect.width || 600;
|
|
735
|
+
const height = rect.height || 600;
|
|
736
|
+
clone.setAttribute("width", String(width));
|
|
737
|
+
clone.setAttribute("height", String(height));
|
|
738
|
+
return {
|
|
739
|
+
svgString: new XMLSerializer().serializeToString(clone),
|
|
740
|
+
width,
|
|
741
|
+
height
|
|
742
|
+
};
|
|
743
|
+
},
|
|
744
|
+
subscribe: (cb) => {
|
|
745
|
+
subsRef.current.add(cb);
|
|
746
|
+
return () => {
|
|
747
|
+
subsRef.current.delete(cb);
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
react.useImperativeHandle(ref, () => handleRef.current, []);
|
|
752
|
+
const p = paletteFor2(isDark);
|
|
753
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
754
|
+
"div",
|
|
755
|
+
{
|
|
756
|
+
ref: containerRef,
|
|
757
|
+
id: containerId,
|
|
758
|
+
style: {
|
|
759
|
+
width: "100%",
|
|
760
|
+
height: "100%",
|
|
761
|
+
background: p.view3dBg,
|
|
762
|
+
position: "relative",
|
|
763
|
+
// Clip JSXGraph mesh3d/bounding-box paths that project outside the
|
|
764
|
+
// board container (Bug #4) — without this they overlap LeftPanel and
|
|
765
|
+
// block pointer events.
|
|
766
|
+
overflow: "hidden"
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
);
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
var EditorPanel;
|
|
774
|
+
var init_EditorPanel = __esm({
|
|
775
|
+
"src/stamps/geometry-3d/editor/EditorPanel.tsx"() {
|
|
776
|
+
"use client";
|
|
777
|
+
init_MiniBoard3D();
|
|
778
|
+
EditorPanel = react.forwardRef(function EditorPanel2({ isDark, initial, onInsert, onClose, isMobile = false, withLeftPanel = false, onBoardReady, onOpenDrawer }, ref) {
|
|
779
|
+
const boardRef = react.useRef(null);
|
|
780
|
+
const [ready, setReady] = react.useState(false);
|
|
781
|
+
const onBoardReadyRef = react.useRef(onBoardReady);
|
|
782
|
+
onBoardReadyRef.current = onBoardReady;
|
|
783
|
+
const setBoard = react.useCallback((h) => {
|
|
784
|
+
boardRef.current = h;
|
|
785
|
+
setReady(!!h);
|
|
786
|
+
onBoardReadyRef.current?.(h);
|
|
787
|
+
}, []);
|
|
788
|
+
const performInsert = react.useCallback(() => {
|
|
789
|
+
const board = boardRef.current;
|
|
790
|
+
if (!board) return false;
|
|
791
|
+
const log = board.getCreationLog();
|
|
792
|
+
if (log.length === 0) return false;
|
|
793
|
+
const view = board.getViewState();
|
|
794
|
+
const state = {
|
|
795
|
+
version: 1,
|
|
796
|
+
bbox: board.getBbox(),
|
|
797
|
+
view,
|
|
798
|
+
showAxes: board.getShowAxes(),
|
|
799
|
+
showMesh: board.getShowMesh(),
|
|
800
|
+
elements: log
|
|
801
|
+
};
|
|
802
|
+
const snap = board.snapshotSVG();
|
|
803
|
+
onInsert(JSON.stringify(state), snap.svgString, snap.width, snap.height);
|
|
804
|
+
return true;
|
|
805
|
+
}, [onInsert]);
|
|
806
|
+
react.useImperativeHandle(
|
|
807
|
+
ref,
|
|
808
|
+
() => ({
|
|
809
|
+
tryInsert: performInsert,
|
|
810
|
+
hasContent: () => (boardRef.current?.getCreationLog().length ?? 0) > 0
|
|
811
|
+
}),
|
|
812
|
+
[performInsert]
|
|
813
|
+
);
|
|
814
|
+
const handleInsert = react.useCallback(() => {
|
|
815
|
+
performInsert();
|
|
816
|
+
}, [performInsert]);
|
|
817
|
+
const wrapperStyle = isMobile ? { position: "fixed", inset: 0, zIndex: 40 } : {
|
|
818
|
+
position: "absolute",
|
|
819
|
+
top: "50%",
|
|
820
|
+
left: withLeftPanel ? "calc(50% + 120px)" : "50%",
|
|
821
|
+
transform: "translate(-50%, -50%)",
|
|
822
|
+
zIndex: 40
|
|
823
|
+
};
|
|
824
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
825
|
+
"div",
|
|
826
|
+
{
|
|
827
|
+
role: "dialog",
|
|
828
|
+
"aria-label": "D\u1EF1ng h\xECnh h\u1ECDc 3D",
|
|
829
|
+
"data-testid": "geom3d-editor-panel",
|
|
830
|
+
"data-stamp-area": "true",
|
|
831
|
+
"data-mobile-editor": isMobile ? "true" : void 0,
|
|
832
|
+
style: wrapperStyle,
|
|
833
|
+
className: [
|
|
834
|
+
isDark ? "theme--dark " : "",
|
|
835
|
+
"flex flex-col overflow-hidden bg-white",
|
|
836
|
+
isMobile ? "h-full w-full" : "h-[600px] max-h-[85vh] w-[760px] max-w-[calc(100vw-280px)] rounded-lg border border-slate-300 shadow-2xl ring-1 ring-black/5"
|
|
837
|
+
].join(" "),
|
|
838
|
+
children: [
|
|
839
|
+
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center gap-2 border-b border-slate-200 bg-gradient-to-r from-blue-600 to-cyan-600 px-3 py-2 text-white", children: [
|
|
840
|
+
isMobile && /* @__PURE__ */ jsxRuntime.jsx(
|
|
841
|
+
"button",
|
|
842
|
+
{
|
|
843
|
+
type: "button",
|
|
844
|
+
onClick: onOpenDrawer,
|
|
845
|
+
"aria-label": "M\u1EDF ng\u0103n c\xF4ng c\u1EE5",
|
|
846
|
+
className: "-ml-1 inline-flex h-10 w-10 items-center justify-center rounded transition hover:bg-white/15",
|
|
847
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
848
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "6", x2: "20", y2: "6" }),
|
|
849
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
|
|
850
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "18", x2: "20", y2: "18" })
|
|
851
|
+
] })
|
|
852
|
+
}
|
|
853
|
+
),
|
|
854
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex flex-1 items-center gap-2 text-sm font-semibold", children: [
|
|
855
|
+
/* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 7 L14 4 L20 7 L14 10 Z M4 7 L4 17 L14 20 L14 10 M14 20 L20 17 L20 7" }) }),
|
|
856
|
+
"H\xECnh h\u1ECDc kh\xF4ng gian (3D)"
|
|
857
|
+
] }),
|
|
858
|
+
isMobile && /* @__PURE__ */ jsxRuntime.jsx(
|
|
859
|
+
"button",
|
|
860
|
+
{
|
|
861
|
+
type: "button",
|
|
862
|
+
onClick: handleInsert,
|
|
863
|
+
disabled: !ready,
|
|
864
|
+
"data-testid": "geom3d-insert-btn-mobile",
|
|
865
|
+
className: "rounded bg-white/15 px-3 py-1.5 text-xs font-semibold transition hover:bg-white/25 disabled:opacity-50",
|
|
866
|
+
children: "Ch\xE8n"
|
|
867
|
+
}
|
|
868
|
+
),
|
|
869
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
870
|
+
"button",
|
|
871
|
+
{
|
|
872
|
+
onClick: onClose,
|
|
873
|
+
"aria-label": "\u0110\xF3ng",
|
|
874
|
+
className: "inline-flex h-9 w-9 items-center justify-center rounded transition hover:bg-white/15",
|
|
875
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
876
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
877
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
878
|
+
] })
|
|
879
|
+
}
|
|
880
|
+
)
|
|
881
|
+
] }),
|
|
882
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(MiniBoard3D, { ref: setBoard, isDark, initialState: initial }) }),
|
|
883
|
+
!isMobile && /* @__PURE__ */ jsxRuntime.jsxs("footer", { className: "flex items-center justify-between border-t border-slate-200 bg-slate-50 px-3 py-2", children: [
|
|
884
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-slate-500", children: "Ch\u1ECDn c\xF4ng c\u1EE5 b\xEAn tr\xE1i, click tr\xEAn b\u1EA3ng \u0111\u1EC3 d\u1EF1ng h\xECnh." }),
|
|
885
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
|
|
886
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
887
|
+
"button",
|
|
888
|
+
{
|
|
889
|
+
onClick: onClose,
|
|
890
|
+
className: "rounded border border-slate-300 bg-white px-3 py-1 text-xs font-medium text-slate-700 transition hover:bg-slate-100",
|
|
891
|
+
children: "Hu\u1EF7"
|
|
892
|
+
}
|
|
893
|
+
),
|
|
894
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
895
|
+
"button",
|
|
896
|
+
{
|
|
897
|
+
onClick: handleInsert,
|
|
898
|
+
disabled: !ready,
|
|
899
|
+
"data-testid": "geom3d-insert-btn",
|
|
900
|
+
className: "rounded bg-blue-600 px-3 py-1 text-xs font-medium text-white transition hover:bg-blue-700 disabled:opacity-50",
|
|
901
|
+
children: "Ch\xE8n"
|
|
902
|
+
}
|
|
903
|
+
)
|
|
904
|
+
] })
|
|
905
|
+
] })
|
|
906
|
+
]
|
|
907
|
+
}
|
|
908
|
+
);
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
// src/stamps/geometry-3d/editor/tools.ts
|
|
914
|
+
function letterForGroup3D(g) {
|
|
915
|
+
const idx = GROUP_ORDER_3D.indexOf(g);
|
|
916
|
+
return idx >= 0 ? String.fromCharCode(A_CODE_3D + idx) : "";
|
|
917
|
+
}
|
|
918
|
+
var GROUP_LABELS_3D, GROUP_ORDER_3D, A_CODE_3D, TOOLS_3D;
|
|
919
|
+
var init_tools = __esm({
|
|
920
|
+
"src/stamps/geometry-3d/editor/tools.ts"() {
|
|
921
|
+
GROUP_LABELS_3D = {
|
|
922
|
+
view: "Xem",
|
|
923
|
+
primitive: "C\u01A1 b\u1EA3n",
|
|
924
|
+
solid: "Kh\u1ED1i \u0111a di\u1EC7n",
|
|
925
|
+
curved: "Kh\u1ED1i cong",
|
|
926
|
+
meta: "Kh\xE1c"
|
|
927
|
+
};
|
|
928
|
+
GROUP_ORDER_3D = [
|
|
929
|
+
"view",
|
|
930
|
+
"primitive",
|
|
931
|
+
"solid",
|
|
932
|
+
"curved",
|
|
933
|
+
"meta"
|
|
934
|
+
];
|
|
935
|
+
A_CODE_3D = "A".charCodeAt(0);
|
|
936
|
+
TOOLS_3D = [
|
|
937
|
+
{ key: "move", label: "Di chuy\u1EC3n", group: "view", stepsRequired: 0 },
|
|
938
|
+
{ key: "point", label: "\u0110i\u1EC3m", group: "primitive", stepsRequired: 1, hint: "Nh\u1EADp (x, y, z)" },
|
|
939
|
+
{ key: "segment", label: "\u0110o\u1EA1n th\u1EB3ng", group: "primitive", stepsRequired: 2 },
|
|
940
|
+
{ key: "line", label: "\u0110\u01B0\u1EDDng th\u1EB3ng", group: "primitive", stepsRequired: 2 },
|
|
941
|
+
{ key: "plane", label: "M\u1EB7t ph\u1EB3ng", group: "primitive", stepsRequired: 3 },
|
|
942
|
+
{ key: "triangle", label: "Tam gi\xE1c", group: "primitive", stepsRequired: 3 },
|
|
943
|
+
{
|
|
944
|
+
key: "polygon",
|
|
945
|
+
label: "\u0110a gi\xE1c",
|
|
946
|
+
group: "primitive",
|
|
947
|
+
stepsRequired: 3,
|
|
948
|
+
hint: "Click tr\u1EDF l\u1EA1i \u0111i\u1EC3m \u0111\u1EA7u \u0111\u1EC3 \u0111\xF3ng"
|
|
949
|
+
},
|
|
950
|
+
{ key: "tetrahedron", label: "T\u1EE9 di\u1EC7n", group: "solid", stepsRequired: 4 },
|
|
951
|
+
{
|
|
952
|
+
key: "parallelepiped",
|
|
953
|
+
label: "H\xECnh h\u1ED9p",
|
|
954
|
+
group: "solid",
|
|
955
|
+
stepsRequired: 1,
|
|
956
|
+
hint: "1 \u0111\u1EC9nh + 3 vector"
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
key: "prism",
|
|
960
|
+
label: "L\u0103ng tr\u1EE5",
|
|
961
|
+
group: "solid",
|
|
962
|
+
stepsRequired: 3,
|
|
963
|
+
hint: "\u0110a gi\xE1c \u0111\xE1y + chi\u1EC1u cao"
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
key: "pyramid",
|
|
967
|
+
label: "Ch\xF3p",
|
|
968
|
+
group: "solid",
|
|
969
|
+
stepsRequired: 4,
|
|
970
|
+
hint: "\u0110a gi\xE1c \u0111\xE1y + \u0111\u1EC9nh"
|
|
971
|
+
},
|
|
972
|
+
{ key: "sphere", label: "M\u1EB7t c\u1EA7u", group: "curved", stepsRequired: 1, hint: "T\xE2m + b\xE1n k\xEDnh" },
|
|
973
|
+
{
|
|
974
|
+
key: "cone",
|
|
975
|
+
label: "H\xECnh n\xF3n",
|
|
976
|
+
group: "curved",
|
|
977
|
+
stepsRequired: 2,
|
|
978
|
+
hint: "T\xE2m \u0111\xE1y + b\xE1n k\xEDnh + \u0111\u1EC9nh"
|
|
979
|
+
},
|
|
980
|
+
{
|
|
981
|
+
key: "cylinder",
|
|
982
|
+
label: "H\xECnh tr\u1EE5",
|
|
983
|
+
group: "curved",
|
|
984
|
+
stepsRequired: 1,
|
|
985
|
+
hint: "T\xE2m \u0111\xE1y + b\xE1n k\xEDnh + chi\u1EC1u cao"
|
|
986
|
+
},
|
|
987
|
+
{ key: "label", label: "Nh\xE3n", group: "meta", stepsRequired: 1, hint: "G\u1EAFn v\xE0o \u0111i\u1EC3m" }
|
|
988
|
+
];
|
|
989
|
+
}
|
|
990
|
+
});
|
|
991
|
+
function ToolButton({ toolKey, label, hint, active, onClick, icon, badge }) {
|
|
992
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
993
|
+
"button",
|
|
994
|
+
{
|
|
995
|
+
type: "button",
|
|
996
|
+
title: hint ? `${label} \u2014 ${hint}` : label,
|
|
997
|
+
"aria-label": label,
|
|
998
|
+
"aria-pressed": active,
|
|
999
|
+
onClick,
|
|
1000
|
+
"data-active": active || void 0,
|
|
1001
|
+
"data-tool": toolKey,
|
|
1002
|
+
className: [
|
|
1003
|
+
"relative flex h-8 items-center justify-center rounded-md transition",
|
|
1004
|
+
active ? "bg-blue-600 text-white shadow-sm" : "text-slate-700 hover:bg-slate-100 hover:text-slate-900"
|
|
1005
|
+
].join(" "),
|
|
1006
|
+
children: [
|
|
1007
|
+
icon,
|
|
1008
|
+
badge
|
|
1009
|
+
]
|
|
1010
|
+
}
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
var stroke, ICONS_3D;
|
|
1014
|
+
var init_toolButtons = __esm({
|
|
1015
|
+
"src/stamps/geometry-3d/editor/toolButtons.tsx"() {
|
|
1016
|
+
"use client";
|
|
1017
|
+
stroke = {
|
|
1018
|
+
fill: "none",
|
|
1019
|
+
stroke: "currentColor",
|
|
1020
|
+
strokeWidth: 1.5,
|
|
1021
|
+
strokeLinecap: "round",
|
|
1022
|
+
strokeLinejoin: "round"
|
|
1023
|
+
};
|
|
1024
|
+
ICONS_3D = {
|
|
1025
|
+
move: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", ...stroke, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M5 9l-3 3 3 3M19 9l3 3-3 3M9 5l3-3 3 3M9 19l3 3 3-3" }) }),
|
|
1026
|
+
point: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "3", fill: "currentColor" }) }),
|
|
1027
|
+
segment: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", ...stroke, children: [
|
|
1028
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "4", y1: "20", x2: "20", y2: "4" }),
|
|
1029
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "4", cy: "20", r: "1.5", fill: "currentColor", stroke: "none" }),
|
|
1030
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "20", cy: "4", r: "1.5", fill: "currentColor", stroke: "none" })
|
|
1031
|
+
] }),
|
|
1032
|
+
line: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", ...stroke, children: /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "2", y1: "22", x2: "22", y2: "2" }) }),
|
|
1033
|
+
plane: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", ...stroke, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 18 L8 8 L21 6 L16 18 Z" }) }),
|
|
1034
|
+
triangle: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", ...stroke, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 4 L21 20 L3 20 Z" }) }),
|
|
1035
|
+
polygon: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", ...stroke, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 3 L20 9 L17 19 L7 19 L4 9 Z" }) }),
|
|
1036
|
+
tetrahedron: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", ...stroke, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 3 L20 20 L4 20 Z M12 3 L12 20" }) }),
|
|
1037
|
+
parallelepiped: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", ...stroke, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 7 L14 4 L20 7 L14 10 Z M4 7 L4 17 L14 20 L14 10 M14 20 L20 17 L20 7" }) }),
|
|
1038
|
+
prism: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", ...stroke, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 4 L18 8 L18 20 L12 16 Z M12 4 L6 8 L6 20 L12 16 M6 8 L12 12 L18 8 M6 20 L18 20" }) }),
|
|
1039
|
+
pyramid: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", ...stroke, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 3 L4 20 L20 20 Z M12 3 L12 20" }) }),
|
|
1040
|
+
sphere: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", ...stroke, children: [
|
|
1041
|
+
/* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "8" }),
|
|
1042
|
+
/* @__PURE__ */ jsxRuntime.jsx("ellipse", { cx: "12", cy: "12", rx: "8", ry: "3" })
|
|
1043
|
+
] }),
|
|
1044
|
+
cone: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", ...stroke, children: [
|
|
1045
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 3 L4 20 L20 20 Z" }),
|
|
1046
|
+
/* @__PURE__ */ jsxRuntime.jsx("ellipse", { cx: "12", cy: "20", rx: "8", ry: "2" })
|
|
1047
|
+
] }),
|
|
1048
|
+
cylinder: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "20", height: "20", viewBox: "0 0 24 24", ...stroke, children: [
|
|
1049
|
+
/* @__PURE__ */ jsxRuntime.jsx("ellipse", { cx: "12", cy: "5", rx: "6", ry: "2" }),
|
|
1050
|
+
/* @__PURE__ */ jsxRuntime.jsx("ellipse", { cx: "12", cy: "19", rx: "6", ry: "2" }),
|
|
1051
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "5", x2: "6", y2: "19" }),
|
|
1052
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "5", x2: "18", y2: "19" })
|
|
1053
|
+
] }),
|
|
1054
|
+
label: /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", ...stroke, children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 4 H 16 L 20 8 L 16 12 H 4 Z" }) })
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
function MobileToolDrawer({
|
|
1059
|
+
title,
|
|
1060
|
+
headerIcon,
|
|
1061
|
+
chips,
|
|
1062
|
+
actions,
|
|
1063
|
+
groups,
|
|
1064
|
+
activeTool,
|
|
1065
|
+
onToolSelect,
|
|
1066
|
+
drawerOpen,
|
|
1067
|
+
onDrawerClose,
|
|
1068
|
+
isDark,
|
|
1069
|
+
testId
|
|
1070
|
+
}) {
|
|
1071
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1072
|
+
drawerOpen && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1073
|
+
"div",
|
|
1074
|
+
{
|
|
1075
|
+
className: "stamp-drawer-backdrop",
|
|
1076
|
+
onPointerDown: onDrawerClose,
|
|
1077
|
+
"aria-hidden": "true"
|
|
1078
|
+
}
|
|
1079
|
+
),
|
|
1080
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1081
|
+
"aside",
|
|
1082
|
+
{
|
|
1083
|
+
role: "complementary",
|
|
1084
|
+
"aria-label": title,
|
|
1085
|
+
"aria-hidden": !drawerOpen ? "true" : void 0,
|
|
1086
|
+
"data-testid": testId,
|
|
1087
|
+
"data-stamp-area": "true",
|
|
1088
|
+
"data-mobile-drawer": "true",
|
|
1089
|
+
"data-geo-mobile": "true",
|
|
1090
|
+
"data-drawer-state": drawerOpen ? "open" : "closed",
|
|
1091
|
+
className: [
|
|
1092
|
+
isDark ? "theme--dark " : "",
|
|
1093
|
+
"stamp-drawer-mobile flex flex-col border-r border-slate-200 bg-white shadow-md"
|
|
1094
|
+
].join(""),
|
|
1095
|
+
children: [
|
|
1096
|
+
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center justify-between border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white px-4 py-3", children: [
|
|
1097
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex items-center gap-2 text-base font-semibold text-slate-800", children: [
|
|
1098
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex h-7 w-7 items-center justify-center rounded-lg bg-emerald-50 text-emerald-700", children: headerIcon }),
|
|
1099
|
+
title
|
|
1100
|
+
] }),
|
|
1101
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1102
|
+
"button",
|
|
1103
|
+
{
|
|
1104
|
+
type: "button",
|
|
1105
|
+
onClick: onDrawerClose,
|
|
1106
|
+
"aria-label": "\u0110\xF3ng ng\u0103n c\xF4ng c\u1EE5",
|
|
1107
|
+
className: "inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
|
|
1108
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
1109
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
1110
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
1111
|
+
] })
|
|
1112
|
+
}
|
|
1113
|
+
)
|
|
1114
|
+
] }),
|
|
1115
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "sticky top-0 z-10 flex items-center gap-2 border-b border-slate-200 bg-white/95 px-3 py-2 backdrop-blur", children: [
|
|
1116
|
+
chips.map((c) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1117
|
+
"button",
|
|
1118
|
+
{
|
|
1119
|
+
type: "button",
|
|
1120
|
+
role: "switch",
|
|
1121
|
+
"aria-pressed": c.pressed,
|
|
1122
|
+
"aria-label": c.label,
|
|
1123
|
+
"data-testid": c.testId,
|
|
1124
|
+
onClick: () => c.onToggle(!c.pressed),
|
|
1125
|
+
className: "geo-mobile-chip",
|
|
1126
|
+
children: [
|
|
1127
|
+
c.icon,
|
|
1128
|
+
c.label
|
|
1129
|
+
]
|
|
1130
|
+
},
|
|
1131
|
+
c.label
|
|
1132
|
+
)),
|
|
1133
|
+
actions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "ml-auto flex items-center gap-1", children: actions.map((a) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1134
|
+
"button",
|
|
1135
|
+
{
|
|
1136
|
+
type: "button",
|
|
1137
|
+
onClick: a.onClick,
|
|
1138
|
+
disabled: a.disabled,
|
|
1139
|
+
"aria-label": a.label,
|
|
1140
|
+
title: a.title ?? a.label,
|
|
1141
|
+
className: "inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent",
|
|
1142
|
+
children: a.icon
|
|
1143
|
+
},
|
|
1144
|
+
a.label
|
|
1145
|
+
)) })
|
|
1146
|
+
] }),
|
|
1147
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1148
|
+
"div",
|
|
1149
|
+
{
|
|
1150
|
+
className: "min-h-0 flex-1 overflow-y-auto",
|
|
1151
|
+
style: { paddingBottom: "calc(0.75rem + env(safe-area-inset-bottom))" },
|
|
1152
|
+
children: groups.map((g) => /* @__PURE__ */ jsxRuntime.jsxs("section", { className: "px-3 pt-3 pb-1", children: [
|
|
1153
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h4", { className: "mb-2 flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-slate-500", children: [
|
|
1154
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "h-1 w-1 rounded-full bg-emerald-500" }),
|
|
1155
|
+
g.groupLabel
|
|
1156
|
+
] }),
|
|
1157
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-3 gap-2", children: g.tools.map((t) => {
|
|
1158
|
+
const active = activeTool === t.key;
|
|
1159
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1160
|
+
"button",
|
|
1161
|
+
{
|
|
1162
|
+
type: "button",
|
|
1163
|
+
"aria-label": t.label,
|
|
1164
|
+
"aria-pressed": active,
|
|
1165
|
+
"data-tool": t.key,
|
|
1166
|
+
onClick: () => {
|
|
1167
|
+
onToolSelect(t.key);
|
|
1168
|
+
onDrawerClose();
|
|
1169
|
+
},
|
|
1170
|
+
className: [
|
|
1171
|
+
"flex flex-col items-center justify-center gap-1.5 rounded-2xl px-2 py-3 transition active:scale-95",
|
|
1172
|
+
active ? "geo-mobile-tool-active" : "bg-slate-50 text-slate-700 hover:bg-slate-100"
|
|
1173
|
+
].join(" "),
|
|
1174
|
+
children: [
|
|
1175
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-6 w-6 items-center justify-center", children: t.icon }),
|
|
1176
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-center text-[11px] font-medium leading-tight line-clamp-2", children: t.label })
|
|
1177
|
+
]
|
|
1178
|
+
},
|
|
1179
|
+
t.key
|
|
1180
|
+
);
|
|
1181
|
+
}) })
|
|
1182
|
+
] }, g.group))
|
|
1183
|
+
}
|
|
1184
|
+
)
|
|
1185
|
+
]
|
|
1186
|
+
}
|
|
1187
|
+
)
|
|
1188
|
+
] });
|
|
1189
|
+
}
|
|
1190
|
+
var init_MobileToolDrawer = __esm({
|
|
1191
|
+
"src/stamps/shared/MobileToolDrawer.tsx"() {
|
|
1192
|
+
"use client";
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
function Shell({ title, icon, onClose, children, isDark }) {
|
|
1196
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1197
|
+
"aside",
|
|
1198
|
+
{
|
|
1199
|
+
role: "complementary",
|
|
1200
|
+
"aria-label": title,
|
|
1201
|
+
"data-testid": "geom3d-left-panel",
|
|
1202
|
+
"data-stamp-area": "true",
|
|
1203
|
+
className: [
|
|
1204
|
+
isDark ? "theme--dark " : "",
|
|
1205
|
+
"absolute left-0 top-0 z-30 flex h-full w-60 flex-col border-r border-slate-200 bg-white shadow-md animate-in slide-in-from-left duration-200"
|
|
1206
|
+
].join(""),
|
|
1207
|
+
children: [
|
|
1208
|
+
/* @__PURE__ */ jsxRuntime.jsxs("header", { className: "flex items-center justify-between border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white px-3 py-2", children: [
|
|
1209
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h3", { className: "flex items-center gap-2 text-sm font-semibold text-slate-800", children: [
|
|
1210
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-base leading-none", children: icon }),
|
|
1211
|
+
title
|
|
1212
|
+
] }),
|
|
1213
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1214
|
+
"button",
|
|
1215
|
+
{
|
|
1216
|
+
onClick: onClose,
|
|
1217
|
+
"aria-label": "\u0110\xF3ng",
|
|
1218
|
+
className: "rounded p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-800",
|
|
1219
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(CloseIcon, {})
|
|
1220
|
+
}
|
|
1221
|
+
)
|
|
1222
|
+
] }),
|
|
1223
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-0 flex-1 overflow-y-auto p-3 space-y-4", children })
|
|
1224
|
+
]
|
|
1225
|
+
}
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
function Section({ label, children }) {
|
|
1229
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
|
|
1230
|
+
/* @__PURE__ */ jsxRuntime.jsx("h4", { className: "mb-1.5 text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: label }),
|
|
1231
|
+
children
|
|
1232
|
+
] });
|
|
1233
|
+
}
|
|
1234
|
+
function CloseIcon() {
|
|
1235
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
1236
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" }),
|
|
1237
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" })
|
|
1238
|
+
] });
|
|
1239
|
+
}
|
|
1240
|
+
function UndoIcon() {
|
|
1241
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
1242
|
+
/* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "3 7 3 13 9 13" }),
|
|
1243
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3.51 13a9 9 0 1 0 2.13-9.36L3 7" })
|
|
1244
|
+
] });
|
|
1245
|
+
}
|
|
1246
|
+
function ResetViewIcon() {
|
|
1247
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
1248
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }),
|
|
1249
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M3 3v5h5" })
|
|
1250
|
+
] });
|
|
1251
|
+
}
|
|
1252
|
+
function AxisIcon3D() {
|
|
1253
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
1254
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "20", x2: "12", y2: "4" }),
|
|
1255
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "12", x2: "22", y2: "6" }),
|
|
1256
|
+
/* @__PURE__ */ jsxRuntime.jsx("line", { x1: "12", y1: "12", x2: "2", y2: "18" })
|
|
1257
|
+
] });
|
|
1258
|
+
}
|
|
1259
|
+
function MeshIcon() {
|
|
1260
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
1261
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 8 L12 4 L20 8 L12 12 Z" }),
|
|
1262
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 8 L4 16 L12 20 L12 12" }),
|
|
1263
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 20 L20 16 L20 8" })
|
|
1264
|
+
] });
|
|
1265
|
+
}
|
|
1266
|
+
function useToolHoverTooltip() {
|
|
1267
|
+
const [hover, setHover] = react.useState(null);
|
|
1268
|
+
const [portalReady, setPortalReady] = react.useState(false);
|
|
1269
|
+
const hoverTimerRef = react.useRef(null);
|
|
1270
|
+
react.useEffect(() => {
|
|
1271
|
+
setPortalReady(true);
|
|
1272
|
+
return () => {
|
|
1273
|
+
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
|
1274
|
+
};
|
|
1275
|
+
}, []);
|
|
1276
|
+
const showHover = react.useCallback((el, t) => {
|
|
1277
|
+
if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current);
|
|
1278
|
+
hoverTimerRef.current = setTimeout(() => {
|
|
1279
|
+
const r = el.getBoundingClientRect();
|
|
1280
|
+
setHover({ label: t.label, hint: t.hint, x: r.right, y: r.top + r.height / 2 });
|
|
1281
|
+
}, TOOLTIP_DELAY_MS);
|
|
1282
|
+
}, []);
|
|
1283
|
+
const hideHover = react.useCallback(() => {
|
|
1284
|
+
if (hoverTimerRef.current) {
|
|
1285
|
+
clearTimeout(hoverTimerRef.current);
|
|
1286
|
+
hoverTimerRef.current = null;
|
|
1287
|
+
}
|
|
1288
|
+
setHover(null);
|
|
1289
|
+
}, []);
|
|
1290
|
+
return { hover, portalReady, showHover, hideHover };
|
|
1291
|
+
}
|
|
1292
|
+
function useHandleState(handle) {
|
|
1293
|
+
const [tool, setTool] = react.useState("move");
|
|
1294
|
+
const [showAxes, setShowAxes] = react.useState(true);
|
|
1295
|
+
const [showMesh, setShowMesh] = react.useState(false);
|
|
1296
|
+
const [canUndo, setCanUndo] = react.useState(false);
|
|
1297
|
+
react.useEffect(() => {
|
|
1298
|
+
if (!handle) return;
|
|
1299
|
+
const sync = () => {
|
|
1300
|
+
setTool(handle.getTool());
|
|
1301
|
+
setShowAxes(handle.getShowAxes());
|
|
1302
|
+
setShowMesh(handle.getShowMesh());
|
|
1303
|
+
setCanUndo(handle.canUndo());
|
|
1304
|
+
};
|
|
1305
|
+
sync();
|
|
1306
|
+
return handle.subscribe(sync);
|
|
1307
|
+
}, [handle]);
|
|
1308
|
+
return { tool, showAxes, showMesh, canUndo };
|
|
1309
|
+
}
|
|
1310
|
+
function DesktopPanel(props) {
|
|
1311
|
+
const { handle, onResetView, onClose, isDark, chordGroup } = props;
|
|
1312
|
+
const { tool, showAxes, showMesh, canUndo } = useHandleState(handle);
|
|
1313
|
+
const { hover, portalReady, showHover, hideHover } = useToolHoverTooltip();
|
|
1314
|
+
const grouped = react.useMemo(() => {
|
|
1315
|
+
return TOOLS_3D.reduce(
|
|
1316
|
+
(acc, t) => {
|
|
1317
|
+
var _a;
|
|
1318
|
+
(acc[_a = t.group] ?? (acc[_a] = [])).push(t);
|
|
1319
|
+
return acc;
|
|
1320
|
+
},
|
|
1321
|
+
{}
|
|
1322
|
+
);
|
|
1323
|
+
}, []);
|
|
1324
|
+
const orderedGroups = react.useMemo(
|
|
1325
|
+
() => GROUP_ORDER_3D.filter((g) => grouped[g]),
|
|
1326
|
+
[grouped]
|
|
1327
|
+
);
|
|
1328
|
+
const activeGroupTools = chordGroup ? grouped[chordGroup] ?? null : null;
|
|
1329
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1330
|
+
/* @__PURE__ */ jsxRuntime.jsxs(Shell, { title: "H\xECnh h\u1ECDc 3D", icon: Geom3DIconHeader, onClose, isDark, children: [
|
|
1331
|
+
/* @__PURE__ */ jsxRuntime.jsx(Section, { label: "B\u1ED1 c\u1EE5c", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 flex-wrap text-[11px] text-slate-700", children: [
|
|
1332
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "inline-flex select-none items-center gap-1.5", children: [
|
|
1333
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1334
|
+
"input",
|
|
1335
|
+
{
|
|
1336
|
+
type: "checkbox",
|
|
1337
|
+
checked: showAxes,
|
|
1338
|
+
onChange: (e) => handle?.setShowAxes(e.target.checked),
|
|
1339
|
+
"data-testid": "toggle-axes"
|
|
1340
|
+
}
|
|
1341
|
+
),
|
|
1342
|
+
"Tr\u1EE5c"
|
|
1343
|
+
] }),
|
|
1344
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "inline-flex select-none items-center gap-1.5", children: [
|
|
1345
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1346
|
+
"input",
|
|
1347
|
+
{
|
|
1348
|
+
type: "checkbox",
|
|
1349
|
+
checked: showMesh,
|
|
1350
|
+
onChange: (e) => handle?.setShowMesh(e.target.checked),
|
|
1351
|
+
"data-testid": "toggle-mesh"
|
|
1352
|
+
}
|
|
1353
|
+
),
|
|
1354
|
+
"L\u01B0\u1EDBi"
|
|
1355
|
+
] }),
|
|
1356
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1357
|
+
"button",
|
|
1358
|
+
{
|
|
1359
|
+
type: "button",
|
|
1360
|
+
onClick: onResetView,
|
|
1361
|
+
title: "Reset g\xF3c nh\xECn",
|
|
1362
|
+
"aria-label": "Reset view",
|
|
1363
|
+
className: "ml-auto inline-flex items-center justify-center rounded p-1 text-slate-600 transition hover:bg-slate-100 hover:text-slate-900",
|
|
1364
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(ResetViewIcon, {})
|
|
1365
|
+
}
|
|
1366
|
+
),
|
|
1367
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1368
|
+
"button",
|
|
1369
|
+
{
|
|
1370
|
+
type: "button",
|
|
1371
|
+
onClick: () => handle?.undo(),
|
|
1372
|
+
disabled: !canUndo,
|
|
1373
|
+
title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
|
|
1374
|
+
"aria-label": "Ho\xE0n t\xE1c",
|
|
1375
|
+
className: "inline-flex items-center justify-center rounded p-1 text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 disabled:cursor-not-allowed disabled:text-slate-300 disabled:hover:bg-transparent",
|
|
1376
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(UndoIcon, {})
|
|
1377
|
+
}
|
|
1378
|
+
)
|
|
1379
|
+
] }) }),
|
|
1380
|
+
orderedGroups.map((group) => {
|
|
1381
|
+
const tools = grouped[group];
|
|
1382
|
+
const isChordActive = chordGroup === group;
|
|
1383
|
+
const dimmed = chordGroup !== null && !isChordActive;
|
|
1384
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1385
|
+
"section",
|
|
1386
|
+
{
|
|
1387
|
+
"data-chord-group": group,
|
|
1388
|
+
"data-chord-active": isChordActive ? "true" : "false",
|
|
1389
|
+
className: [
|
|
1390
|
+
"rounded-md transition",
|
|
1391
|
+
isChordActive ? "bg-blue-50 ring-1 ring-blue-400 p-1" : "p-0",
|
|
1392
|
+
dimmed ? "opacity-55" : "opacity-100"
|
|
1393
|
+
].join(" "),
|
|
1394
|
+
children: [
|
|
1395
|
+
/* @__PURE__ */ jsxRuntime.jsxs("h4", { className: "mb-1.5 flex items-center justify-between text-[10px] font-semibold uppercase tracking-wider text-slate-500", children: [
|
|
1396
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: GROUP_LABELS_3D[group] }),
|
|
1397
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1398
|
+
"span",
|
|
1399
|
+
{
|
|
1400
|
+
"data-testid": `chord-letter-${group}`,
|
|
1401
|
+
className: [
|
|
1402
|
+
"font-mono text-[10px] leading-none transition",
|
|
1403
|
+
isChordActive ? "text-blue-700 font-bold" : "text-slate-400"
|
|
1404
|
+
].join(" "),
|
|
1405
|
+
children: letterForGroup3D(group)
|
|
1406
|
+
}
|
|
1407
|
+
)
|
|
1408
|
+
] }),
|
|
1409
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-4 gap-1", children: tools.map((t, i) => {
|
|
1410
|
+
const isActive = tool === t.key;
|
|
1411
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1412
|
+
ToolButton,
|
|
1413
|
+
{
|
|
1414
|
+
toolKey: t.key,
|
|
1415
|
+
label: t.label,
|
|
1416
|
+
hint: t.hint,
|
|
1417
|
+
active: isActive,
|
|
1418
|
+
onClick: () => handle?.setTool(t.key),
|
|
1419
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1420
|
+
"span",
|
|
1421
|
+
{
|
|
1422
|
+
onMouseEnter: (e) => showHover(e.currentTarget.closest("button"), t),
|
|
1423
|
+
onMouseLeave: hideHover,
|
|
1424
|
+
onFocus: (e) => showHover(e.currentTarget.closest("button"), t),
|
|
1425
|
+
onBlur: hideHover,
|
|
1426
|
+
children: ICONS_3D[t.key]
|
|
1427
|
+
}
|
|
1428
|
+
),
|
|
1429
|
+
badge: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1430
|
+
"span",
|
|
1431
|
+
{
|
|
1432
|
+
"data-testid": `chord-num-${t.key}`,
|
|
1433
|
+
className: [
|
|
1434
|
+
"pointer-events-none absolute bottom-0 right-0.5 font-mono text-[9px] leading-none transition",
|
|
1435
|
+
isActive ? "text-white/70" : isChordActive ? "text-blue-700 font-bold" : "text-slate-400"
|
|
1436
|
+
].join(" "),
|
|
1437
|
+
children: i + 1
|
|
1438
|
+
}
|
|
1439
|
+
)
|
|
1440
|
+
},
|
|
1441
|
+
t.key
|
|
1442
|
+
);
|
|
1443
|
+
}) })
|
|
1444
|
+
]
|
|
1445
|
+
},
|
|
1446
|
+
group
|
|
1447
|
+
);
|
|
1448
|
+
}),
|
|
1449
|
+
chordGroup && activeGroupTools && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1450
|
+
"div",
|
|
1451
|
+
{
|
|
1452
|
+
"data-testid": "chord-hint",
|
|
1453
|
+
className: "mt-1 rounded border border-blue-200 bg-blue-50/60 px-2 py-1 text-[11px] leading-snug text-slate-600",
|
|
1454
|
+
children: [
|
|
1455
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono font-semibold text-blue-700", children: letterForGroup3D(chordGroup) }),
|
|
1456
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "mx-1 text-slate-400", children: "\u2192" }),
|
|
1457
|
+
activeGroupTools.map((t, i) => /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "mr-2 inline-block", children: [
|
|
1458
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-mono font-semibold text-blue-700", children: i + 1 }),
|
|
1459
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-1", children: t.label })
|
|
1460
|
+
] }, t.key)),
|
|
1461
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-slate-400", children: "Esc hu\u1EF7" })
|
|
1462
|
+
]
|
|
1463
|
+
}
|
|
1464
|
+
)
|
|
1465
|
+
] }),
|
|
1466
|
+
portalReady && hover && typeof document !== "undefined" ? reactDom.createPortal(
|
|
1467
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1468
|
+
"div",
|
|
1469
|
+
{
|
|
1470
|
+
role: "tooltip",
|
|
1471
|
+
className: "pointer-events-none fixed w-max max-w-[220px] rounded-md bg-slate-900 px-2 py-1 text-left text-[11px] leading-tight text-white shadow-lg",
|
|
1472
|
+
style: {
|
|
1473
|
+
left: hover.x + 8,
|
|
1474
|
+
top: hover.y,
|
|
1475
|
+
transform: "translate(0, -50%)",
|
|
1476
|
+
zIndex: 2147483600
|
|
1477
|
+
},
|
|
1478
|
+
children: [
|
|
1479
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "block font-medium", children: hover.label }),
|
|
1480
|
+
hover.hint && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-0.5 block text-slate-300", children: hover.hint })
|
|
1481
|
+
]
|
|
1482
|
+
}
|
|
1483
|
+
),
|
|
1484
|
+
document.body
|
|
1485
|
+
) : null
|
|
1486
|
+
] });
|
|
1487
|
+
}
|
|
1488
|
+
function MobilePanel(props) {
|
|
1489
|
+
const { handle, onResetView, isDark, drawerOpen, onDrawerClose } = props;
|
|
1490
|
+
const { tool, showAxes, showMesh, canUndo } = useHandleState(handle);
|
|
1491
|
+
const groups = react.useMemo(() => {
|
|
1492
|
+
const acc = /* @__PURE__ */ new Map();
|
|
1493
|
+
for (const t of TOOLS_3D) {
|
|
1494
|
+
if (!acc.has(t.group)) acc.set(t.group, []);
|
|
1495
|
+
acc.get(t.group).push(t);
|
|
1496
|
+
}
|
|
1497
|
+
return Array.from(acc.entries()).map(([group, tools]) => ({
|
|
1498
|
+
group,
|
|
1499
|
+
groupLabel: GROUP_LABELS_3D[group],
|
|
1500
|
+
tools: tools.map((t) => ({ key: t.key, label: t.label, icon: ICONS_3D[t.key] }))
|
|
1501
|
+
}));
|
|
1502
|
+
}, []);
|
|
1503
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1504
|
+
MobileToolDrawer,
|
|
1505
|
+
{
|
|
1506
|
+
title: "H\xECnh h\u1ECDc 3D",
|
|
1507
|
+
headerIcon: Geom3DIconHeader,
|
|
1508
|
+
testId: "geom3d-left-panel",
|
|
1509
|
+
isDark,
|
|
1510
|
+
drawerOpen: !!drawerOpen,
|
|
1511
|
+
onDrawerClose: () => onDrawerClose?.(),
|
|
1512
|
+
chips: [
|
|
1513
|
+
{
|
|
1514
|
+
label: "Tr\u1EE5c",
|
|
1515
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(AxisIcon3D, {}),
|
|
1516
|
+
pressed: showAxes,
|
|
1517
|
+
onToggle: (b) => handle?.setShowAxes(b),
|
|
1518
|
+
testId: "toggle-axes"
|
|
1519
|
+
},
|
|
1520
|
+
{
|
|
1521
|
+
label: "L\u01B0\u1EDBi",
|
|
1522
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(MeshIcon, {}),
|
|
1523
|
+
pressed: showMesh,
|
|
1524
|
+
onToggle: (b) => handle?.setShowMesh(b),
|
|
1525
|
+
testId: "toggle-mesh"
|
|
1526
|
+
}
|
|
1527
|
+
],
|
|
1528
|
+
actions: [
|
|
1529
|
+
{
|
|
1530
|
+
label: "Reset view",
|
|
1531
|
+
title: "Reset g\xF3c nh\xECn",
|
|
1532
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(ResetViewIcon, {}),
|
|
1533
|
+
onClick: onResetView
|
|
1534
|
+
},
|
|
1535
|
+
{
|
|
1536
|
+
label: "Ho\xE0n t\xE1c",
|
|
1537
|
+
title: "Ho\xE0n t\xE1c (Ctrl/Cmd+Z)",
|
|
1538
|
+
icon: /* @__PURE__ */ jsxRuntime.jsx(UndoIcon, {}),
|
|
1539
|
+
onClick: () => handle?.undo(),
|
|
1540
|
+
disabled: !canUndo
|
|
1541
|
+
}
|
|
1542
|
+
],
|
|
1543
|
+
groups,
|
|
1544
|
+
activeTool: tool,
|
|
1545
|
+
onToolSelect: (k) => handle?.setTool(k)
|
|
1546
|
+
}
|
|
1547
|
+
);
|
|
1548
|
+
}
|
|
1549
|
+
function LeftPanel(props) {
|
|
1550
|
+
if (props.isMobile) return /* @__PURE__ */ jsxRuntime.jsx(MobilePanel, { ...props });
|
|
1551
|
+
return /* @__PURE__ */ jsxRuntime.jsx(DesktopPanel, { ...props });
|
|
1552
|
+
}
|
|
1553
|
+
var TOOLTIP_DELAY_MS, Geom3DIconHeader;
|
|
1554
|
+
var init_LeftPanel = __esm({
|
|
1555
|
+
"src/stamps/geometry-3d/editor/LeftPanel.tsx"() {
|
|
1556
|
+
"use client";
|
|
1557
|
+
init_tools();
|
|
1558
|
+
init_toolButtons();
|
|
1559
|
+
init_MobileToolDrawer();
|
|
1560
|
+
TOOLTIP_DELAY_MS = 400;
|
|
1561
|
+
Geom3DIconHeader = /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 7 L14 4 L20 7 L14 10 Z M4 7 L4 17 L14 20 L14 10 M14 20 L20 17 L20 7" }) });
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
function isFieldFocused() {
|
|
1565
|
+
const ae = typeof document !== "undefined" ? document.activeElement : null;
|
|
1566
|
+
return !!(ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable));
|
|
1567
|
+
}
|
|
1568
|
+
function useChordShortcut(args) {
|
|
1569
|
+
const { groupOrder, tools, onSelect, enabled } = args;
|
|
1570
|
+
const [chordGroup, setChordGroup] = react.useState(null);
|
|
1571
|
+
const groupOrderRef = react.useRef(groupOrder);
|
|
1572
|
+
const toolsRef = react.useRef(tools);
|
|
1573
|
+
const onSelectRef = react.useRef(onSelect);
|
|
1574
|
+
const chordGroupRef = react.useRef(null);
|
|
1575
|
+
groupOrderRef.current = groupOrder;
|
|
1576
|
+
toolsRef.current = tools;
|
|
1577
|
+
onSelectRef.current = onSelect;
|
|
1578
|
+
const cancel = react.useCallback(() => {
|
|
1579
|
+
chordGroupRef.current = null;
|
|
1580
|
+
setChordGroup(null);
|
|
1581
|
+
}, []);
|
|
1582
|
+
react.useEffect(() => {
|
|
1583
|
+
if (!enabled) return;
|
|
1584
|
+
const setChord = (next) => {
|
|
1585
|
+
chordGroupRef.current = next;
|
|
1586
|
+
setChordGroup(next);
|
|
1587
|
+
};
|
|
1588
|
+
const onKey = (e) => {
|
|
1589
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
1590
|
+
if (isFieldFocused()) return;
|
|
1591
|
+
const key = e.key;
|
|
1592
|
+
const lower = key.length === 1 ? key.toLowerCase() : key;
|
|
1593
|
+
if (key === "Escape") {
|
|
1594
|
+
if (chordGroupRef.current !== null) {
|
|
1595
|
+
e.preventDefault();
|
|
1596
|
+
e.stopPropagation();
|
|
1597
|
+
setChord(null);
|
|
1598
|
+
}
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
if (lower.length === 1 && lower >= "a" && lower <= "z") {
|
|
1602
|
+
const idx = lower.charCodeAt(0) - A_CODE;
|
|
1603
|
+
if (idx < groupOrderRef.current.length) {
|
|
1604
|
+
e.preventDefault();
|
|
1605
|
+
e.stopPropagation();
|
|
1606
|
+
setChord(groupOrderRef.current[idx]);
|
|
1607
|
+
}
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
if (key >= "1" && key <= "9") {
|
|
1611
|
+
const active = chordGroupRef.current;
|
|
1612
|
+
if (active === null) return;
|
|
1613
|
+
const n = key.charCodeAt(0) - "1".charCodeAt(0);
|
|
1614
|
+
const toolsInGroup = toolsRef.current.filter(
|
|
1615
|
+
(t) => t.group === active
|
|
1616
|
+
);
|
|
1617
|
+
e.preventDefault();
|
|
1618
|
+
e.stopPropagation();
|
|
1619
|
+
if (n < toolsInGroup.length) {
|
|
1620
|
+
onSelectRef.current(toolsInGroup[n].key);
|
|
1621
|
+
}
|
|
1622
|
+
setChord(null);
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
window.addEventListener("keydown", onKey, { capture: true });
|
|
1627
|
+
return () => {
|
|
1628
|
+
window.removeEventListener("keydown", onKey, { capture: true });
|
|
1629
|
+
};
|
|
1630
|
+
}, [enabled]);
|
|
1631
|
+
return { chordGroup, cancel };
|
|
1632
|
+
}
|
|
1633
|
+
var A_CODE;
|
|
1634
|
+
var init_useChordShortcut = __esm({
|
|
1635
|
+
"src/stamps/shared/useChordShortcut.ts"() {
|
|
1636
|
+
A_CODE = "a".charCodeAt(0);
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
// src/stamps/shared/svgToImage.ts
|
|
1641
|
+
async function hashString(input) {
|
|
1642
|
+
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
1643
|
+
const buf = new TextEncoder().encode(input);
|
|
1644
|
+
const digest = await crypto.subtle.digest("SHA-256", buf);
|
|
1645
|
+
return Array.from(new Uint8Array(digest)).slice(0, 16).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1646
|
+
}
|
|
1647
|
+
let h1 = 2166136261;
|
|
1648
|
+
let h2 = 3421674724;
|
|
1649
|
+
for (let i = 0; i < input.length; i++) {
|
|
1650
|
+
const c = input.charCodeAt(i);
|
|
1651
|
+
h1 ^= c;
|
|
1652
|
+
h1 = Math.imul(h1, 16777619);
|
|
1653
|
+
h2 ^= c + i;
|
|
1654
|
+
h2 = Math.imul(h2, 1099511628211 & 4294967295);
|
|
1655
|
+
}
|
|
1656
|
+
return (h1 >>> 0).toString(16).padStart(8, "0") + (h2 >>> 0).toString(16).padStart(8, "0");
|
|
1657
|
+
}
|
|
1658
|
+
function parseSize(svg, attr) {
|
|
1659
|
+
const re = new RegExp(`<svg[^>]*\\s${attr}="(\\d+(?:\\.\\d+)?)`, "i");
|
|
1660
|
+
const m = svg.match(re);
|
|
1661
|
+
if (m) return Math.max(1, Math.round(parseFloat(m[1])));
|
|
1662
|
+
const vb = svg.match(/viewBox="([\d.\s-]+)"/i);
|
|
1663
|
+
if (vb) {
|
|
1664
|
+
const parts = vb[1].trim().split(/\s+/).map(parseFloat);
|
|
1665
|
+
if (parts.length === 4) return Math.max(1, Math.round(attr === "width" ? parts[2] : parts[3]));
|
|
1666
|
+
}
|
|
1667
|
+
return attr === "width" ? 200 : 100;
|
|
1668
|
+
}
|
|
1669
|
+
async function svgToImageElement(svg) {
|
|
1670
|
+
const width = parseSize(svg, "width");
|
|
1671
|
+
const height = parseSize(svg, "height");
|
|
1672
|
+
const utf8 = unescape(encodeURIComponent(svg));
|
|
1673
|
+
const dataURL = "data:image/svg+xml;base64," + btoa(utf8);
|
|
1674
|
+
const fileId = await hashString(dataURL);
|
|
1675
|
+
return { dataURL, fileId, width, height, mimeType: "image/svg+xml" };
|
|
1676
|
+
}
|
|
1677
|
+
var init_svgToImage = __esm({
|
|
1678
|
+
"src/stamps/shared/svgToImage.ts"() {
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
// src/stamps/shared/insertImage.ts
|
|
1683
|
+
function buildStampImageElement(api, fileId, width, height, customData, x, y) {
|
|
1684
|
+
const appState = api?.getAppState() ?? { scrollX: 0, scrollY: 0, width: 800, height: 600, zoom: { value: 1 } };
|
|
1685
|
+
const cx = x ?? appState.scrollX + (appState.width ?? 800) / 2 / (appState.zoom?.value ?? 1) - width / 2;
|
|
1686
|
+
const cy = y ?? appState.scrollY + (appState.height ?? 600) / 2 / (appState.zoom?.value ?? 1) - height / 2;
|
|
1687
|
+
return {
|
|
1688
|
+
type: "image",
|
|
1689
|
+
id: "stamp_" + Date.now() + "_" + Math.random().toString(36).slice(2, 8),
|
|
1690
|
+
x: cx,
|
|
1691
|
+
y: cy,
|
|
1692
|
+
width,
|
|
1693
|
+
height,
|
|
1694
|
+
fileId,
|
|
1695
|
+
customData,
|
|
1696
|
+
angle: 0,
|
|
1697
|
+
strokeColor: "transparent",
|
|
1698
|
+
backgroundColor: "transparent",
|
|
1699
|
+
fillStyle: "solid",
|
|
1700
|
+
strokeWidth: 1,
|
|
1701
|
+
strokeStyle: "solid",
|
|
1702
|
+
roughness: 0,
|
|
1703
|
+
opacity: 100,
|
|
1704
|
+
groupIds: [],
|
|
1705
|
+
roundness: null,
|
|
1706
|
+
seed: Math.floor(Math.random() * 1e9),
|
|
1707
|
+
versionNonce: 0,
|
|
1708
|
+
version: 1,
|
|
1709
|
+
isDeleted: false,
|
|
1710
|
+
boundElements: null,
|
|
1711
|
+
updated: Date.now(),
|
|
1712
|
+
link: null,
|
|
1713
|
+
locked: false,
|
|
1714
|
+
status: "saved",
|
|
1715
|
+
scale: [1, 1]
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
async function insertStampImage(api, opts) {
|
|
1719
|
+
const { dataURL, fileId, width, height, mimeType } = await svgToImageElement(opts.svgString);
|
|
1720
|
+
api.addFiles([{ id: fileId, dataURL, mimeType, created: Date.now() }]);
|
|
1721
|
+
const customData = opts.makeCustomData(width, height);
|
|
1722
|
+
const elements = api.getSceneElements();
|
|
1723
|
+
const editingId = opts.editingElementId ?? null;
|
|
1724
|
+
if (editingId) {
|
|
1725
|
+
const updated = elements.map(
|
|
1726
|
+
(e) => e.id === editingId ? { ...e, fileId, customData, width, height } : e
|
|
1727
|
+
);
|
|
1728
|
+
api.updateScene({ elements: updated, appState: clearAppStateAfterInsert() });
|
|
1729
|
+
return { fileId, width, height, elementId: editingId };
|
|
1730
|
+
}
|
|
1731
|
+
const newElement = buildStampImageElement(
|
|
1732
|
+
api,
|
|
1733
|
+
fileId,
|
|
1734
|
+
width,
|
|
1735
|
+
height,
|
|
1736
|
+
customData,
|
|
1737
|
+
opts.position?.x,
|
|
1738
|
+
opts.position?.y
|
|
1739
|
+
);
|
|
1740
|
+
api.updateScene({
|
|
1741
|
+
elements: [...elements, newElement],
|
|
1742
|
+
appState: clearAppStateAfterInsert()
|
|
1743
|
+
});
|
|
1744
|
+
return { fileId, width, height, elementId: newElement.id };
|
|
1745
|
+
}
|
|
1746
|
+
var clearAppStateAfterInsert;
|
|
1747
|
+
var init_insertImage = __esm({
|
|
1748
|
+
"src/stamps/shared/insertImage.ts"() {
|
|
1749
|
+
init_svgToImage();
|
|
1750
|
+
clearAppStateAfterInsert = () => ({
|
|
1751
|
+
selectedElementIds: {},
|
|
1752
|
+
croppingElementId: null
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
});
|
|
1756
|
+
function readMatch(query) {
|
|
1757
|
+
if (typeof window === "undefined" || !window.matchMedia) return false;
|
|
1758
|
+
try {
|
|
1759
|
+
return window.matchMedia(query).matches;
|
|
1760
|
+
} catch {
|
|
1761
|
+
return false;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
function useIsMobile() {
|
|
1765
|
+
const [state, setState] = react.useState(() => ({
|
|
1766
|
+
isMobile: readMatch(MOBILE_QUERY),
|
|
1767
|
+
isTouchOnly: readMatch(NO_HOVER_QUERY)
|
|
1768
|
+
}));
|
|
1769
|
+
react.useEffect(() => {
|
|
1770
|
+
if (typeof window === "undefined" || !window.matchMedia) return;
|
|
1771
|
+
const mql = window.matchMedia(MOBILE_QUERY);
|
|
1772
|
+
const tql = window.matchMedia(NO_HOVER_QUERY);
|
|
1773
|
+
const update = () => {
|
|
1774
|
+
setState({ isMobile: mql.matches, isTouchOnly: tql.matches });
|
|
1775
|
+
};
|
|
1776
|
+
update();
|
|
1777
|
+
mql.addEventListener("change", update);
|
|
1778
|
+
tql.addEventListener("change", update);
|
|
1779
|
+
return () => {
|
|
1780
|
+
mql.removeEventListener("change", update);
|
|
1781
|
+
tql.removeEventListener("change", update);
|
|
1782
|
+
};
|
|
1783
|
+
}, []);
|
|
1784
|
+
return state;
|
|
1785
|
+
}
|
|
1786
|
+
var MOBILE_QUERY, NO_HOVER_QUERY;
|
|
1787
|
+
var init_useIsMobile = __esm({
|
|
1788
|
+
"src/stamps/shared/useIsMobile.ts"() {
|
|
1789
|
+
"use client";
|
|
1790
|
+
MOBILE_QUERY = "(max-width: 768px)";
|
|
1791
|
+
NO_HOVER_QUERY = "(hover: none)";
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
|
|
1795
|
+
// src/stamps/geometry-3d/host.tsx
|
|
1796
|
+
var host_exports = {};
|
|
1797
|
+
__export(host_exports, {
|
|
1798
|
+
Geometry3DStampHost: () => Geometry3DStampHost
|
|
1799
|
+
});
|
|
1800
|
+
function parseInitial(editingElement) {
|
|
1801
|
+
if (!editingElement) return null;
|
|
1802
|
+
if (!isGeometry3DCustomData(editingElement.customData)) return null;
|
|
1803
|
+
try {
|
|
1804
|
+
return parseSerializedBoard3D(editingElement.customData.jsonState);
|
|
1805
|
+
} catch {
|
|
1806
|
+
return null;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
var Geometry3DStampHost;
|
|
1810
|
+
var init_host = __esm({
|
|
1811
|
+
"src/stamps/geometry-3d/host.tsx"() {
|
|
1812
|
+
"use client";
|
|
1813
|
+
init_EditorPanel();
|
|
1814
|
+
init_LeftPanel();
|
|
1815
|
+
init_tools();
|
|
1816
|
+
init_useChordShortcut();
|
|
1817
|
+
init_insertImage();
|
|
1818
|
+
init_useIsMobile();
|
|
1819
|
+
init_serialize();
|
|
1820
|
+
Geometry3DStampHost = react.forwardRef(
|
|
1821
|
+
function Geometry3DStampHost2({ api, editingElement, onClose, isDark }, ref) {
|
|
1822
|
+
const editorRef = react.useRef(null);
|
|
1823
|
+
const { isMobile } = useIsMobile();
|
|
1824
|
+
const [drawerOpen, setDrawerOpen] = react.useState(false);
|
|
1825
|
+
const [boardHandle, setBoardHandle] = react.useState(null);
|
|
1826
|
+
const initial = react.useMemo(
|
|
1827
|
+
() => parseInitial(editingElement),
|
|
1828
|
+
[editingElement]
|
|
1829
|
+
);
|
|
1830
|
+
const handleBoardReady = react.useCallback((h) => {
|
|
1831
|
+
setBoardHandle((prev) => prev === h ? prev : h);
|
|
1832
|
+
}, []);
|
|
1833
|
+
const { chordGroup } = useChordShortcut({
|
|
1834
|
+
groupOrder: GROUP_ORDER_3D,
|
|
1835
|
+
tools: TOOLS_3D,
|
|
1836
|
+
onSelect: (key) => boardHandle?.setTool(key),
|
|
1837
|
+
enabled: !isMobile
|
|
1838
|
+
});
|
|
1839
|
+
const handleResetView = react.useCallback(() => {
|
|
1840
|
+
boardHandle?.resetView();
|
|
1841
|
+
}, [boardHandle]);
|
|
1842
|
+
const handleInsert = react.useCallback(
|
|
1843
|
+
async (jsonState, svgString, width, height) => {
|
|
1844
|
+
if (!api) return;
|
|
1845
|
+
await insertStampImage(api, {
|
|
1846
|
+
svgString,
|
|
1847
|
+
makeCustomData: () => ({
|
|
1848
|
+
kind: "geometry3d",
|
|
1849
|
+
version: 1,
|
|
1850
|
+
jsonState,
|
|
1851
|
+
svgWidth: width,
|
|
1852
|
+
svgHeight: height
|
|
1853
|
+
}),
|
|
1854
|
+
editingElementId: editingElement?.id ?? null
|
|
1855
|
+
});
|
|
1856
|
+
onClose();
|
|
1857
|
+
},
|
|
1858
|
+
[api, editingElement, onClose]
|
|
1859
|
+
);
|
|
1860
|
+
react.useImperativeHandle(
|
|
1861
|
+
ref,
|
|
1862
|
+
() => ({
|
|
1863
|
+
tryInsert: () => editorRef.current?.tryInsert() ?? false,
|
|
1864
|
+
hasContent: () => editorRef.current?.hasContent() ?? false
|
|
1865
|
+
}),
|
|
1866
|
+
[]
|
|
1867
|
+
);
|
|
1868
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1869
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1870
|
+
LeftPanel,
|
|
1871
|
+
{
|
|
1872
|
+
handle: boardHandle,
|
|
1873
|
+
onResetView: handleResetView,
|
|
1874
|
+
onClose,
|
|
1875
|
+
isDark,
|
|
1876
|
+
isMobile,
|
|
1877
|
+
drawerOpen,
|
|
1878
|
+
onDrawerClose: () => setDrawerOpen(false),
|
|
1879
|
+
chordGroup
|
|
1880
|
+
}
|
|
1881
|
+
),
|
|
1882
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1883
|
+
EditorPanel,
|
|
1884
|
+
{
|
|
1885
|
+
ref: editorRef,
|
|
1886
|
+
isDark,
|
|
1887
|
+
initial,
|
|
1888
|
+
onInsert: handleInsert,
|
|
1889
|
+
onClose,
|
|
1890
|
+
isMobile,
|
|
1891
|
+
withLeftPanel: !isMobile,
|
|
1892
|
+
onBoardReady: handleBoardReady,
|
|
1893
|
+
onOpenDrawer: () => setDrawerOpen(true)
|
|
1894
|
+
}
|
|
1895
|
+
)
|
|
1896
|
+
] });
|
|
1897
|
+
}
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
// src/stamps/geometry-3d/index.tsx
|
|
1903
|
+
init_serialize();
|
|
1904
|
+
|
|
1905
|
+
// src/stamps/geometry-3d/render.ts
|
|
1906
|
+
init_serialize();
|
|
1907
|
+
var OUTPUT_WIDTH = 1024;
|
|
1908
|
+
var OUTPUT_HEIGHT = 768;
|
|
1909
|
+
async function renderGeometry3DSvgFromState(jsonState) {
|
|
1910
|
+
const state = parseSerializedBoard3D(jsonState);
|
|
1911
|
+
const JXG = (await import('jsxgraph')).default;
|
|
1912
|
+
const div = document.createElement("div");
|
|
1913
|
+
div.style.cssText = `position:absolute;left:-9999px;top:-9999px;width:${OUTPUT_WIDTH}px;height:${OUTPUT_HEIGHT}px;`;
|
|
1914
|
+
document.body.appendChild(div);
|
|
1915
|
+
try {
|
|
1916
|
+
JXG.Options.text.display = "internal";
|
|
1917
|
+
const board = JXG.JSXGraph.initBoard(div, {
|
|
1918
|
+
boundingbox: state.bbox,
|
|
1919
|
+
axis: false,
|
|
1920
|
+
showCopyright: false,
|
|
1921
|
+
showNavigation: false,
|
|
1922
|
+
renderer: "svg"
|
|
1923
|
+
});
|
|
1924
|
+
const view = board.create(
|
|
1925
|
+
"view3d",
|
|
1926
|
+
[
|
|
1927
|
+
[-5, -5],
|
|
1928
|
+
[10, 10],
|
|
1929
|
+
[
|
|
1930
|
+
[state.view.bbox3D[0], state.view.bbox3D[3]],
|
|
1931
|
+
[state.view.bbox3D[1], state.view.bbox3D[4]],
|
|
1932
|
+
[state.view.bbox3D[2], state.view.bbox3D[5]]
|
|
1933
|
+
]
|
|
1934
|
+
],
|
|
1935
|
+
{
|
|
1936
|
+
az: { slider: { visible: false }, value: state.view.azimuth },
|
|
1937
|
+
el: { slider: { visible: false }, value: state.view.elevation },
|
|
1938
|
+
projection: "central"
|
|
1939
|
+
}
|
|
1940
|
+
);
|
|
1941
|
+
if (!state.showAxes) {
|
|
1942
|
+
view.defaultAxes = [];
|
|
1943
|
+
}
|
|
1944
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
1945
|
+
for (const el of state.elements) {
|
|
1946
|
+
const parents = el.parents.map(
|
|
1947
|
+
(p) => typeof p === "string" && p.startsWith("@id:") ? idMap.get(p.slice(4)) : p
|
|
1948
|
+
);
|
|
1949
|
+
const obj = view.create(el.type, parents, {
|
|
1950
|
+
...el.attributes,
|
|
1951
|
+
id: el.id,
|
|
1952
|
+
name: el.label
|
|
1953
|
+
});
|
|
1954
|
+
idMap.set(el.id, obj);
|
|
1955
|
+
}
|
|
1956
|
+
const svg = div.querySelector("svg");
|
|
1957
|
+
if (!svg) {
|
|
1958
|
+
throw new Error("renderGeometry3DSvgFromState: SVG not produced");
|
|
1959
|
+
}
|
|
1960
|
+
const clone = svg.cloneNode(true);
|
|
1961
|
+
clone.setAttribute("width", String(OUTPUT_WIDTH));
|
|
1962
|
+
clone.setAttribute("height", String(OUTPUT_HEIGHT));
|
|
1963
|
+
const svgString = new XMLSerializer().serializeToString(clone);
|
|
1964
|
+
try {
|
|
1965
|
+
JXG.JSXGraph.freeBoard(board);
|
|
1966
|
+
} catch {
|
|
1967
|
+
}
|
|
1968
|
+
return { svgString, width: OUTPUT_WIDTH, height: OUTPUT_HEIGHT };
|
|
1969
|
+
} finally {
|
|
1970
|
+
document.body.removeChild(div);
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
var Geometry3DStampHost3 = react.lazy(
|
|
1974
|
+
() => Promise.resolve().then(() => (init_host(), host_exports)).then((m) => ({ default: m.Geometry3DStampHost }))
|
|
1975
|
+
);
|
|
1976
|
+
var Geometry3DIcon = /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1977
|
+
"svg",
|
|
1978
|
+
{
|
|
1979
|
+
width: "20",
|
|
1980
|
+
height: "20",
|
|
1981
|
+
viewBox: "0 0 24 24",
|
|
1982
|
+
fill: "none",
|
|
1983
|
+
stroke: "currentColor",
|
|
1984
|
+
strokeWidth: "1.6",
|
|
1985
|
+
strokeLinecap: "round",
|
|
1986
|
+
strokeLinejoin: "round",
|
|
1987
|
+
"aria-hidden": "true",
|
|
1988
|
+
children: [
|
|
1989
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 9 L4 20 L14 20 L14 9 Z" }),
|
|
1990
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 9 L10 4 L20 4 L14 9 Z" }),
|
|
1991
|
+
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M14 9 L20 4 L20 15 L14 20 Z" })
|
|
1992
|
+
]
|
|
1993
|
+
}
|
|
1994
|
+
);
|
|
1995
|
+
var geometry3dStamp = {
|
|
1996
|
+
kind: "geometry3d",
|
|
1997
|
+
experimental: true,
|
|
1998
|
+
shortcutKey: "d",
|
|
1999
|
+
toolbarLabel: "D",
|
|
2000
|
+
toolbarTitle: "H\xECnh 3D (D)",
|
|
2001
|
+
toolbarIcon: Geometry3DIcon,
|
|
2002
|
+
toolbarTestId: "stamp-toolbar-geometry3d",
|
|
2003
|
+
matchesCustomData: isGeometry3DCustomData,
|
|
2004
|
+
async renderSvgFromCustomData(data) {
|
|
2005
|
+
if (!isGeometry3DCustomData(data)) {
|
|
2006
|
+
throw new Error("geometry3dStamp.renderSvgFromCustomData: customData kh\xF4ng ph\u1EA3i geometry3d");
|
|
2007
|
+
}
|
|
2008
|
+
const { svgString } = await renderGeometry3DSvgFromState(data.jsonState);
|
|
2009
|
+
return svgString;
|
|
2010
|
+
},
|
|
2011
|
+
restoreFileFromCustomData: async (element) => {
|
|
2012
|
+
const data = element.customData;
|
|
2013
|
+
const fileId = element.fileId;
|
|
2014
|
+
if (!data || !fileId) return null;
|
|
2015
|
+
if (!isGeometry3DCustomData(data)) return null;
|
|
2016
|
+
try {
|
|
2017
|
+
const { svgString } = await renderGeometry3DSvgFromState(data.jsonState);
|
|
2018
|
+
const dataURL = `data:image/svg+xml;base64,${typeof btoa !== "undefined" ? btoa(unescape(encodeURIComponent(svgString))) : Buffer.from(svgString).toString("base64")}`;
|
|
2019
|
+
return { fileId, dataURL, mimeType: "image/svg+xml" };
|
|
2020
|
+
} catch {
|
|
2021
|
+
return null;
|
|
2022
|
+
}
|
|
2023
|
+
},
|
|
2024
|
+
Host: Geometry3DStampHost3
|
|
2025
|
+
};
|
|
2026
|
+
|
|
2027
|
+
exports.geometry3dStamp = geometry3dStamp;
|
|
2028
|
+
exports.isGeometry3DCustomData = isGeometry3DCustomData;
|
|
2029
|
+
//# sourceMappingURL=geometry-3d.js.map
|
|
2030
|
+
//# sourceMappingURL=geometry-3d.js.map
|