1/**
2 * Pure geometry helpers that turn a SurfaceModel into renderable / exportable
3 * arrays. No three.js and no OpenCASCADE here — both the renderer and the
4 * export writers depend on this module.
5 */
6import type { NurbsSurface, SurfaceModel, TriMesh } from './types'
8/** Axis-aligned bounds of the model, plus a center and radius for framing. */
9export interface Bounds {
10 min: [number, number, number]
11 max: [number, number, number]
12 center: [number, number, number]
13 radius: number
14}
16function extend(min: number[], max: number[], x: number, y: number, z: number) {
17 if (x < min[0]) min[0] = x
18 if (y < min[1]) min[1] = y
19 if (z < min[2]) min[2] = z
20 if (x > max[0]) max[0] = x
21 if (y > max[1]) max[1] = y
22 if (z > max[2]) max[2] = z
23}
25export function modelBounds(model: SurfaceModel): Bounds {
26 const min = [Infinity, Infinity, Infinity]
27 const max = [-Infinity, -Infinity, -Infinity]
28 for (const p of model.patches) {
29 const pos = p.tri.positions
30 for (let i = 0; i + 2 < pos.length; i += 3) extend(min, max, pos[i], pos[i + 1], pos[i + 2])
31 // include control poles so the control-net view stays in frame
32 if (p.kind === 'nurbs' && p.nurbs) {
33 const poles = p.nurbs.poles
34 for (let i = 0; i + 2 < poles.length; i += 3) extend(min, max, poles[i], poles[i + 1], poles[i + 2])
35 }
36 }
37 if (!isFinite(min[0])) {
38 min[0] = min[1] = min[2] = -1
39 max[0] = max[1] = max[2] = 1
40 }
41 const center: [number, number, number] = [
42 (min[0] + max[0]) / 2,
43 (min[1] + max[1]) / 2,
44 (min[2] + max[2]) / 2,
45 ]
46 const radius =
47 0.5 * Math.max(max[0] - min[0], max[1] - min[1], max[2] - min[2], 1e-6)
48 return { min: min as [number, number, number], max: max as [number, number, number], center, radius }
49}
51/**
52 * Line segments of a NURBS control net: the pole grid connected along u and v.
53 * Returns flat xyz pairs (each 6 numbers = one segment) plus the poles as points.
54 */
55export function controlNet(nurbs: NurbsSurface): { segments: Float32Array; points: Float32Array } {
56 const { nu, nv, poles } = nurbs
57 const pole = (i: number, j: number, c: number) => poles[(i * nv + j) * 3 + c]
58 const segs: number[] = []
59 for (let i = 0; i < nu; i++) {
60 for (let j = 0; j < nv; j++) {
61 if (i + 1 < nu) {
62 segs.push(pole(i, j, 0), pole(i, j, 1), pole(i, j, 2))
63 segs.push(pole(i + 1, j, 0), pole(i + 1, j, 1), pole(i + 1, j, 2))
64 }
65 if (j + 1 < nv) {
66 segs.push(pole(i, j, 0), pole(i, j, 1), pole(i, j, 2))
67 segs.push(pole(i, j + 1, 0), pole(i, j + 1, 1), pole(i, j + 1, 2))
68 }
69 }
70 }
71 return { segments: new Float32Array(segs), points: poles.slice() }
72}
74/** Merge every patch triangulation into one indexed mesh (for file export). */
75export function mergeTriMeshes(model: SurfaceModel): TriMesh {
76 let nVerts = 0
77 let nIndices = 0
78 for (const p of model.patches) {
79 nVerts += p.tri.positions.length / 3
80 nIndices += p.tri.indices.length
81 }
82 const positions = new Float32Array(nVerts * 3)
83 const normals = new Float32Array(nVerts * 3)
84 const indices = new Uint32Array(nIndices)
85 let vOff = 0
86 let iOff = 0
87 for (const p of model.patches) {
88 const t = p.tri
89 positions.set(t.positions, vOff * 3)
90 normals.set(t.normals, vOff * 3)
91 for (let k = 0; k < t.indices.length; k++) indices[iOff + k] = t.indices[k] + vOff
92 vOff += t.positions.length / 3
93 iOff += t.indices.length
94 }
95 return { positions, normals, indices }
96}