mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-05 19:45:54 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user