use super::*;
use crate::AnalyticSurface;
impl<'a> SolidBuilder<'a> {
pub(super) fn split_off_plane_planar_faces(
&mut self,
pending: &mut Vec<PendingFace>,
) -> Result<usize, String> {
let band = crate::tolerance::assembler_weld(crate::BooleanOptions::default().tolerance);
let debug = std::env::var("BREP_DEBUG_PLANAR_SPLIT").is_ok();
let mut split = 0usize;
let mut faces = Vec::with_capacity(pending.len());
for face in pending.drain(..) {
match self.plan_planar_split(&face, band)? {
Some(plan) => {
if debug {
eprintln!(
"PLANAR SPLIT surface #{}: {} vertices {:.3e} off the plane -> {} triangles",
face.surface_ref,
plan.vertex_ids.len(),
plan.off_plane,
plan.triangles.len()
);
}
let pieces = self.build_planar_triangles(&face, &plan)?;
faces.extend(pieces);
split += 1;
}
None => faces.push(face),
}
}
*pending = faces;
if debug {
eprintln!("PLANAR SPLIT replaced {split} face(s) (band {band:.1e})");
}
Ok(split)
}
fn plan_planar_split(&self, face: &PendingFace, band: f64) -> Result<Option<SplitPlan>, String> {
let Some(AnalyticSurface::Plane {
origin,
u_dir,
v_dir,
..
}) = face.surface.analytic()
else {
return Ok(None);
};
let [(specs, true)] = face.bounds.as_slice() else {
return Ok(None);
};
if specs.len() < 3 {
return Ok(None);
}
let Ok(normal) = u_dir.cross(*v_dir).normalized() else {
return Ok(None);
};
let Ok(e1) = u_dir.normalized() else {
return Ok(None);
};
let e2 = normal.cross(e1);
let mut vertex_ids = Vec::with_capacity(specs.len());
let mut off_plane = 0.0f64;
for (index, &(edge_id, forward)) in specs.iter().enumerate() {
let edge = self.edge_record(edge_id);
if edge.degenerate {
return Ok(None);
}
let (from, to) = if forward {
(edge.start_vertex_id, edge.end_vertex_id)
} else {
(edge.end_vertex_id, edge.start_vertex_id)
};
let (next_id, next_forward) = specs[(index + 1) % specs.len()];
let next = self.edge_record(next_id);
let next_from = if next_forward { next.start_vertex_id } else { next.end_vertex_id };
if next_from != to {
return Ok(None);
}
let start = self.vertex_point(from);
let end = self.vertex_point(to);
if !edge_is_straight(edge, start, end, band)? {
return Ok(None);
}
vertex_ids.push(from);
off_plane = off_plane.max(start.sub(*origin).dot(normal).abs());
}
if off_plane <= band {
return Ok(None);
}
let mut sorted = vertex_ids.clone();
sorted.sort_unstable();
if sorted.windows(2).any(|pair| pair[0] == pair[1]) {
return Ok(None);
}
let polygon: Vec<[f64; 2]> = vertex_ids
.iter()
.map(|&id| {
let delta = self.vertex_point(id).sub(*origin);
[delta.dot(e1), delta.dot(e2)]
})
.collect();
let Some(triangles) = ear_clip(&polygon) else {
return Ok(None);
};
Ok(Some(SplitPlan {
vertex_ids,
triangles,
off_plane,
}))
}
fn build_planar_triangles(
&mut self,
face: &PendingFace,
plan: &SplitPlan,
) -> Result<Vec<PendingFace>, String> {
let specs = &face.bounds[0].0;
let count = plan.vertex_ids.len();
let outward = {
let Some(AnalyticSurface::Plane { u_dir, v_dir, .. }) = face.surface.analytic() else {
return Err("step_import: planar split planned on a non-plane".into());
};
let normal = u_dir.cross(*v_dir).normalized()?;
if face.same_sense {
normal
} else {
normal.scale(-1.0)
}
};
let mut pieces = Vec::with_capacity(plan.triangles.len());
for triangle in &plan.triangles {
let ids = [
plan.vertex_ids[triangle[0]],
plan.vertex_ids[triangle[1]],
plan.vertex_ids[triangle[2]],
];
let a = self.vertex_point(ids[0]);
let b = self.vertex_point(ids[1]);
let c = self.vertex_point(ids[2]);
let ab = b.sub(a);
let u_length = ab.length();
if !(u_length > 0.0) {
return Err("step_import: planar split produced a degenerate triangle".into());
}
let u_direction = ab.scale(1.0 / u_length);
let ac = c.sub(a);
let c_u = ac.dot(u_direction);
let v = ac.sub(u_direction.scale(c_u));
let v_length = v.length();
if !(v_length > 0.0) {
return Err("step_import: planar split produced a degenerate triangle".into());
}
let v_direction = v.scale(1.0 / v_length);
let u_low = c_u.min(0.0);
let u_high = c_u.max(u_length);
let surface = make_plane(
a.add(u_direction.scale(u_low)),
u_direction,
v_direction,
u_high - u_low,
v_length,
)?;
let same_sense = u_direction.cross(v_direction).dot(outward) > 0.0;
let mut triangle_specs = Vec::with_capacity(3);
for side in 0..3 {
let from_index = triangle[side];
let to_index = triangle[(side + 1) % 3];
let spec = if (from_index + 1) % count == to_index {
specs[from_index]
} else {
let diagonal = self.faceted_edge(ids[side], ids[(side + 1) % 3])?;
if let Some(capture) = self.readings.as_mut() {
capture.written.remove(&diagonal.0);
}
if self.readings.as_ref().is_some_and(|c| !c.stages.contains_key(&diagonal.0)) {
self.capture_stage(diagonal.0, || "planar-split diagonal".into());
}
diagonal
};
triangle_specs.push(spec);
}
pieces.push(PendingFace {
face_ref: face.face_ref,
surface,
surface_ref: usize::MAX,
same_sense,
bounds: vec![(triangle_specs, true)],
});
}
Ok(pieces)
}
}
struct SplitPlan {
vertex_ids: Vec<u64>,
triangles: Vec<[usize; 3]>,
off_plane: f64,
}
fn edge_is_straight(edge: &EdgeRecord, start: Vec3, end: Vec3, band: f64) -> Result<bool, String> {
let chord = end.sub(start);
let length = chord.length();
if length <= band {
return Ok(false);
}
let direction = chord.scale(1.0 / length);
for fraction in [0.25, 0.5, 0.75] {
let point = edge.curve.evaluate(edge.t0 + (edge.t1 - edge.t0) * fraction)?;
let delta = point.sub(start);
let along = delta.dot(direction);
let off = delta.sub(direction.scale(along)).length();
if off > band || along < -band || along > length + band {
return Ok(false);
}
}
Ok(true)
}
pub(in crate::step_import) fn ear_clip(polygon: &[[f64; 2]]) -> Option<Vec<[usize; 3]>> {
let count = polygon.len();
if count < 3 {
return None;
}
let signed_area: f64 = (0..count)
.map(|index| {
let a = polygon[index];
let b = polygon[(index + 1) % count];
a[0] * b[1] - b[0] * a[1]
})
.sum();
if signed_area == 0.0 {
return None;
}
let orientation = signed_area.signum();
let cross = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| -> f64 {
(b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
};
let extent = polygon
.iter()
.fold(0.0f64, |max, point| max.max(point[0].abs()).max(point[1].abs()))
.max(1e-300);
let area_floor = extent * extent * 1e-14;
let inside_or_on = |p: [f64; 2], a: [f64; 2], b: [f64; 2], c: [f64; 2]| -> bool {
let s0 = cross(a, b, p) * orientation;
let s1 = cross(b, c, p) * orientation;
let s2 = cross(c, a, p) * orientation;
s0 >= -area_floor && s1 >= -area_floor && s2 >= -area_floor
};
let mut indices: Vec<usize> = (0..count).collect();
let mut triangles = Vec::with_capacity(count - 2);
while indices.len() > 3 {
let remaining = indices.len();
let mut clipped = false;
for offset in 0..remaining {
let previous = indices[(offset + remaining - 1) % remaining];
let current = indices[offset];
let next = indices[(offset + 1) % remaining];
let (a, b, c) = (polygon[previous], polygon[current], polygon[next]);
if cross(a, b, c) * orientation <= area_floor {
continue;
}
let blocked = indices.iter().any(|&other| {
other != previous
&& other != current
&& other != next
&& inside_or_on(polygon[other], a, b, c)
});
if blocked {
continue;
}
triangles.push([previous, current, next]);
indices.remove(offset);
clipped = true;
break;
}
if !clipped {
return None;
}
}
let (a, b, c) = (polygon[indices[0]], polygon[indices[1]], polygon[indices[2]]);
if cross(a, b, c) * orientation <= area_floor {
return None;
}
triangles.push([indices[0], indices[1], indices[2]]);
Some(triangles)
}