///|
/// A plane used to extract a coordination or quantity-takeoff section.
pub(all) struct SectionPlane {
origin : Point3
normal : Vector3
} derive(Debug)
///|
/// Controls numerical tolerance and segment limits for a section cut.
pub(all) struct SectionOptions {
tolerance : Double
include_coplanar_edges : Bool
maximum_segments : Int
join_tolerance : Double
} derive(Debug)
///|
/// One line segment produced by intersecting a triangle with a plane.
pub(all) struct SectionSegment {
product_id : Int
source_triangle : Int
start : Point3
end : Point3
length : Double
coplanar : Bool
} derive(Debug)
///|
/// A connected sequence of section points assembled from raw segments.
pub(all) struct SectionPolyline {
product_id : Int
points : Array[Point3]
closed : Bool
length : Double
area : Double
} derive(Debug)
///|
/// Aggregate output of a section extraction.
pub(all) struct SectionResult {
plane : SectionPlane
segments : Array[SectionSegment]
polylines : Array[SectionPolyline]
product_ids : Array[Int]
intersected_triangle_count : Int
total_length : Double
bounds : BoundingBox
} derive(Debug)
///|
pub fn SectionPlane::new(origin~ : Point3, normal~ : Vector3) -> SectionPlane {
{ origin, normal: normal.normalized() }
}
///|
pub fn SectionPlane::horizontal(height : Double) -> SectionPlane {
SectionPlane::new(origin={ x: 0.0, y: 0.0, z: height }, normal=Vector3::{
x: 0.0,
y: 0.0,
z: 1.0,
})
}
///|
pub fn SectionPlane::vertical_x(offset : Double) -> SectionPlane {
SectionPlane::new(origin={ x: offset, y: 0.0, z: 0.0 }, normal=Vector3::{
x: 1.0,
y: 0.0,
z: 0.0,
})
}
///|
pub fn SectionPlane::vertical_y(offset : Double) -> SectionPlane {
SectionPlane::new(origin={ x: 0.0, y: offset, z: 0.0 }, normal=Vector3::{
x: 0.0,
y: 1.0,
z: 0.0,
})
}
///|
pub fn SectionPlane::is_valid(self : SectionPlane) -> Bool {
self.normal.length() > 0.0
}
///|
pub fn SectionPlane::signed_distance(
self : SectionPlane,
point : Point3,
) -> Double {
Vector3::from_points(self.origin, point).dot(self.normal)
}
///|
pub fn SectionPlane::project(self : SectionPlane, point : Point3) -> Point3 {
let distance = self.signed_distance(point)
{
x: point.x - self.normal.x * distance,
y: point.y - self.normal.y * distance,
z: point.z - self.normal.z * distance,
}
}
///|
pub fn SectionPlane::headline(self : SectionPlane) -> String {
"origin=(" +
self.origin.x.to_string() +
"," +
self.origin.y.to_string() +
"," +
self.origin.z.to_string() +
") normal=(" +
self.normal.x.to_string() +
"," +
self.normal.y.to_string() +
"," +
self.normal.z.to_string() +
")"
}
///|
pub fn default_section_options() -> SectionOptions {
{
tolerance: 0.000001,
include_coplanar_edges: false,
maximum_segments: 0,
join_tolerance: 0.0001,
}
}
///|
pub fn SectionOptions::with_tolerance(
self : SectionOptions,
tolerance : Double,
) -> SectionOptions {
let value = if tolerance < 0.0 { 0.0 } else { tolerance }
{
..self,
tolerance: value,
join_tolerance: if self.join_tolerance < value {
value
} else {
self.join_tolerance
},
}
}
///|
pub fn SectionOptions::including_coplanar_edges(
self : SectionOptions,
enabled : Bool,
) -> SectionOptions {
{ ..self, include_coplanar_edges: enabled }
}
///|
pub fn SectionOptions::with_maximum_segments(
self : SectionOptions,
maximum : Int,
) -> SectionOptions {
{ ..self, maximum_segments: if maximum < 0 { 0 } else { maximum } }
}
///|
pub fn SectionOptions::with_join_tolerance(
self : SectionOptions,
tolerance : Double,
) -> SectionOptions {
{ ..self, join_tolerance: if tolerance < 0.0 { 0.0 } else { tolerance } }
}
///|
fn section_point_distance(left : Point3, right : Point3) -> Double {
Vector3::from_points(left, right).length()
}
///|
fn section_interpolate(
left : Point3,
right : Point3,
amount : Double,
) -> Point3 {
{
x: left.x + (right.x - left.x) * amount,
y: left.y + (right.y - left.y) * amount,
z: left.z + (right.z - left.z) * amount,
}
}
///|
fn section_add_unique(
points : Array[Point3],
point : Point3,
tolerance : Double,
) -> Unit {
if !points.any(item => section_point_distance(item, point) <= tolerance) {
points.push(point)
}
}
///|
fn section_edge_point(
left : Point3,
right : Point3,
left_distance : Double,
right_distance : Double,
tolerance : Double,
) -> Point3? {
let left_on = left_distance.abs() <= tolerance
let right_on = right_distance.abs() <= tolerance
if left_on && right_on {
Some(left)
} else if left_on {
Some(left)
} else if right_on {
Some(right)
} else if (left_distance < 0.0 && right_distance > 0.0) ||
(left_distance > 0.0 && right_distance < 0.0) {
let amount = left_distance.abs() /
(left_distance.abs() + right_distance.abs())
Some(section_interpolate(left, right, amount))
} else {
None
}
}
///|
fn section_triangle_points(
plane : SectionPlane,
a : Point3,
b : Point3,
c : Point3,
options : SectionOptions,
) -> (Array[Point3], Bool) {
let da = plane.signed_distance(a)
let db = plane.signed_distance(b)
let dc = plane.signed_distance(c)
let points : Array[Point3] = []
for
item in [
section_edge_point(a, b, da, db, options.tolerance),
section_edge_point(b, c, db, dc, options.tolerance),
section_edge_point(c, a, dc, da, options.tolerance),
] {
match item {
Some(point) => section_add_unique(points, point, options.tolerance)
None => ()
}
}
let coplanar = da.abs() <= options.tolerance &&
db.abs() <= options.tolerance &&
dc.abs() <= options.tolerance
if coplanar && !options.include_coplanar_edges {
([], true)
} else {
(points, coplanar)
}
}
///|
fn section_farthest_pair(points : Array[Point3]) -> (Point3, Point3)? {
if points.length() < 2 {
return None
}
let mut first = points[0]
let mut second = points[1]
let mut longest = section_point_distance(first, second)
for i in 0.. longest {
longest = distance
first = points[i]
second = points[j]
}
}
}
Some((first, second))
}
///|
/// Intersect a standalone mesh, retaining the source triangle index.
pub fn section_mesh(
product_id : Int,
mesh : Mesh,
plane : SectionPlane,
options? : SectionOptions = default_section_options(),
) -> Array[SectionSegment] {
let result : Array[SectionSegment] = []
if !plane.is_valid() {
return result
}
for index in 0.. {
let (points, coplanar) = section_triangle_points(
plane, a, b, c, options,
)
match section_farthest_pair(points) {
Some((start, end)) if section_point_distance(start, end) >
options.tolerance =>
result.push({
product_id,
source_triangle: index,
start,
end,
length: section_point_distance(start, end),
coplanar,
})
_ => ()
}
}
_ => ()
}
if options.maximum_segments > 0 &&
result.length() >= options.maximum_segments {
break
}
}
result
}
///|
fn section_append_orient(
points : Array[Point3],
segment : SectionSegment,
tolerance : Double,
) -> Bool {
if points.length() == 0 {
points.push(segment.start)
points.push(segment.end)
return true
}
let first = points[0]
let last = points[points.length() - 1]
if section_point_distance(last, segment.start) <= tolerance {
points.push(segment.end)
true
} else if section_point_distance(last, segment.end) <= tolerance {
points.push(segment.start)
true
} else if section_point_distance(first, segment.end) <= tolerance {
points.insert(0, segment.start)
true
} else if section_point_distance(first, segment.start) <= tolerance {
points.insert(0, segment.end)
true
} else {
false
}
}
///|
fn section_polyline_length(points : Array[Point3]) -> Double {
let mut total = 0.0
for index in 1.. Double {
if points.length() < 3 {
return 0.0
}
let axis = plane.normal
let mut area = 0.0
for index in 0.. Array[SectionPolyline] {
let remaining = segments.copy()
let result : Array[SectionPolyline] = []
while remaining.length() > 0 {
let seed = remaining.remove(0)
let points : Array[Point3] = [seed.start, seed.end]
let mut changed = true
while changed {
changed = false
let mut index = 0
while index < remaining.length() {
if remaining[index].product_id == seed.product_id &&
section_append_orient(points, remaining[index], tolerance) {
ignore(remaining.remove(index))
changed = true
} else {
index += 1
}
}
}
let closed = points.length() > 2 &&
section_point_distance(points[0], points[points.length() - 1]) <=
tolerance
result.push({
product_id: seed.product_id,
points,
closed,
length: section_polyline_length(points),
area: section_projected_area(points, plane),
})
}
result
}
///|
fn section_add_product_id(ids : Array[Int], product_id : Int) -> Unit {
if !ids.contains(product_id) {
ids.push(product_id)
}
}
///|
/// Extract a section across all products with supported mesh geometry.
pub fn section_model(
model : Model,
plane : SectionPlane,
options? : SectionOptions = default_section_options(),
) -> SectionResult raise ParseError {
let index = model.index()
let segments : Array[SectionSegment] = []
for product in model.products() {
let mesh = mesh_for_product_advanced(index, product)
segments.append(section_mesh(product.id, mesh, plane, options~))
if options.maximum_segments > 0 &&
segments.length() >= options.maximum_segments {
break
}
}
let polylines = section_build_polylines(
segments,
plane,
options.join_tolerance,
)
let ids : Array[Int] = []
for segment in segments {
section_add_product_id(ids, segment.product_id)
}
{
plane,
segments,
polylines,
product_ids: ids,
intersected_triangle_count: segments.length(),
total_length: segments.fold(init=0.0, (sum, item) => sum + item.length),
bounds: segments.fold(init=BoundingBox::empty(), (result, item) => {
merge_bounds(result, BoundingBox::from_points([item.start, item.end]))
}),
}
}
///|
pub fn SectionResult::segment_count(self : SectionResult) -> Int {
self.segments.length()
}
///|
pub fn SectionResult::polyline_count(self : SectionResult) -> Int {
self.polylines.length()
}
///|
pub fn SectionResult::product_count(self : SectionResult) -> Int {
self.product_ids.length()
}
///|
pub fn SectionResult::closed_count(self : SectionResult) -> Int {
self.polylines.fold(init=0, (count, line) => {
if line.closed {
count + 1
} else {
count
}
})
}
///|
pub fn SectionResult::total_area(self : SectionResult) -> Double {
self.polylines.fold(init=0.0, (sum, line) => sum + line.area)
}
///|
pub fn SectionResult::segments_for_product(
self : SectionResult,
product_id : Int,
) -> Array[SectionSegment] {
self.segments.filter(segment => segment.product_id == product_id)
}
///|
pub fn SectionResult::polylines_for_product(
self : SectionResult,
product_id : Int,
) -> Array[SectionPolyline] {
self.polylines.filter(line => line.product_id == product_id)
}
///|
pub fn SectionResult::headline(self : SectionResult) -> String {
"products=" +
self.product_count().to_string() +
" triangles=" +
self.intersected_triangle_count.to_string() +
" segments=" +
self.segment_count().to_string() +
" polylines=" +
self.polyline_count().to_string() +
" length=" +
self.total_length.to_string() +
" area=" +
self.total_area().to_string()
}
///|
fn section_point_json(point : Point3) -> String {
"[" +
point.x.to_string() +
"," +
point.y.to_string() +
"," +
point.z.to_string() +
"]"
}
///|
pub fn SectionResult::to_json(self : SectionResult) -> String {
let segments = self.segments
.map(segment => {
"{\"product_id\":" +
segment.product_id.to_string() +
",\"triangle\":" +
segment.source_triangle.to_string() +
",\"start\":" +
section_point_json(segment.start) +
",\"end\":" +
section_point_json(segment.end) +
",\"length\":" +
segment.length.to_string() +
"}"
})
.join(",")
let lines = self.polylines
.map(line => {
"{\"product_id\":" +
line.product_id.to_string() +
",\"closed\":" +
line.closed.to_string() +
",\"length\":" +
line.length.to_string() +
",\"area\":" +
line.area.to_string() +
",\"points\":[" +
line.points.map(section_point_json).join(",") +
"]}"
})
.join(",")
"{\"product_ids\":[" +
self.product_ids.map(id => id.to_string()).join(",") +
"],\"segment_count\":" +
self.segment_count().to_string() +
",\"polyline_count\":" +
self.polyline_count().to_string() +
",\"total_length\":" +
self.total_length.to_string() +
",\"total_area\":" +
self.total_area().to_string() +
",\"segments\":[" +
segments +
"],\"polylines\":[" +
lines +
"]}"
}
///|
pub fn SectionResult::to_csv(self : SectionResult) -> String {
let builder = StringBuilder()
builder <+
"product_id,triangle,start_x,start_y,start_z,end_x,end_y,end_z,length,coplanar\n"
for segment in self.segments {
builder <+
"\{segment.product_id},\{segment.source_triangle},\{segment.start.x},\{segment.start.y},\{segment.start.z},\{segment.end.x},\{segment.end.y},\{segment.end.z},\{segment.length},\{segment.coplanar}\n"
}
builder.to_string()
}
///|
pub fn SectionResult::to_markdown(self : SectionResult) -> String {
let builder = StringBuilder()
builder <+ "# Section analysis\n\n"
builder <+ "- \{self.headline()}\n"
builder <+ "- Plane: \{self.plane.headline()}\n"
builder <+ "- Closed polylines: \{self.closed_count()}\n\n"
builder <+ "| Product | Triangle | Length | Coplanar |\n"
builder <+ "| ---: | ---: | ---: | --- |\n"
for segment in self.segments {
builder <+
"| \{segment.product_id} | \{segment.source_triangle} | \{segment.length} | \{segment.coplanar} |\n"
}
builder.to_string()
}
///|
pub fn SectionPolyline::point_count(self : SectionPolyline) -> Int {
self.points.length()
}
///|
pub fn SectionPolyline::is_degenerate(
self : SectionPolyline,
tolerance? : Double = 0.000001,
) -> Bool {
self.length <= tolerance || self.points.length() < 2
}
///|
pub fn SectionPolyline::first(self : SectionPolyline) -> Point3? {
self.points.get(0)
}
///|
pub fn SectionPolyline::last(self : SectionPolyline) -> Point3? {
if self.points.length() == 0 {
None
} else {
Some(self.points[self.points.length() - 1])
}
}
///|
pub fn section_result_product_histogram(
result : SectionResult,
) -> Map[Int, Int] {
let histogram : Map[Int, Int] = Map([])
for segment in result.segments {
histogram.update_or_default(segment.product_id, 1, count => count + 1)
}
histogram
}
///|
pub fn section_result_long_segments(
result : SectionResult,
minimum_length : Double,
) -> Array[SectionSegment] {
result.segments.filter(segment => segment.length >= minimum_length)
}
///|
pub fn section_result_bounds_for(
result : SectionResult,
product_id : Int,
) -> BoundingBox {
result
.segments_for_product(product_id)
.fold(init=BoundingBox::empty(), (bounds, segment) => {
merge_bounds(bounds, BoundingBox::from_points([segment.start, segment.end]))
})
}
///|
pub fn section_result_summary(result : SectionResult) -> Array[String] {
[
result.headline(),
"closed=" + result.closed_count().to_string(),
"products=" + result.product_ids.map(id => id.to_string()).join(","),
]
}