///|
/// A topology problem that can be surfaced before export or clash analysis.
pub(all) enum TopologyIssueKind {
InvalidIndex
DegenerateTriangle
BoundaryEdge
NonManifoldEdge
DisconnectedComponent
EmptyMesh
} derive(Debug, Eq)
///|
pub(all) struct TopologyIssue {
kind : TopologyIssueKind
triangle : Int?
vertex_a : Int?
vertex_b : Int?
message : String
} derive(Debug)
///|
pub(all) struct TopologyReport {
point_count : Int
triangle_count : Int
valid_triangle_count : Int
edge_count : Int
boundary_edge_count : Int
non_manifold_edge_count : Int
degenerate_triangle_count : Int
invalid_index_count : Int
component_count : Int
euler_characteristic : Int
surface_area : Double
signed_volume : Double
issues : Array[TopologyIssue]
} derive(Debug)
///|
pub fn topology_issue_name(kind : TopologyIssueKind) -> String {
match kind {
InvalidIndex => "invalid-index"
DegenerateTriangle => "degenerate-triangle"
BoundaryEdge => "boundary-edge"
NonManifoldEdge => "non-manifold-edge"
DisconnectedComponent => "disconnected-component"
EmptyMesh => "empty-mesh"
}
}
///|
fn topology_edge_key(left : Int, right : Int) -> String {
if left < right {
left.to_string() + ":" + right.to_string()
} else {
right.to_string() + ":" + left.to_string()
}
}
///|
fn topology_edge(
edges : Map[String, Int],
adjacency : Map[Int, Array[Int]],
left : Int,
right : Int,
) -> Unit {
let key = topology_edge_key(left, right)
edges.update_or_default(key, 1, count => count + 1)
adjacency.update_or_default(left, [right], values => {
if !values.contains(right) {
values.push(right)
}
values
})
adjacency.update_or_default(right, [left], values => {
if !values.contains(left) {
values.push(left)
}
values
})
}
///|
fn topology_issue(
issues : Array[TopologyIssue],
kind : TopologyIssueKind,
triangle : Int?,
vertex_a : Int?,
vertex_b : Int?,
message : String,
) -> Unit {
issues.push({ kind, triangle, vertex_a, vertex_b, message })
}
///|
fn topology_edge_vertex(key : String, wanted : Int) -> Int? {
let mut index = 0
for part in key.split(":") {
if index == wanted {
let text = part.to_owned()
return Some(stream_parse_int(text, 0, text.length()))
}
index += 1
}
None
}
///|
fn topology_valid_index(index : Int, point_count : Int) -> Bool {
index >= 0 && index < point_count
}
///|
fn topology_component_count(adjacency : Map[Int, Array[Int]]) -> Int {
let visited : Map[Int, Bool] = Map([])
let mut components = 0
for start, _ in adjacency {
if visited.contains(start) {
continue
}
components += 1
let queue : Array[Int] = [start]
while queue.length() > 0 {
let current = queue.remove(0)
if visited.contains(current) {
continue
}
visited[current] = true
for neighbor in adjacency.get(current).unwrap_or([]) {
if !visited.contains(neighbor) {
queue.push(neighbor)
}
}
}
}
components
}
///|
fn topology_component_issues(
issues : Array[TopologyIssue],
adjacency : Map[Int, Array[Int]],
component_count : Int,
) -> Unit {
if component_count > 1 {
issues.push({
kind: DisconnectedComponent,
triangle: None,
vertex_a: None,
vertex_b: None,
message: "mesh contains " +
component_count.to_string() +
" disconnected components",
})
}
ignore(adjacency)
}
///|
/// Analyze edge incidence, manifoldness and connected components of a mesh.
pub fn topology_audit(mesh : Mesh) -> TopologyReport {
let issues : Array[TopologyIssue] = []
let edges : Map[String, Int] = Map([])
let adjacency : Map[Int, Array[Int]] = Map([])
let mut valid_triangles = 0
let mut degenerate = 0
let mut invalid = 0
for index in 0.. 2 {
non_manifold += 1
topology_issue(
issues,
NonManifoldEdge,
None,
topology_edge_vertex(key, 0),
topology_edge_vertex(key, 1),
"edge is referenced by more than two triangles",
)
}
}
let components = topology_component_count(adjacency)
topology_component_issues(issues, adjacency, components)
if mesh.points.length() == 0 || mesh.triangles.length() == 0 {
issues.push({
kind: EmptyMesh,
triangle: None,
vertex_a: None,
vertex_b: None,
message: "mesh contains no renderable triangles",
})
}
{
point_count: mesh.points.length(),
triangle_count: mesh.triangles.length(),
valid_triangle_count: valid_triangles,
edge_count: edges.length(),
boundary_edge_count: boundary,
non_manifold_edge_count: non_manifold,
degenerate_triangle_count: degenerate,
invalid_index_count: invalid,
component_count: components,
euler_characteristic: mesh.points.length() -
edges.length() +
valid_triangles,
surface_area: mesh_area(mesh),
signed_volume: mesh_volume(mesh),
issues,
}
}
///|
pub fn TopologyReport::is_manifold(self : TopologyReport) -> Bool {
self.non_manifold_edge_count == 0 && self.invalid_index_count == 0
}
///|
pub fn TopologyReport::is_closed(self : TopologyReport) -> Bool {
self.boundary_edge_count == 0 && self.valid_triangle_count > 0
}
///|
pub fn TopologyReport::is_renderable(self : TopologyReport) -> Bool {
self.valid_triangle_count > 0 && self.invalid_index_count == 0
}
///|
pub fn TopologyReport::is_solid_candidate(self : TopologyReport) -> Bool {
self.is_manifold() &&
self.is_closed() &&
self.component_count == 1 &&
self.signed_volume().abs() > 0.000000001
}
///|
pub fn TopologyReport::issue_count(self : TopologyReport) -> Int {
self.issues.length()
}
///|
pub fn TopologyReport::issues_of(
self : TopologyReport,
kind : TopologyIssueKind,
) -> Array[TopologyIssue] {
self.issues.filter(issue => issue.kind == kind)
}
///|
pub fn TopologyReport::signed_volume(self : TopologyReport) -> Double {
self.signed_volume
}
///|
pub fn TopologyReport::headline(self : TopologyReport) -> String {
"points=" +
self.point_count.to_string() +
" triangles=" +
self.triangle_count.to_string() +
" edges=" +
self.edge_count.to_string() +
" components=" +
self.component_count.to_string() +
" boundary=" +
self.boundary_edge_count.to_string() +
" non_manifold=" +
self.non_manifold_edge_count.to_string()
}
///|
pub fn TopologyIssue::headline(self : TopologyIssue) -> String {
let triangle = match self.triangle {
Some(value) => " triangle=" + value.to_string()
None => ""
}
"[" + topology_issue_name(self.kind) + "]" + triangle + " " + self.message
}
///|
pub fn TopologyIssue::to_json(self : TopologyIssue) -> String {
let triangle = match self.triangle {
Some(value) => value.to_string()
None => "null"
}
let left = match self.vertex_a {
Some(value) => value.to_string()
None => "null"
}
let right = match self.vertex_b {
Some(value) => value.to_string()
None => "null"
}
"{\"kind\":\"" +
topology_issue_name(self.kind) +
"\",\"triangle\":" +
triangle +
",\"vertex_a\":" +
left +
",\"vertex_b\":" +
right +
",\"message\":\"" +
self.message +
"\"}"
}
///|
pub fn TopologyReport::to_json(self : TopologyReport) -> String {
"{\"point_count\":" +
self.point_count.to_string() +
",\"triangle_count\":" +
self.triangle_count.to_string() +
",\"valid_triangle_count\":" +
self.valid_triangle_count.to_string() +
",\"edge_count\":" +
self.edge_count.to_string() +
",\"boundary_edge_count\":" +
self.boundary_edge_count.to_string() +
",\"non_manifold_edge_count\":" +
self.non_manifold_edge_count.to_string() +
",\"component_count\":" +
self.component_count.to_string() +
",\"euler_characteristic\":" +
self.euler_characteristic.to_string() +
",\"surface_area\":" +
self.surface_area.to_string() +
",\"signed_volume\":" +
self.signed_volume.to_string() +
",\"issues\":[" +
self.issues.map(issue => issue.to_json()).join(",") +
"]}"
}
///|
pub fn TopologyReport::to_markdown(self : TopologyReport) -> String {
let builder = StringBuilder()
builder <+ "# Mesh topology audit\n\n"
builder <+ "- \{self.headline()}\n"
builder <+ "- Renderable: \{self.is_renderable()}\n"
builder <+ "- Manifold: \{self.is_manifold()}\n"
builder <+ "- Closed: \{self.is_closed()}\n"
builder <+ "- Solid candidate: \{self.is_solid_candidate()}\n"
builder <+ "- Euler characteristic: \{self.euler_characteristic}\n"
builder <+ "- Surface area: \{self.surface_area}\n"
builder <+ "- Signed volume: \{self.signed_volume}\n\n"
builder <+ "| Kind | Triangle | Message |\n| --- | ---: | --- |\n"
for issue in self.issues {
let triangle = match issue.triangle {
Some(value) => value.to_string()
None => ""
}
builder <+
"|\{topology_issue_name(issue.kind)}|\{triangle}|\{issue.message}|\n"
}
builder.to_string()
}
///|
pub fn topology_issue_histogram(report : TopologyReport) -> Map[String, Int] {
let result : Map[String, Int] = Map([])
for issue in report.issues {
result.update_or_default(topology_issue_name(issue.kind), 1, count => {
count + 1
})
}
result
}
///|
pub fn topology_reports_for_meshes(
meshes : Array[Mesh],
) -> Array[TopologyReport] {
meshes.map(mesh => topology_audit(mesh))
}
///|
pub fn topology_total_surface_area(reports : Array[TopologyReport]) -> Double {
reports.fold(init=0.0, (sum, report) => sum + report.surface_area)
}
///|
pub fn topology_total_volume(reports : Array[TopologyReport]) -> Double {
reports.fold(init=0.0, (sum, report) => sum + report.signed_volume.abs())
}
///|
pub fn topology_unhealthy(
reports : Array[TopologyReport],
) -> Array[TopologyReport] {
reports.filter(report => !report.is_renderable() || !report.is_manifold())
}
///|
pub fn topology_summary(reports : Array[TopologyReport]) -> Array[String] {
[
"meshes=" + reports.length().to_string(),
"unhealthy=" + topology_unhealthy(reports).length().to_string(),
"surface_area=" + topology_total_surface_area(reports).to_string(),
"volume=" + topology_total_volume(reports).to_string(),
]
}