// SPDX-License-Identifier: Apache-2.0
///|
pub(all) struct Face3 {
a : Int32
b : Int32
c : Int32
}
///|
/// Owns snapshots of finite positions, valid face indices and per-face declared
/// normals. Unreferenced and coincident vertices are permitted. Attribute/UV
/// document semantics are not represented by this geometry-only type.
pub struct IndexedMesh {
priv positions : Array[Vec3]
priv indices : Array[Face3]
priv normals : Array[Vec3]
}
///|
/// Copies input arrays. Empty normals mean zero declared normals; otherwise the
/// length must equal faces.length(). Degenerate faces are retained for diagnosis.
///
/// ```moonbit nocheck
/// let mesh = @stlkit.IndexedMesh::new(
/// [@stlkit.Vec3::zero(), @stlkit.Vec3::new(1, 0, 0), @stlkit.Vec3::new(0, 1, 0)],
/// [{ a: 0, b: 1, c: 2, }],
/// )
/// ```
pub fn IndexedMesh::new(
vertices : Array[Vec3],
faces : Array[Face3],
normals~ : Array[Vec3] = [],
) -> Result[IndexedMesh, String] {
if faces.length() > 715827882 {
return Err("indexed face count overflows expanded corner count")
}
for i, point in vertices {
if !finite_vertex(point) {
return Err("indexed vertex \{i} is non-finite")
}
}
if !normals.is_empty() && normals.length() != faces.length() {
return Err("indexed declared normals must match face count")
}
for i, normal in normals {
if !finite_vertex(normal) {
return Err("indexed normal \{i} is non-finite")
}
}
for i, face in faces {
for index in [face.a, face.b, face.c] {
if index < 0 || index >= vertices.length() {
return Err("indexed face \{i} references invalid vertex \{index}")
}
}
}
Ok({
positions: vertices.copy(),
indices: faces.copy(),
normals: if normals.is_empty() {
Array::make(faces.length(), Vec3::zero())
} else {
normals.copy()
},
})
}
///|
pub fn IndexedMesh::vertex_count(self : IndexedMesh) -> Int32 {
self.positions.length()
}
///|
pub fn IndexedMesh::triangle_count(self : IndexedMesh) -> Int32 {
self.indices.length()
}
///|
/// Returns a detached copy, preserving the constructor's validation invariant.
pub fn IndexedMesh::vertices(self : IndexedMesh) -> Array[Vec3] {
self.positions.copy()
}
///|
pub fn IndexedMesh::faces(self : IndexedMesh) -> Array[Face3] {
self.indices.copy()
}
///|
pub fn IndexedMesh::declared_normals(self : IndexedMesh) -> Array[Vec3] {
self.normals.copy()
}
///|
pub fn IndexedMesh::unused_vertex_count(self : IndexedMesh) -> Int32 {
let used = FixedArray::make(self.positions.length(), false)
for face in self.indices {
used[face.a] = true
used[face.b] = true
used[face.c] = true
}
used.iter().filter(value => !value).count()
}
///|
pub fn IndexedMesh::to_mesh(self : IndexedMesh) -> Mesh {
let triangles : Array[Triangle] = []
for i, face in self.indices {
triangles.push({
normal: self.normals[i],
a: self.positions[face.a],
b: self.positions[face.b],
c: self.positions[face.c],
})
}
Mesh::new(triangles)
}
///|
fn indexed_exact_key(point : Vec3) -> PositionKey {
{
x: Exact(if point.x == 0.0 { 0.0 } else { point.x }),
y: Exact(if point.y == 0.0 { 0.0 } else { point.y }),
z: Exact(if point.z == 0.0 { 0.0 } else { point.z }),
}
}
///|
/// Exact coordinate deduplication (including signed zero), not tolerance welding.
/// Face order, winding, degenerate faces and declared normals are preserved.
pub fn Mesh::to_indexed(self : Mesh) -> Result[IndexedMesh, String] {
if self.triangles.length() > 715827882 {
return Err("indexed face count overflows expanded corner count")
}
let positions : Array[Vec3] = []
let indices : Array[Face3] = []
let normals : Array[Vec3] = []
let table : Map[PositionKey, Int32] = Map([])
for i, triangle in self.triangles {
if !finite_vertex(triangle.normal) {
return Err("indexed normal \{i} is non-finite")
}
let ids : Array[Int32] = []
for point in [triangle.a, triangle.b, triangle.c] {
if !finite_vertex(point) {
return Err("indexed vertex on face \{i} is non-finite")
}
let key = indexed_exact_key(point)
let id = match table.get(key) {
Some(id) => id
None => {
let id = positions.length()
positions.push(point)
table.set(key, id)
id
}
}
ids.push(id)
}
indices.push({ a: ids[0], b: ids[1], c: ids[2], })
normals.push(triangle.normal)
}
Ok({ positions, indices, normals, })
}
///|
pub fn IndexedMesh::surface_area(self : IndexedMesh) -> Double {
let mut total = 0.0
for face in self.indices {
total += Triangle::from_vertices(
self.positions[face.a],
self.positions[face.b],
self.positions[face.c],
).area()
}
total
}
///|
/// Same conditional oriented-volume interpretation as Mesh::volume().
pub fn IndexedMesh::volume(self : IndexedMesh) -> Double {
let mut total = 0.0
for face in self.indices {
total += self.positions[face.a].dot(
self.positions[face.b].cross(self.positions[face.c]),
) /
6.0
}
total.abs()
}
///|
/// Bounds of referenced geometry only. Unused vertices do not change the bounds;
/// a document with no faces returns None, even if it stores unused positions.
pub fn IndexedMesh::bounding_box(self : IndexedMesh) -> Aabb? {
let mut bounds : Aabb? = None
for face in self.indices {
for index in [face.a, face.b, face.c] {
let point = self.positions[index]
bounds = Some(
match bounds {
None => { min: point, max: point, }
Some(box) =>
{
min: Vec3::new(
box.min.x.min(point.x),
box.min.y.min(point.y),
box.min.z.min(point.z),
),
max: Vec3::new(
box.max.x.max(point.x),
box.max.y.max(point.y),
box.max.z.max(point.z),
),
}
},
)
}
}
bounds
}
///|
/// Compatibility implementation: temporarily expands Mesh and uses the existing
/// coordinate-welding topology rules. This does not validate index identity as
/// a separate topology, nor promise reduced peak memory during validation.
pub fn IndexedMesh::validate(
self : IndexedMesh,
tolerance~ : Double = default_weld_tolerance(),
) -> ValidationReport {
validate(self.to_mesh(), tolerance)
}