///|
fn geometry_inventory_product(
index : ModelIndex,
product : Product,
) -> ProductGeometryReport raise ParseError {
let geometries = geometries_for_product(index, product)
let mesh = mesh_for_product_advanced(index, product)
let bounds = if mesh.points.length() > 0 {
bounds_of_mesh(mesh)
} else {
geometries.fold(init=BoundingBox::empty(), (result, geometry) => {
merge_bounds(
result,
BoundingBox::from_points(
transform_points(index, product, geometry.points),
),
)
})
}
let stats = mesh_stats(mesh)
let estimated = geometries.fold(init=0.0, (result, geometry) => {
result + estimated_volume(geometry).unwrap_or(0.0)
})
let volume = if mesh.triangles.length() > 0 {
stats.volume().abs()
} else {
estimated
}
let valid_mesh_count = if mesh.triangles.length() > 0 &&
mesh_is_renderable(mesh) {
1
} else {
0
}
{
product_id: product.id,
geometry_count: geometries.length(),
mesh_count: if mesh.triangles.length() > 0 {
1
} else {
0
},
point_count: mesh.points.length(),
triangle_count: mesh.triangles.length(),
bounds,
surface_area: stats.surface_area,
volume,
estimated_volume: mesh.triangles.length() == 0 && estimated > 0.0,
valid_mesh_count,
}
}
///|
/// Build a geometry inventory with one row per product.
pub fn geometry_inventory(model : Model) -> GeometryInventory raise ParseError {
let index = model.index()
let rows : Array[ProductGeometryReport] = []
let mut represented = 0
let mut empty = 0
let mut invalid = 0
let mut area = 0.0
let mut volume = 0.0
for product in model.products() {
let row = geometry_inventory_product(index, product)
rows.push(row)
if row.geometry_count > 0 {
represented += 1
} else {
empty += 1
}
if row.mesh_count > 0 && row.valid_mesh_count == 0 {
invalid += 1
}
area += row.surface_area
volume += row.volume
}
{
products: rows,
represented_product_count: represented,
empty_representation_count: empty,
invalid_mesh_count: invalid,
total_surface_area: area,
total_volume: volume,
}
}
///|
pub fn geometry_report_for_product(
model : Model,
product_id : Int,
) -> ProductGeometryReport? raise ParseError {
let index = model.index()
match product_by_id(model.products(), product_id) {
Some(product) => Some(geometry_inventory_product(index, product))
None => None
}
}
///|
pub fn ProductGeometryReport::has_mesh(self : ProductGeometryReport) -> Bool {
self.mesh_count > 0 && self.triangle_count > 0
}
///|
pub fn ProductGeometryReport::is_valid(self : ProductGeometryReport) -> Bool {
self.valid_mesh_count == self.mesh_count
}
///|
pub fn ProductGeometryReport::center(self : ProductGeometryReport) -> Point3 {
self.bounds.center()
}
///|
pub fn ProductGeometryReport::headline(self : ProductGeometryReport) -> String {
"#" +
self.product_id.to_string() +
" geometries=" +
self.geometry_count.to_string() +
" triangles=" +
self.triangle_count.to_string() +
" area=" +
self.surface_area.to_string() +
" volume=" +
self.volume.to_string()
}
///|
pub fn GeometryInventory::product_count(self : GeometryInventory) -> Int {
self.products.length()
}
///|
pub fn GeometryInventory::represented_ratio(self : GeometryInventory) -> Double {
if self.products.length() == 0 {
0.0
} else {
self.represented_product_count.to_double() /
self.products.length().to_double()
}
}
///|
pub fn GeometryInventory::valid_ratio(self : GeometryInventory) -> Double {
let represented = self.products.filter(row => row.mesh_count > 0).length()
if represented == 0 {
1.0
} else {
(represented - self.invalid_mesh_count).to_double() /
represented.to_double()
}
}
///|
pub fn GeometryInventory::headline(self : GeometryInventory) -> String {
"products=" +
self.products.length().to_string() +
" represented=" +
self.represented_product_count.to_string() +
" empty=" +
self.empty_representation_count.to_string() +
" invalid_mesh=" +
self.invalid_mesh_count.to_string() +
" area=" +
self.total_surface_area.to_string() +
" volume=" +
self.total_volume.to_string()
}
///|
pub fn GeometryInventory::by_product(
self : GeometryInventory,
id : Int,
) -> ProductGeometryReport? {
for row in self.products {
if row.product_id == id {
return Some(row)
}
}
None
}
///|
pub fn GeometryInventory::largest_by_volume(
self : GeometryInventory,
) -> ProductGeometryReport? {
let mut result : ProductGeometryReport? = None
for row in self.products {
match result {
Some(current) if current.volume >= row.volume => ()
_ => result = Some(row)
}
}
result
}
///|
pub fn GeometryInventory::largest_by_area(
self : GeometryInventory,
) -> ProductGeometryReport? {
let mut result : ProductGeometryReport? = None
for row in self.products {
match result {
Some(current) if current.surface_area >= row.surface_area => ()
_ => result = Some(row)
}
}
result
}
///|
fn geometry_centroid(points : Array[Point3]) -> Point3 {
if points.length() == 0 {
return { x: 0.0, y: 0.0, z: 0.0 }
}
let (x, y, z) = points.fold(init=(0.0, 0.0, 0.0), (result, point) => {
let (x, y, z) = result
(x + point.x, y + point.y, z + point.z)
})
let count = points.length().to_double()
{ x: x / count, y: y / count, z: z / count }
}
///|
pub fn mesh_centroid(mesh : Mesh) -> Point3 {
geometry_centroid(mesh.points)
}
///|
pub fn mesh_world_centroid(
index : ModelIndex,
product : Product,
) -> Point3 raise ParseError {
mesh_centroid(mesh_for_product_advanced(index, product))
}
///|
pub fn mesh_diagonal(mesh : Mesh) -> Double {
let bounds = bounds_of_mesh(mesh)
Vector3::from_points(bounds.min, bounds.max).length()
}
///|
pub fn mesh_aspect_ratio(mesh : Mesh) -> Double {
let bounds = bounds_of_mesh(mesh)
let values = [bounds.width(), bounds.height(), bounds.depth()]
let mut minimum = 1.0e300
let mut maximum = 0.0
for value in values {
if value > 0.0 && value < minimum {
minimum = value
}
if value > maximum {
maximum = value
}
}
if minimum == 1.0e300 || minimum == 0.0 {
0.0
} else {
maximum / minimum
}
}
///|
pub fn mesh_area_by_normal(
mesh : Mesh,
axis : Vector3,
tolerance? : Double = 0.000001,
) -> Double {
let direction = axis.normalized()
mesh.triangles.fold(init=0.0, (result, triangle) => {
let normal = triangle_normal(mesh, triangle)
if normal.length() == 0.0 ||
normal.normalized().dot(direction).abs() < tolerance {
result
} else {
result + triangle_area(mesh, triangle)
}
})
}
///|
pub fn mesh_horizontal_area(mesh : Mesh) -> Double {
mesh_area_by_normal(mesh, { x: 0.0, y: 0.0, z: 1.0 })
}
///|
pub fn mesh_vertical_area(mesh : Mesh) -> Double {
mesh_area_by_normal(mesh, { x: 1.0, y: 0.0, z: 0.0 }) +
mesh_area_by_normal(mesh, { x: 0.0, y: 1.0, z: 0.0 })
}
///|
pub fn mesh_triangle_quality(mesh : Mesh, triangle_index : Int) -> Double? {
match mesh.triangles.get(triangle_index) {
Some(triangle) => {
let (a, b, c) = triangle
match (mesh.points.get(a), mesh.points.get(b), mesh.points.get(c)) {
(Some(left), Some(middle), Some(right)) => {
let ab = Vector3::from_points(left, middle).length()
let bc = Vector3::from_points(middle, right).length()
let ca = Vector3::from_points(right, left).length()
let longest = if ab > bc {
if ab > ca {
ab
} else {
ca
}
} else if bc > ca {
bc
} else {
ca
}
let shortest = if ab < bc {
if ab < ca {
ab
} else {
ca
}
} else if bc < ca {
bc
} else {
ca
}
if longest == 0.0 {
Some(0.0)
} else {
Some(shortest / longest)
}
}
_ => None
}
}
None => None
}
}
///|
pub fn mesh_quality_histogram(mesh : Mesh, bins : Int) -> Map[Int, Int] {
let count = if bins <= 0 { 10 } else { bins }
let result : Map[Int, Int] = Map([])
for index in 0.. {
let bucket = @math.floor(value * count.to_double()).to_int()
let key = if bucket >= count { count - 1 } else { bucket }
result.update_or_default(key, 1, old => old + 1)
}
None => ()
}
}
result
}
///|
pub fn mesh_normal_histogram(mesh : Mesh, bins : Int) -> Map[String, Int] {
let count = if bins <= 0 { 8 } else { bins }
mesh.triangles.fold(init=Map([]), (result, triangle) => {
let normal = triangle_normal(mesh, triangle).normalized()
let key = @math.floor((normal.z + 1.0) * 0.5 * count.to_double())
.to_int()
.to_string()
result.update_or_default(key, 1, old => old + 1)
result
})
}
///|
pub fn mesh_edge_length_statistics(mesh : Mesh) -> (Double, Double, Double) {
let mut minimum = 1.0e300
let mut maximum = 0.0
let mut total = 0.0
let mut count = 0
for triangle in mesh.triangles {
let (a, b, c) = triangle
for pair in [(a, b), (b, c), (c, a)] {
let (left, right) = pair
match (mesh.points.get(left), mesh.points.get(right)) {
(Some(start), Some(end)) => {
let length = Vector3::from_points(start, end).length()
if length < minimum {
minimum = length
}
if length > maximum {
maximum = length
}
total += length
count += 1
}
_ => ()
}
}
}
(
if count == 0 {
0.0
} else {
minimum
},
maximum,
if count == 0 {
0.0
} else {
total / count.to_double()
},
)
}
///|
pub fn ProductGeometryReport::to_json(self : ProductGeometryReport) -> String {
"{\"product_id\":" +
self.product_id.to_string() +
",\"geometry_count\":" +
self.geometry_count.to_string() +
",\"mesh_count\":" +
self.mesh_count.to_string() +
",\"point_count\":" +
self.point_count.to_string() +
",\"triangle_count\":" +
self.triangle_count.to_string() +
",\"surface_area\":" +
self.surface_area.to_string() +
",\"volume\":" +
self.volume.to_string() +
",\"estimated_volume\":" +
self.estimated_volume.to_string() +
"}"
}
///|
pub fn GeometryInventory::to_json(self : GeometryInventory) -> String {
"{\"represented_product_count\":" +
self.represented_product_count.to_string() +
",\"empty_representation_count\":" +
self.empty_representation_count.to_string() +
",\"invalid_mesh_count\":" +
self.invalid_mesh_count.to_string() +
",\"total_surface_area\":" +
self.total_surface_area.to_string() +
",\"total_volume\":" +
self.total_volume.to_string() +
",\"products\":[" +
self.products.map(row => row.to_json()).join(",") +
"]}"
}
///|
pub fn GeometryInventory::to_csv(self : GeometryInventory) -> String {
let builder = StringBuilder()
builder <+
"product_id,geometry_count,points,triangles,area,volume,estimated,valid\n"
for row in self.products {
builder <+
"\{row.product_id},\{row.geometry_count},\{row.point_count},\{row.triangle_count},\{row.surface_area},\{row.volume},\{row.estimated_volume},\{row.is_valid()}\n"
}
builder.to_string()
}
///|
pub fn geometry_inventory_summary(
inventory : GeometryInventory,
) -> Array[String] {
[
inventory.headline(),
"represented_ratio=" + inventory.represented_ratio().to_string(),
"valid_ratio=" + inventory.valid_ratio().to_string(),
]
}