///|
pub fn default_mesh_pipeline_options() -> MeshPipelineOptions {
{
weld_tolerance: 0.000001,
degenerate_tolerance: 0.000000001,
maximum_triangles: 0,
recenter: false,
scale: 1.0,
flip_winding: false,
generate_normals: true,
compact_vertices: true,
}
}
///|
pub fn MeshPipelineOptions::with_weld_tolerance(
self : MeshPipelineOptions,
value : Double,
) -> MeshPipelineOptions {
{ ..self, weld_tolerance: if value < 0.0 { 0.0 } else { value } }
}
///|
pub fn MeshPipelineOptions::with_degenerate_tolerance(
self : MeshPipelineOptions,
value : Double,
) -> MeshPipelineOptions {
{ ..self, degenerate_tolerance: if value < 0.0 { 0.0 } else { value } }
}
///|
pub fn MeshPipelineOptions::with_maximum_triangles(
self : MeshPipelineOptions,
value : Int,
) -> MeshPipelineOptions {
{ ..self, maximum_triangles: if value < 0 { 0 } else { value } }
}
///|
pub fn MeshPipelineOptions::recentered(
self : MeshPipelineOptions,
value : Bool,
) -> MeshPipelineOptions {
{ ..self, recenter: value }
}
///|
pub fn MeshPipelineOptions::scaled(
self : MeshPipelineOptions,
value : Double,
) -> MeshPipelineOptions {
{ ..self, scale: if value == 0.0 { 1.0 } else { value } }
}
///|
pub fn MeshPipelineOptions::flipped(
self : MeshPipelineOptions,
value : Bool,
) -> MeshPipelineOptions {
{ ..self, flip_winding: value }
}
///|
pub fn MeshPipelineOptions::with_normals(
self : MeshPipelineOptions,
value : Bool,
) -> MeshPipelineOptions {
{ ..self, generate_normals: value }
}
///|
pub fn MeshPipelineOptions::compacting(
self : MeshPipelineOptions,
value : Bool,
) -> MeshPipelineOptions {
{ ..self, compact_vertices: value }
}
///|
fn mesh_zero_vector() -> Vector3 {
{ x: 0.0, y: 0.0, z: 0.0 }
}
///|
fn mesh_valid_index(mesh : Mesh, index : Int) -> Bool {
index >= 0 && index < mesh.points.length()
}
///|
fn mesh_triangle_valid(mesh : Mesh, triangle : (Int, Int, Int)) -> Bool {
let (a, b, c) = triangle
mesh_valid_index(mesh, a) &&
mesh_valid_index(mesh, b) &&
mesh_valid_index(mesh, c) &&
a != b &&
b != c &&
a != c
}
///|
/// Check index safety, duplicate vertices and geometric degeneracy before export.
pub fn validate_mesh(
mesh : Mesh,
tolerance? : Double = 0.000000001,
) -> MeshValidation {
let issues : Array[MeshIssue] = []
let referenced : Map[Int, Bool] = Map([])
let mut degenerate = 0
for point_index, _ in mesh.points {
ignore(point_index)
}
for triangle_index, triangle in mesh.triangles {
let (a, b, c) = triangle
if !mesh_triangle_valid(mesh, triangle) {
issues.push({
code: "MESH_INDEX",
message: "triangle references an invalid or repeated point",
point_index: None,
triangle_index: Some(triangle_index),
})
continue
}
referenced[a] = true
referenced[b] = true
referenced[c] = true
if triangle_area(mesh, triangle) <= tolerance {
degenerate += 1
issues.push({
code: "MESH_DEGENERATE",
message: "triangle area is below tolerance",
point_index: None,
triangle_index: Some(triangle_index),
})
}
}
let mut unreferenced = 0
for index in 0.. Mesh {
{
points: mesh.points,
triangles: mesh.triangles.map(triangle => {
let (a, b, c) = triangle
(a, c, b)
}),
}
}
///|
fn mesh_transform_points(mesh : Mesh, recenter : Bool, scale : Double) -> Mesh {
let bounds = bounds_of_mesh(mesh)
let center = if recenter {
bounds.center()
} else {
{ x: 0.0, y: 0.0, z: 0.0 }
}
let safe_scale = if scale == 0.0 { 1.0 } else { scale }
{
points: mesh.points.map(point => {
x: (point.x - center.x) * safe_scale,
y: (point.y - center.y) * safe_scale,
z: (point.z - center.z) * safe_scale,
}),
triangles: mesh.triangles,
}
}
///|
fn mesh_compact_vertices(mesh : Mesh) -> Mesh {
let remap : Map[Int, Int] = Map([])
let points : Array[Point3] = []
for triangle in mesh.triangles {
let (a, b, c) = triangle
for old_index in [a, b, c] {
if !remap.contains(old_index) {
match mesh.points.get(old_index) {
Some(point) => {
remap[old_index] = points.length()
points.push(point)
}
None => ()
}
}
}
}
let triangles : Array[(Int, Int, Int)] = []
for triangle in mesh.triangles {
let (a, b, c) = triangle
match (remap.get(a), remap.get(b), remap.get(c)) {
(Some(left), Some(middle), Some(right)) if left != middle &&
middle != right &&
left != right => triangles.push((left, middle, right))
_ => ()
}
}
{ points, triangles }
}
///|
fn mesh_remove_duplicate_triangles(mesh : Mesh) -> Mesh {
let seen : Map[String, Bool] = Map([])
let triangles : Array[(Int, Int, Int)] = []
for triangle in mesh.triangles {
let (a, b, c) = triangle
let mut first = a
let mut second = b
let mut third = c
if second < first {
let swap = first
first = second
second = swap
}
if third < second {
let swap = second
second = third
third = swap
}
if second < first {
let swap = first
first = second
second = swap
}
let key = first.to_string() +
":" +
second.to_string() +
":" +
third.to_string()
if !seen.contains(key) {
seen[key] = true
triangles.push(triangle)
}
}
{ points: mesh.points, triangles }
}
///|
fn mesh_pipeline_normals(mesh : Mesh) -> Array[Vector3] {
let normals : Array[Vector3] = []
for _ in mesh.points {
normals.push(mesh_zero_vector())
}
for triangle in mesh.triangles {
let (a, b, c) = triangle
if mesh_triangle_valid(mesh, triangle) {
let normal = triangle_normal(mesh, triangle)
for index in [a, b, c] {
match normals.get(index) {
Some(value) => normals[index] = value.add(normal)
None => ()
}
}
}
}
normals.map(normal => normal.normalized())
}
///|
fn mesh_pipeline_stats(
input : Mesh,
output : Mesh,
valid : Bool,
) -> MeshPipelineStats {
let analysis = mesh_stats(output)
{
input_points: input.points.length(),
input_triangles: input.triangles.length(),
output_points: output.points.length(),
output_triangles: output.triangles.length(),
removed_points: if input.points.length() > output.points.length() {
input.points.length() - output.points.length()
} else {
0
},
removed_triangles: if input.triangles.length() > output.triangles.length() {
input.triangles.length() - output.triangles.length()
} else {
0
},
surface_area: analysis.surface_area,
volume: analysis.volume(),
bounds: bounds_of_mesh(output),
valid,
}
}
///|
/// Apply deterministic cleanup, welding, orientation and optional LOD limits.
pub fn process_mesh(
mesh : Mesh,
options? : MeshPipelineOptions = default_mesh_pipeline_options(),
) -> MeshPipelineResult {
let initial = mesh
let valid_input = validate_mesh(
initial,
tolerance=options.degenerate_tolerance,
)
let cleaned = remove_degenerate_triangles(
initial,
options.degenerate_tolerance,
)
let welded = if options.weld_tolerance > 0.0 {
weld_mesh(cleaned, options.weld_tolerance)
} else {
cleaned
}
let unique = mesh_remove_duplicate_triangles(welded)
let limited = limit_mesh_triangles(unique, options.maximum_triangles)
let oriented = if options.flip_winding { mesh_flip(limited) } else { limited }
let transformed = mesh_transform_points(
oriented,
options.recenter,
options.scale,
)
let compacted = if options.compact_vertices {
mesh_compact_vertices(transformed)
} else {
transformed
}
let validation = validate_mesh(
compacted,
tolerance=options.degenerate_tolerance,
)
{
mesh: compacted,
normals: if options.generate_normals {
mesh_pipeline_normals(compacted)
} else {
[]
},
stats: mesh_pipeline_stats(
initial,
compacted,
valid_input.valid && validation.valid,
),
}
}
///|
pub fn mesh_pipeline(
mesh : Mesh,
options? : MeshPipelineOptions = default_mesh_pipeline_options(),
) -> MeshPipelineResult {
process_mesh(mesh, options~)
}
///|
pub fn mesh_for_product_pipeline(
index : ModelIndex,
product : Product,
options? : MeshPipelineOptions = default_mesh_pipeline_options(),
) -> MeshPipelineResult raise ParseError {
process_mesh(mesh_for_product_advanced(index, product), options~)
}
///|
pub fn MeshPipelineResult::is_valid(self : MeshPipelineResult) -> Bool {
self.stats.valid
}
///|
pub fn MeshPipelineResult::point_count(self : MeshPipelineResult) -> Int {
self.mesh.points.length()
}
///|
pub fn MeshPipelineResult::triangle_count(self : MeshPipelineResult) -> Int {
self.mesh.triangles.length()
}
///|
pub fn MeshPipelineResult::bounds(self : MeshPipelineResult) -> BoundingBox {
self.stats.bounds
}
///|
pub fn MeshPipelineResult::headline(self : MeshPipelineResult) -> String {
"points=" +
self.mesh.points.length().to_string() +
" triangles=" +
self.mesh.triangles.length().to_string() +
" area=" +
self.stats.surface_area.to_string() +
" volume=" +
self.stats.volume.to_string() +
" valid=" +
self.stats.valid.to_string()
}
///|
pub fn MeshPipelineStats::headline(self : MeshPipelineStats) -> String {
"input=" +
self.input_triangles.to_string() +
" output=" +
self.output_triangles.to_string() +
" removed=" +
self.removed_triangles.to_string() +
" area=" +
self.surface_area.to_string() +
" volume=" +
self.volume.to_string()
}
///|
pub fn MeshValidation::headline(self : MeshValidation) -> String {
"valid=" +
self.valid.to_string() +
" issues=" +
self.issues.length().to_string() +
" referenced_points=" +
self.referenced_point_count.to_string() +
" unreferenced_points=" +
self.unreferenced_point_count.to_string()
}
///|
pub fn MeshIssue::headline(self : MeshIssue) -> String {
self.code + ": " + self.message
}
///|
pub fn mesh_validation(mesh : Mesh) -> MeshValidation {
validate_mesh(mesh)
}
///|
pub fn mesh_is_renderable(mesh : Mesh) -> Bool {
let report = validate_mesh(mesh)
report.valid && mesh.points.length() > 0 && mesh.triangles.length() > 0
}
///|
pub fn mesh_normals(mesh : Mesh) -> Array[Vector3] {
mesh_pipeline_normals(mesh)
}
///|
pub fn mesh_transform_scale(mesh : Mesh, scale : Double) -> Mesh {
mesh_transform_points(mesh, false, scale)
}
///|
pub fn mesh_centered(mesh : Mesh) -> Mesh {
mesh_transform_points(mesh, true, 1.0)
}
///|
pub fn mesh_flip_winding(mesh : Mesh) -> Mesh {
mesh_flip(mesh)
}
///|
pub fn mesh_compact(mesh : Mesh) -> Mesh {
mesh_compact_vertices(mesh)
}
///|
pub fn mesh_unique_triangles(mesh : Mesh) -> Mesh {
mesh_remove_duplicate_triangles(mesh)
}
///|
fn mesh_lod_cell_size(mesh : Mesh, target_triangles : Int) -> Double {
let safe_target = if target_triangles < 1 { 1 } else { target_triangles }
let bounds = bounds_of_mesh(mesh)
let volume = bounds.volume()
if volume <= 0.0 || mesh.triangles.length() <= safe_target {
0.0
} else {
let ratio = mesh.triangles.length().to_double() / safe_target.to_double()
let base = (volume / ratio).abs()
mesh_cube_root(base)
}
}
///|
fn mesh_cube_root(value : Double) -> Double {
if value <= 0.0 {
return 0.0
}
let mut guess = if value > 1.0 { value / 3.0 } else { 1.0 }
for _ in 0..<16 {
guess = (2.0 * guess + value / (guess * guess)) / 3.0
}
guess
}
///|
/// Generate a coarse level of detail by welding vertices into a deterministic grid.
pub fn mesh_lod(mesh : Mesh, target_triangles : Int) -> Mesh {
let target = if target_triangles < 1 { 1 } else { target_triangles }
if mesh.triangles.length() <= target {
return mesh
}
let cell = mesh_lod_cell_size(mesh, target)
let welded = weld_mesh(mesh, if cell <= 0.0 { 0.000001 } else { cell })
let compacted = mesh_compact_vertices(mesh_remove_duplicate_triangles(welded))
limit_mesh_triangles(compacted, target)
}
///|
pub fn mesh_lod_result(
mesh : Mesh,
target_triangles : Int,
options? : MeshPipelineOptions = default_mesh_pipeline_options(),
) -> MeshPipelineResult {
process_mesh(mesh_lod(mesh, target_triangles), options~)
}
///|
pub fn mesh_area(mesh : Mesh) -> Double {
mesh_stats(mesh).surface_area
}
///|
pub fn mesh_volume(mesh : Mesh) -> Double {
mesh_stats(mesh).volume().abs()
}
///|
pub fn mesh_bounds_json(mesh : Mesh) -> String {
let bounds = bounds_of_mesh(mesh)
"{\"min\":[" +
bounds.min.x.to_string() +
"," +
bounds.min.y.to_string() +
"," +
bounds.min.z.to_string() +
"],\"max\":[" +
bounds.max.x.to_string() +
"," +
bounds.max.y.to_string() +
"," +
bounds.max.z.to_string() +
"],\"empty\":" +
bounds.empty.to_string() +
"}"
}
///|
pub fn MeshPipelineResult::to_json(self : MeshPipelineResult) -> String {
"{\"points\":" +
self.mesh.points.length().to_string() +
",\"triangles\":" +
self.mesh.triangles.length().to_string() +
",\"normals\":" +
self.normals.length().to_string() +
",\"surface_area\":" +
self.stats.surface_area.to_string() +
",\"volume\":" +
self.stats.volume.to_string() +
",\"valid\":" +
self.stats.valid.to_string() +
",\"bounds\":" +
mesh_bounds_json(self.mesh) +
"}"
}
///|
pub fn MeshPipelineResult::to_obj(self : MeshPipelineResult) -> String {
let builder = StringBuilder()
builder <+ "# moonbit-ifc mesh pipeline\n"
for point in self.mesh.points {
builder <+ "v \{point.x} \{point.y} \{point.z}\n"
}
for normal in self.normals {
builder <+ "vn \{normal.x} \{normal.y} \{normal.z}\n"
}
for triangle in self.mesh.triangles {
let (a, b, c) = triangle
builder <+ "f \{a + 1} \{b + 1} \{c + 1}\n"
}
builder.to_string()
}
///|
pub fn mesh_pipeline_summary(result : MeshPipelineResult) -> Array[String] {
[
result.headline(),
result.stats.headline(),
result.bounds().volume().to_string(),
]
}