STEP import: a round face off its frame's quarter lines is formed within one turn

A cylinder, cone, sphere or torus face of less than one turn that does not
start on a quarter line of its frame, with more than three quarters left to
it, spilled into a fifth quarter of the quarter grid: its NURBS form ran past
one turn and SMLib refused the solid ("an edge lies 12-16 mm off the face's
surface"), which came in as surfaces.

Before its edges are labelled on it, such a face's frame is turned about its
own axis (brep::form::turned: the same surface, its angle parameter measured
from further on) so it starts on a quarter line and its form spans four
quarters. Round faces a circle edge about both their axes joins share one
quarter grid, so they turn together by one angle, chosen so every one of them
fits in four quarters; a group with a face that cannot be turned keeps the
file's frames. Each face's box on its label is measured once and reused.

Engine assembly: PULLEY_WATER_PUMP and MOUNT_SCAVENGE_PUMP_01 now come in as
solids (refused 5 -> 3); Poppy Humanoid unchanged (4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5Lzv2n8fyKqD61NCHSF
This commit is contained in:
jyviko
2026-10-28 16:15:55 -04:00
committed by Irakli Kourtis
parent 2dd505d836
commit ee4d4b5a55
4 changed files with 407 additions and 20 deletions
+54 -9
View File
@@ -8,7 +8,9 @@
//! frame therefore has the same parameter at the same angle, whatever quarters it spans: a circle
//! edge's form and its cylinder's form agree along it, so its pcurve is a straight line in (u, v).
//! A form spans the quarters its interval needs (at most five for one turn), so an arc across the
//! frame's x axis has a form like any other.
//! frame's x axis has a form like any other. A face of five would cover part of its surface twice:
//! its frame is turned about its axis first ([`turned`], [`turn_quarters`]), so that it starts on
//! a quarter line and its form spans four.
//!
//! A circle or ellipse is the arcs (an ellipse scaled by its radii, which keeps it exact: its
//! parameter is the eccentric angle). A cylinder or cone is the arcs times a degree-1 line along the
@@ -151,16 +153,13 @@ pub fn surface(s: &Surface, domain: [[f64; 2]; 2]) -> Option<(Surface, [[f64; 2]
surface_within(s, domain, 0.0)
}
/// [`surface`] for a face whose boundary lies within `tolerance` (mm) of its surface: where the
/// domain ends within `tolerance` of a quarter's end (the angle that far along the widest circle
/// of the face in that direction), the form ends there too. A face's box is the extent of its
/// boundary's points ([`extent`]), which may run past a quarter's end by as much as its edges lie
/// off the surface; the form then covers no quarter for that alone (a torus face of half a tube
/// would otherwise go round the whole tube).
pub fn surface_within(s: &Surface, [u, v]: [[f64; 2]; 2], tolerance: f64) -> Option<(Surface, [[f64; 2]; 2])> {
/// The angles `tolerance` (mm) makes along a face's two directions ([`surface_within`]): on the
/// widest circle of the face about the axis (over `v`, the height of a cylinder or cone), and on
/// a sphere's meridian or a torus's tube.
fn reaches(s: &Surface, v: [f64; 2], tolerance: f64) -> (f64, f64) {
// The angle `tolerance` makes on a circle of `radius`.
let reach = |radius: f64| if tolerance > 0.0 && radius > 0.0 && radius.is_finite() { tolerance / radius } else { 0.0 };
let (reach_u, reach_v) = match s {
match s {
Surface::Cylinder { radius, .. } => (reach(radius.abs()), 0.0),
Surface::Cone { radius_at_origin, half_angle, .. } => {
let slope = half_angle.tan();
@@ -169,7 +168,53 @@ pub fn surface_within(s: &Surface, [u, v]: [[f64; 2]; 2], tolerance: f64) -> Opt
Surface::Sphere { radius, .. } => (reach(radius.abs()), reach(radius.abs())),
Surface::Torus { major_radius, minor_radius, .. } => (reach(major_radius.abs() + minor_radius.abs()), reach(minor_radius.abs())),
_ => (0.0, 0.0),
}
}
/// How many quarters a cylinder, cone, sphere or torus face over `[u, v]` (its box on the label,
/// as for [`surface_within`]) spans about the axis on its frame's quarter grid: four for a whole
/// turn, up to five for less, when the face does not start on a quarter line and more than three
/// quarters are left to it, so that its form runs past one turn (a NURBS that covers part of its
/// surface twice). [`turned`] puts its start on a quarter line. `None` for another surface.
pub fn turn_quarters(s: &Surface, [u, v]: [[f64; 2]; 2], tolerance: f64) -> Option<usize> {
if !matches!(s, Surface::Cylinder { .. } | Surface::Cone { .. } | Surface::Sphere { .. } | Surface::Torus { .. }) {
return None;
}
Some(face_quarters(u, reaches(s, v, tolerance).0)?.0.1)
}
/// A cylinder, cone, sphere or torus with its frame turned by `angle` about its own axis (its x
/// and y axes turned right-handed about z): the same surface, its angle parameter `angle` less at
/// every point. `None` for another surface.
pub fn turned(s: &Surface, angle: f64) -> Option<Surface> {
let turn = |f: &Frame| -> Option<Frame> {
let (o, x, _, z) = axes(f)?;
let (sin, cos) = angle.sin_cos();
let nx = x * cos + z.cross(x) * sin;
let ny = z.cross(nx);
Some(Frame { origin: [o.x, o.y, o.z], x_axis: [nx.x, nx.y, nx.z], y_axis: [ny.x, ny.y, ny.z] })
};
Some(match s {
Surface::Cylinder { frame, radius } => Surface::Cylinder { frame: turn(frame)?, radius: *radius },
Surface::Cone { frame, radius_at_origin, half_angle } => {
Surface::Cone { frame: turn(frame)?, radius_at_origin: *radius_at_origin, half_angle: *half_angle }
}
Surface::Sphere { frame, radius } => Surface::Sphere { frame: turn(frame)?, radius: *radius },
Surface::Torus { frame, major_radius, minor_radius } => {
Surface::Torus { frame: turn(frame)?, major_radius: *major_radius, minor_radius: *minor_radius }
}
_ => return None,
})
}
/// [`surface`] for a face whose boundary lies within `tolerance` (mm) of its surface: where the
/// domain ends within `tolerance` of a quarter's end (the angle that far along the widest circle
/// of the face in that direction), the form ends there too. A face's box is the extent of its
/// boundary's points ([`extent`]), which may run past a quarter's end by as much as its edges lie
/// off the surface; the form then covers no quarter for that alone (a torus face of half a tube
/// would otherwise go round the whole tube).
pub fn surface_within(s: &Surface, [u, v]: [[f64; 2]; 2], tolerance: f64) -> Option<(Surface, [[f64; 2]; 2])> {
let (reach_u, reach_v) = reaches(s, v, tolerance);
let (grid, fu) = face_quarters(u, reach_u)?;
let (u_knots, ring) = arcs(grid.0, grid.1);
// The ring of arcs times a profile `(control, weight)` mapped to (radial, axial) about the
+146
View File
@@ -2331,3 +2331,149 @@ fn a_refused_solid_comes_in_as_a_surface_body_of_the_faces_that_read() {
assert!(!back.is_surface(), "closed");
assert!(rel(measure(&back).unwrap().volume, 250.0 * pi) < 1e-6, "{}", measure(&back).unwrap().volume);
}
/// A sector of a cylinder (radius 5, height 10) about z, `sweep` radians from `start` on the
/// wall's frame: a pie slice of the cylinder, its two sides through the axis.
fn cylinder_sector(start: f64, sweep: f64) -> String {
let (r, h) = (5.0, 10.0);
let p = |x: f64, y: f64, z: f64| format!("({x:.17},{y:.17},{z:.17})");
let (a0, a1) = (start, start + sweep);
let (c0, s0, c1, s1) = (a0.cos(), a0.sin(), a1.cos(), a1.sin());
let line = |id: u32, from: (f64, f64, f64), to: (f64, f64, f64)| {
let d = Vec3::new(to.0 - from.0, to.1 - from.1, to.2 - from.2);
let u = d.normalized().unwrap();
format!(
"#{id}=LINE('',#{},#{});\n#{}=CARTESIAN_POINT('',{});\n#{}=VECTOR('',#{},{:.17});\n#{}=DIRECTION('',{});\n",
id + 1,
id + 2,
id + 1,
p(from.0, from.1, from.2),
id + 2,
id + 3,
d.len(),
id + 3,
p(u.x, u.y, u.z)
)
};
let (a_0, a_1, p0, p1, q0, q1) =
((0.0, 0.0, 0.0), (0.0, 0.0, h), (r * c0, r * s0, 0.0), (r * c0, r * s0, h), (r * c1, r * s1, 0.0), (r * c1, r * s1, h));
let mut lines = String::new();
for (id, from, to) in [(100, a_0, a_1), (110, a_0, p0), (120, a_0, q0), (130, a_1, p1), (140, a_1, q1), (150, p0, p1), (160, q0, q1)] {
lines.push_str(&line(id, from, to));
}
format!(
"{HEAD}
#1=CARTESIAN_POINT('',(0.,0.,0.));
#2=DIRECTION('',(0.,0.,1.));
#3=DIRECTION('',(1.,0.,0.));
#4=DIRECTION('',(0.,0.,-1.));
#5=AXIS2_PLACEMENT_3D('',#1,#2,#3);
#6=AXIS2_PLACEMENT_3D('',#1,#4,#3);
#7=CARTESIAN_POINT('',(0.,0.,{h:?}));
#8=AXIS2_PLACEMENT_3D('',#7,#2,#3);
#10=VERTEX_POINT('',#11);
#11=CARTESIAN_POINT('',{});
#12=VERTEX_POINT('',#13);
#13=CARTESIAN_POINT('',{});
#14=VERTEX_POINT('',#15);
#15=CARTESIAN_POINT('',{});
#16=VERTEX_POINT('',#17);
#17=CARTESIAN_POINT('',{});
#18=VERTEX_POINT('',#19);
#19=CARTESIAN_POINT('',{});
#20=VERTEX_POINT('',#21);
#21=CARTESIAN_POINT('',{});
{lines}#30=CIRCLE('',#5,{r:?});
#31=CIRCLE('',#8,{r:?});
#40=EDGE_CURVE('',#10,#12,#100,.T.);
#41=EDGE_CURVE('',#10,#14,#110,.T.);
#42=EDGE_CURVE('',#10,#18,#120,.T.);
#43=EDGE_CURVE('',#12,#16,#130,.T.);
#44=EDGE_CURVE('',#12,#20,#140,.T.);
#45=EDGE_CURVE('',#14,#16,#150,.T.);
#46=EDGE_CURVE('',#18,#20,#160,.T.);
#47=EDGE_CURVE('',#14,#18,#30,.T.);
#48=EDGE_CURVE('',#16,#20,#31,.T.);
#50=CYLINDRICAL_SURFACE('',#5,{r:?});
#51=EDGE_LOOP('',(#200,#201,#202,#203));
#200=ORIENTED_EDGE('',*,*,#47,.T.);
#201=ORIENTED_EDGE('',*,*,#46,.T.);
#202=ORIENTED_EDGE('',*,*,#48,.F.);
#203=ORIENTED_EDGE('',*,*,#45,.F.);
#52=FACE_OUTER_BOUND('',#51,.T.);
#53=ADVANCED_FACE('Wall',(#52),#50,.T.);
#54=PLANE('',#6);
#55=EDGE_LOOP('',(#204,#205,#206));
#204=ORIENTED_EDGE('',*,*,#42,.T.);
#205=ORIENTED_EDGE('',*,*,#47,.F.);
#206=ORIENTED_EDGE('',*,*,#41,.F.);
#56=FACE_OUTER_BOUND('',#55,.T.);
#57=ADVANCED_FACE('Bottom',(#56),#54,.T.);
#58=PLANE('',#8);
#59=EDGE_LOOP('',(#207,#208,#209));
#207=ORIENTED_EDGE('',*,*,#43,.T.);
#208=ORIENTED_EDGE('',*,*,#48,.T.);
#209=ORIENTED_EDGE('',*,*,#44,.F.);
#60=FACE_OUTER_BOUND('',#59,.T.);
#61=ADVANCED_FACE('Top',(#60),#58,.T.);
#62=DIRECTION('',{});
#63=AXIS2_PLACEMENT_3D('',#1,#62,#2);
#64=PLANE('',#63);
#65=EDGE_LOOP('',(#210,#211,#212,#213));
#210=ORIENTED_EDGE('',*,*,#41,.T.);
#211=ORIENTED_EDGE('',*,*,#45,.T.);
#212=ORIENTED_EDGE('',*,*,#43,.F.);
#213=ORIENTED_EDGE('',*,*,#40,.F.);
#66=FACE_OUTER_BOUND('',#65,.T.);
#67=ADVANCED_FACE('Start',(#66),#64,.T.);
#68=DIRECTION('',{});
#69=AXIS2_PLACEMENT_3D('',#1,#68,#2);
#70=PLANE('',#69);
#71=EDGE_LOOP('',(#214,#215,#216,#217));
#214=ORIENTED_EDGE('',*,*,#40,.T.);
#215=ORIENTED_EDGE('',*,*,#44,.T.);
#216=ORIENTED_EDGE('',*,*,#46,.F.);
#217=ORIENTED_EDGE('',*,*,#42,.F.);
#72=FACE_OUTER_BOUND('',#71,.T.);
#73=ADVANCED_FACE('End',(#72),#70,.T.);
#80=CLOSED_SHELL('',(#53,#57,#61,#67,#73));
#81=MANIFOLD_SOLID_BREP('Sector',#80);
#82=SHAPE_REPRESENTATION('',(#81),$);
{TAIL}",
p(a_0.0, a_0.1, a_0.2),
p(a_1.0, a_1.1, a_1.2),
p(p0.0, p0.1, p0.2),
p(p1.0, p1.1, p1.2),
p(q0.0, q0.1, q0.2),
p(q1.0, q1.1, q1.2),
p(s0, -c0, 0.0),
p(-s1, c1, 0.0),
)
}
/// A round face of less than one turn that does not start on a quarter line of its frame (a
/// 300° sector of a cylinder from 75°, or 350° from 200°) needed a fifth quarter on the frame's quarter grid, so its
/// form ran past one turn and SMLib refused the solid (it came in as surfaces, an edge "12 mm off
/// the face's surface"). Its frame is turned about its axis to start on a quarter line: the same
/// cylinder, a form of four quarters, and the solid with its exact volume and area. One that fits
/// in four quarters as it is (300° from 37°) keeps its frame.
#[test]
fn a_round_face_off_its_frames_quarter_lines_is_formed_within_one_turn() {
for (start, sweep) in
[(75f64.to_radians(), 300f64.to_radians()), (37f64.to_radians(), 300f64.to_radians()), (200f64.to_radians(), 350f64.to_radians())]
{
let imp = step_import(&cylinder_sector(start, sweep)).unwrap();
assert_eq!(imp.bodies.len(), 1, "{:?}", imp.warnings);
let b = &imp.bodies[0];
assert!(b.closed && b.refused.is_none(), "{start} {sweep}: {:?} {:?}", b.refused, imp.warnings);
let wall = b.face_origin.iter().position(|f| *f == 53).unwrap();
let Some(solidcraft_geom::brep::Surface::RationalSpline { u_knots, .. }) = &b.body.record().faces[wall].nurbs else {
panic!("the wall's form")
};
assert!(u_knots[u_knots.len() - 1] - u_knots[0] <= 4.0, "{start}: the wall's form spans {u_knots:?}");
let m = measure(&b.body).unwrap();
let (volume, area) = (0.5 * sweep * 25.0 * 10.0, sweep * 25.0 + sweep * 5.0 * 10.0 + 2.0 * 5.0 * 10.0);
assert!(rel(m.volume, volume) < 1e-6, "{start}: volume {} vs {volume}", m.volume);
assert!(rel(m.area, area) < 1e-6, "{start}: area {} vs {area}", m.area);
}
}
+29 -11
View File
@@ -50,6 +50,7 @@ use crate::p21::{Entity, Exchange, Param};
use crate::read::{R, Reader};
use crate::units;
mod frames;
mod seams;
mod swept;
@@ -510,6 +511,8 @@ struct FaceB {
outer: Option<usize>,
/// The face's normal is the surface's natural normal.
forward: bool,
/// A round face's box on its label (`brep::form::extent`), once measured.
extent: Option<[[f64; 2]; 2]>,
}
struct Builder<'b, 'a> {
@@ -869,7 +872,7 @@ impl<'b, 'a> Builder<'b, 'a> {
}
}
let i = index(self.faces.len())?;
self.faces.push(FaceB { step: id, surface, loops, outer, forward: sense });
self.faces.push(FaceB { step: id, surface, loops, outer, forward: sense, extent: None });
Ok((i, sense))
}
@@ -948,6 +951,9 @@ impl<'b, 'a> Builder<'b, 'a> {
if self.sense_mismatches > 0 {
notes.push(format!("{} edges run against their same_sense (read by their vertices)", self.sense_mismatches));
}
// Round faces whose forms would run past one turn start on a quarter line of their
// frames, before their edges are labelled on them.
self.turn_frames()?;
// Each edge's label and NURBS form: a circle about a revolved face's axis labelled on that
// face's frame, so its form lies on the face's quarter grid.
let mut frames: Vec<Vec<brep::Frame>> = vec![Vec::new(); self.edges.len()];
@@ -1234,6 +1240,23 @@ impl<'b, 'a> Builder<'b, 'a> {
std::iter::once(outer).chain((0..face.loops.len()).filter(|l| *l != outer)).collect()
}
/// A face's box on its label over its edge loops (`brep::form::extent`); `None` where the
/// boundary does not project onto it.
fn face_extent(&self, fi: usize) -> R<Option<[[f64; 2]; 2]>> {
let face = self.faces.get(fi).ok_or("face reference")?;
let mut boundary = Vec::with_capacity(face.loops.len());
for l in &face.loops {
let LoopB::Edges(es) = l else { continue };
let mut b = Vec::with_capacity(es.len());
for &(e, forward) in es {
let edge = self.edges.get(e as usize).ok_or("edge reference")?;
b.push((&edge.curve, edge.interval, forward));
}
boundary.push(b);
}
Ok(brep::form::extent(&face.surface, &boundary))
}
/// The pcurves of a face's edge loops (in its edge-loop order), its domain, whether it was
/// turned over, and its NURBS form. Picks the outer loop when the file
/// marks none (or marks one that another loop encloses), and turns the face over when its
@@ -1258,16 +1281,11 @@ impl<'b, 'a> Builder<'b, 'a> {
// boundary on its label; then every pcurve on the forms.
let form = match brep::form::frame_of(&label) {
Some(_) => {
let mut boundary = Vec::with_capacity(edge_loops.len());
for l in &edge_loops {
let mut b = Vec::with_capacity(l.len());
for &(e, forward) in l {
let edge = self.edges.get(e as usize).ok_or("edge reference")?;
b.push((&edge.curve, edge.interval, forward));
}
boundary.push(b);
}
let extent = brep::form::extent(&label, &boundary).ok_or_else(|| unsupported("a face whose boundary does not lie on its surface"))?;
let extent = match face.extent {
Some(e) => Some(e),
None => self.face_extent(fi)?,
};
let extent = extent.ok_or_else(|| unsupported("a face whose boundary does not lie on its surface"))?;
Some(brep::form::surface_within(&label, extent, tol).ok_or_else(|| unsupported("a face of more than one turn"))?)
}
None => None,
+178
View File
@@ -0,0 +1,178 @@
//! Frames turned so that a round face's form covers its face once. A cylinder, cone, sphere or
//! torus face's NURBS form spans the quarters of its frame's quarter grid that its angle interval
//! needs (`brep::form`); a face of less than one turn that does not start on a quarter line and
//! has more than three quarters left spills into a fifth, so its form runs past one turn and
//! covers part of its surface twice, which no kernel takes as a face. The file's reference
//! direction is the writer's: the face's frame is turned about its own axis (the same surface,
//! its angle parameter measured from further on) so that the face starts on a quarter line, and
//! its form spans the four quarters (or fewer) it covers.
//!
//! A circle edge about the axis of the round faces it bounds is labelled on one of their frames
//! (`brep::form::edge`), so its form and theirs share one quarter grid and its pcurves are
//! straight lines; the faces joined that way (a cylinder and the coaxial fillet beside it) are
//! turned together, by one angle about their common axis, and keep that. They are turned only
//! when one of them spills and one angle puts every one of them within four quarters (a start
//! on a quarter line, or where two of them spill, maybe an end); a face
//! whose frame cannot be turned (a placed revolution, its angle zero at its profile) keeps its
//! group as the file has it.
use solidcraft_geom::Vec3;
use solidcraft_geom::brep::{self, Curve as BCurve};
use super::{Builder, LoopB, R};
/// A circle edge bounds a face about the circle's own axis: the faces it joins share a grid.
fn about_axis(edge: &BCurve, interval: [f64; 2], frame: &brep::Frame, tol: f64) -> bool {
matches!(edge, BCurve::Circle { .. }) && brep::form::on_frame(edge, interval, frame, tol).is_some()
}
/// The face's quarters about the axis with its frame turned by `angle` (its box shifted to
/// match), or `None` when its form cannot be measured.
fn quarters_turned(surface: &brep::Surface, [u, v]: [[f64; 2]; 2], angle: f64, tol: f64) -> Option<usize> {
brep::form::turn_quarters(surface, [[u[0] - angle, u[1] - angle], v], tol)
}
/// Union-find root of `i`.
fn root(parent: &mut [usize], mut i: usize) -> usize {
while let Some(&p) = parent.get(i) {
if p == i {
break;
}
let up = parent.get(p).copied().unwrap_or(p);
if let Some(slot) = parent.get_mut(i) {
*slot = up;
}
i = p;
}
i
}
impl Builder<'_, '_> {
/// Measure every round face's box on its label, and turn the frames of the faces whose
/// forms would run past one turn, with the faces their circle edges join them to, so that
/// each starts on a quarter line. Returns how many faces were turned.
pub(super) fn turn_frames(&mut self) -> R<usize> {
let n = self.faces.len();
let frames: Vec<Option<brep::Frame>> = self.faces.iter().map(|f| brep::form::frame_of(&f.surface)).collect();
for fi in 0..n {
if frames.get(fi).is_some_and(Option::is_some) {
let extent = self.face_extent(fi)?;
if let Some(f) = self.faces.get_mut(fi) {
f.extent = extent;
}
}
}
// Each face's quarters about its axis as the file frames it: a spill is five.
let spills: Vec<bool> =
self.faces.iter().map(|f| f.extent.and_then(|e| quarters_turned(&f.surface, e, 0.0, self.tol)).is_some_and(|q| q > 4)).collect();
if !spills.iter().any(|s| *s) {
return Ok(0);
}
// Round faces a circle edge about both their axes joins.
let mut parent: Vec<usize> = (0..n).collect();
let mut bounding: Vec<Vec<usize>> = vec![Vec::new(); self.edges.len()];
for (fi, f) in self.faces.iter().enumerate() {
if frames.get(fi).is_none_or(Option::is_none) {
continue;
}
for l in &f.loops {
if let LoopB::Edges(es) = l {
for (e, _) in es {
if let Some(b) = bounding.get_mut(*e as usize)
&& !b.contains(&fi)
{
b.push(fi);
}
}
}
}
}
for (edge, faces) in self.edges.iter().zip(&bounding) {
let joined: Vec<usize> = faces
.iter()
.copied()
.filter(|fi| frames.get(*fi).and_then(Option::as_ref).is_some_and(|fr| about_axis(&edge.curve, edge.interval, fr, self.tol)))
.collect();
for w in joined.windows(2) {
let (a, b) = (root(&mut parent, w[0]), root(&mut parent, w[1]));
if a != b
&& let Some(slot) = parent.get_mut(a)
{
*slot = b;
}
}
}
let mut groups: Vec<Vec<usize>> = Vec::new();
let mut group_of: Vec<Option<usize>> = vec![None; n];
for fi in 0..n {
if frames.get(fi).is_none_or(Option::is_none) {
continue;
}
let r = root(&mut parent, fi);
let g = match group_of.get(r).copied().flatten() {
Some(g) => g,
None => {
groups.push(Vec::new());
if let Some(slot) = group_of.get_mut(r) {
*slot = Some(groups.len() - 1);
}
groups.len() - 1
}
};
if let Some(members) = groups.get_mut(g) {
members.push(fi);
}
}
let mut turned = 0;
for g in &groups {
if !g.iter().any(|fi| spills.get(*fi).copied().unwrap_or(false)) {
continue;
}
let Some(lead) = g.first().and_then(|fi| frames.get(*fi)).and_then(Option::as_ref) else { continue };
let axis = Vec3::from(lead.x_axis).cross(Vec3::from(lead.y_axis));
// Each face's turn about its own axis for a turn `phi` about the lead's: its axis
// along the lead's or against it.
let mut members = Vec::with_capacity(g.len());
for &fi in g {
let (Some(face), Some(Some(fr))) = (self.faces.get(fi), frames.get(fi)) else { continue };
let sign = Vec3::from(fr.x_axis).cross(Vec3::from(fr.y_axis)).dot(axis).signum();
members.push((fi, sign, face.extent));
}
// A face that cannot be turned, or measured, keeps its group's frames.
if members.len() != g.len()
|| members.iter().any(|(fi, _, e)| e.is_none() || self.faces.get(*fi).and_then(|f| brep::form::turned(&f.surface, 0.0)).is_none())
{
continue;
}
let fits = |phi: f64| {
members.iter().all(|(fi, sign, e)| match (self.faces.get(*fi), e) {
(Some(f), Some(e)) => quarters_turned(&f.surface, *e, sign * phi, self.tol).is_some_and(|q| q <= 4),
_ => false,
})
};
// Each face fits for the turns that leave its start no further past a quarter line
// than the room its sweep leaves in four quarters: an arc of turns from the one that
// puts its start on a quarter line to the one that puts its end on one. Where the
// group's arcs meet, the ends of their meeting are among those turns: a spilling
// face's start on its frame's x axis first, then any face's start, then any's end.
let spilling = |fi: &usize| spills.get(*fi).copied().unwrap_or(false);
let starts =
|all: bool| members.iter().filter(move |(fi, _, _)| all || spilling(fi)).filter_map(|(_, sign, e)| e.map(|e| sign * e[0][0]));
let ends = members.iter().filter_map(|(_, sign, e)| e.map(|e| sign * e[0][1]));
let Some(phi) = starts(false).chain(starts(true)).chain(ends).find(|phi| fits(*phi)) else {
continue;
};
for &(fi, sign, _) in &members {
let Some(face) = self.faces.get_mut(fi) else { continue };
let angle = sign * phi;
let Some(surface) = brep::form::turned(&face.surface, angle) else { continue };
face.surface = surface;
if let Some(e) = face.extent.as_mut() {
e[0] = [e[0][0] - angle, e[0][1] - angle];
}
turned += 1;
}
}
Ok(turned)
}
}