brepjs-viewer 0.1.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/CHANGELOG.md +1 -0
- package/LICENSE +191 -0
- package/README.md +31 -0
- package/dist/EdgeRenderer.d.ts +12 -0
- package/dist/GradientBackground.d.ts +6 -0
- package/dist/InfiniteGrid.d.ts +9 -0
- package/dist/Renderer.d.ts +11 -0
- package/dist/SceneLighting.d.ts +1 -0
- package/dist/SceneSetup.d.ts +31 -0
- package/dist/SelectionHighlight.d.ts +9 -0
- package/dist/ViewerCanvas.d.ts +14 -0
- package/dist/ViewerControls.d.ts +20 -0
- package/dist/ViewerInfoPanel.d.ts +12 -0
- package/dist/ViewerSectionControls.d.ts +15 -0
- package/dist/ViewerSelectionPanel.d.ts +8 -0
- package/dist/brepjs-viewer.cjs +1194 -0
- package/dist/brepjs-viewer.js +1156 -0
- package/dist/geometry.d.ts +12 -0
- package/dist/index.d.ts +11 -0
- package/dist/types.d.ts +40 -0
- package/package.json +50 -0
|
@@ -0,0 +1,1194 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let react = require("react");
|
|
25
|
+
let three = require("three");
|
|
26
|
+
three = __toESM(three, 1);
|
|
27
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
28
|
+
let _react_three_fiber = require("@react-three/fiber");
|
|
29
|
+
let _react_three_drei = require("@react-three/drei");
|
|
30
|
+
//#region src/geometry.ts
|
|
31
|
+
function buildGeometry(data) {
|
|
32
|
+
const geo = new three.BufferGeometry();
|
|
33
|
+
geo.setAttribute("position", new three.BufferAttribute(data.position, 3));
|
|
34
|
+
geo.setAttribute("normal", new three.BufferAttribute(data.normal, 3));
|
|
35
|
+
geo.setIndex(new three.BufferAttribute(data.index, 1));
|
|
36
|
+
return geo;
|
|
37
|
+
}
|
|
38
|
+
function meshBounds(data) {
|
|
39
|
+
const p = data.position;
|
|
40
|
+
if (p.length < 3) return {
|
|
41
|
+
min: [
|
|
42
|
+
0,
|
|
43
|
+
0,
|
|
44
|
+
0
|
|
45
|
+
],
|
|
46
|
+
max: [
|
|
47
|
+
0,
|
|
48
|
+
0,
|
|
49
|
+
0
|
|
50
|
+
]
|
|
51
|
+
};
|
|
52
|
+
let minX = Infinity, minY = Infinity, minZ = Infinity;
|
|
53
|
+
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
|
|
54
|
+
for (let i = 0; i + 2 < p.length; i += 3) {
|
|
55
|
+
const x = p[i];
|
|
56
|
+
const y = p[i + 1];
|
|
57
|
+
const z = p[i + 2];
|
|
58
|
+
if (x < minX) minX = x;
|
|
59
|
+
if (x > maxX) maxX = x;
|
|
60
|
+
if (y < minY) minY = y;
|
|
61
|
+
if (y > maxY) maxY = y;
|
|
62
|
+
if (z < minZ) minZ = z;
|
|
63
|
+
if (z > maxZ) maxZ = z;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
min: [
|
|
67
|
+
minX,
|
|
68
|
+
minY,
|
|
69
|
+
minZ
|
|
70
|
+
],
|
|
71
|
+
max: [
|
|
72
|
+
maxX,
|
|
73
|
+
maxY,
|
|
74
|
+
maxZ
|
|
75
|
+
]
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function meshSize(data) {
|
|
79
|
+
const { min, max } = meshBounds(data);
|
|
80
|
+
return [
|
|
81
|
+
max[0] - min[0],
|
|
82
|
+
max[1] - min[1],
|
|
83
|
+
max[2] - min[2]
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
function sectionPlane(axis, position, flip = false) {
|
|
87
|
+
const normal = new three.Vector3(axis === "x" ? 1 : 0, axis === "y" ? 1 : 0, axis === "z" ? 1 : 0);
|
|
88
|
+
if (flip) normal.negate();
|
|
89
|
+
const point = new three.Vector3(axis === "x" ? position : 0, axis === "y" ? position : 0, axis === "z" ? position : 0);
|
|
90
|
+
return new three.Plane(normal, -normal.dot(point));
|
|
91
|
+
}
|
|
92
|
+
function findFaceGroupAt(groups, triangleIndex) {
|
|
93
|
+
const off = triangleIndex * 3;
|
|
94
|
+
let lo = 0;
|
|
95
|
+
let hi = groups.length - 1;
|
|
96
|
+
while (lo <= hi) {
|
|
97
|
+
const mid = lo + hi >>> 1;
|
|
98
|
+
const g = groups[mid];
|
|
99
|
+
if (!g) break;
|
|
100
|
+
if (off < g.start) hi = mid - 1;
|
|
101
|
+
else if (off >= g.start + g.count) lo = mid + 1;
|
|
102
|
+
else return g;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/Renderer.tsx
|
|
108
|
+
function Renderer({ data, viewMode = "solid", clippingPlanes, onFacePick, onFaceHover, onFaceContextMenu }) {
|
|
109
|
+
const pickable = Boolean(data.faceGroups && data.faceInfos && (onFacePick || onFaceHover || onFaceContextMenu));
|
|
110
|
+
const geometry = (0, react.useMemo)(() => buildGeometry(data), [
|
|
111
|
+
data.position,
|
|
112
|
+
data.normal,
|
|
113
|
+
data.index
|
|
114
|
+
]);
|
|
115
|
+
(0, react.useEffect)(() => () => geometry.dispose(), [geometry]);
|
|
116
|
+
(0, react.useEffect)(() => () => {
|
|
117
|
+
document.body.style.cursor = "";
|
|
118
|
+
}, []);
|
|
119
|
+
const faceInfoById = (0, react.useMemo)(() => {
|
|
120
|
+
if (!data.faceInfos) return null;
|
|
121
|
+
const m = /* @__PURE__ */ new Map();
|
|
122
|
+
for (const i of data.faceInfos) m.set(i.faceId, i);
|
|
123
|
+
return m;
|
|
124
|
+
}, [data.faceInfos]);
|
|
125
|
+
const resolveFace = (0, react.useCallback)((e) => {
|
|
126
|
+
if (!data.faceGroups || !faceInfoById) return null;
|
|
127
|
+
const t = e.faceIndex;
|
|
128
|
+
if (t === void 0 || t === null) return null;
|
|
129
|
+
const g = findFaceGroupAt(data.faceGroups, t);
|
|
130
|
+
return g ? faceInfoById.get(g.faceId) ?? null : null;
|
|
131
|
+
}, [data.faceGroups, faceInfoById]);
|
|
132
|
+
const onClick = (0, react.useCallback)((e) => {
|
|
133
|
+
const info = resolveFace(e);
|
|
134
|
+
if (!info) return;
|
|
135
|
+
e.stopPropagation();
|
|
136
|
+
onFacePick?.(info, e.shiftKey, {
|
|
137
|
+
x: e.clientX,
|
|
138
|
+
y: e.clientY
|
|
139
|
+
});
|
|
140
|
+
}, [resolveFace, onFacePick]);
|
|
141
|
+
const onCtx = (0, react.useCallback)((e) => {
|
|
142
|
+
const info = resolveFace(e);
|
|
143
|
+
if (!info) return;
|
|
144
|
+
e.stopPropagation();
|
|
145
|
+
e.nativeEvent.preventDefault();
|
|
146
|
+
onFaceContextMenu?.(info, {
|
|
147
|
+
x: e.clientX,
|
|
148
|
+
y: e.clientY
|
|
149
|
+
});
|
|
150
|
+
}, [resolveFace, onFaceContextMenu]);
|
|
151
|
+
const onMove = (0, react.useCallback)((e) => {
|
|
152
|
+
const info = resolveFace(e);
|
|
153
|
+
if (!info) return;
|
|
154
|
+
onFaceHover?.(info, {
|
|
155
|
+
x: e.clientX,
|
|
156
|
+
y: e.clientY
|
|
157
|
+
});
|
|
158
|
+
}, [resolveFace, onFaceHover]);
|
|
159
|
+
const onOver = (0, react.useCallback)(() => {
|
|
160
|
+
document.body.style.cursor = "pointer";
|
|
161
|
+
}, []);
|
|
162
|
+
const onOut = (0, react.useCallback)(() => {
|
|
163
|
+
document.body.style.cursor = "";
|
|
164
|
+
onFaceHover?.(null);
|
|
165
|
+
}, [onFaceHover]);
|
|
166
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("mesh", {
|
|
167
|
+
geometry,
|
|
168
|
+
...pickable ? {
|
|
169
|
+
onClick,
|
|
170
|
+
onContextMenu: onCtx,
|
|
171
|
+
onPointerOver: onOver,
|
|
172
|
+
onPointerOut: onOut,
|
|
173
|
+
onPointerMove: onMove
|
|
174
|
+
} : {},
|
|
175
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("meshStandardMaterial", {
|
|
176
|
+
color: data.color ?? "#d4d8dc",
|
|
177
|
+
metalness: 0,
|
|
178
|
+
roughness: .45,
|
|
179
|
+
emissive: data.color ?? "#d4d8dc",
|
|
180
|
+
emissiveIntensity: .08,
|
|
181
|
+
side: viewMode === "solid" ? three.FrontSide : three.DoubleSide,
|
|
182
|
+
polygonOffset: true,
|
|
183
|
+
polygonOffsetFactor: 1,
|
|
184
|
+
polygonOffsetUnits: 1,
|
|
185
|
+
wireframe: viewMode === "wireframe",
|
|
186
|
+
transparent: viewMode === "xray",
|
|
187
|
+
opacity: viewMode === "xray" ? .35 : 1,
|
|
188
|
+
depthWrite: viewMode !== "xray",
|
|
189
|
+
clippingPlanes: clippingPlanes ?? null
|
|
190
|
+
})
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/EdgeRenderer.tsx
|
|
195
|
+
function findGroupAt(groups, vertexIndex) {
|
|
196
|
+
let lo = 0;
|
|
197
|
+
let hi = groups.length - 1;
|
|
198
|
+
while (lo <= hi) {
|
|
199
|
+
const mid = lo + hi >>> 1;
|
|
200
|
+
const group = groups[mid];
|
|
201
|
+
if (!group) break;
|
|
202
|
+
if (vertexIndex < group.start) hi = mid - 1;
|
|
203
|
+
else if (vertexIndex >= group.start + group.count) lo = mid + 1;
|
|
204
|
+
else return group;
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
function EdgeRenderer({ edges, edgeGroups, edgeInfos, clippingPlanes, onEdgePick, onEdgeHover, onEdgeContextMenu }) {
|
|
209
|
+
const pickable = Boolean(edgeGroups && edgeInfos && (onEdgePick || onEdgeHover || onEdgeContextMenu));
|
|
210
|
+
const viewportHeight = (0, _react_three_fiber.useThree)((s) => s.size.height);
|
|
211
|
+
const viewportHeightRef = (0, react.useRef)(viewportHeight);
|
|
212
|
+
viewportHeightRef.current = viewportHeight;
|
|
213
|
+
const lastEdgeId = (0, react.useRef)(null);
|
|
214
|
+
const geometry = (0, react.useMemo)(() => {
|
|
215
|
+
const geo = new three.BufferGeometry();
|
|
216
|
+
geo.setAttribute("position", new three.BufferAttribute(edges, 3));
|
|
217
|
+
return geo;
|
|
218
|
+
}, [edges]);
|
|
219
|
+
(0, react.useEffect)(() => {
|
|
220
|
+
return () => {
|
|
221
|
+
geometry.dispose();
|
|
222
|
+
};
|
|
223
|
+
}, [geometry]);
|
|
224
|
+
const edgeInfoById = (0, react.useMemo)(() => {
|
|
225
|
+
if (!edgeInfos) return null;
|
|
226
|
+
const byId = /* @__PURE__ */ new Map();
|
|
227
|
+
for (const info of edgeInfos) byId.set(info.edgeId, info);
|
|
228
|
+
return byId;
|
|
229
|
+
}, [edgeInfos]);
|
|
230
|
+
const resolveEdge = (0, react.useCallback)((event) => {
|
|
231
|
+
if (!edgeGroups || !edgeInfoById) return null;
|
|
232
|
+
const vertexIndex = event.index;
|
|
233
|
+
if (vertexIndex === void 0) return null;
|
|
234
|
+
const group = findGroupAt(edgeGroups, vertexIndex);
|
|
235
|
+
if (!group) return null;
|
|
236
|
+
return edgeInfoById.get(group.edgeId) ?? null;
|
|
237
|
+
}, [edgeGroups, edgeInfoById]);
|
|
238
|
+
const handleClick = (0, react.useCallback)((event) => {
|
|
239
|
+
const info = resolveEdge(event);
|
|
240
|
+
if (!info) return;
|
|
241
|
+
event.stopPropagation();
|
|
242
|
+
onEdgePick?.(info, event.shiftKey, {
|
|
243
|
+
x: event.clientX,
|
|
244
|
+
y: event.clientY
|
|
245
|
+
});
|
|
246
|
+
}, [resolveEdge, onEdgePick]);
|
|
247
|
+
const handleContextMenu = (0, react.useCallback)((event) => {
|
|
248
|
+
const info = resolveEdge(event);
|
|
249
|
+
if (!info) return;
|
|
250
|
+
event.stopPropagation();
|
|
251
|
+
event.nativeEvent.preventDefault();
|
|
252
|
+
onEdgeContextMenu?.(info, {
|
|
253
|
+
x: event.clientX,
|
|
254
|
+
y: event.clientY
|
|
255
|
+
});
|
|
256
|
+
}, [resolveEdge, onEdgeContextMenu]);
|
|
257
|
+
const handlePointerOver = (0, react.useCallback)(() => {
|
|
258
|
+
document.body.style.cursor = "pointer";
|
|
259
|
+
}, []);
|
|
260
|
+
const handlePointerOut = (0, react.useCallback)(() => {
|
|
261
|
+
document.body.style.cursor = "";
|
|
262
|
+
onEdgeHover?.(null);
|
|
263
|
+
lastEdgeId.current = null;
|
|
264
|
+
}, [onEdgeHover]);
|
|
265
|
+
const handlePointerMove = (0, react.useCallback)((event) => {
|
|
266
|
+
const info = resolveEdge(event);
|
|
267
|
+
if (!info) return;
|
|
268
|
+
event.stopPropagation();
|
|
269
|
+
lastEdgeId.current = info.edgeId;
|
|
270
|
+
onEdgeHover?.(info, {
|
|
271
|
+
x: event.clientX,
|
|
272
|
+
y: event.clientY
|
|
273
|
+
});
|
|
274
|
+
}, [resolveEdge, onEdgeHover]);
|
|
275
|
+
(0, react.useEffect)(() => {
|
|
276
|
+
return () => {
|
|
277
|
+
document.body.style.cursor = "";
|
|
278
|
+
onEdgeHover?.(null);
|
|
279
|
+
};
|
|
280
|
+
}, [onEdgeHover]);
|
|
281
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("lineSegments", {
|
|
282
|
+
geometry,
|
|
283
|
+
renderOrder: 1,
|
|
284
|
+
raycast: (0, react.useCallback)(function(raycaster, intersects) {
|
|
285
|
+
const lineParams = raycaster.params.Line;
|
|
286
|
+
if (!lineParams) {
|
|
287
|
+
three.LineSegments.prototype.raycast.call(this, raycaster, intersects);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const previousThreshold = lineParams.threshold;
|
|
291
|
+
lineParams.threshold = computeWorldThreshold(this, raycaster, viewportHeightRef.current);
|
|
292
|
+
three.LineSegments.prototype.raycast.call(this, raycaster, intersects);
|
|
293
|
+
lineParams.threshold = previousThreshold;
|
|
294
|
+
}, []),
|
|
295
|
+
...pickable ? {
|
|
296
|
+
onClick: handleClick,
|
|
297
|
+
onContextMenu: handleContextMenu,
|
|
298
|
+
onPointerOver: handlePointerOver,
|
|
299
|
+
onPointerOut: handlePointerOut,
|
|
300
|
+
onPointerMove: handlePointerMove
|
|
301
|
+
} : {},
|
|
302
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("lineBasicMaterial", {
|
|
303
|
+
color: "#000000",
|
|
304
|
+
depthTest: true,
|
|
305
|
+
linewidth: 2,
|
|
306
|
+
clippingPlanes: clippingPlanes ?? null
|
|
307
|
+
})
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
var PICK_THRESHOLD_PX = 6;
|
|
311
|
+
var FALLBACK_WORLD_THRESHOLD = .15;
|
|
312
|
+
function computeWorldThreshold(lines, raycaster, viewportHeight) {
|
|
313
|
+
const camera = raycaster.camera;
|
|
314
|
+
if (!camera || viewportHeight <= 0) return FALLBACK_WORLD_THRESHOLD;
|
|
315
|
+
if (camera.isPerspectiveCamera) {
|
|
316
|
+
const persp = camera;
|
|
317
|
+
if (!lines.geometry.boundingSphere) lines.geometry.computeBoundingSphere();
|
|
318
|
+
const sphere = lines.geometry.boundingSphere;
|
|
319
|
+
if (!sphere) return FALLBACK_WORLD_THRESHOLD;
|
|
320
|
+
const center = new three.Vector3().copy(sphere.center).applyMatrix4(lines.matrixWorld);
|
|
321
|
+
const distance = persp.position.distanceTo(center);
|
|
322
|
+
const fovRad = persp.fov * Math.PI / 180;
|
|
323
|
+
return 2 * distance * Math.tan(fovRad / 2) / viewportHeight * PICK_THRESHOLD_PX;
|
|
324
|
+
}
|
|
325
|
+
if (camera.isOrthographicCamera) {
|
|
326
|
+
const ortho = camera;
|
|
327
|
+
return (ortho.top - ortho.bottom) / ortho.zoom / viewportHeight * PICK_THRESHOLD_PX;
|
|
328
|
+
}
|
|
329
|
+
return FALLBACK_WORLD_THRESHOLD;
|
|
330
|
+
}
|
|
331
|
+
//#endregion
|
|
332
|
+
//#region src/SelectionHighlight.tsx
|
|
333
|
+
var FACE_COLOR = "#4ACECC";
|
|
334
|
+
var EDGE_COLOR = "#fbbf24";
|
|
335
|
+
function buildFaceHighlightGeometry(data, faceIds) {
|
|
336
|
+
if (faceIds.length === 0 || !data.faceGroups || !data.index) return null;
|
|
337
|
+
const groupById = new Map(data.faceGroups.map((g) => [g.faceId, g]));
|
|
338
|
+
let total = 0;
|
|
339
|
+
const ranges = [];
|
|
340
|
+
for (const id of faceIds) {
|
|
341
|
+
const g = groupById.get(id);
|
|
342
|
+
if (g) {
|
|
343
|
+
ranges.push({
|
|
344
|
+
start: g.start,
|
|
345
|
+
count: g.count
|
|
346
|
+
});
|
|
347
|
+
total += g.count;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (total === 0) return null;
|
|
351
|
+
const newIndex = new Uint32Array(total);
|
|
352
|
+
let off = 0;
|
|
353
|
+
for (const r of ranges) {
|
|
354
|
+
newIndex.set(data.index.subarray(r.start, r.start + r.count), off);
|
|
355
|
+
off += r.count;
|
|
356
|
+
}
|
|
357
|
+
const geo = new three.BufferGeometry();
|
|
358
|
+
geo.setAttribute("position", new three.BufferAttribute(data.position, 3));
|
|
359
|
+
geo.setAttribute("normal", new three.BufferAttribute(data.normal, 3));
|
|
360
|
+
geo.setIndex(new three.BufferAttribute(newIndex, 1));
|
|
361
|
+
return geo;
|
|
362
|
+
}
|
|
363
|
+
function buildEdgeHighlightGeometry(data, edgeIds) {
|
|
364
|
+
if (edgeIds.length === 0 || !data.edgeGroups) return null;
|
|
365
|
+
const groupById = new Map(data.edgeGroups.map((g) => [g.edgeId, g]));
|
|
366
|
+
let totalVerts = 0;
|
|
367
|
+
const ranges = [];
|
|
368
|
+
for (const id of edgeIds) {
|
|
369
|
+
const g = groupById.get(id);
|
|
370
|
+
if (g) {
|
|
371
|
+
ranges.push({
|
|
372
|
+
start: g.start,
|
|
373
|
+
count: g.count
|
|
374
|
+
});
|
|
375
|
+
totalVerts += g.count;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (totalVerts === 0) return null;
|
|
379
|
+
const newPos = new Float32Array(totalVerts * 3);
|
|
380
|
+
let off = 0;
|
|
381
|
+
for (const r of ranges) {
|
|
382
|
+
newPos.set(data.edges.subarray(r.start * 3, (r.start + r.count) * 3), off);
|
|
383
|
+
off += r.count * 3;
|
|
384
|
+
}
|
|
385
|
+
const geo = new three.BufferGeometry();
|
|
386
|
+
geo.setAttribute("position", new three.BufferAttribute(newPos, 3));
|
|
387
|
+
return geo;
|
|
388
|
+
}
|
|
389
|
+
function SelectionHighlight({ data, selectedFaceIds = [], selectedEdgeIds = [], hoverFaceId = null, hoverEdgeId = null }) {
|
|
390
|
+
const selectedFaceKey = selectedFaceIds.join(",");
|
|
391
|
+
const selectedEdgeKey = selectedEdgeIds.join(",");
|
|
392
|
+
const faceIds = (0, react.useMemo)(() => selectedFaceIds.filter((id) => data.faceGroups?.some((g) => g.faceId === id)), [data, selectedFaceKey]);
|
|
393
|
+
const edgeIds = (0, react.useMemo)(() => selectedEdgeIds.filter((id) => data.edgeGroups?.some((g) => g.edgeId === id)), [data, selectedEdgeKey]);
|
|
394
|
+
const faceKey = faceIds.join(",");
|
|
395
|
+
const edgeKey = edgeIds.join(",");
|
|
396
|
+
const faceGeometry = (0, react.useMemo)(() => buildFaceHighlightGeometry(data, faceIds), [data, faceKey]);
|
|
397
|
+
const edgeGeometry = (0, react.useMemo)(() => buildEdgeHighlightGeometry(data, edgeIds), [data, edgeKey]);
|
|
398
|
+
const resolvedHoverFaceId = (0, react.useMemo)(() => {
|
|
399
|
+
if (hoverFaceId === null) return null;
|
|
400
|
+
if (!data.faceGroups?.some((g) => g.faceId === hoverFaceId)) return null;
|
|
401
|
+
return hoverFaceId;
|
|
402
|
+
}, [hoverFaceId, data.faceGroups]);
|
|
403
|
+
const resolvedHoverEdgeId = (0, react.useMemo)(() => {
|
|
404
|
+
if (hoverEdgeId === null) return null;
|
|
405
|
+
if (!data.edgeGroups?.some((g) => g.edgeId === hoverEdgeId)) return null;
|
|
406
|
+
return hoverEdgeId;
|
|
407
|
+
}, [hoverEdgeId, data.edgeGroups]);
|
|
408
|
+
const hoverFaceGeometry = (0, react.useMemo)(() => resolvedHoverFaceId !== null ? buildFaceHighlightGeometry(data, [resolvedHoverFaceId]) : null, [data, resolvedHoverFaceId]);
|
|
409
|
+
const hoverEdgeGeometry = (0, react.useMemo)(() => resolvedHoverEdgeId !== null ? buildEdgeHighlightGeometry(data, [resolvedHoverEdgeId]) : null, [data, resolvedHoverEdgeId]);
|
|
410
|
+
const shouldRenderHoverFace = resolvedHoverFaceId !== null && !faceIds.includes(resolvedHoverFaceId);
|
|
411
|
+
const shouldRenderHoverEdge = resolvedHoverEdgeId !== null && !edgeIds.includes(resolvedHoverEdgeId);
|
|
412
|
+
(0, react.useEffect)(() => {
|
|
413
|
+
return () => {
|
|
414
|
+
faceGeometry?.dispose();
|
|
415
|
+
};
|
|
416
|
+
}, [faceGeometry]);
|
|
417
|
+
(0, react.useEffect)(() => {
|
|
418
|
+
return () => {
|
|
419
|
+
edgeGeometry?.dispose();
|
|
420
|
+
};
|
|
421
|
+
}, [edgeGeometry]);
|
|
422
|
+
(0, react.useEffect)(() => {
|
|
423
|
+
return () => {
|
|
424
|
+
hoverFaceGeometry?.dispose();
|
|
425
|
+
};
|
|
426
|
+
}, [hoverFaceGeometry]);
|
|
427
|
+
(0, react.useEffect)(() => {
|
|
428
|
+
return () => {
|
|
429
|
+
hoverEdgeGeometry?.dispose();
|
|
430
|
+
};
|
|
431
|
+
}, [hoverEdgeGeometry]);
|
|
432
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
433
|
+
faceGeometry && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("mesh", {
|
|
434
|
+
geometry: faceGeometry,
|
|
435
|
+
raycast: skipRaycast,
|
|
436
|
+
renderOrder: 2,
|
|
437
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("meshStandardMaterial", {
|
|
438
|
+
color: FACE_COLOR,
|
|
439
|
+
emissive: FACE_COLOR,
|
|
440
|
+
emissiveIntensity: .6,
|
|
441
|
+
metalness: 0,
|
|
442
|
+
roughness: .3,
|
|
443
|
+
side: three.DoubleSide,
|
|
444
|
+
transparent: true,
|
|
445
|
+
opacity: .55,
|
|
446
|
+
depthWrite: false,
|
|
447
|
+
polygonOffset: true,
|
|
448
|
+
polygonOffsetFactor: -2,
|
|
449
|
+
polygonOffsetUnits: -2
|
|
450
|
+
})
|
|
451
|
+
}),
|
|
452
|
+
edgeGeometry && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("lineSegments", {
|
|
453
|
+
geometry: edgeGeometry,
|
|
454
|
+
raycast: skipRaycast,
|
|
455
|
+
renderOrder: 3,
|
|
456
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("lineBasicMaterial", {
|
|
457
|
+
color: EDGE_COLOR,
|
|
458
|
+
depthTest: false,
|
|
459
|
+
transparent: true
|
|
460
|
+
})
|
|
461
|
+
}),
|
|
462
|
+
hoverFaceGeometry && shouldRenderHoverFace && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("mesh", {
|
|
463
|
+
geometry: hoverFaceGeometry,
|
|
464
|
+
raycast: skipRaycast,
|
|
465
|
+
renderOrder: 1,
|
|
466
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("meshStandardMaterial", {
|
|
467
|
+
color: FACE_COLOR,
|
|
468
|
+
emissive: FACE_COLOR,
|
|
469
|
+
emissiveIntensity: .35,
|
|
470
|
+
metalness: 0,
|
|
471
|
+
roughness: .3,
|
|
472
|
+
side: three.DoubleSide,
|
|
473
|
+
transparent: true,
|
|
474
|
+
opacity: .22,
|
|
475
|
+
depthWrite: false,
|
|
476
|
+
polygonOffset: true,
|
|
477
|
+
polygonOffsetFactor: -2,
|
|
478
|
+
polygonOffsetUnits: -2
|
|
479
|
+
})
|
|
480
|
+
}),
|
|
481
|
+
hoverEdgeGeometry && shouldRenderHoverEdge && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("lineSegments", {
|
|
482
|
+
geometry: hoverEdgeGeometry,
|
|
483
|
+
raycast: skipRaycast,
|
|
484
|
+
renderOrder: 2,
|
|
485
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("lineBasicMaterial", {
|
|
486
|
+
color: EDGE_COLOR,
|
|
487
|
+
depthTest: false,
|
|
488
|
+
transparent: true,
|
|
489
|
+
opacity: .55
|
|
490
|
+
})
|
|
491
|
+
})
|
|
492
|
+
] });
|
|
493
|
+
}
|
|
494
|
+
function skipRaycast() {}
|
|
495
|
+
//#endregion
|
|
496
|
+
//#region src/GradientBackground.tsx
|
|
497
|
+
var vertexShader$1 = `
|
|
498
|
+
varying vec2 vUv;
|
|
499
|
+
void main() {
|
|
500
|
+
vUv = uv;
|
|
501
|
+
gl_Position = vec4(position.xy, 0.9999, 1.0);
|
|
502
|
+
}
|
|
503
|
+
`;
|
|
504
|
+
var fragmentShader$1 = `
|
|
505
|
+
uniform vec3 colorTop;
|
|
506
|
+
uniform vec3 colorBottom;
|
|
507
|
+
varying vec2 vUv;
|
|
508
|
+
void main() {
|
|
509
|
+
gl_FragColor = vec4(mix(colorBottom, colorTop, vUv.y), 1.0);
|
|
510
|
+
}
|
|
511
|
+
`;
|
|
512
|
+
function GradientBackground({ colorTop = "#2a2a3e", colorBottom = "#2a2a3e" }) {
|
|
513
|
+
const matRef = (0, react.useRef)(null);
|
|
514
|
+
(0, react.useEffect)(() => {
|
|
515
|
+
return () => {
|
|
516
|
+
matRef.current?.dispose();
|
|
517
|
+
};
|
|
518
|
+
}, []);
|
|
519
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("mesh", {
|
|
520
|
+
renderOrder: -1,
|
|
521
|
+
frustumCulled: false,
|
|
522
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("planeGeometry", { args: [2, 2] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("shaderMaterial", {
|
|
523
|
+
ref: matRef,
|
|
524
|
+
vertexShader: vertexShader$1,
|
|
525
|
+
fragmentShader: fragmentShader$1,
|
|
526
|
+
uniforms: {
|
|
527
|
+
colorTop: { value: new three.Color(colorTop) },
|
|
528
|
+
colorBottom: { value: new three.Color(colorBottom) }
|
|
529
|
+
},
|
|
530
|
+
depthWrite: false,
|
|
531
|
+
depthTest: false
|
|
532
|
+
})]
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
//#endregion
|
|
536
|
+
//#region src/InfiniteGrid.tsx
|
|
537
|
+
var vertexShader = `
|
|
538
|
+
varying vec2 vWorldPos;
|
|
539
|
+
void main() {
|
|
540
|
+
vec4 worldPos = modelMatrix * vec4(position, 1.0);
|
|
541
|
+
vWorldPos = worldPos.xz;
|
|
542
|
+
gl_Position = projectionMatrix * viewMatrix * worldPos;
|
|
543
|
+
}
|
|
544
|
+
`;
|
|
545
|
+
var fragmentShader = `
|
|
546
|
+
uniform float cellSize;
|
|
547
|
+
uniform vec3 lineColor;
|
|
548
|
+
uniform float lineOpacity;
|
|
549
|
+
uniform float fadeStart;
|
|
550
|
+
uniform float fadeEnd;
|
|
551
|
+
|
|
552
|
+
varying vec2 vWorldPos;
|
|
553
|
+
|
|
554
|
+
void main() {
|
|
555
|
+
vec2 coord = vWorldPos / cellSize;
|
|
556
|
+
vec2 grid = abs(fract(coord - 0.5) - 0.5);
|
|
557
|
+
vec2 line = fwidth(coord);
|
|
558
|
+
vec2 gridAA = smoothstep(line * 0.5, line * 1.5, grid);
|
|
559
|
+
float gridLine = 1.0 - min(gridAA.x, gridAA.y);
|
|
560
|
+
|
|
561
|
+
float dist = length(vWorldPos);
|
|
562
|
+
float fade = 1.0 - smoothstep(fadeStart, fadeEnd, dist);
|
|
563
|
+
|
|
564
|
+
float alpha = gridLine * lineOpacity * fade;
|
|
565
|
+
if (alpha < 0.001) discard;
|
|
566
|
+
|
|
567
|
+
gl_FragColor = vec4(lineColor, alpha);
|
|
568
|
+
}
|
|
569
|
+
`;
|
|
570
|
+
function InfiniteGrid({ cellSize = 10, lineColor = "#888898", lineOpacity = .1, fadeStart = 50, fadeEnd = 200 }) {
|
|
571
|
+
const matRef = (0, react.useRef)(null);
|
|
572
|
+
const planeSize = fadeEnd * 2.5;
|
|
573
|
+
(0, react.useEffect)(() => {
|
|
574
|
+
return () => {
|
|
575
|
+
matRef.current?.dispose();
|
|
576
|
+
};
|
|
577
|
+
}, []);
|
|
578
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("mesh", {
|
|
579
|
+
rotation: [
|
|
580
|
+
-Math.PI / 2,
|
|
581
|
+
0,
|
|
582
|
+
0
|
|
583
|
+
],
|
|
584
|
+
position: [
|
|
585
|
+
0,
|
|
586
|
+
-.01,
|
|
587
|
+
0
|
|
588
|
+
],
|
|
589
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("planeGeometry", { args: [planeSize, planeSize] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("shaderMaterial", {
|
|
590
|
+
ref: matRef,
|
|
591
|
+
vertexShader,
|
|
592
|
+
fragmentShader,
|
|
593
|
+
uniforms: {
|
|
594
|
+
cellSize: { value: cellSize },
|
|
595
|
+
lineColor: { value: new three.Color(lineColor) },
|
|
596
|
+
lineOpacity: { value: lineOpacity },
|
|
597
|
+
fadeStart: { value: fadeStart },
|
|
598
|
+
fadeEnd: { value: fadeEnd }
|
|
599
|
+
},
|
|
600
|
+
transparent: true,
|
|
601
|
+
depthWrite: false,
|
|
602
|
+
side: three.DoubleSide,
|
|
603
|
+
polygonOffset: true,
|
|
604
|
+
polygonOffsetFactor: 1,
|
|
605
|
+
polygonOffsetUnits: 1
|
|
606
|
+
})]
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
//#endregion
|
|
610
|
+
//#region src/SceneLighting.tsx
|
|
611
|
+
function SceneLighting() {
|
|
612
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
613
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("hemisphereLight", { args: [
|
|
614
|
+
"#ffffff",
|
|
615
|
+
"#1a1a2e",
|
|
616
|
+
.65
|
|
617
|
+
] }),
|
|
618
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("directionalLight", {
|
|
619
|
+
position: [
|
|
620
|
+
-50,
|
|
621
|
+
60,
|
|
622
|
+
80
|
|
623
|
+
],
|
|
624
|
+
intensity: .85,
|
|
625
|
+
color: "#fff8f0"
|
|
626
|
+
}),
|
|
627
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("directionalLight", {
|
|
628
|
+
position: [
|
|
629
|
+
40,
|
|
630
|
+
-40,
|
|
631
|
+
30
|
|
632
|
+
],
|
|
633
|
+
intensity: .15,
|
|
634
|
+
color: "#e0e8ff"
|
|
635
|
+
})
|
|
636
|
+
] });
|
|
637
|
+
}
|
|
638
|
+
//#endregion
|
|
639
|
+
//#region src/SceneSetup.tsx
|
|
640
|
+
function SceneSetup({ autoRotate = false, target, gridVisible = true, gridProps, controlsProps, controlsRef, onControlsStart }) {
|
|
641
|
+
const optionalControls = {
|
|
642
|
+
...controlsRef ? { ref: controlsRef } : {},
|
|
643
|
+
...target ? { target } : {},
|
|
644
|
+
...onControlsStart ? { onStart: onControlsStart } : {}
|
|
645
|
+
};
|
|
646
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
647
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SceneLighting, {}),
|
|
648
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(GradientBackground, {}),
|
|
649
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_three_drei.OrbitControls, {
|
|
650
|
+
makeDefault: true,
|
|
651
|
+
autoRotate,
|
|
652
|
+
autoRotateSpeed: 1.5,
|
|
653
|
+
...optionalControls,
|
|
654
|
+
...controlsProps
|
|
655
|
+
}),
|
|
656
|
+
gridVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InfiniteGrid, { ...gridProps })
|
|
657
|
+
] });
|
|
658
|
+
}
|
|
659
|
+
//#endregion
|
|
660
|
+
//#region src/ViewerCanvas.tsx
|
|
661
|
+
var VIEW_DIR = {
|
|
662
|
+
iso: new three.Vector3(.6, .5, .6),
|
|
663
|
+
front: new three.Vector3(0, .3, 1),
|
|
664
|
+
top: new three.Vector3(0, 1, -.01),
|
|
665
|
+
right: new three.Vector3(1, .3, 0)
|
|
666
|
+
};
|
|
667
|
+
function Framing({ data, view, fitSignal, projection, onFirstFrame }) {
|
|
668
|
+
const camera = (0, _react_three_fiber.useThree)((s) => s.camera);
|
|
669
|
+
const invalidate = (0, _react_three_fiber.useThree)((s) => s.invalidate);
|
|
670
|
+
const fired = (0, react.useRef)(false);
|
|
671
|
+
const onFirstFrameRef = (0, react.useRef)(onFirstFrame);
|
|
672
|
+
onFirstFrameRef.current = onFirstFrame;
|
|
673
|
+
const { center, radius } = (0, react.useMemo)(() => {
|
|
674
|
+
const g = buildGeometry(data);
|
|
675
|
+
g.computeBoundingSphere();
|
|
676
|
+
const s = g.boundingSphere ?? new three.Sphere(new three.Vector3(), 1);
|
|
677
|
+
const result = {
|
|
678
|
+
center: s.center.clone(),
|
|
679
|
+
radius: s.radius || 1
|
|
680
|
+
};
|
|
681
|
+
g.dispose();
|
|
682
|
+
return result;
|
|
683
|
+
}, [data]);
|
|
684
|
+
(0, react.useEffect)(() => {
|
|
685
|
+
const dir = VIEW_DIR[view].clone().normalize();
|
|
686
|
+
camera.position.copy(center).addScaledVector(dir, radius * 3);
|
|
687
|
+
camera.near = radius / 100;
|
|
688
|
+
camera.far = radius * 100;
|
|
689
|
+
const ortho = camera;
|
|
690
|
+
if (ortho.isOrthographicCamera) {
|
|
691
|
+
const viewSize = Math.min(ortho.right - ortho.left, ortho.top - ortho.bottom);
|
|
692
|
+
if (viewSize > 0 && radius > 0) ortho.zoom = viewSize / (radius * 2.4);
|
|
693
|
+
}
|
|
694
|
+
camera.lookAt(center);
|
|
695
|
+
camera.updateProjectionMatrix();
|
|
696
|
+
invalidate();
|
|
697
|
+
if (!fired.current) {
|
|
698
|
+
fired.current = true;
|
|
699
|
+
onFirstFrameRef.current?.();
|
|
700
|
+
}
|
|
701
|
+
}, [
|
|
702
|
+
camera,
|
|
703
|
+
invalidate,
|
|
704
|
+
center,
|
|
705
|
+
radius,
|
|
706
|
+
view,
|
|
707
|
+
fitSignal,
|
|
708
|
+
projection
|
|
709
|
+
]);
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
function OrthoCamera() {
|
|
713
|
+
const camera = (0, _react_three_fiber.useThree)((s) => s.camera);
|
|
714
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_three_drei.OrthographicCamera, {
|
|
715
|
+
makeDefault: true,
|
|
716
|
+
position: (0, react.useRef)([
|
|
717
|
+
camera.position.x,
|
|
718
|
+
camera.position.y,
|
|
719
|
+
camera.position.z
|
|
720
|
+
]).current,
|
|
721
|
+
zoom: 20,
|
|
722
|
+
near: .1,
|
|
723
|
+
far: 2e3
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
function LocalClipping() {
|
|
727
|
+
const gl = (0, _react_three_fiber.useThree)((s) => s.gl);
|
|
728
|
+
(0, react.useEffect)(() => {
|
|
729
|
+
gl.localClippingEnabled = true;
|
|
730
|
+
return () => {
|
|
731
|
+
gl.localClippingEnabled = false;
|
|
732
|
+
};
|
|
733
|
+
}, [gl]);
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
function ViewerCanvas({ data, view = "iso", fitSignal, autoRotate = false, gridVisible = true, projection = "perspective", onFirstFrame, children }) {
|
|
737
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_react_three_fiber.Canvas, {
|
|
738
|
+
frameloop: autoRotate ? "always" : "demand",
|
|
739
|
+
gl: { preserveDrawingBuffer: true },
|
|
740
|
+
children: [
|
|
741
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(LocalClipping, {}),
|
|
742
|
+
projection === "orthographic" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OrthoCamera, {}),
|
|
743
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SceneSetup, {
|
|
744
|
+
autoRotate,
|
|
745
|
+
gridVisible
|
|
746
|
+
}),
|
|
747
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Framing, {
|
|
748
|
+
data,
|
|
749
|
+
view,
|
|
750
|
+
fitSignal,
|
|
751
|
+
projection,
|
|
752
|
+
onFirstFrame
|
|
753
|
+
}),
|
|
754
|
+
children
|
|
755
|
+
]
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
//#endregion
|
|
759
|
+
//#region src/types.ts
|
|
760
|
+
var VIEW_NAMES = [
|
|
761
|
+
"iso",
|
|
762
|
+
"front",
|
|
763
|
+
"top",
|
|
764
|
+
"right"
|
|
765
|
+
];
|
|
766
|
+
//#endregion
|
|
767
|
+
//#region src/ViewerControls.tsx
|
|
768
|
+
var MODE_LABELS = {
|
|
769
|
+
solid: "Solid",
|
|
770
|
+
wireframe: "Wire",
|
|
771
|
+
xray: "X-ray"
|
|
772
|
+
};
|
|
773
|
+
var VIEW_LABELS = {
|
|
774
|
+
iso: "Iso",
|
|
775
|
+
front: "Front",
|
|
776
|
+
top: "Top",
|
|
777
|
+
right: "Right"
|
|
778
|
+
};
|
|
779
|
+
function ViewerControls({ viewMode, onViewModeChange, showEdges, onToggleEdges, showGrid, onToggleGrid, autoRotate, onToggleAutoRotate, projection, onToggleProjection, activeView, onView, onFit, onScreenshot, className }) {
|
|
780
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
781
|
+
className,
|
|
782
|
+
style: className ? void 0 : containerStyle$3,
|
|
783
|
+
children: [
|
|
784
|
+
(onFit || onScreenshot) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Group, { children: [onFit && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Btn$1, {
|
|
785
|
+
label: "Fit",
|
|
786
|
+
onClick: onFit
|
|
787
|
+
}), onScreenshot && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Btn$1, {
|
|
788
|
+
label: "Snap",
|
|
789
|
+
onClick: onScreenshot
|
|
790
|
+
})] }),
|
|
791
|
+
viewMode && onViewModeChange && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Group, { children: Object.keys(MODE_LABELS).map((mode) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Btn$1, {
|
|
792
|
+
label: MODE_LABELS[mode],
|
|
793
|
+
active: viewMode === mode,
|
|
794
|
+
onClick: () => {
|
|
795
|
+
onViewModeChange(mode);
|
|
796
|
+
}
|
|
797
|
+
}, mode)) }),
|
|
798
|
+
(onToggleEdges || onToggleGrid || onToggleAutoRotate || onToggleProjection) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Group, { children: [
|
|
799
|
+
onToggleEdges && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Btn$1, {
|
|
800
|
+
label: "Edges",
|
|
801
|
+
active: showEdges,
|
|
802
|
+
onClick: onToggleEdges
|
|
803
|
+
}),
|
|
804
|
+
onToggleGrid && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Btn$1, {
|
|
805
|
+
label: "Grid",
|
|
806
|
+
active: showGrid,
|
|
807
|
+
onClick: onToggleGrid
|
|
808
|
+
}),
|
|
809
|
+
onToggleAutoRotate && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Btn$1, {
|
|
810
|
+
label: "Spin",
|
|
811
|
+
active: autoRotate,
|
|
812
|
+
onClick: onToggleAutoRotate
|
|
813
|
+
}),
|
|
814
|
+
onToggleProjection && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Btn$1, {
|
|
815
|
+
label: projection === "orthographic" ? "Ortho" : "Persp",
|
|
816
|
+
active: projection === "orthographic",
|
|
817
|
+
onClick: onToggleProjection
|
|
818
|
+
})
|
|
819
|
+
] }),
|
|
820
|
+
onView && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Group, { children: VIEW_NAMES.map((view) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Btn$1, {
|
|
821
|
+
label: VIEW_LABELS[view],
|
|
822
|
+
active: activeView === view,
|
|
823
|
+
onClick: () => {
|
|
824
|
+
onView(view);
|
|
825
|
+
}
|
|
826
|
+
}, view)) })
|
|
827
|
+
]
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
function Group({ children }) {
|
|
831
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
832
|
+
style: groupStyle,
|
|
833
|
+
children
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
function Btn$1({ label, active, onClick }) {
|
|
837
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
838
|
+
type: "button",
|
|
839
|
+
onClick,
|
|
840
|
+
"aria-pressed": active,
|
|
841
|
+
style: {
|
|
842
|
+
...buttonStyle$1,
|
|
843
|
+
...active ? activeButtonStyle$1 : null
|
|
844
|
+
},
|
|
845
|
+
children: label
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
var containerStyle$3 = {
|
|
849
|
+
position: "absolute",
|
|
850
|
+
top: 12,
|
|
851
|
+
right: 12,
|
|
852
|
+
zIndex: 10,
|
|
853
|
+
display: "flex",
|
|
854
|
+
flexDirection: "column",
|
|
855
|
+
gap: 6,
|
|
856
|
+
pointerEvents: "none",
|
|
857
|
+
fontFamily: "ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif"
|
|
858
|
+
};
|
|
859
|
+
var groupStyle = {
|
|
860
|
+
display: "flex",
|
|
861
|
+
flexDirection: "column",
|
|
862
|
+
gap: 4,
|
|
863
|
+
padding: 4,
|
|
864
|
+
borderRadius: 8,
|
|
865
|
+
background: "rgba(26, 29, 33, 0.7)",
|
|
866
|
+
backdropFilter: "blur(6px)",
|
|
867
|
+
border: "1px solid rgba(255, 255, 255, 0.08)"
|
|
868
|
+
};
|
|
869
|
+
var buttonStyle$1 = {
|
|
870
|
+
cursor: "pointer",
|
|
871
|
+
pointerEvents: "auto",
|
|
872
|
+
padding: "4px 10px",
|
|
873
|
+
borderRadius: 5,
|
|
874
|
+
fontSize: 12,
|
|
875
|
+
fontWeight: 500,
|
|
876
|
+
lineHeight: 1.2,
|
|
877
|
+
color: "#9aa3ad",
|
|
878
|
+
background: "rgba(255, 255, 255, 0.04)",
|
|
879
|
+
border: "1px solid transparent",
|
|
880
|
+
transition: "color 120ms, background 120ms"
|
|
881
|
+
};
|
|
882
|
+
var activeButtonStyle$1 = {
|
|
883
|
+
color: "#6ee7d7",
|
|
884
|
+
background: "rgba(45, 212, 191, 0.16)",
|
|
885
|
+
borderColor: "rgba(45, 212, 191, 0.3)"
|
|
886
|
+
};
|
|
887
|
+
//#endregion
|
|
888
|
+
//#region src/ViewerInfoPanel.tsx
|
|
889
|
+
function ViewerInfoPanel({ dims, volume, area, triangles, valid, unit, className }) {
|
|
890
|
+
const u = unit ? ` ${unit}` : "";
|
|
891
|
+
const rows = [];
|
|
892
|
+
if (dims) rows.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row$1, {
|
|
893
|
+
label: "Size",
|
|
894
|
+
value: `${fmt$1(dims[0])} × ${fmt$1(dims[1])} × ${fmt$1(dims[2])}${u}`
|
|
895
|
+
}, "size"));
|
|
896
|
+
if (volume !== void 0) rows.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row$1, {
|
|
897
|
+
label: "Volume",
|
|
898
|
+
value: `${fmt$1(volume)}${unit ? ` ${unit}³` : ""}`
|
|
899
|
+
}, "vol"));
|
|
900
|
+
if (area !== void 0) rows.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row$1, {
|
|
901
|
+
label: "Area",
|
|
902
|
+
value: `${fmt$1(area)}${unit ? ` ${unit}²` : ""}`
|
|
903
|
+
}, "area"));
|
|
904
|
+
if (triangles !== void 0) rows.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row$1, {
|
|
905
|
+
label: "Triangles",
|
|
906
|
+
value: triangles.toLocaleString()
|
|
907
|
+
}, "tris"));
|
|
908
|
+
if (valid !== void 0) rows.push(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row$1, {
|
|
909
|
+
label: "Validity",
|
|
910
|
+
value: valid ? "valid" : "invalid",
|
|
911
|
+
valueColor: valid ? "#6ee7d7" : "#f0a0a0"
|
|
912
|
+
}, "valid"));
|
|
913
|
+
if (rows.length === 0) return null;
|
|
914
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
915
|
+
className,
|
|
916
|
+
style: className ? void 0 : containerStyle$2,
|
|
917
|
+
children: rows
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
function Row$1({ label, value, valueColor }) {
|
|
921
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
922
|
+
style: rowStyle$1,
|
|
923
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
924
|
+
style: labelStyle$1,
|
|
925
|
+
children: label
|
|
926
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
927
|
+
style: {
|
|
928
|
+
...valueStyle$1,
|
|
929
|
+
...valueColor ? { color: valueColor } : null
|
|
930
|
+
},
|
|
931
|
+
children: value
|
|
932
|
+
})]
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
function fmt$1(n) {
|
|
936
|
+
if (!Number.isFinite(n)) return "—";
|
|
937
|
+
if (Number.isInteger(n)) return n.toLocaleString();
|
|
938
|
+
return Number(n.toFixed(2)).toLocaleString();
|
|
939
|
+
}
|
|
940
|
+
var containerStyle$2 = {
|
|
941
|
+
position: "absolute",
|
|
942
|
+
left: 12,
|
|
943
|
+
bottom: 12,
|
|
944
|
+
zIndex: 10,
|
|
945
|
+
display: "flex",
|
|
946
|
+
flexDirection: "column",
|
|
947
|
+
gap: 2,
|
|
948
|
+
padding: "8px 10px",
|
|
949
|
+
borderRadius: 8,
|
|
950
|
+
minWidth: 150,
|
|
951
|
+
background: "rgba(26, 29, 33, 0.7)",
|
|
952
|
+
backdropFilter: "blur(6px)",
|
|
953
|
+
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
954
|
+
fontFamily: "ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif"
|
|
955
|
+
};
|
|
956
|
+
var rowStyle$1 = {
|
|
957
|
+
display: "flex",
|
|
958
|
+
justifyContent: "space-between",
|
|
959
|
+
gap: 16,
|
|
960
|
+
fontSize: 12,
|
|
961
|
+
lineHeight: 1.5
|
|
962
|
+
};
|
|
963
|
+
var labelStyle$1 = { color: "#7b8591" };
|
|
964
|
+
var valueStyle$1 = {
|
|
965
|
+
color: "#c8cdd3",
|
|
966
|
+
fontVariantNumeric: "tabular-nums"
|
|
967
|
+
};
|
|
968
|
+
//#endregion
|
|
969
|
+
//#region src/ViewerSelectionPanel.tsx
|
|
970
|
+
var SURFACE_LABELS = {
|
|
971
|
+
PLANE: "Planar",
|
|
972
|
+
CYLINDER: "Cylindrical",
|
|
973
|
+
CONE: "Conical",
|
|
974
|
+
SPHERE: "Spherical",
|
|
975
|
+
TORUS: "Toroidal",
|
|
976
|
+
BEZIER_SURFACE: "Bézier",
|
|
977
|
+
BSPLINE_SURFACE: "B-spline",
|
|
978
|
+
REVOLUTION_SURFACE: "Revolved",
|
|
979
|
+
EXTRUSION_SURFACE: "Extruded",
|
|
980
|
+
OFFSET_SURFACE: "Offset",
|
|
981
|
+
OTHER_SURFACE: "Surface"
|
|
982
|
+
};
|
|
983
|
+
function surfaceLabel(t) {
|
|
984
|
+
return SURFACE_LABELS[t] ?? "Surface";
|
|
985
|
+
}
|
|
986
|
+
function fmt(n) {
|
|
987
|
+
if (!Number.isFinite(n)) return "—";
|
|
988
|
+
if (Number.isInteger(n)) return n.toLocaleString();
|
|
989
|
+
return Number(n.toFixed(2)).toLocaleString();
|
|
990
|
+
}
|
|
991
|
+
function ViewerSelectionPanel({ face, onClear, unit, className }) {
|
|
992
|
+
if (!face) return null;
|
|
993
|
+
const [nx, ny, nz] = face.normal;
|
|
994
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
995
|
+
className,
|
|
996
|
+
style: className ? void 0 : containerStyle$1,
|
|
997
|
+
children: [
|
|
998
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
999
|
+
style: headerStyle,
|
|
1000
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1001
|
+
style: titleStyle,
|
|
1002
|
+
children: [surfaceLabel(face.surfaceType), " face"]
|
|
1003
|
+
}), onClear && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1004
|
+
type: "button",
|
|
1005
|
+
onClick: onClear,
|
|
1006
|
+
"aria-label": "Clear selection",
|
|
1007
|
+
style: closeStyle,
|
|
1008
|
+
children: "✕"
|
|
1009
|
+
})]
|
|
1010
|
+
}),
|
|
1011
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1012
|
+
label: "Area",
|
|
1013
|
+
value: `${fmt(face.area)}${unit ? ` ${unit}²` : ""}`
|
|
1014
|
+
}),
|
|
1015
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Row, {
|
|
1016
|
+
label: "Normal",
|
|
1017
|
+
value: `${fmt(nx)}, ${fmt(ny)}, ${fmt(nz)}`
|
|
1018
|
+
})
|
|
1019
|
+
]
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
function Row({ label, value }) {
|
|
1023
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1024
|
+
style: rowStyle,
|
|
1025
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1026
|
+
style: labelStyle,
|
|
1027
|
+
children: label
|
|
1028
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1029
|
+
style: valueStyle,
|
|
1030
|
+
children: value
|
|
1031
|
+
})]
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
var containerStyle$1 = {
|
|
1035
|
+
position: "absolute",
|
|
1036
|
+
left: 12,
|
|
1037
|
+
top: 12,
|
|
1038
|
+
zIndex: 10,
|
|
1039
|
+
display: "flex",
|
|
1040
|
+
flexDirection: "column",
|
|
1041
|
+
gap: 2,
|
|
1042
|
+
padding: "8px 10px",
|
|
1043
|
+
borderRadius: 8,
|
|
1044
|
+
minWidth: 170,
|
|
1045
|
+
background: "rgba(26, 29, 33, 0.7)",
|
|
1046
|
+
backdropFilter: "blur(6px)",
|
|
1047
|
+
border: "1px solid rgba(74, 206, 204, 0.3)",
|
|
1048
|
+
fontFamily: "ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif"
|
|
1049
|
+
};
|
|
1050
|
+
var headerStyle = {
|
|
1051
|
+
display: "flex",
|
|
1052
|
+
justifyContent: "space-between",
|
|
1053
|
+
alignItems: "center",
|
|
1054
|
+
gap: 12,
|
|
1055
|
+
marginBottom: 2
|
|
1056
|
+
};
|
|
1057
|
+
var titleStyle = {
|
|
1058
|
+
color: "#6ee7d7",
|
|
1059
|
+
fontSize: 12,
|
|
1060
|
+
fontWeight: 600
|
|
1061
|
+
};
|
|
1062
|
+
var closeStyle = {
|
|
1063
|
+
cursor: "pointer",
|
|
1064
|
+
border: "none",
|
|
1065
|
+
background: "transparent",
|
|
1066
|
+
color: "#7b8591",
|
|
1067
|
+
fontSize: 12,
|
|
1068
|
+
lineHeight: 1,
|
|
1069
|
+
padding: 0
|
|
1070
|
+
};
|
|
1071
|
+
var rowStyle = {
|
|
1072
|
+
display: "flex",
|
|
1073
|
+
justifyContent: "space-between",
|
|
1074
|
+
gap: 16,
|
|
1075
|
+
fontSize: 12,
|
|
1076
|
+
lineHeight: 1.5
|
|
1077
|
+
};
|
|
1078
|
+
var labelStyle = { color: "#7b8591" };
|
|
1079
|
+
var valueStyle = {
|
|
1080
|
+
color: "#c8cdd3",
|
|
1081
|
+
fontVariantNumeric: "tabular-nums"
|
|
1082
|
+
};
|
|
1083
|
+
//#endregion
|
|
1084
|
+
//#region src/ViewerSectionControls.tsx
|
|
1085
|
+
var AXES = [
|
|
1086
|
+
"x",
|
|
1087
|
+
"y",
|
|
1088
|
+
"z"
|
|
1089
|
+
];
|
|
1090
|
+
function ViewerSectionControls({ enabled, onToggle, axis, onAxisChange, position, min, max, onPositionChange, flip, onToggleFlip, className }) {
|
|
1091
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1092
|
+
className,
|
|
1093
|
+
style: className ? void 0 : containerStyle,
|
|
1094
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Btn, {
|
|
1095
|
+
label: "Section",
|
|
1096
|
+
active: enabled,
|
|
1097
|
+
onClick: onToggle
|
|
1098
|
+
}), enabled && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1099
|
+
AXES.map((a) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Btn, {
|
|
1100
|
+
label: a.toUpperCase(),
|
|
1101
|
+
active: axis === a,
|
|
1102
|
+
onClick: () => {
|
|
1103
|
+
onAxisChange(a);
|
|
1104
|
+
}
|
|
1105
|
+
}, a)),
|
|
1106
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1107
|
+
type: "range",
|
|
1108
|
+
min,
|
|
1109
|
+
max,
|
|
1110
|
+
step: (max - min) / 200 || .01,
|
|
1111
|
+
value: position,
|
|
1112
|
+
disabled: min === max,
|
|
1113
|
+
onChange: (e) => {
|
|
1114
|
+
onPositionChange(Number(e.target.value));
|
|
1115
|
+
},
|
|
1116
|
+
"aria-label": "Section position",
|
|
1117
|
+
style: {
|
|
1118
|
+
...sliderStyle,
|
|
1119
|
+
...min === max ? { opacity: .4 } : null
|
|
1120
|
+
}
|
|
1121
|
+
}),
|
|
1122
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Btn, {
|
|
1123
|
+
label: "Flip",
|
|
1124
|
+
active: flip,
|
|
1125
|
+
onClick: onToggleFlip
|
|
1126
|
+
})
|
|
1127
|
+
] })]
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
function Btn({ label, active, onClick }) {
|
|
1131
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1132
|
+
type: "button",
|
|
1133
|
+
onClick,
|
|
1134
|
+
"aria-pressed": active,
|
|
1135
|
+
style: {
|
|
1136
|
+
...buttonStyle,
|
|
1137
|
+
...active ? activeButtonStyle : null
|
|
1138
|
+
},
|
|
1139
|
+
children: label
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
var containerStyle = {
|
|
1143
|
+
position: "absolute",
|
|
1144
|
+
bottom: 12,
|
|
1145
|
+
left: "50%",
|
|
1146
|
+
transform: "translateX(-50%)",
|
|
1147
|
+
zIndex: 10,
|
|
1148
|
+
display: "flex",
|
|
1149
|
+
alignItems: "center",
|
|
1150
|
+
gap: 4,
|
|
1151
|
+
padding: 4,
|
|
1152
|
+
borderRadius: 8,
|
|
1153
|
+
background: "rgba(26, 29, 33, 0.7)",
|
|
1154
|
+
backdropFilter: "blur(6px)",
|
|
1155
|
+
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
1156
|
+
fontFamily: "ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif"
|
|
1157
|
+
};
|
|
1158
|
+
var buttonStyle = {
|
|
1159
|
+
cursor: "pointer",
|
|
1160
|
+
padding: "4px 10px",
|
|
1161
|
+
borderRadius: 5,
|
|
1162
|
+
fontSize: 12,
|
|
1163
|
+
fontWeight: 500,
|
|
1164
|
+
lineHeight: 1.2,
|
|
1165
|
+
color: "#9aa3ad",
|
|
1166
|
+
background: "rgba(255, 255, 255, 0.04)",
|
|
1167
|
+
border: "1px solid transparent"
|
|
1168
|
+
};
|
|
1169
|
+
var activeButtonStyle = {
|
|
1170
|
+
color: "#6ee7d7",
|
|
1171
|
+
background: "rgba(45, 212, 191, 0.16)",
|
|
1172
|
+
borderColor: "rgba(45, 212, 191, 0.3)"
|
|
1173
|
+
};
|
|
1174
|
+
var sliderStyle = {
|
|
1175
|
+
width: 160,
|
|
1176
|
+
accentColor: "#4ACECC",
|
|
1177
|
+
margin: "0 4px"
|
|
1178
|
+
};
|
|
1179
|
+
//#endregion
|
|
1180
|
+
exports.EdgeRenderer = EdgeRenderer;
|
|
1181
|
+
exports.Renderer = Renderer;
|
|
1182
|
+
exports.SceneSetup = SceneSetup;
|
|
1183
|
+
exports.SelectionHighlight = SelectionHighlight;
|
|
1184
|
+
exports.VIEW_NAMES = VIEW_NAMES;
|
|
1185
|
+
exports.ViewerCanvas = ViewerCanvas;
|
|
1186
|
+
exports.ViewerControls = ViewerControls;
|
|
1187
|
+
exports.ViewerInfoPanel = ViewerInfoPanel;
|
|
1188
|
+
exports.ViewerSectionControls = ViewerSectionControls;
|
|
1189
|
+
exports.ViewerSelectionPanel = ViewerSelectionPanel;
|
|
1190
|
+
exports.buildGeometry = buildGeometry;
|
|
1191
|
+
exports.findFaceGroupAt = findFaceGroupAt;
|
|
1192
|
+
exports.meshBounds = meshBounds;
|
|
1193
|
+
exports.meshSize = meshSize;
|
|
1194
|
+
exports.sectionPlane = sectionPlane;
|