mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-06 20:02:16 +00:00
kernel: emboss on a cylinder builds on the schema 4 record; any pick on the wall is accepted
The emboss is built from the body's record: the wall's face loses the profiles' footprints, each profile adds planar walls and its cap on the wall's cylinder at the new radius, and SMLib builds the body from the finished record (assemble.rs: a record taken apart and finished again in canonical order, pcurves from the record's routine for new uses, the record's own for kept ones). surf_of and with_same_surface read the record's analytic surfaces; the topological offset and draft that only the old kernel used are deleted (SMLib's offset faces and draft in offset_backend replace them). #3782: the saved pick had to lie on the half of the wall the sketch is tangent to (its angle inside that face's span). The wall is every exterior cylindrical face of the body (a stepped or interrupted cylinder is refused before), so the pick is checked on the cylinder between the endcaps. The sleeve test's far-side pick now succeeds; a pick on the bore is still refused. (not built: base in progress) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
@@ -0,0 +1,456 @@
|
||||
//! Bodies built face by face on the schema 4 record: an operation that rebuilds part of a body
|
||||
//! from its own topology (an emboss on a cylinder, a raised band, a deleted face healed by its
|
||||
//! neighbours) starts from the body's record, takes faces away, adds faces bounded by new or
|
||||
//! existing edges, and finishes into a record in canonical order (docs/native-brep.md): pcurves
|
||||
//! from the record's one routine ([`solidcraft_geom::brep::pcurve`]) for every new use, the
|
||||
//! record's own for every use kept, edge uses, regions and shells. SMLib builds the body from
|
||||
//! that record, as from any other.
|
||||
//!
|
||||
//! Faces keep the record's convention as the operation gives them: each face's loops are seen
|
||||
//! from the side the material is not on (the outward side), the outer loop counter-clockwise
|
||||
//! and holes clockwise there; [`Assembly::finish`] stores them relative to the surface's natural
|
||||
//! normal, as the record does.
|
||||
|
||||
use solidcraft_geom::Vec3;
|
||||
use solidcraft_geom::brep::eval::{CurveEval, SurfaceEval};
|
||||
use solidcraft_geom::brep::pcurve::{EdgeUse, pcurves_within};
|
||||
use solidcraft_geom::brep::{Brep, Curve, Curve2, Edge, EdgeUseRef, Face, Loop, Natural, OrientedEdge, OrientedFace, Region, Shell, Surface, VERSION, Vertex};
|
||||
|
||||
use crate::body::Body;
|
||||
use crate::history::{EdgeOrigin, EdgeProv, FaceProv, History};
|
||||
use crate::{KernelError, Result};
|
||||
|
||||
fn invalid(m: impl std::fmt::Display) -> KernelError {
|
||||
KernelError::Failed(format!("rebuilding the B-rep: {m}"))
|
||||
}
|
||||
|
||||
/// One edge of the assembly: its curve on `interval`, from `vertices[0]` to `vertices[1]`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct AEdge {
|
||||
pub curve: Curve,
|
||||
pub interval: [f64; 2],
|
||||
pub vertices: [u32; 2],
|
||||
pub tolerance: f64,
|
||||
/// The source record's edge this edge is, kept as it was.
|
||||
pub source: Option<usize>,
|
||||
}
|
||||
|
||||
/// One use of an edge in a face's loop: the loop runs along it from its start (`forward`) or
|
||||
/// from its end, with the source record's pcurve where the use is one the record had.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct AUse {
|
||||
pub edge: u32,
|
||||
pub forward: bool,
|
||||
pub given: Option<Curve2>,
|
||||
}
|
||||
|
||||
/// One face of the assembly: its surface, whether its natural normal points out of the
|
||||
/// material, its loops (outer first) seen from outside, and the shell it belongs to (a shell of
|
||||
/// the source record, by index).
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct AFace {
|
||||
pub surface: Surface,
|
||||
pub natural_out: bool,
|
||||
pub loops: Vec<Vec<AUse>>,
|
||||
pub shell: usize,
|
||||
/// The source record's face whose surface and parameter box this face keeps.
|
||||
pub domain: Option<[[f64; 2]; 2]>,
|
||||
}
|
||||
|
||||
/// A body taken apart: vertices, edges and faces that [`Assembly::finish`] builds into a record.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Assembly {
|
||||
pub tolerance: f64,
|
||||
pub vertices: Vec<Vertex>,
|
||||
pub edges: Vec<AEdge>,
|
||||
/// `None`: a face taken away.
|
||||
pub faces: Vec<Option<AFace>>,
|
||||
/// The source's regions, by shell index (shells keep their indices; a shell left with no
|
||||
/// face is dropped).
|
||||
regions: Vec<Region>,
|
||||
shells: usize,
|
||||
}
|
||||
|
||||
/// A finished record, with where each face of the assembly went in it (`None`: taken away).
|
||||
pub(crate) struct Finished {
|
||||
pub record: Brep,
|
||||
pub faces: Vec<Option<usize>>,
|
||||
/// For each edge of the record, the assembly edge it is.
|
||||
pub edges: Vec<usize>,
|
||||
}
|
||||
|
||||
impl Assembly {
|
||||
/// The record `b` taken apart, every face and edge as it is (face i and edge i of the
|
||||
/// assembly are the record's). Only face shells: a record with wire or vertex shells is not
|
||||
/// rebuilt here.
|
||||
pub(crate) fn from_record(b: &Brep) -> Result<Assembly> {
|
||||
let mut faces: Vec<Option<AFace>> = vec![None; b.faces.len()];
|
||||
for (s, shell) in b.shells.iter().enumerate() {
|
||||
let Shell::Faces { faces: uses, wires } = shell else { return Err(invalid("a wire or vertex shell")) };
|
||||
if !wires.is_empty() {
|
||||
return Err(invalid("wire edges inside a shell"));
|
||||
}
|
||||
for of in uses {
|
||||
let f = b.faces.get(of.face as usize).ok_or_else(|| invalid("face reference"))?;
|
||||
let mut loops = Vec::with_capacity(f.loops.len());
|
||||
for l in &f.loops {
|
||||
let Loop::Edges { edges } = l else { return Err(invalid("a vertex loop")) };
|
||||
let mut lp: Vec<AUse> = edges.iter().map(|u| AUse { edge: u.edge, forward: u.forward, given: Some(u.pcurve.clone()) }).collect();
|
||||
// Stored about the natural normal; the assembly sees loops from outside.
|
||||
if !of.forward {
|
||||
reverse(&mut lp);
|
||||
}
|
||||
loops.push(lp);
|
||||
}
|
||||
let slot = faces.get_mut(of.face as usize).ok_or_else(|| invalid("face reference"))?;
|
||||
if slot.is_some() {
|
||||
return Err(invalid("a face used by two shells"));
|
||||
}
|
||||
*slot = Some(AFace { surface: f.surface.clone(), natural_out: of.forward, loops, shell: s, domain: Some(f.domain) });
|
||||
}
|
||||
}
|
||||
if faces.iter().any(Option::is_none) {
|
||||
return Err(invalid("a face no shell uses"));
|
||||
}
|
||||
let edges = b
|
||||
.edges
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, e)| AEdge { curve: e.curve.clone(), interval: e.interval, vertices: e.vertices, tolerance: e.tolerance, source: Some(i) })
|
||||
.collect();
|
||||
Ok(Assembly { tolerance: b.tolerance, vertices: b.vertices.clone(), edges, faces, regions: b.regions.clone(), shells: b.shells.len() })
|
||||
}
|
||||
|
||||
/// A new vertex at `p`.
|
||||
pub(crate) fn vertex(&mut self, p: Vec3) -> Result<u32> {
|
||||
if !p.is_finite() {
|
||||
return Err(invalid("a vertex off the finite range"));
|
||||
}
|
||||
self.vertices.push(Vertex { point: [p.x, p.y, p.z], tolerance: self.tolerance });
|
||||
index(self.vertices.len() - 1)
|
||||
}
|
||||
|
||||
/// A vertex's point.
|
||||
pub(crate) fn point(&self, v: u32) -> Option<Vec3> {
|
||||
self.vertices.get(v as usize).map(|v| Vec3::from(v.point))
|
||||
}
|
||||
|
||||
/// A new edge on `curve` over `interval`, from `a` to `b`.
|
||||
pub(crate) fn edge(&mut self, curve: Curve, interval: [f64; 2], a: u32, b: u32) -> Result<u32> {
|
||||
self.edges.push(AEdge { curve, interval, vertices: [a, b], tolerance: self.tolerance, source: None });
|
||||
index(self.edges.len() - 1)
|
||||
}
|
||||
|
||||
/// A new straight edge from vertex `a` to vertex `b` (t from 0 to 1).
|
||||
pub(crate) fn line(&mut self, a: u32, b: u32) -> Result<u32> {
|
||||
let (Some(p), Some(q)) = (self.point(a), self.point(b)) else { return Err(invalid("vertex reference")) };
|
||||
if p.dist(q) <= self.tolerance {
|
||||
return Err(invalid("a line of no length"));
|
||||
}
|
||||
self.edge(Curve::Line { start: [p.x, p.y, p.z], end: [q.x, q.y, q.z] }, [0.0, 1.0], a, b)
|
||||
}
|
||||
|
||||
/// A new face.
|
||||
pub(crate) fn face(&mut self, face: AFace) {
|
||||
self.faces.push(Some(face));
|
||||
}
|
||||
|
||||
/// Where an edge use starts and ends (in the loop's direction).
|
||||
pub(crate) fn ends(&self, u: &AUse) -> Option<(u32, u32)> {
|
||||
let e = self.edges.get(u.edge as usize)?;
|
||||
let [a, b] = e.vertices;
|
||||
Some(if u.forward { (a, b) } else { (b, a) })
|
||||
}
|
||||
|
||||
/// The point of edge `e` at its parameter `t`.
|
||||
pub(crate) fn edge_point(&self, e: u32, t: f64) -> Option<Vec3> {
|
||||
let e = self.edges.get(e as usize)?;
|
||||
CurveEval::new(&e.curve).ok()?.point(t)
|
||||
}
|
||||
|
||||
/// The record of the faces left and added, in canonical order.
|
||||
pub(crate) fn finish(&self) -> Result<Finished> {
|
||||
// Faces in canonical order: by region, shell and the shell's own order (the assembly's).
|
||||
let mut by_shell: Vec<Vec<usize>> = vec![Vec::new(); self.shells];
|
||||
for (i, f) in self.faces.iter().enumerate() {
|
||||
if let Some(f) = f {
|
||||
by_shell.get_mut(f.shell).ok_or_else(|| invalid("shell reference"))?.push(i);
|
||||
}
|
||||
}
|
||||
let mut shell_order: Vec<usize> = Vec::new();
|
||||
let mut shell_new: Vec<Option<u32>> = vec![None; self.shells];
|
||||
let mut regions: Vec<Region> = Vec::new();
|
||||
for region in &self.regions {
|
||||
let mut shells = Vec::new();
|
||||
for &s in ®ion.shells {
|
||||
let s = s as usize;
|
||||
if by_shell.get(s).is_none_or(Vec::is_empty) {
|
||||
continue;
|
||||
}
|
||||
let slot = shell_new.get_mut(s).ok_or_else(|| invalid("shell reference"))?;
|
||||
let n = match slot {
|
||||
Some(n) => *n,
|
||||
None => {
|
||||
let n = index(shell_order.len())?;
|
||||
shell_order.push(s);
|
||||
*slot = Some(n);
|
||||
n
|
||||
}
|
||||
};
|
||||
shells.push(n);
|
||||
}
|
||||
// A bounded region whose outer shell went is gone (its faces were all taken away).
|
||||
if regions.is_empty() || region.shells.first().is_some_and(|s| by_shell.get(*s as usize).is_some_and(|f| !f.is_empty())) {
|
||||
regions.push(Region { shells, void: region.void });
|
||||
}
|
||||
}
|
||||
let order: Vec<usize> = shell_order.iter().filter_map(|s| by_shell.get(*s)).flatten().copied().collect();
|
||||
let mut face_map: Vec<Option<usize>> = vec![None; self.faces.len()];
|
||||
for (k, i) in order.iter().enumerate() {
|
||||
if let Some(slot) = face_map.get_mut(*i) {
|
||||
*slot = Some(k);
|
||||
}
|
||||
}
|
||||
// Each face's pcurves and stored loops, in canonical face order.
|
||||
let mut faces: Vec<Face> = Vec::with_capacity(order.len());
|
||||
let mut forward: Vec<bool> = Vec::with_capacity(order.len());
|
||||
for &i in &order {
|
||||
let f = self.faces.get(i).and_then(Option::as_ref).ok_or_else(|| invalid("face reference"))?;
|
||||
let loops: Vec<Vec<AUse>> = f
|
||||
.loops
|
||||
.iter()
|
||||
.map(|l| {
|
||||
let mut l = l.clone();
|
||||
if !f.natural_out {
|
||||
reverse(&mut l);
|
||||
}
|
||||
l
|
||||
})
|
||||
.collect();
|
||||
let uses: Vec<Vec<EdgeUse<'_>>> = loops
|
||||
.iter()
|
||||
.map(|l| {
|
||||
l.iter()
|
||||
.map(|u| {
|
||||
let e = self.edges.get(u.edge as usize).ok_or_else(|| invalid("edge reference"))?;
|
||||
Ok(EdgeUse { edge: u.edge, curve: &e.curve, interval: e.interval, forward: u.forward, tolerance: e.tolerance, given: u.given.as_ref() })
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
let pcurves = pcurves_within(&f.surface, &uses, self.tolerance, f.domain).map_err(|e| invalid(format!("face {i}: {e}")))?;
|
||||
let domain = domain_of(&f.surface, &pcurves, f.domain).ok_or_else(|| invalid(format!("face {i}: no parameter box")))?;
|
||||
let stored = loops
|
||||
.iter()
|
||||
.zip(&pcurves)
|
||||
.map(|(l, pc)| Loop::Edges {
|
||||
edges: l.iter().zip(pc).map(|(u, pcurve)| OrientedEdge { edge: u.edge, forward: u.forward, pcurve: pcurve.clone() }).collect(),
|
||||
})
|
||||
.collect();
|
||||
faces.push(Face { surface: f.surface.clone(), domain, loops: stored });
|
||||
forward.push(f.natural_out);
|
||||
}
|
||||
// Edges in first appearance walking faces → loops → uses (backwards in a face whose
|
||||
// side in its shell is reversed), with their uses in that order.
|
||||
let mut edge_new: Vec<Option<u32>> = vec![None; self.edges.len()];
|
||||
let mut edge_order: Vec<usize> = Vec::new();
|
||||
let mut walked: Vec<Vec<EdgeUseRef>> = Vec::new();
|
||||
for (fi, (f, fw)) in faces.iter().zip(&forward).enumerate() {
|
||||
for (li, l) in f.loops.iter().enumerate() {
|
||||
let Loop::Edges { edges } = l else { continue };
|
||||
let n = edges.len();
|
||||
for k in 0..n {
|
||||
let p = if *fw { k } else { n - 1 - k };
|
||||
let u = edges.get(p).ok_or_else(|| invalid("edge use"))?;
|
||||
let slot = edge_new.get_mut(u.edge as usize).ok_or_else(|| invalid("edge reference"))?;
|
||||
let ne = match slot {
|
||||
Some(ne) => *ne,
|
||||
None => {
|
||||
let ne = index(edge_order.len())?;
|
||||
edge_order.push(u.edge as usize);
|
||||
walked.push(Vec::new());
|
||||
*slot = Some(ne);
|
||||
ne
|
||||
}
|
||||
};
|
||||
let at = EdgeUseRef { face: index(fi)?, r#loop: index(li)?, position: index(p)?, upper: !u.forward };
|
||||
walked.get_mut(ne as usize).ok_or_else(|| invalid("edge use"))?.push(at);
|
||||
}
|
||||
}
|
||||
}
|
||||
for f in &mut faces {
|
||||
for l in &mut f.loops {
|
||||
if let Loop::Edges { edges } = l {
|
||||
for u in edges {
|
||||
u.edge = edge_new.get(u.edge as usize).copied().flatten().ok_or_else(|| invalid("edge reference"))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Vertices in first appearance walking edges (start, end).
|
||||
let mut vertex_new: Vec<Option<u32>> = vec![None; self.vertices.len()];
|
||||
let mut vertices: Vec<Vertex> = Vec::new();
|
||||
let mut edges: Vec<Edge> = Vec::with_capacity(edge_order.len());
|
||||
for (e, uses) in edge_order.iter().zip(walked) {
|
||||
let a = self.edges.get(*e).ok_or_else(|| invalid("edge reference"))?;
|
||||
let mut ends = [0u32; 2];
|
||||
for (slot, v) in ends.iter_mut().zip(a.vertices) {
|
||||
let nv = vertex_new.get_mut(v as usize).ok_or_else(|| invalid("vertex reference"))?;
|
||||
*slot = match nv {
|
||||
Some(n) => *n,
|
||||
None => {
|
||||
let n = index(vertices.len())?;
|
||||
vertices.push(self.vertices.get(v as usize).cloned().ok_or_else(|| invalid("vertex reference"))?);
|
||||
*nv = Some(n);
|
||||
n
|
||||
}
|
||||
};
|
||||
}
|
||||
edges.push(Edge { curve: a.curve.clone(), interval: a.interval, vertices: ends, tolerance: a.tolerance, uses });
|
||||
}
|
||||
let shells = shell_order
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let faces = by_shell
|
||||
.get(*s)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|i| {
|
||||
let k = face_map.get(*i).copied().flatten().ok_or_else(|| invalid("face reference"))?;
|
||||
Ok(OrientedFace { face: index(k)?, forward: *forward.get(k).ok_or_else(|| invalid("face reference"))? })
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
Ok(Shell::Faces { faces, wires: Vec::new() })
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
let record = Brep { version: VERSION, tolerance: self.tolerance, regions, shells, faces, edges, vertices };
|
||||
record.validate().map_err(invalid)?;
|
||||
record.check_geometry().map_err(invalid)?;
|
||||
Ok(Finished { record, faces: face_map, edges: edge_order })
|
||||
}
|
||||
}
|
||||
|
||||
/// Reverse a loop: the same edges, the other way round.
|
||||
fn reverse(l: &mut [AUse]) {
|
||||
l.reverse();
|
||||
for u in l {
|
||||
u.forward = !u.forward;
|
||||
}
|
||||
}
|
||||
|
||||
fn index(i: usize) -> Result<u32> {
|
||||
u32::try_from(i).map_err(|_| invalid("too many items"))
|
||||
}
|
||||
|
||||
/// A face's parameter box: the source face's where it kept it, else the box of its pcurves'
|
||||
/// control points (which holds the curves), inside the surface's natural domain.
|
||||
fn domain_of(surface: &Surface, pcurves: &[Vec<Curve2>], kept: Option<[[f64; 2]; 2]>) -> Option<[[f64; 2]; 2]> {
|
||||
let mut lo = [f64::INFINITY; 2];
|
||||
let mut hi = [f64::NEG_INFINITY; 2];
|
||||
for c in pcurves.iter().flatten() {
|
||||
let points: Vec<[f64; 2]> = match c {
|
||||
Curve2::Spline { points, .. } => points.clone(),
|
||||
Curve2::RationalSpline { homogeneous, .. } => homogeneous.iter().map(|h| [h[0] / h[2], h[1] / h[2]]).collect(),
|
||||
};
|
||||
for p in points {
|
||||
for d in 0..2 {
|
||||
if let (Some(l), Some(h), Some(x)) = (lo.get_mut(d), hi.get_mut(d), p.get(d)) {
|
||||
*l = l.min(*x);
|
||||
*h = h.max(*x);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(k) = kept {
|
||||
// The source's box, grown to hold any loop the face has now.
|
||||
for d in 0..2 {
|
||||
if let (Some(l), Some(h), Some(r)) = (lo.get_mut(d), hi.get_mut(d), k.get(d)) {
|
||||
*l = l.min(r[0]);
|
||||
*h = h.max(r[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
let natural = surface.natural();
|
||||
let mut out = [[0.0; 2]; 2];
|
||||
for d in 0..2 {
|
||||
let (mut a, mut b) = (*lo.get(d)?, *hi.get(d)?);
|
||||
if !(a.is_finite() && b.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
match natural.get(d)? {
|
||||
Natural::Periodic => b = b.min(a + std::f64::consts::TAU),
|
||||
Natural::Range(x, y) => {
|
||||
a = a.max(*x);
|
||||
b = b.min(*y);
|
||||
}
|
||||
}
|
||||
if !(a < b) {
|
||||
return None;
|
||||
}
|
||||
*out.get_mut(d)? = [a, b];
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// The body SMLib builds from `record`, and whether its faces and edges are numbered as the
|
||||
/// record's (then face i of the body is face i of the record, and so for edges).
|
||||
pub(crate) fn body_of(record: &Brep) -> Result<(Body, bool)> {
|
||||
let (brep, same) = crate::native::smlib_record_ordered(record)?;
|
||||
let (body, kept) = crate::native::body_from_smlib_ordered(&brep)?;
|
||||
Ok((body, same.is_some() && kept.is_some()))
|
||||
}
|
||||
|
||||
/// The body of `finished`, built from the one operand `from`, with the history whose faces are
|
||||
/// `faces` (in the assembly's face order, taken-away faces skipped): each kept edge the operand
|
||||
/// edge it is, each new one where its two faces meet. No history where SMLib renumbered the
|
||||
/// record.
|
||||
pub(crate) fn body_with_history(finished: &Finished, assembly: &Assembly, from: &Body, faces: Vec<FaceProv>) -> Result<Body> {
|
||||
let (body, same) = body_of(&finished.record)?;
|
||||
let counts = crate::record::counts(&[from]);
|
||||
if !same {
|
||||
return Ok(body.with_checked_history(None, &counts));
|
||||
}
|
||||
// The provenance of each record face, from the assembly's face order.
|
||||
let mut provs: Vec<Option<FaceProv>> = vec![None; finished.record.faces.len()];
|
||||
let mut given = faces.into_iter();
|
||||
for k in finished.faces.iter().flatten() {
|
||||
let p = given.next().ok_or_else(|| invalid("history: fewer faces than the body"))?;
|
||||
*provs.get_mut(*k).ok_or_else(|| invalid("history: face"))? = Some(p);
|
||||
}
|
||||
let faces: Vec<FaceProv> = provs.into_iter().collect::<Option<_>>().ok_or_else(|| invalid("history: a face without provenance"))?;
|
||||
let edges = finished
|
||||
.record
|
||||
.edges
|
||||
.iter()
|
||||
.zip(&finished.edges)
|
||||
.map(|(e, a)| {
|
||||
let mut between: Vec<usize> = e.uses.iter().map(|u| u.face as usize).collect();
|
||||
between.sort_unstable();
|
||||
between.dedup();
|
||||
let origins = match assembly.edges.get(*a).and_then(|x| x.source) {
|
||||
Some(edge) => vec![EdgeOrigin::Edge { operand: 0, edge }],
|
||||
None => crate::record::meeting(&between, &faces),
|
||||
};
|
||||
EdgeProv { faces: between, origins }
|
||||
})
|
||||
.collect();
|
||||
Ok(body.with_checked_history(Some(History { operands: 1, faces, edges }), &counts))
|
||||
}
|
||||
|
||||
/// Whether the face `face` of `b` has its natural normal out of the material.
|
||||
pub(crate) fn natural_out(b: &Brep, face: usize) -> Option<bool> {
|
||||
b.shells.iter().find_map(|s| match s {
|
||||
Shell::Faces { faces, .. } => faces.iter().find(|f| f.face as usize == face).map(|f| f.forward),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The outward unit normal of face `face` of `b` at the parameters of `p`, a point on it.
|
||||
pub(crate) fn outward_normal(b: &Brep, face: usize, p: Vec3) -> Option<Vec3> {
|
||||
let f = b.faces.get(face)?;
|
||||
let s = SurfaceEval::new(&f.surface).ok()?;
|
||||
let uv = s.project(p, None, f64::INFINITY)?;
|
||||
let (_, su, sv) = s.derivs(uv[0], uv[1])?;
|
||||
let n = su.cross(sv).normalized()?;
|
||||
Some(if natural_out(b, face)? { n } else { -n })
|
||||
}
|
||||
+234
-149
@@ -1,12 +1,16 @@
|
||||
//! Exact radial embossing of orthogonal profiles on complete cylindrical walls.
|
||||
use crate::body::{Body, Solid, from_p3, p3};
|
||||
//! Exact radial embossing of orthogonal profiles on complete cylindrical walls, built on the
|
||||
//! body's schema 4 record: the wall's face loses the profiles' footprints, and each profile
|
||||
//! adds its walls (planes, along the axis or square to it) and its raised or sunk cap (the
|
||||
//! wall's cylinder at the new radius).
|
||||
use crate::assemble::{AFace, AUse, Assembly};
|
||||
use crate::body::Body;
|
||||
use crate::history::Change;
|
||||
use crate::numbered::NumberedRegion;
|
||||
use crate::offset::{Surf, surf_of};
|
||||
use crate::record::Operand;
|
||||
use crate::offset::{Surf, face_edges, surf_of};
|
||||
use crate::{KernelError, Result, guard};
|
||||
use solidcraft_geom::brep::eval::CurveEval;
|
||||
use solidcraft_geom::brep::{Brep, Curve, Frame, Loop, Surface};
|
||||
use solidcraft_geom::{Loop2, Plane, Region2, Seg2, Vec2, Vec3};
|
||||
use truck_modeling::{self as mt, Transformed, builder};
|
||||
|
||||
fn fail(s: &str) -> KernelError {
|
||||
KernelError::Invalid(format!("cylinder emboss: {s}"))
|
||||
@@ -19,31 +23,51 @@ fn angle(p: Vec3, o: Vec3, axis: Vec3, radial: Vec3, tangent: Vec3) -> f64 {
|
||||
fn unwrap(a: f64, around: f64) -> f64 {
|
||||
around + (a - around + std::f64::consts::PI).rem_euclid(std::f64::consts::TAU) - std::f64::consts::PI
|
||||
}
|
||||
fn arr(v: Vec3) -> [f64; 3] {
|
||||
[v.x, v.y, v.z]
|
||||
}
|
||||
|
||||
fn wall_angles(face: &mt::Face, o: Vec3, axis: Vec3, radial: Vec3, tangent: Vec3, tol: f64) -> Option<(f64, f64)> {
|
||||
if face.boundaries().len() != 1 || face.edge_iter().count() != 4 {
|
||||
/// The angles a wall face spans about the axis, seen from `radial`: from one of its arcs about
|
||||
/// the axis (a face of one loop of four edges).
|
||||
fn wall_angles(b: &Brep, face: usize, o: Vec3, axis: Vec3, radial: Vec3, tangent: Vec3) -> Option<(f64, f64)> {
|
||||
let f = b.faces.get(face)?;
|
||||
let [Loop::Edges { edges }] = f.loops.as_slice() else { return None };
|
||||
if edges.len() != 4 {
|
||||
return None;
|
||||
}
|
||||
for edge in face.edge_iter() {
|
||||
if let Some(Some(mid)) = crate::offset::arc_mid(&edge, tol * 10.) {
|
||||
let am = angle(mid, o, axis, radial, tangent);
|
||||
let aa = unwrap(angle(from_p3(edge.front().point()), o, axis, radial, tangent), am);
|
||||
let ab = unwrap(angle(from_p3(edge.back().point()), o, axis, radial, tangent), am);
|
||||
return Some((aa.min(ab), aa.max(ab)));
|
||||
for u in edges {
|
||||
let e = b.edges.get(u.edge as usize)?;
|
||||
let Curve::Circle { frame, .. } = &e.curve else { continue };
|
||||
let z = Vec3::from(frame.x_axis).cross(Vec3::from(frame.y_axis));
|
||||
if z.dot(axis).abs() < 1.0 - 1e-9 {
|
||||
continue;
|
||||
}
|
||||
let c = CurveEval::new(&e.curve).ok()?;
|
||||
let [t0, t1] = e.interval;
|
||||
let am = angle(c.point(0.5 * (t0 + t1))?, o, axis, radial, tangent);
|
||||
let aa = unwrap(angle(c.point(t0)?, o, axis, radial, tangent), am);
|
||||
let ab = unwrap(angle(c.point(t1)?, o, axis, radial, tangent), am);
|
||||
return Some((aa.min(ab), aa.max(ab)));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The body's record, for the queries below (a mesh body has none).
|
||||
fn record(body: &Body) -> Result<Brep> {
|
||||
crate::native::brep_record(body)
|
||||
}
|
||||
|
||||
/// Certify a tessellated pick and project it onto the cylinder used by emboss.
|
||||
pub fn cylinder_emboss_anchor(body: &Body, face: usize, at: Vec3) -> Result<Vec3> {
|
||||
if !at.is_finite() || at.len() > 1e9 {
|
||||
return Err(fail("invalid selected face point"));
|
||||
}
|
||||
guard("cylinder emboss anchor", || {
|
||||
let f = body.solid.face_iter().nth(face).ok_or_else(|| fail("selected face"))?;
|
||||
let tol = (body.size() * 1e-8).max(1e-8);
|
||||
let Some(Surf::Cylinder { o, a, r, convex: true }) = surf_of(f, tol) else {
|
||||
let rec = record(body)?;
|
||||
if face >= rec.faces.len() {
|
||||
return Err(fail("selected face"));
|
||||
}
|
||||
let Some(Surf::Cylinder { o, a, r, convex: true }) = surf_of(&rec, face) else {
|
||||
return Err(fail("selected face is not an exterior cylinder"));
|
||||
};
|
||||
let w = at - o;
|
||||
@@ -92,13 +116,14 @@ pub fn infer_cylinder_emboss(body: &Body, plane: &Plane, regions: &[Region2], de
|
||||
return Ok(None);
|
||||
}
|
||||
guard("cylinder emboss inference", || {
|
||||
let rec = record(body)?;
|
||||
let tol = (body.size() * 1e-8).max(1e-8);
|
||||
let mut tangent = false;
|
||||
for (index, face) in body.solid.face_iter().enumerate() {
|
||||
if index >= 50_000 {
|
||||
for face in 0..rec.faces.len() {
|
||||
if face >= 50_000 {
|
||||
return Err(fail("target search exceeds face work limit"));
|
||||
}
|
||||
if let Some(Surf::Cylinder { o, a, r, convex: true }) = surf_of(face, tol) {
|
||||
if let Some(Surf::Cylinder { o, a, r, convex: true }) = surf_of(&rec, face) {
|
||||
let w = plane.origin - o;
|
||||
let normal = plane.normal();
|
||||
let signed = w.dot(normal);
|
||||
@@ -111,7 +136,7 @@ pub fn infer_cylinder_emboss(body: &Body, plane: &Plane, regions: &[Region2], de
|
||||
if !profile_interval_overlaps(plane, regions, o, a.cross(radial), -std::f64::consts::PI * r, std::f64::consts::PI * r)? {
|
||||
continue;
|
||||
}
|
||||
if let Some((lo, hi)) = wall_angles(face, o, a, radial, a.cross(radial), tol) {
|
||||
if let Some((lo, hi)) = wall_angles(&rec, face, o, a, radial, a.cross(radial)) {
|
||||
if lo >= -tol / r || hi <= tol / r {
|
||||
continue;
|
||||
}
|
||||
@@ -119,7 +144,7 @@ pub fn infer_cylinder_emboss(body: &Body, plane: &Plane, regions: &[Region2], de
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let relevant = match face_axial_bounds(face, o, a)? {
|
||||
let relevant = match face_axial_bounds(&rec, face, o, a)? {
|
||||
Some((lo, hi)) => profile_interval_overlaps(plane, regions, o, a, lo, hi)?,
|
||||
None => true,
|
||||
};
|
||||
@@ -148,18 +173,14 @@ pub fn has_cylinder_emboss_wall(body: &Body) -> Result<bool> {
|
||||
return Ok(false);
|
||||
}
|
||||
guard("cylinder emboss target", || {
|
||||
let tol = (body.size() * 1e-8).max(1e-8);
|
||||
for (index, face) in body.solid.face_iter().enumerate() {
|
||||
if index >= 50_000 {
|
||||
return Err(fail("target search exceeds face work limit"));
|
||||
}
|
||||
if matches!(surf_of(face, tol), Some(Surf::Cylinder { convex: true, .. })) {
|
||||
return Ok(true);
|
||||
}
|
||||
let rec = record(body)?;
|
||||
if rec.faces.len() > 50_000 {
|
||||
return Err(fail("target search exceeds face work limit"));
|
||||
}
|
||||
Ok(false)
|
||||
Ok((0..rec.faces.len()).any(|face| matches!(surf_of(&rec, face), Some(Surf::Cylinder { convex: true, .. }))))
|
||||
})
|
||||
}
|
||||
|
||||
fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Region2], depth: f64, qualify: bool) -> Result<EmbossOutcome> {
|
||||
body.require_brep("cylinder emboss")?;
|
||||
if !depth.is_finite() || depth.abs() < 1e-6 || depth.abs() > 1e6 || regions.is_empty() || regions.len() > 64 {
|
||||
@@ -175,13 +196,14 @@ fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Regi
|
||||
return Err(fail("invalid sketch frame"));
|
||||
}
|
||||
guard("cylinder emboss", || {
|
||||
if body.solid.boundaries().len() != 1 || body.solid.face_iter().take(17).count() > 16 || body.solid.edge_iter().take(257).count() > 256 {
|
||||
let rec = record(body)?;
|
||||
if rec.shells.len() != 1 || rec.faces.len() > 16 || rec.edges.len() > 256 {
|
||||
return Err(fail("requires a complete cylinder or coaxial sleeve"));
|
||||
}
|
||||
let faces: Vec<_> = body.solid.face_iter().cloned().collect();
|
||||
let tol = (body.size() * 1e-8).max(1e-8);
|
||||
let surfaces: Vec<_> =
|
||||
faces.iter().map(|f| surf_of(f, tol).ok_or_else(|| fail("requires only cylindrical walls and planar endcaps"))).collect::<Result<_>>()?;
|
||||
let surfaces: Vec<Surf> = (0..rec.faces.len())
|
||||
.map(|f| surf_of(&rec, f).ok_or_else(|| fail("requires only cylindrical walls and planar endcaps")))
|
||||
.collect::<Result<_>>()?;
|
||||
let mut chosen = None;
|
||||
for (fi, s) in surfaces.iter().enumerate() {
|
||||
let Surf::Cylinder { o, a, r, convex: true } = *s else { continue };
|
||||
@@ -192,11 +214,7 @@ fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Regi
|
||||
}
|
||||
let radial = radial.normalized().ok_or_else(|| fail("radial frame"))?;
|
||||
let tangent = a.cross(radial);
|
||||
let f = faces.get(fi).ok_or_else(|| fail("face"))?;
|
||||
if f.boundaries().len() != 1 || f.edge_iter().count() != 4 {
|
||||
continue;
|
||||
}
|
||||
let Some((alo, ahi)) = wall_angles(f, o, a, radial, tangent, tol) else {
|
||||
let Some((alo, ahi)) = wall_angles(&rec, fi, o, a, radial, tangent) else {
|
||||
continue;
|
||||
};
|
||||
if alo >= -tol / r || ahi <= tol / r {
|
||||
@@ -246,10 +264,13 @@ fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Regi
|
||||
return Err(fail("depth reaches the axis or inner wall"));
|
||||
}
|
||||
if let Some(at) = anchor {
|
||||
// The saved pick identifies the wall, not the face the sketch is tangent to: every
|
||||
// exterior cylindrical face of the body is that wall (a stepped or interrupted
|
||||
// cylinder was refused above), stored as several faces between its seams. So the
|
||||
// pick lies on it wherever it is on the cylinder between the endcaps (#3782).
|
||||
let h = (at - o).dot(axis);
|
||||
let v = at - o - axis * h;
|
||||
let theta = angle(at, o, axis, radial, tangent);
|
||||
if (v.len() - r).abs() > tol * 10.0 || theta <= alo + tol / r || theta >= ahi - tol / r || h <= lo + tol || h >= hi - tol {
|
||||
if (v.len() - r).abs() > tol * 10.0 || h <= lo + tol || h >= hi - tol {
|
||||
return Err(fail("saved selection no longer identifies the sketch's cylindrical face"));
|
||||
}
|
||||
}
|
||||
@@ -307,39 +328,15 @@ fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Regi
|
||||
let at = o + axis * p.y + (radial * (p.x / r).cos() + tangent * (p.x / r).sin()) * r;
|
||||
return Ok(EmbossOutcome::Anchor(fi, at));
|
||||
}
|
||||
let at = |p: Vec2, radius: f64| o + axis * p.y + (radial * (p.x / r).cos() + tangent * (p.x / r).sin()) * radius;
|
||||
let selected = faces.get(fi).ok_or_else(|| fail("face"))?;
|
||||
let mut base_patches = vec![selected.absolute_boundaries().clone()];
|
||||
let wall = Wrap { o, axis, radial, tangent, r };
|
||||
let mut asm = Assembly::from_record(&rec)?;
|
||||
let selected = asm.faces.get_mut(fi).and_then(Option::take).ok_or_else(|| fail("face"))?;
|
||||
// The wall's pieces left around the footprints: the face itself, then one per profile
|
||||
// hole (the original radius inside it).
|
||||
let mut base_patches: Vec<Vec<Vec<AUse>>> = vec![selected.loops.clone()];
|
||||
let mut hole_patches = Vec::new();
|
||||
let mut footprints = Vec::new();
|
||||
let mut added = Vec::new();
|
||||
let k = rr / r;
|
||||
let coord = |v: Vec3| [v.x, v.y, v.z];
|
||||
let av = coord(axis);
|
||||
let ov = coord(o);
|
||||
let mut m = [[0.0; 4]; 4];
|
||||
for (j, col) in m.iter_mut().take(3).enumerate() {
|
||||
for (i, slot) in col.iter_mut().take(3).enumerate() {
|
||||
*slot = if i == j { k } else { 0.0 } + (1.0 - k) * av.get(i).copied().unwrap_or(0.0) * av.get(j).copied().unwrap_or(0.0);
|
||||
}
|
||||
}
|
||||
for (i, oi) in ov.iter().enumerate() {
|
||||
let translation =
|
||||
*oi - (0..3).map(|j| m.get(j).and_then(|row| row.get(i)).copied().unwrap_or(0.0) * ov.get(j).copied().unwrap_or(0.0)).sum::<f64>();
|
||||
if let Some(slot) = m.get_mut(3).and_then(|row| row.get_mut(i)) {
|
||||
*slot = translation;
|
||||
}
|
||||
}
|
||||
if let Some(slot) = m.get_mut(3).and_then(|row| row.get_mut(3)) {
|
||||
*slot = 1.0;
|
||||
}
|
||||
let [c0, c1, c2, c3] = m;
|
||||
let [a, b, c, d] = c0;
|
||||
let [e, f, g, h] = c1;
|
||||
let [i, j, l, n] = c2;
|
||||
let [p, q, s, t] = c3;
|
||||
let mat = mt::Matrix4::new(a, b, c, d, e, f, g, h, i, j, l, n, p, q, s, t);
|
||||
let raised = selected.oriented_surface().transformed(mat);
|
||||
let mut added: Vec<AFace> = Vec::new();
|
||||
// History of the faces added (walls from their profile segments, the raised or sunk
|
||||
// region the profile's end cap), in the order added.
|
||||
let mut added_history = Vec::new();
|
||||
@@ -347,20 +344,21 @@ fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Regi
|
||||
let mut cap = Vec::new();
|
||||
for (li, numbered_loop) in reg.loops().enumerate() {
|
||||
let lp = &numbered_loop.lp;
|
||||
let (base, top, walls) = wrapped_loop(lp, r, rr, &at)?;
|
||||
let (base, top, walls) = wall.wrapped_loop(&mut asm, lp, rr, selected.shell)?;
|
||||
// One wall per segment, in order.
|
||||
added_history.extend((0..walls.len()).map(|j| crate::record::profile(numbered_loop.numbers(j))));
|
||||
let hole = reversed(&base);
|
||||
if li == 0 {
|
||||
footprints.push((lp, base.inverse()));
|
||||
footprints.push((lp, hole));
|
||||
} else {
|
||||
let index = base_patches.len();
|
||||
base_patches.push(vec![base.inverse()]);
|
||||
base_patches.push(vec![hole]);
|
||||
hole_patches.push((lp, index));
|
||||
}
|
||||
cap.push(top);
|
||||
added.extend(walls);
|
||||
}
|
||||
added.push(mt::Face::try_new(cap, raised.clone()).map_err(|e| fail(&e.to_string()))?);
|
||||
added.push(AFace { surface: wall.cylinder(rr), natural_out: true, loops: cap, shell: selected.shell, domain: None });
|
||||
added_history.push(crate::record::cap(true));
|
||||
}
|
||||
// An island inside a profile hole cuts that hole's original-radius patch,
|
||||
@@ -375,27 +373,176 @@ fn emboss_impl(body: &Body, anchor: Option<Vec3>, plane: &Plane, regions: &[Regi
|
||||
.unwrap_or(0);
|
||||
base_patches.get_mut(owner).ok_or_else(|| fail("profile ownership"))?.push(wire);
|
||||
}
|
||||
let mut all: Vec<_> = faces.iter().enumerate().filter(|(i, _)| *i != fi).map(|(_, f)| f.clone()).collect();
|
||||
// History (one operand, the body; profile numbering over `regions`), face by face as
|
||||
// gathered: the other faces kept, the wall's pieces left around the footprints (split
|
||||
// when the profile's holes leave more than one), then the faces added.
|
||||
let pieces = if base_patches.len() > 1 { Change::Split } else { Change::Modified };
|
||||
let mut history: Vec<_> = crate::record::kept_except(faces.len(), fi).collect();
|
||||
let mut history: Vec<_> = crate::record::kept_except(rec.faces.len(), fi).collect();
|
||||
history.extend(std::iter::repeat_n(crate::record::from_face(0, fi, pieces), base_patches.len()));
|
||||
history.extend(added_history);
|
||||
for patch in base_patches {
|
||||
all.push(mt::Face::try_new(patch, selected.oriented_surface()).map_err(|e| fail(&e.to_string()))?);
|
||||
for (k, loops) in base_patches.into_iter().enumerate() {
|
||||
// The face's own parameter box only where it keeps its outer loop.
|
||||
let domain = if k == 0 { selected.domain } else { None };
|
||||
asm.face(AFace { surface: selected.surface.clone(), natural_out: selected.natural_out, loops, shell: selected.shell, domain });
|
||||
}
|
||||
all.extend(added);
|
||||
let solid = Solid::try_new(vec![all.into()]).map_err(|e| fail(&e.to_string()))?;
|
||||
let result = Body::new(solid)?;
|
||||
for f in added {
|
||||
asm.face(f);
|
||||
}
|
||||
let finished = asm.finish()?;
|
||||
let result = crate::assemble::body_with_history(&finished, &asm, body, history)?;
|
||||
if !result.validity().is_empty() {
|
||||
return Err(fail("rebuilt cylinder is not a closed manifold"));
|
||||
}
|
||||
Ok(EmbossOutcome::Body(crate::record::attach(result, &[Operand::new(body)], Some(history), Default::default())?))
|
||||
Ok(EmbossOutcome::Body(result))
|
||||
})
|
||||
}
|
||||
|
||||
/// A loop's uses the other way round.
|
||||
fn reversed(l: &[AUse]) -> Vec<AUse> {
|
||||
l.iter().rev().map(|u| AUse { edge: u.edge, forward: !u.forward, given: None }).collect()
|
||||
}
|
||||
|
||||
/// The wall a profile wraps onto: its axis, the sketch's radial and tangent directions there,
|
||||
/// and its radius.
|
||||
struct Wrap {
|
||||
o: Vec3,
|
||||
axis: Vec3,
|
||||
radial: Vec3,
|
||||
tangent: Vec3,
|
||||
r: f64,
|
||||
}
|
||||
|
||||
impl Wrap {
|
||||
/// The point at wrapped profile coordinates `p` (arc length at the wall, height) at `radius`.
|
||||
fn at(&self, p: Vec2, radius: f64) -> Vec3 {
|
||||
let t = p.x / self.r;
|
||||
self.o + self.axis * p.y + (self.radial * t.cos() + self.tangent * t.sin()) * radius
|
||||
}
|
||||
|
||||
/// The wall's cylinder at `radius` (its angle measured from the sketch's radial).
|
||||
fn cylinder(&self, radius: f64) -> Surface {
|
||||
Surface::Cylinder { frame: Frame { origin: arr(self.o), x_axis: arr(self.radial), y_axis: arr(self.tangent) }, radius }
|
||||
}
|
||||
|
||||
/// An edge from vertex `a` at `p` to vertex `b` at `q` (wrapped coordinates) at `radius`:
|
||||
/// an arc about the axis where the segment runs round the wall, else a line along it. The
|
||||
/// use that runs from `a` to `b`.
|
||||
fn edge(&self, asm: &mut Assembly, a: u32, b: u32, p: Vec2, q: Vec2, radius: f64) -> Result<AUse> {
|
||||
if (q.x - p.x).abs() <= 1e-10 {
|
||||
return Ok(AUse { edge: asm.line(a, b)?, forward: true, given: None });
|
||||
}
|
||||
let frame = Frame { origin: arr(self.o + self.axis * p.y), x_axis: arr(self.radial), y_axis: arr(self.tangent) };
|
||||
let (ta, tb) = (p.x / self.r, q.x / self.r);
|
||||
let forward = ta < tb;
|
||||
let (interval, ends) = if forward { ([ta, tb], (a, b)) } else { ([tb, ta], (b, a)) };
|
||||
Ok(AUse { edge: asm.edge(Curve::Circle { frame, radius }, interval, ends.0, ends.1)?, forward, given: None })
|
||||
}
|
||||
|
||||
/// A profile loop wrapped onto the wall: its footprint at the wall's radius and its cap's
|
||||
/// loop at `rr`, both running as the loop does, and one planar wall per segment between
|
||||
/// them (each loop seen from outside the body).
|
||||
fn wrapped_loop(&self, asm: &mut Assembly, lp: &Loop2, rr: f64, shell: usize) -> Result<(Vec<AUse>, Vec<AUse>, Vec<AFace>)> {
|
||||
let corners: Vec<Vec2> = lp.segs.iter().map(Seg2::start).collect();
|
||||
let base: Vec<u32> = corners.iter().map(|p| asm.vertex(self.at(*p, self.r))).collect::<Result<_>>()?;
|
||||
let top: Vec<u32> = corners.iter().map(|p| asm.vertex(self.at(*p, rr))).collect::<Result<_>>()?;
|
||||
let radial: Vec<u32> = base.iter().zip(&top).map(|(a, b)| asm.line(*a, *b)).collect::<Result<_>>()?;
|
||||
let n = corners.len();
|
||||
let (mut be, mut te, mut walls) = (Vec::with_capacity(n), Vec::with_capacity(n), Vec::with_capacity(n));
|
||||
for i in 0..n {
|
||||
let j = (i + 1) % n;
|
||||
let (Some(&p), Some(&q), Some(&ba), Some(&bb), Some(&ta), Some(&tb), Some(&ra), Some(&rb)) =
|
||||
(corners.get(i), corners.get(j), base.get(i), base.get(j), top.get(i), top.get(j), radial.get(i), radial.get(j))
|
||||
else {
|
||||
return Err(fail("loop topology"));
|
||||
};
|
||||
let eb = self.edge(asm, ba, bb, p, q, self.r)?;
|
||||
let et = self.edge(asm, ta, tb, p, q, rr)?;
|
||||
// The wall's plane, its natural normal along the loop's turning: out of the
|
||||
// material whether the cap is raised or sunk.
|
||||
let mid = (p + q) * 0.5;
|
||||
let ring = [self.at(p, self.r), self.at(mid, self.r), self.at(q, self.r), self.at(q, rr), self.at(mid, rr), self.at(p, rr)];
|
||||
let normal = newell(&ring).ok_or_else(|| fail("a wall of no area"))?;
|
||||
let u = (ring[2] - ring[0]).normalized().ok_or_else(|| fail("a wall of no width"))?;
|
||||
let u = (u - normal * u.dot(normal)).normalized().ok_or_else(|| fail("a wall of no width"))?;
|
||||
let surface = Surface::Plane { origin: arr(ring[0]), u_axis: arr(u), v_axis: arr(normal.cross(u)) };
|
||||
let lp = vec![
|
||||
eb.clone(),
|
||||
AUse { edge: rb, forward: true, given: None },
|
||||
AUse { edge: et.edge, forward: !et.forward, given: None },
|
||||
AUse { edge: ra, forward: false, given: None },
|
||||
];
|
||||
walls.push(AFace { surface, natural_out: true, loops: vec![lp], shell, domain: None });
|
||||
be.push(eb);
|
||||
te.push(et);
|
||||
}
|
||||
Ok((be, te, walls))
|
||||
}
|
||||
}
|
||||
|
||||
/// The unit normal of a planar ring of points, along its turning (right-hand rule).
|
||||
fn newell(ring: &[Vec3]) -> Option<Vec3> {
|
||||
let n = ring.len();
|
||||
let mut s = Vec3::ZERO;
|
||||
for i in 0..n {
|
||||
let (a, b) = (*ring.get(i)?, *ring.get((i + 1) % n)?);
|
||||
s = s + a.cross(b);
|
||||
}
|
||||
s.normalized()
|
||||
}
|
||||
|
||||
/// The span along `axis` from `o` of face `face`'s edges, from their exact bounds (a line's
|
||||
/// ends, a circle's extreme heights, a spline's control points); `None` for any other curve.
|
||||
fn face_axial_bounds(b: &Brep, face: usize, o: Vec3, axis: Vec3) -> Result<Option<(f64, f64)>> {
|
||||
let mut lo = f64::INFINITY;
|
||||
let mut hi = f64::NEG_INFINITY;
|
||||
let mut count = 0usize;
|
||||
let mut add = |z0: f64, z1: f64| -> Result<()> {
|
||||
count = count.saturating_add(1);
|
||||
if count > 100_000 || !z0.is_finite() || !z1.is_finite() {
|
||||
return Err(fail("target boundary work limit or nonfinite geometry"));
|
||||
}
|
||||
lo = lo.min(z0);
|
||||
hi = hi.max(z1);
|
||||
Ok(())
|
||||
};
|
||||
let height = |p: Vec3| (p - o).dot(axis);
|
||||
for e in face_edges(b, face) {
|
||||
let edge = b.edges.get(e as usize).ok_or_else(|| fail("edge"))?;
|
||||
match &edge.curve {
|
||||
Curve::Line { .. } => {
|
||||
let c = CurveEval::new(&edge.curve).map_err(|e| fail(&e.to_string()))?;
|
||||
for t in edge.interval {
|
||||
let z = height(c.point(t).ok_or_else(|| fail("edge point"))?);
|
||||
add(z, z)?;
|
||||
}
|
||||
}
|
||||
Curve::Circle { frame, radius } => {
|
||||
let (x, y) = (Vec3::from(frame.x_axis), Vec3::from(frame.y_axis));
|
||||
let z = height(Vec3::from(frame.origin));
|
||||
let reach = radius * x.dot(axis).hypot(y.dot(axis));
|
||||
add(z - reach, z + reach)?;
|
||||
}
|
||||
Curve::Spline { points, .. } => {
|
||||
for p in points {
|
||||
let z = height(Vec3::from(*p));
|
||||
add(z, z)?;
|
||||
}
|
||||
}
|
||||
Curve::RationalSpline { homogeneous, .. } => {
|
||||
for h in homogeneous {
|
||||
if !h[3].is_finite() || h[3] <= 0.0 {
|
||||
return Ok(None);
|
||||
}
|
||||
let z = height(Vec3::new(h[0] / h[3], h[1] / h[3], h[2] / h[3]));
|
||||
add(z, z)?;
|
||||
}
|
||||
}
|
||||
Curve::Ellipse { .. } | Curve::Polyline { .. } | Curve::SurfaceIntersection { .. } => return Ok(None),
|
||||
}
|
||||
}
|
||||
Ok((lo.is_finite() && hi.is_finite()).then_some((lo, hi)))
|
||||
}
|
||||
|
||||
fn profile_interval_overlaps(plane: &Plane, regions: &[Region2], o: Vec3, axis: Vec3, lo: f64, hi: f64) -> Result<bool> {
|
||||
let z0 = (plane.origin - o).dot(axis);
|
||||
let direction = Vec2::new(plane.x.dot(axis), plane.y.dot(axis));
|
||||
@@ -455,70 +602,6 @@ fn profile_interval_overlaps(plane: &Plane, regions: &[Region2], o: Vec3, axis:
|
||||
Ok(overlaps)
|
||||
}
|
||||
|
||||
fn face_axial_bounds(face: &mt::Face, o: Vec3, axis: Vec3) -> Result<Option<(f64, f64)>> {
|
||||
let mut lo = f64::INFINITY;
|
||||
let mut hi = f64::NEG_INFINITY;
|
||||
let mut count = 0usize;
|
||||
let mut add = |point: Vec3| -> Result<()> {
|
||||
count = count.saturating_add(1);
|
||||
if count > 100_000 || !point.is_finite() {
|
||||
return Err(fail("target boundary work limit or nonfinite geometry"));
|
||||
}
|
||||
let z = (point - o).dot(axis);
|
||||
lo = lo.min(z);
|
||||
hi = hi.max(z);
|
||||
Ok(())
|
||||
};
|
||||
for edge in face.edge_iter() {
|
||||
match edge.curve() {
|
||||
mt::Curve::Line(_) => {
|
||||
add(from_p3(edge.front().point()))?;
|
||||
add(from_p3(edge.back().point()))?;
|
||||
}
|
||||
mt::Curve::BSplineCurve(curve) => {
|
||||
for point in curve.control_points() {
|
||||
add(from_p3(*point))?;
|
||||
}
|
||||
}
|
||||
mt::Curve::NurbsCurve(curve) => {
|
||||
for point in curve.control_points() {
|
||||
if !point.w.is_finite() || point.w <= 0.0 {
|
||||
return Ok(None);
|
||||
}
|
||||
add(Vec3::new(point.x / point.w, point.y / point.w, point.z / point.w))?;
|
||||
}
|
||||
}
|
||||
mt::Curve::IntersectionCurve(_) => return Ok(None),
|
||||
}
|
||||
}
|
||||
Ok((lo.is_finite() && hi.is_finite()).then_some((lo, hi)))
|
||||
}
|
||||
|
||||
fn wrapped_loop(lp: &Loop2, r: f64, rr: f64, at: &impl Fn(Vec2, f64) -> Vec3) -> Result<(mt::Wire, mt::Wire, Vec<mt::Face>)> {
|
||||
let base: Vec<_> = lp.segs.iter().map(|s| builder::vertex(p3(at(s.start(), r)))).collect();
|
||||
let top: Vec<_> = lp.segs.iter().map(|s| builder::vertex(p3(at(s.start(), rr)))).collect();
|
||||
let radial: Vec<_> = base.iter().zip(&top).map(|(a, b)| builder::line(a, b)).collect();
|
||||
let mut be = Vec::new();
|
||||
let mut te = Vec::new();
|
||||
let mut walls = Vec::new();
|
||||
for (i, s) in lp.segs.iter().enumerate() {
|
||||
let next = (i + 1) % lp.segs.len();
|
||||
let (Some(a), Some(b), Some(c), Some(d), Some(ra), Some(rb)) =
|
||||
(base.get(i), base.get(next), top.get(i), top.get(next), radial.get(i), radial.get(next))
|
||||
else {
|
||||
return Err(fail("loop topology"));
|
||||
};
|
||||
let circular = (s.end().x - s.start().x).abs() > 1e-10;
|
||||
let eb = if circular { builder::circle_arc(a, b, p3(at(s.mid(), r))) } else { builder::line(a, b) };
|
||||
let et = if circular { builder::circle_arc(c, d, p3(at(s.mid(), rr))) } else { builder::line(c, d) };
|
||||
let w: mt::Wire = vec![eb.clone(), rb.clone(), et.inverse(), ra.inverse()].into();
|
||||
walls.push(builder::try_attach_plane(&[w]).map_err(|e| fail(&e.to_string()))?);
|
||||
be.push(eb);
|
||||
te.push(et);
|
||||
}
|
||||
Ok((be.into(), te.into(), walls))
|
||||
}
|
||||
|
||||
fn validate_profiles(regions: &[Region2], tol: f64) -> Result<()> {
|
||||
let loops: Vec<_> = regions.iter().flat_map(|r| std::iter::once(&r.outer).chain(&r.holes)).collect();
|
||||
let edges: Vec<_> = loops.iter().enumerate().flat_map(|(l, lp)| lp.segs.iter().enumerate().map(move |(i, s)| (l, i, *s))).collect();
|
||||
@@ -744,7 +827,9 @@ mod tests {
|
||||
] {
|
||||
assert!(emboss_cylinder(&sleeve, &plane(), &profiles, d).is_err());
|
||||
}
|
||||
assert!(emboss_cylinder_at(&sleeve, -Vec3::Y * 10. + Vec3::Z * 7.5, &plane(), &[profile()], 1.).is_err());
|
||||
// A pick anywhere on the exterior wall identifies it (#3782); one on the bore does not.
|
||||
assert!(emboss_cylinder_at(&sleeve, -Vec3::Y * 10. + Vec3::Z * 7.5, &plane(), &[profile()], 1.).is_ok());
|
||||
assert!(emboss_cylinder_at(&sleeve, -Vec3::Y * 8. + Vec3::Z * 7.5, &plane(), &[profile()], 1.).is_err());
|
||||
assert_eq!(crate::step_export(&[&sleeve], "unchanged").unwrap(), before);
|
||||
}
|
||||
}
|
||||
|
||||
+94
-1371
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user