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