1/** The app's internal surface-mesh representation and connectivity helpers. */
3export interface SurfaceMeshData {
4 /** xyz triples, one per vertex */
5 positions: Float32Array
6 /** cellSize vertex indices per cell, Gmsh corner order (counterclockwise) */
7 cells: Uint32Array
8 /** nodes per cell: 3 (triangles) or 4 (quads) — never mixed */
9 cellSize: 3 | 4
10 /** the canonical Gmsh MSH 4.1 file the solver reads */
11 mshBytes: Uint8Array
12}
14export interface SurfaceMeshInfo {
15 numVertices: number
16 numCells: number
17 /** every edge shared by exactly two cells */
18 closed: boolean
19 /** some edge shared by more than two cells */
20 nonManifold: boolean
21 warnings: string[]
22}
24/**
25 * Classify the mesh from its edge incidence: closed (all edges shared by 2
26 * cells), open (some boundary edges), or non-manifold (an edge on >2 cells).
27 */
28export function edgeClassification(
29 cells: Uint32Array,
30 cellSize: number,
31): {
32 closed: boolean
33 nonManifold: boolean
34} {
35 const counts = new Map<number, number>()
36 const nc = cells.length / cellSize
37 for (let k = 0; k < nc; k++) {
38 for (let e = 0; e < cellSize; e++) {
39 const a = cells[k * cellSize + e]
40 const b = cells[k * cellSize + ((e + 1) % cellSize)]
41 // 2^26 > any vertex count we accept; safe integer key for the pair
42 const key = a < b ? a * 67108864 + b : b * 67108864 + a
43 counts.set(key, (counts.get(key) ?? 0) + 1)
44 }
45 }
46 let closed = true
47 let nonManifold = false
48 for (const c of counts.values()) {
49 if (c !== 2) closed = false
50 if (c > 2) nonManifold = true
51 }
52 return { closed, nonManifold }
53}
55/** Axis-aligned bounding box diagonal, for camera framing. */
56export function meshBounds(positions: Float32Array): {
57 center: [number, number, number]
58 size: number
59} {
60 let minX = Infinity, minY = Infinity, minZ = Infinity
61 let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity
62 for (let i = 0; i < positions.length; i += 3) {
63 const x = positions[i], y = positions[i + 1], z = positions[i + 2]
64 if (x < minX) minX = x
65 if (y < minY) minY = y
66 if (z < minZ) minZ = z
67 if (x > maxX) maxX = x
68 if (y > maxY) maxY = y
69 if (z > maxZ) maxZ = z
70 }
71 const size = Math.max(maxX - minX, maxY - minY, maxZ - minZ) || 1
72 return { center: [(minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2], size }
73}