kernel: dead code the ports left behind, deleted

Every lib warning under --features smlib outside the files agents are editing
(assemble.rs, sweep_path.rs) pointed at code nothing calls any more:
- body: shell_nesting;
- boolean_record: of_pair and its Traced alias;
- planar_regularize: the generated-wall regularization (and its branches in the
  certified one, which was its only other path); planar_sources:
  with_shell_planar_sources;
- plane_fit: plane_through, EdgeSampler, planar_face, plane_frame (the patch is
  fill's); only loop_area is left;
- polyhedron: solve3; prism: wall_along; offset: Surf::normal;
- record: regions, region; shell_groups: groups, parents and their limits;
- splitface: side_of_point, Analytic, analytic_face, face_sides, canonical;
- spline_conv.rs, all of it (surfaces builds NURBS records); an unused import in
  tess/uv.rs.
The tests of deleted items go with them: prism's wall_along test, the shell
grouping test in body_lump_tests, and the generated-regularization assertion in
regularization_protection_tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
jyviko
2026-10-22 07:49:17 -04:00
parent 6d1726a01b
commit f57070d737
16 changed files with 10 additions and 637 deletions
-21
View File
@@ -1251,27 +1251,6 @@ impl Body {
}
Ok(contacts)
}
/// For each shell: whether it bounds material (else a void or an open/wire shell), and the
/// shell directly around it (the outer shell of the region it lies in; `None` in the open).
pub(crate) fn shell_nesting(&self) -> (Vec<bool>, Vec<Option<usize>>) {
let n = self.record.shells.len();
let (mut material, mut parent) = (vec![false; n], vec![None; n]);
for (r, region) in self.record.regions.iter().enumerate() {
let around = if r == 0 { None } else { region.shells.first().map(|&s| s as usize) };
for (k, &s) in region.shells.iter().enumerate() {
let s = s as usize;
if r > 0 && k == 0 {
if let Some(m) = material.get_mut(s) {
*m = !region.void;
}
} else if let Some(p) = parent.get_mut(s) {
*p = around;
}
}
}
(material, parent)
}
}
/// Points of an edge's curve over its interval, within `chord` of it: lines by their ends, the
-35
View File
@@ -175,41 +175,6 @@ fn unowned_overlapping_and_touching_cavities_are_errors_not_dropped_geometry() {
assert!(body.lumps().is_err());
}
#[test]
fn shell_grouping_preserves_large_disconnected_patterns_and_bounds_hostile_work() {
let mesh = cube(1.0).mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
let copies: Vec<_> = (0..512)
.map(|i| {
let mut m = mesh.clone();
for p in &mut m.positions {
p.x += f64::from(i) * 4.0;
}
m
})
.collect();
assert_eq!(shell_groups::groups(&copies, &vec![8.0; 512], 1e-8).unwrap().len(), 512);
let excessive = vec![Mesh::default(); shell_groups::MAX_SHELLS + 1];
assert!(shell_groups::groups(&excessive, &vec![1.0; excessive.len()], 1e-8).unwrap_err().to_string().contains("shell count"));
let mut oversized = mesh.clone();
oversized.positions.resize(shell_groups::MAX_POINTS + 1, Vec3::ZERO);
assert!(shell_groups::groups(&[oversized], &[8.0], 1e-8).unwrap_err().to_string().contains("grouping limit"));
// Two stacks of one slanted triangle, in parallel planes 1 µm apart: every pair's boxes
// overlap but none touch, so all 7000² pairs need testing.
let slanted = |dz: f64| Mesh {
positions: vec![Vec3::new(0.0, 0.0, dz), Vec3::new(1.0, 0.0, 1.0 + dz), Vec3::new(0.0, 1.0, 1.0 + dz)],
triangles: vec![[0, 1, 2]; 7000],
..Mesh::default()
};
let (repeated, lifted) = (slanted(0.0), slanted(1e-3));
assert!(shell_groups::groups(&[repeated, lifted], &[8.0, 8.0], 1e-8).unwrap_err().to_string().contains("boundary work"));
let mut outer = cube(2.0).mesh(&crate::MeshTolerance::chord(0.01)).unwrap();
let mut inner = mesh;
outer.triangles = outer.triangles.iter().copied().cycle().take(4000).collect();
inner.triangles = inner.triangles.iter().copied().cycle().take(4000).collect();
inner.positions.resize(4000, Vec3::ZERO);
assert!(shell_groups::groups(&[outer, inner], &[64.0, -8.0], 1e-8).unwrap_err().to_string().contains("containment work"));
}
#[test]
fn translated_and_scaled_nested_voids_keep_material_membership() {
let body = nested_island();
+1 -10
View File
@@ -1,7 +1,7 @@
//! Composing the exact history of a multi-step operation (a boolean through several steps, a
//! local modifier on a healed body): each step's history over its own operands is placed among
//! the operation's operands ([`placed`], [`kept`]) and substituted into the next
//! ([`through`], [`of_pair`], [`crate::record::after`]). The operands of a boolean are `[a, b]`:
//! ([`through`], [`crate::record::after`]). The operands of a boolean are `[a, b]`:
//! the target, then the tool. A step that cannot report its history leaves the result without
//! one.
@@ -37,15 +37,6 @@ pub(crate) fn operand(body: &Body, operand: usize, counts: &[(usize, usize)]) ->
body.clone().with_checked_history(kept(body, operand, counts.len()), counts)
}
/// A body with its history over the operation's operands (`None`: not known).
pub(crate) type Traced = (Body, Option<History>);
/// The history over the operation's operands of `result`, made from `x` and `y` (in that order)
/// whose histories over them are given.
pub(crate) fn of_pair(result: &Body, x: &Option<History>, y: &Option<History>) -> Option<History> {
composed(result.history()?.clone().over(&[x.as_ref()?, y.as_ref()?]))
}
/// `body` healed as the local modifiers take it (faces on one surface as one face, as after a
/// join; faces split on purpose kept apart), with its history over `[body]`
/// ([`crate::heal::heal`]), each face keeping the construction source its faces shared and the
-1
View File
@@ -119,7 +119,6 @@ mod smlib_sweeps_tests;
mod smooth_edges;
#[cfg(test)]
mod sphere_lens_tests;
mod spline_conv;
mod splitface;
mod step;
mod step_in;
-28
View File
@@ -69,34 +69,6 @@ pub(crate) fn surf_of(b: &Brep, face: usize) -> Option<Surf> {
}
impl Surf {
/// Outward unit normal of the surface at `p` (on or near it).
pub(crate) fn normal(&self, p: Vec3) -> Option<Vec3> {
match *self {
Surf::Plane { n, .. } => Some(n),
Surf::Cylinder { o, a, convex, .. } => {
let v = p - o;
let radial = (v - a * v.dot(a)).normalized()?;
Some(if convex { radial } else { -radial })
}
Surf::Sphere { c, convex, .. } => {
let radial = (p - c).normalized()?;
Some(if convex { radial } else { -radial })
}
Surf::Cone { v, a, half, convex } => {
let w = p - v;
let q = (w - a * w.dot(a)).normalized()?;
let n = q * half.cos() - a * half.sin();
Some(if convex { n } else { -n })
}
Surf::Torus { o, a, big, h, convex, .. } => {
let w = p - o;
let u = (w - a * w.dot(a)).normalized()?;
let n = (p - (o + a * h + u * big)).normalized()?;
Some(if convex { n } else { -n })
}
}
}
/// Whether `other` is this surface, with the same outward side, within `tol`.
pub(crate) fn same(&self, other: &Surf, tol: f64) -> bool {
match (*self, *other) {
+6 -49
View File
@@ -27,33 +27,6 @@ fn failed(e: impl std::fmt::Display) -> KernelError {
impl Body {
pub(crate) fn regularize_certified_planar_faces(self) -> Result<Self> {
self.regularize_planar_faces(false)
}
/// Only newly produced Cut walls call this path. The temporary planes carry
/// construction support, never persistent ancestor/source-role authority.
pub(crate) fn regularize_generated_planar_faces(self) -> Result<Self> {
let body = self.regularize_planar_faces(true)?;
let sources = body.face_sources().map(|sources| {
sources
.iter()
.map(|source| {
let mut source = source.clone();
if let Some(super::FaceSource::ExtrudeSide { plane, coalesced, .. }) = &mut source {
if plane.is_some() && !*coalesced {
source = None;
} else {
*plane = None;
}
}
source
})
.collect()
});
Ok(body.with_face_sources(sources.map(Arc::new)))
}
fn regularize_planar_faces(self, generated: bool) -> Result<Self> {
if self.regularization_protected {
return Ok(self);
}
@@ -73,16 +46,7 @@ impl Body {
.zip(sources)
.map(|(actual, s)| {
s.as_ref()
.and_then(|source| {
if generated {
match source {
super::FaceSource::ExtrudeSide { segment: solidcraft_geom::Seg2::Line { .. }, plane: Some(p), .. } => Some(*p),
_ => None,
}
} else {
super::planar_sources::certified_plane(source)
}
})
.and_then(super::planar_sources::certified_plane)
.filter(|p| actual.is_some_and(|actual| super::planar_sources::same_support(p, &actual)))
})
.collect();
@@ -97,8 +61,8 @@ impl Body {
}
}
}
// The edges between two faces of one shell on one certified plane (from one source,
// unless the faces are generated walls), off every authored split line.
// The edges between two faces of one shell on one certified plane, from one source,
// off every authored split line.
let mut eligible = Vec::new();
let mut identity = 16_000_000usize;
let mut work = 2_000_000usize;
@@ -120,7 +84,7 @@ impl Body {
};
let Some(left) = identity.checked_sub(cost) else { return Ok(None) };
identity = left;
if !generated && sa != sb {
if sa != sb {
continue;
}
let Some(cost) = split_points.checked_mul(6).and_then(|n| n.checked_add(1)) else { return Ok(None) };
@@ -160,25 +124,18 @@ impl Body {
let (body, or) = crate::native::body_from_smlib_ordered(&r)?;
let or = or.ok_or_else(|| failed("the result's record is numbered unlike SMLib's body"))?;
let h = r.history().map_err(failed)?.history(std::slice::from_ref(&ob), &or).map_err(failed)?;
// Each face keeps the source of the first face it is made of; a generated wall made
// of several is coalesced.
// Each face keeps the source of the first face it is made of.
let mut metadata = 2_000_000usize;
let mut output = Vec::with_capacity(h.faces.len());
for f in &h.faces {
let members: Vec<usize> =
f.origins.iter().filter_map(|o| if let crate::history::FaceOrigin::Face { face, .. } = o { Some(*face) } else { None }).collect();
let mut source = members.iter().min().and_then(|m| sources.get(*m)).cloned().flatten();
let source = members.iter().min().and_then(|m| sources.get(*m)).cloned().flatten();
if let Some(s) = &source {
let Some(cost) = super::planar_sources::descriptor_cost(s) else { return Ok(None) };
let Some(left) = metadata.checked_sub(cost) else { return Ok(None) };
metadata = left;
}
if generated
&& members.len() > 1
&& let Some(super::FaceSource::ExtrudeSide { coalesced, .. }) = &mut source
{
*coalesced = true;
}
output.push(source);
}
let body = body
-6
View File
@@ -141,12 +141,6 @@ impl Body {
self.map_planar_sources(&[input], None, None, &[], false)
}
/// Generate distinct outer/inner roles from the exact original supports of
/// an inside shell. Generated rims and curved walls are intentionally absent.
pub(crate) fn with_shell_planar_sources(self, input: &Body, thickness: f64, open: &[Vec3]) -> Result<Self> {
self.map_planar_sources(&[input], Some(thickness), None, open, false)
}
/// Retain exact shell roles, and construction roles for Cut, only on unchanged
/// supports without a competing coplanar tool. No imported or fitted role is inferred.
pub(crate) fn with_preserved_boolean_planar_sources(self, input: &Body, tool: &Body, constructed: bool) -> Result<Self> {
+1 -107
View File
@@ -1,27 +1,6 @@
//! Planes fitted to points around a loop (Newell's method), for planar faces of polygons read
//! from files and for patches filling a planar loop of edges, and points sampled along record
//! edges (shared with the fill of a loop that is not planar).
//! The area vector of a loop of points (Newell's method): its normal and the area it encloses.
use solidcraft_geom::Vec3;
use solidcraft_geom::brep::eval::CurveEval;
use solidcraft_geom::brep::{Brep, Curve, Edge, Surface};
use crate::Result;
/// The plane through points round a loop: its normal by Newell's method (turning with the
/// loop, the plane's natural normal), through their centroid; `None` for fewer than three points
/// or no area.
pub(crate) fn plane_through(pts: &[Vec3]) -> Option<Surface> {
if pts.len() < 3 {
return None;
}
let z = loop_area(pts).normalized()?;
let o = pts.iter().fold(Vec3::ZERO, |a, p| a + *p) / pts.len() as f64;
let a = if z.x.abs() < 0.9 { Vec3::X } else { Vec3::Y };
let x = (a - z * a.dot(z)).normalized()?;
let y = z.cross(x);
Some(Surface::Plane { origin: [o.x, o.y, o.z], u_axis: [x.x, x.y, x.z], v_axis: [y.x, y.y, y.z] })
}
/// The area vector of a closed polygon (Newell's method): normal to the loop, as long as the area
/// it encloses, pointing the way the loop runs round by the right-hand rule.
@@ -34,88 +13,3 @@ pub(crate) fn loop_area(points: &[Vec3]) -> Vec3 {
n * 0.5
}
/// Points along a record edge, in the direction a loop runs along it, at `s ∈ [0, 1]` of its
/// interval: the record curve's own points (an intersection curve traced as the record holds it).
pub(crate) struct EdgeSampler {
curve: CurveEval,
range: (f64, f64),
}
impl EdgeSampler {
/// The sampler of `edge` run from its start (`forward`) or from its end.
pub(crate) fn new(edge: &Edge, forward: bool) -> Option<EdgeSampler> {
let [t0, t1] = edge.interval;
let curve = CurveEval::new(&edge.curve).ok()?;
(t0.is_finite() && t1.is_finite() && t0 < t1).then_some(EdgeSampler { curve, range: if forward { (t0, t1) } else { (t1, t0) } })
}
pub(crate) fn at(&mut self, s: f64) -> Option<Vec3> {
let (t0, t1) = self.range;
let t = if s >= 1.0 { t1 } else { t0 + (t1 - t0) * s };
self.curve.point(t).filter(|p| p.is_finite())
}
}
/// The plane of the closed loop `chain` of edges of `b` (each with the way the loop runs along
/// it) when its edges are certainly in one plane, `None` otherwise (also when an edge cannot be
/// sampled). The plane is fitted to points along the edges ([`plane_through`], its natural normal
/// turning with the loop); the edges are certified within `tol` of it from their records: a line
/// by its ends, a circle or ellipse by its frame, a polyline, B-spline or NURBS curve with
/// positive weights by its points (the curve lies in their convex hull), an intersection curve
/// only when one of its two surfaces is a plane that the loop's points lie on (the curve lies in
/// that plane). Samples alone are never trusted: a spline can bulge out of the plane between them.
pub(crate) fn planar_face(b: &Brep, chain: &[(u32, bool)], tol: f64) -> Result<Option<Surface>> {
const SAMPLES: usize = 16;
let edges: Vec<&Edge> = match chain.iter().map(|(e, _)| b.edges.get(*e as usize)).collect::<Option<_>>() {
Some(e) => e,
None => return Ok(None),
};
let mut pts = Vec::with_capacity(chain.len() * SAMPLES);
for (e, (_, forward)) in edges.iter().zip(chain) {
let Some(mut sampler) = EdgeSampler::new(e, *forward) else { return Ok(None) };
for k in 0..SAMPLES {
let Some(p) = sampler.at(k as f64 / SAMPLES as f64) else { return Ok(None) };
pts.push(p);
}
}
let Some(plane) = plane_through(&pts) else { return Ok(None) };
let Some((o, n)) = plane_frame(&plane) else { return Ok(None) };
let on = |p: Vec3, o: Vec3, n: Vec3| (p - o).dot(n).abs() <= tol;
let on_plane = |p: Vec3| on(p, o, n);
if !pts.iter().all(|p| on_plane(*p)) {
return Ok(None);
}
for e in &edges {
let certified = match &e.curve {
Curve::Line { start, end } => on_plane(Vec3::from(*start)) && on_plane(Vec3::from(*end)),
Curve::Circle { frame, .. } | Curve::Ellipse { frame, .. } => {
let z = Vec3::from(frame.x_axis).cross(Vec3::from(frame.y_axis));
on_plane(Vec3::from(frame.origin)) && z.cross(n).len() <= 1e-9
}
Curve::Polyline { points } | Curve::Spline { points, .. } => points.iter().all(|p| on_plane(Vec3::from(*p))),
Curve::RationalSpline { homogeneous, .. } => {
homogeneous.iter().all(|h| h[3] > 0.0 && on_plane(Vec3::new(h[0] / h[3], h[1] / h[3], h[2] / h[3])))
}
// A support plane holds the loop when the loop's points lie on it.
Curve::SurfaceIntersection { surfaces, .. } => {
surfaces.iter().filter_map(|s| plane_frame(s)).any(|(qo, qn)| pts.iter().all(|p| on(*p, qo, qn)))
}
};
if !certified {
return Ok(None);
}
}
Ok(Some(plane))
}
/// A plane record's origin and unit normal (`None` for any other surface).
fn plane_frame(s: &Surface) -> Option<(Vec3, Vec3)> {
match s {
Surface::Plane { origin, u_axis, v_axis } => Some((Vec3::from(*origin), Vec3::from(*u_axis).cross(Vec3::from(*v_axis)).normalized()?)),
Surface::PlaneThrough { origin, u_point, v_point } => {
let o = Vec3::from(*origin);
Some((o, (Vec3::from(*u_point) - o).cross(Vec3::from(*v_point) - o).normalized()?))
}
_ => None,
}
}
-24
View File
@@ -205,30 +205,6 @@ pub fn draft(b: &Body, faces: &[Vec3], neutral: &solidcraft_geom::Plane, pull: V
}
}
pub(crate) fn solve3_pub(m: [[f64; 3]; 3], r: [f64; 3]) -> Option<[f64; 3]> {
solve3(m, r)
}
fn solve3(m: [[f64; 3]; 3], r: [f64; 3]) -> Option<[f64; 3]> {
let det = |a: [[f64; 3]; 3]| {
a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1]) - a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
+ a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0])
};
let d = det(m);
if d.abs() < 1e-9 {
return None;
}
let mut out = [0.0; 3];
for (k, o) in out.iter_mut().enumerate() {
let mut mk = m;
for (row, rv) in mk.iter_mut().zip(r) {
row[k] = rv;
}
*o = det(mk) / d;
}
Some(out)
}
/// Move the faces at the given points along their outward normals by `distance` (negative: into
/// the body), SMLib's offset of faces: the faces around them are extended or trimmed to meet them.
pub fn offset_faces(b: &Body, at: &[Vec3], distance: f64) -> Result<Body> {
-95
View File
@@ -238,83 +238,6 @@ mod sheets {
}
}
/// Certify that a curved wall is swept along `d` (unit) from its surface record: a cylinder or
/// a revolved line about an axis along `d`, or a spline net whose rows run along `d` (either
/// parameter) with weights that separate into one factor per parameter. Seam vertices alone
/// cannot establish this: a half-cylinder's seam normals can both be perpendicular to a
/// transverse direction, silently filling its hole. A plane is no curved wall.
pub(crate) fn wall_along(surface: &Surface, d: Vec3, tol: f64, remaining: &mut usize) -> bool {
let v3 = |p: [f64; 3]| Vec3::new(p[0], p[1], p[2]);
let along = |a: Vec3| a.normalized().is_some_and(|a| a.cross(d).len() < 1e-9);
let points: Vec<Vec<(Vec3, f64)>> = match surface {
Surface::Cylinder { frame, .. } => return along(v3(frame.x_axis).cross(v3(frame.y_axis))),
Surface::PlacedRevolution { profile, axis, matrix, .. } => {
let solidcraft_geom::brep::Curve::Line { start, end } = &**profile else { return false };
let (Some(a), Some(m)) = (v3(*axis).normalized(), (v3(*end) - v3(*start)).normalized()) else { return false };
if a.cross(m).len() > 1e-9 {
return false; // A conical meridian is not a cylindrical wall.
}
// The axis placed by the column-major matrix (a direction: no translation).
let c = |k: usize| matrix[0][k] * a.x + matrix[1][k] * a.y + matrix[2][k] * a.z;
return c(3).abs() < 1e-12 && along(Vec3::new(c(0), c(1), c(2)));
}
Surface::Spline { points, .. } => points.iter().map(|r| r.iter().map(|p| (v3(*p), 1.0)).collect()).collect(),
Surface::RationalSpline { homogeneous, .. } => {
homogeneous.iter().map(|r| r.iter().map(|p| (Vec3::new(p[0] / p[3], p[1] / p[3], p[2] / p[3]), p[3])).collect()).collect()
}
_ => return false,
};
let (rows, cols) = (points.len(), points.first().map_or(0, Vec::len));
let count = rows.saturating_mul(cols);
if rows < 2 || cols < 2 || count > 16_384 || points.iter().any(|r| r.len() != cols) {
return false;
}
let Some(left) = remaining.checked_sub(count.saturating_mul(2)) else { return false };
*remaining = left;
if points.iter().flatten().any(|(p, w)| !w.is_finite() || *w <= 0.0 || !p.is_finite()) {
return false;
}
let max_weight = points.iter().flatten().map(|p| p.1).fold(0.0, f64::max);
if points.iter().flatten().any(|p| p.1 / max_weight < 1e-12) {
return false;
}
// Either parameter can be the extrusion direction. Projected positions must
// be invariant in that parameter; rational weights must also separate into
// a product of one factor per parameter, otherwise mixed weights bend the wall.
for transpose in [false, true] {
let (groups, length) = if transpose { (cols, rows) } else { (rows, cols) };
let at = |i: usize, j: usize| if transpose { points[j][i] } else { points[i][j] };
let mut valid = true;
let mut extended = false;
for i in 0..groups {
let (base, base_w) = at(i, 0);
for j in 0..length {
let (p, w) = at(i, j);
let delta = p - base;
let magnitude = [p.x.abs(), p.y.abs(), p.z.abs(), base.x.abs(), base.y.abs(), base.z.abs()].into_iter().fold(1.0, f64::max);
if delta.cross(d).len() > tol * 1e-3 + magnitude * f64::EPSILON * 128.0 {
valid = false;
break;
}
extended |= delta.dot(d).abs() > tol * 1e-3;
let lhs = w / max_weight * (at(0, 0).1 / max_weight);
let rhs = base_w / max_weight * (at(0, j).1 / max_weight);
if (lhs - rhs).abs() > 1e-12 * lhs.abs().max(rhs.abs()) {
valid = false;
break;
}
}
if !valid {
break;
}
}
if valid && extended {
return true;
}
}
false
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic)]
mod planar_tests {
@@ -355,24 +278,6 @@ mod planar_tests {
Region2 { outer: Loop2::polygon(&[a, Vec2::new(b.x, a.y), b, Vec2::new(a.x, b.y)]), holes: vec![] }
}
#[test]
fn curved_prism_wall_requires_positive_separable_rational_weights() {
let surface = |mixed: bool| {
let weight = if mixed { 2.0 } else { 1.0 };
Surface::RationalSpline {
u_knots: vec![0., 0., 1., 1.],
v_knots: vec![0., 0., 1., 1.],
homogeneous: vec![
vec![[0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 10.0, 1.0]],
vec![[5.0, 0.0, 0.0, 1.0], [5.0 * weight, 0.0, 10.0 * weight, weight]],
],
}
};
assert!(wall_along(&surface(false), Vec3::Z, 1e-5, &mut 1000));
assert!(!wall_along(&surface(true), Vec3::Z, 1e-5, &mut 1000), "parallel Euclidean controls alone do not certify a rational sweep");
assert!(!wall_along(&surface(false), Vec3::Y, 1e-5, &mut 1000));
assert!(!wall_along(&surface(false), Vec3::Z, 1e-5, &mut 0));
}
#[test]
fn compound_planar_extrusion_normalizes_overlap_and_retains_islands() {
let plane = Plane::named("XY").unwrap();
let a = rectangle(Vec2::ZERO, Vec2::new(10.0, 10.0));
-22
View File
@@ -39,28 +39,6 @@ pub(crate) fn generated(origins: Vec<FaceOrigin>) -> FaceProv {
FaceProv { origins, change: Change::Generated }
}
/// Result faces that are each a region of input faces, given as the input faces under it and
/// whether it is one input face untouched (`whole`). Kept when whole; Merged with several
/// origins; Split when its one origin lies under other result faces too; Modified otherwise;
/// Generated with no origin.
pub(crate) fn regions(faces: Vec<(BTreeSet<FaceOrigin>, bool)>) -> Vec<FaceProv> {
let mut faces: Vec<FaceProv> = faces.into_iter().map(|(origins, whole)| region(origins, whole)).collect();
mark_shared_split(&mut faces);
faces
}
/// A result face that is a region of the input faces `origins` (see [`regions`]), before
/// faces sharing an origin are marked Split.
pub(crate) fn region(origins: BTreeSet<FaceOrigin>, whole: bool) -> FaceProv {
let change = match origins.len() {
0 => Change::Generated,
1 if whole => Change::Kept,
1 => Change::Modified,
_ => Change::Merged,
};
FaceProv { origins: origins.into_iter().collect(), change }
}
pub(crate) use solidcraft_geom::brep::history::mark_shared_split;
/// The origins of a result edge that is (a piece of, or the moved) edges `edges` of operand
@@ -36,7 +36,6 @@ fn stamped_shell_keeps_authored_painted_and_split_boundaries() {
assert!(!mesh.contains(Vec3::new(0., 0., 12.)));
let count = result.face_count();
assert_eq!(result.clone().regularize_certified_planar_faces().unwrap().face_count(), count);
assert_eq!(result.clone().regularize_generated_planar_faces().unwrap().face_count(), count);
let placed = crate::transform(&result, Vec3::new(30., -40., 7.), Vec3::ZERO, Vec3::Z, 0.3).unwrap();
assert!(placed.regularization_protected);
for lump in placed.lumps().unwrap() {
-140
View File
@@ -5,12 +5,7 @@ use solidcraft_geom::{Aabb3, BoxedTriangle, Mesh, Vec3, find_close_pair};
use crate::{KernelError, Result};
pub(super) const MAX_SHELLS: usize = 1024;
pub(super) const MAX_POINTS: usize = 100_000;
pub(super) const MAX_TRIANGLES: usize = 100_000;
const MAX_PAIR_WORK: usize = 40_000_000;
const MAX_PROBES: usize = 2_000_000;
const MAX_CONTAINMENT_WORK: usize = 40_000_000;
fn failed(detail: &str) -> KernelError {
KernelError::Failed(format!("lumps: {detail}"))
@@ -112,138 +107,3 @@ pub(super) fn meshes_meet(a: &Mesh, b: &Mesh, gap: f64) -> Result<bool> {
Ok(b.positions.first().is_some_and(|p| a.contains(*p)) || a.positions.first().is_some_and(|p| b.contains(*p)))
}
pub(super) fn groups(meshes: &[Mesh], volumes: &[f64], gap: f64) -> Result<Vec<Vec<usize>>> {
let parent = parents(meshes, volumes, gap)?;
let mut groups = Vec::new();
for (i, v) in volumes.iter().enumerate() {
if *v <= 0.0 {
continue;
}
let mut group = vec![i];
group.extend(parent.iter().enumerate().filter_map(|(j, p)| (*p == Some(i)).then_some(j)));
groups.push(group);
}
Ok(groups)
}
/// The shell directly around each closed shell (`None` for one in the open): outward shells
/// (positive volume) bound material, inward ones voids, and nesting alternates them.
pub(super) fn parents(meshes: &[Mesh], volumes: &[f64], gap: f64) -> Result<Vec<Option<usize>>> {
if meshes.is_empty() || meshes.len() != volumes.len() || meshes.len() > MAX_SHELLS {
return Err(failed("shell count exceeds the grouping limit or is invalid"));
}
if !gap.is_finite() || gap <= 0.0 {
return Err(failed("invalid shell clearance"));
}
let mut points = 0usize;
let mut triangle_count = 0usize;
let mut triangles = Vec::with_capacity(meshes.len());
let mut bounds = Vec::with_capacity(meshes.len());
for (mesh, volume) in meshes.iter().zip(volumes) {
points = points.checked_add(mesh.positions.len()).ok_or_else(|| failed("point count exceeds the grouping limit"))?;
triangle_count = triangle_count.checked_add(mesh.triangles.len()).ok_or_else(|| failed("triangle count exceeds the grouping limit"))?;
if points > MAX_POINTS || triangle_count > MAX_TRIANGLES {
return Err(failed("mesh exceeds the shell grouping limit"));
}
if !volume.is_finite()
|| *volume == 0.0
|| mesh.positions.is_empty()
|| mesh.triangles.is_empty()
|| mesh.positions.iter().any(|p| !p.is_finite())
{
return Err(failed("cannot determine a closed shell's orientation"));
}
let tris: Vec<_> = mesh.triangles.iter().map(|t| mesh.tri(t).ok_or_else(|| failed("invalid shell triangle"))).collect::<Result<_>>()?;
if tris.iter().any(|[a, b, c]| unit((*b - *a).cross(*c - *a)).is_none()) {
return Err(failed("degenerate shell triangle"));
}
triangles.push(tris.into_iter().map(|t| (t, box_of(&t))).collect::<Vec<BoxedTriangle>>());
bounds.push(mesh.bounds());
}
let mut inside = vec![vec![false; meshes.len()]; meshes.len()];
let mut pair_budget = MAX_PAIR_WORK;
let mut probes = 0usize;
let mut containment_work = 0usize;
for (i, mesh) in meshes.iter().enumerate() {
for j in i + 1..meshes.len() {
let (Some(a_bounds), Some(b_bounds)) = (bounds.get(i), bounds.get(j)) else { return Err(failed("missing shell bounds")) };
if boxes_separate(*a_bounds, *b_bounds, gap) {
continue;
}
let (Some(a), Some(b)) = (triangles.get(i), triangles.get(j)) else { return Err(failed("missing shell triangles")) };
if find_close_pair(a, b, gap, &mut pair_budget, |(x, _), (y, _)| triangles_meet(x, y, gap))
.ok_or_else(|| failed("shell boundary work exceeds the grouping limit"))?
{
return Err(failed("shell boundaries intersect or touch; cavity ownership is ambiguous"));
}
for (inner, outer) in [(i, j), (j, i)] {
let ib = bounds.get(inner).ok_or_else(|| failed("missing shell bounds"))?;
let ob = bounds.get(outer).ok_or_else(|| failed("missing shell bounds"))?;
// With boundary intersections already excluded, a connected shell
// extending beyond another's bounds cannot be wholly contained.
if ib.min.x < ob.min.x - gap
|| ib.min.y < ob.min.y - gap
|| ib.min.z < ob.min.z - gap
|| ib.max.x > ob.max.x + gap
|| ib.max.y > ob.max.y + gap
|| ib.max.z > ob.max.z + gap
{
continue;
}
let inner_mesh = if inner == i { mesh } else { meshes.get(inner).ok_or_else(|| failed("missing shell"))? };
probes = probes.checked_add(inner_mesh.positions.len()).ok_or_else(|| failed("shell probes exceed the grouping limit"))?;
if probes > MAX_PROBES {
return Err(failed("shell probes exceed the grouping limit"));
}
let outer_mesh = meshes.get(outer).ok_or_else(|| failed("missing shell"))?;
let probe_count = inner_mesh.positions.len();
let work = probe_count
.checked_mul(outer_mesh.triangles.len())
.and_then(|n| n.checked_mul(3))
.ok_or_else(|| failed("shell containment work exceeds the grouping limit"))?;
containment_work = containment_work.checked_add(work).ok_or_else(|| failed("shell containment work exceeds the grouping limit"))?;
if containment_work > MAX_CONTAINMENT_WORK {
return Err(failed("shell containment work exceeds the grouping limit"));
}
let mut contained = None;
for p in inner_mesh.positions.iter().copied() {
let hit = outer_mesh.contains(p);
if contained.is_some_and(|previous| previous != hit) {
return Err(failed("shell containment is inconsistent; cavity ownership is ambiguous"));
}
contained = Some(hit);
}
if let Some(slot) = inside.get_mut(inner).and_then(|row| row.get_mut(outer)) {
*slot = contained == Some(true);
}
}
}
}
let mut parent = Vec::with_capacity(meshes.len());
for i in 0..meshes.len() {
let row = inside.get(i).ok_or_else(|| failed("missing containment"))?;
let candidates: Vec<_> = row.iter().enumerate().filter_map(|(j, hit)| hit.then_some(j)).collect();
for (k, a) in candidates.iter().enumerate() {
for b in candidates.iter().skip(k + 1) {
if !inside.get(*a).and_then(|r| r.get(*b)).copied().unwrap_or(false)
&& !inside.get(*b).and_then(|r| r.get(*a)).copied().unwrap_or(false)
{
return Err(failed("multiple overlapping shell owners"));
}
}
}
let owner = candidates.into_iter().min_by(|a, b| {
volumes.get(*a).copied().unwrap_or(f64::INFINITY).abs().total_cmp(&volumes.get(*b).copied().unwrap_or(f64::INFINITY).abs())
});
let outward = volumes.get(i).is_some_and(|v| *v > 0.0);
match owner {
None if !outward => return Err(failed("inward shell has no enclosing material shell")),
Some(p) if volumes.get(p).is_some_and(|v| (*v > 0.0) == outward) => {
return Err(failed("nested shells do not alternate material and void"));
}
_ => {}
}
parent.push(owner);
}
Ok(parent)
}
-28
View File
@@ -1,28 +0,0 @@
//! geom's B-spline curves and surfaces as schema 4 records, weights kept (homogeneous control
//! points carried over as they are). A whole tensor surface as a one-face sheet is
//! `surfaces::tensor_sheet`.
use solidcraft_geom::brep::{Curve, Surface};
use solidcraft_geom::bspline::{HPoint, NurbsSurface};
/// A homogeneous control point as the record's `[x*w, y*w, z*w, w]`.
pub(crate) fn h4(h: HPoint) -> [f64; 4] {
[h.x, h.y, h.z, h.w]
}
/// A NURBS curve of knots and homogeneous control points.
pub(crate) fn nurbs_curve(knots: &[f64], ctrl: &[HPoint]) -> Curve {
Curve::RationalSpline { knots: knots.to_vec(), homogeneous: ctrl.iter().copied().map(h4).collect() }
}
/// A NURBS surface of knots and homogeneous control points (`rows[i][j]`: i along u).
pub(crate) fn nurbs_surface(u_knots: &[f64], v_knots: &[f64], rows: Vec<Vec<[f64; 4]>>) -> Surface {
Surface::RationalSpline { u_knots: u_knots.to_vec(), v_knots: v_knots.to_vec(), homogeneous: rows }
}
/// geom's tensor surface as a record surface.
pub(crate) fn surface(s: &NurbsSurface) -> Surface {
let [nu, nv] = s.count();
let [ku, kv] = s.knots();
nurbs_surface(ku, kv, (0..nu).map(|i| (0..nv).map(|j| h4(s.point(i, j))).collect()).collect())
}
+1 -69
View File
@@ -22,7 +22,7 @@ use std::f64::consts::TAU;
use solidcraft_geom::brep::eval::{CurveEval, SurfaceEval};
use solidcraft_geom::brep::{self, Frame, Surface};
use solidcraft_geom::tess::uv::Starts;
use solidcraft_geom::{Aabb3, Plane, Seg2, UnitVec3, Vec2, Vec3};
use solidcraft_geom::{Aabb3, Plane, Seg2, Vec2, Vec3};
use crate::body::Body;
use crate::history::{EdgeOrigin, FaceOrigin, History};
@@ -358,11 +358,6 @@ impl ToolSide {
}
}
/// Which side of a tool a point is on.
pub(crate) fn side_of_point(tool: &SplitTool, near: &Body, p: Vec3) -> Result<f64> {
Ok(ToolSide::of(tool, near)?.at(p))
}
/// How many fields a tool is (a sketch tool: one per curve).
pub(crate) fn field_count(tool: &SplitTool) -> Result<usize> {
Ok(match tool {
@@ -371,64 +366,6 @@ pub(crate) fn field_count(tool: &SplitTool) -> Result<usize> {
})
}
/// A plane or circular cylinder behind a face's surface, facing as the face does.
pub(crate) enum Analytic {
Plane { n: Vec3 },
Cylinder { o: Vec3, axis: UnitVec3, r: f64, out: f64 },
}
/// The plane or circular cylinder face `face` of `body` lies on (a B-spline whose control net is
/// flat is a plane), with the face's orientation; none for other surfaces.
pub(crate) fn analytic_face(body: &Body, face: usize) -> Result<Option<Analytic>> {
let p = face_patch(body, face)?;
let sign = if p.flip { -1.0 } else { 1.0 };
let flat = |pts: &[Vec3]| -> Option<Vec3> {
let o = *pts.first()?;
let size = pts.iter().map(|q| q.dist(o)).fold(0.0, f64::max);
let n = pts.iter().flat_map(|a| pts.iter().map(move |b| (*a - o).cross(*b - o))).max_by(|a, b| a.len().total_cmp(&b.len()))?.normalized()?;
pts.iter().all(|q| (*q - o).dot(n).abs() <= size * 1e-9).then_some(n)
};
Ok(match &p.surface {
Surface::Plane { u_axis, v_axis, .. } => v3(*u_axis).cross(v3(*v_axis)).normalized().map(|n| Analytic::Plane { n: n * sign }),
Surface::PlaneThrough { origin, u_point, v_point } => {
(v3(*u_point) - v3(*origin)).cross(v3(*v_point) - v3(*origin)).normalized().map(|n| Analytic::Plane { n: n * sign })
}
Surface::Cylinder { frame, radius } => {
v3(frame.x_axis).cross(v3(frame.y_axis)).unit().map(|axis| Analytic::Cylinder { o: v3(frame.origin), axis, r: *radius, out: sign })
}
Surface::Spline { points, .. } => {
let pts: Vec<Vec3> = points.iter().flatten().map(|q| v3(*q)).collect();
// The net's normal, turned to the surface's own.
let m = Measured::new(&p)?;
flat(&pts).and_then(|n| {
let (_, su, sv) = m.eval.derivs(p.domain[0][0], p.domain[1][0])?;
let own = su.cross(sv);
Some(Analytic::Plane { n: if own.dot(n) < 0.0 { -n } else { n } * sign })
})
}
_ => None,
})
}
/// Which side of a tool each face of `body` lies on (in face order): the field at the centre of
/// the face's triangle where it is largest, so well inside the face.
pub(crate) fn face_sides(body: &Body, tool: &SplitTool) -> Result<Vec<f64>> {
let side = ToolSide::of(tool, body)?;
let mesh = body.mesh(&crate::MeshTolerance::chord((body.size() * 2e-3).max(1e-4)))?;
let mut best = vec![0.0f64; body.face_count()];
for (t, f) in mesh.triangles.iter().zip(&mesh.tri_face) {
let Some([a, b, c]) = mesh.tri(t) else { continue };
let x = side.at((a + b + c) * (1.0 / 3.0));
if let Some(slot) = best.get_mut(*f as usize)
&& x.is_finite()
&& x.abs() > slot.abs()
{
*slot = x;
}
}
Ok(best)
}
/// The regions of a body cut by a tool: for each face (in face order), the region it lies in,
/// and how many there are. Faces are in one region when they meet at an edge the tool does not
/// run along; an edge along the tool (a split line) parts them.
@@ -489,11 +426,6 @@ fn smlib_tools(groups: &[Vec<Patch>], reach: &Aabb3) -> Result<Vec<solidcraft_sm
Ok(out)
}
/// The record's position of SMLib's face `i` (identity when `order` is empty).
fn canonical(order: &[usize], i: usize) -> Option<usize> {
if order.is_empty() { Some(i) } else { order.get(i).copied() }
}
/// A result checked by SMLib and brought back with its history over `[body]`.
fn brought_back(name: &str, body: &Body, ob: &solidcraft_smlib::history::Order, r: &solidcraft_smlib::Brep) -> Result<(Body, History)> {
let check = r.check(solidcraft_smlib::CheckLevel::All).map_err(|e| failed(name, e))?;
+1 -1
View File
@@ -3,7 +3,7 @@
//! face's parameter box.
use solidcraft_geom::tess::EvalSurface;
pub(crate) use solidcraft_geom::tess::uv::{Domain, degenerate, domain, project, unwrap};
pub(crate) use solidcraft_geom::tess::uv::{Domain, degenerate, domain, unwrap};
use super::size::Rules;