brep: a face on a cylinder, cone, sphere or torus carries the NURBS form SMLib holds it as

Every SMLib analytic surface is a B-spline surface, and its trims are in that NURBS's
parameters. Face::nurbs keeps that NURBS: the face's domain and pcurves are its (u, v), and
`surface` says what the face is. Face::parameters is the surface every (u, v) evaluation uses.
The SMLib conversion reads and writes it (fork FACE_NURBS_FORM, SMLIB_REV c4483f6); the STEP
writer writes the analytic surface and the face's edges without pcurves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
jyviko
2026-10-22 13:21:24 -04:00
parent 13e49e0698
commit b6a3b31ff9
33 changed files with 233 additions and 84 deletions
@@ -0,0 +1,4 @@
//! A face on a cylinder, cone, sphere or torus may save `nurbs`: the NURBS form it is
//! parameterised on, as SMLib holds it. Its domain and pcurves are then that NURBS's (u, v), and
//! `surface` only says what the face is. An older design has none and keeps the analytic (u, v).
//! The last release refuses a design with this change (the newer format number).
+26 -1
View File
@@ -84,7 +84,16 @@ pub struct OrientedFace {
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Face {
/// What the face is: its surface as the producer names it (a cylinder, a NURBS…).
pub surface: Surface,
/// The NURBS form the face is parameterised on, when it differs from `surface`'s own
/// parameters: a cylinder, cone, sphere or torus as SMLib holds it (every SMLib analytic
/// surface is a B-spline surface, its trims in that NURBS's parameters). When given, `domain`
/// and every pcurve are in this surface's (u, v), and `surface` only says what it is; it is a
/// `Spline` or `RationalSpline` and `surface` is a cylinder, cone, sphere or torus
/// (docs/native-brep.md, "Surfaces").
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nurbs: Option<Surface>,
/// The modelled rectangle in surface parameters, `[[u0, u1], [v0, v1]]`, inside the
/// surface's natural domain and around every loop.
pub domain: [[f64; 2]; 2],
@@ -469,6 +478,14 @@ fn surface(s: &Surface, depth: usize, budget: &mut Budget) -> Result<()> {
}
}
impl Face {
/// The surface the face's (u, v) are on: its NURBS form when it has one, else its surface.
/// Everything that evaluates the face's domain or pcurves goes through this.
pub fn parameters(&self) -> &Surface {
self.nurbs.as_ref().unwrap_or(&self.surface)
}
}
impl Brep {
/// Validate portable structure without calling a kernel or evaluating geometry: resource
/// budgets, geometry records, parameter ranges, references, loops, regions, every edge's
@@ -495,7 +512,15 @@ impl Brep {
}
for f in &self.faces {
surface(&f.surface, 0, &mut budget)?;
let [u, v] = surface_natural(&f.surface);
if let Some(n) = &f.nurbs {
surface(n, 0, &mut budget)?;
require(matches!(n, Surface::Spline { .. } | Surface::RationalSpline { .. }), "NURBS form is not a NURBS")?;
require(
matches!(f.surface, Surface::Cylinder { .. } | Surface::Cone { .. } | Surface::Sphere { .. } | Surface::Torus { .. }),
"NURBS form on a surface that is not a cylinder, cone, sphere or torus",
)?;
}
let [u, v] = surface_natural(f.parameters());
interval(&f.domain[0], u)?;
interval(&f.domain[1], v)?;
budget.add(f.loops.len())?;
+1 -1
View File
@@ -122,7 +122,7 @@ impl Brep {
}
for (fi, f) in self.faces.iter().enumerate() {
let face = index(fi);
let surface = SurfaceEval::new(&f.surface).map_err(|err| unevaluable(Record::Face(face), err))?;
let surface = SurfaceEval::new(f.parameters()).map_err(|err| unevaluable(Record::Face(face), err))?;
let period = surface.domain().period;
// Each edge loop's signed (u, v) area.
let mut areas: Vec<(u32, f64)> = Vec::with_capacity(f.loops.len());
+1
View File
@@ -139,6 +139,7 @@ impl Brep {
let mut out = self.clone();
for f in &mut out.faces {
f.surface = motion.surface(&f.surface);
f.nurbs = f.nurbs.as_ref().map(|n| motion.surface(n));
}
for e in &mut out.edges {
e.curve = motion.curve(&e.curve);
+3 -1
View File
@@ -101,7 +101,7 @@ fn faceted(points: &[[f64; 3]], shells: &[&[&[usize]]], regions: Vec<Region>) ->
let [o, p, q] = [0, 1, 2].map(|k| points[cycle[k]]);
let axis = |a: [f64; 3]| [a[0] - o[0], a[1] - o[1], a[2] - o[2]];
let surface = Surface::Plane { origin: o, u_axis: axis(p), v_axis: axis(q) };
b.faces.push(Face { surface, domain: [[-10., 10.], [-10., 10.]], loops: vec![Loop::Edges { edges: loop_ }] });
b.faces.push(Face { nurbs: None, surface, domain: [[-10., 10.], [-10., 10.]], loops: vec![Loop::Edges { edges: loop_ }] });
}
b.shells.push(Shell::Faces { faces: uses, wires: vec![] });
}
@@ -145,6 +145,7 @@ fn tube() -> Brep {
regions: vec![infinite(vec![0])],
shells: vec![Shell::Faces { faces: vec![OrientedFace { face: 0, forward: true }], wires: vec![] }],
faces: vec![Face {
nurbs: None,
surface: Surface::Cylinder { frame: f.clone(), radius: 2. },
domain: [[0., TAU], [0., 5.]],
loops: vec![Loop::Edges {
@@ -176,6 +177,7 @@ fn one_face(surface: Surface, domain: [[f64; 2]; 2], edges: Vec<(Curve, [f64; 2]
regions: vec![infinite(vec![0])],
shells: vec![Shell::Faces { faces: vec![OrientedFace { face: 0, forward: true }], wires: vec![] }],
faces: vec![Face {
nurbs: None,
surface,
domain,
loops: vec![Loop::Edges { edges: uses.iter().map(|&(edge, forward)| OrientedEdge { edge, forward, pcurve: segment(0., 1.) }).collect() }],
+1 -1
View File
@@ -278,7 +278,7 @@ impl Assembly {
edges: l.iter().zip(pc).map(|(u, pcurve)| OrientedEdge { edge: u.edge, forward: u.forward, pcurve: pcurve.clone() }).collect(),
})
.collect();
Ok(Face { surface: f.surface.clone(), domain, loops: stored })
Ok(Face { nurbs: None, surface: f.surface.clone(), domain, loops: stored })
}
}
+2 -1
View File
@@ -337,7 +337,7 @@ impl EdgeFaces {
let in_loop = in_loop.ok_or_else(|| KernelError::Failed("boundary surface: an edge's face".into()))?;
let pts = (0..=16).map(|k| along(curve, k as f64 / 16.0)).collect::<Option<Vec<_>>>();
let pts = pts.ok_or_else(|| KernelError::Invalid(format!("{label}: an edge can't be evaluated")))?;
let surface = SurfaceEval::new(&face.surface).map_err(|err| KernelError::Failed(format!("boundary surface: {err}")))?;
let surface = SurfaceEval::new(face.parameters()).map_err(|err| KernelError::Failed(format!("boundary surface: {err}")))?;
sides.push(EdgeSide { pts, surface, along: in_loop == forward, hint: None });
}
Ok(EdgeFaces { sides })
@@ -459,6 +459,7 @@ fn face_of(s: &NurbsSurface, corners: &[Vec3]) -> Result<Body> {
uses.extend(pieces);
}
let face = solidcraft_geom::brep::Face {
nurbs: None,
surface: spline_surface(s),
domain: s.domain(),
loops: vec![solidcraft_geom::brep::Loop::Edges { edges: uses }],
+3 -3
View File
@@ -438,7 +438,7 @@ impl Walk {
impl Support {
fn of(r: &Brep, face: usize, length: f64) -> Result<Support> {
let f = r.faces.get(face).ok_or_else(|| KernelError::Invalid("no such face".into()))?;
let surf = SurfaceEval::new(&f.surface).map_err(|e| KernelError::Failed(format!("bridge: {e}")))?;
let surf = SurfaceEval::new(f.parameters()).map_err(|e| KernelError::Failed(format!("bridge: {e}")))?;
let dom = surf.domain();
let range = [0, 1].map(|d| match (dom.period[d], dom.range[d]) {
(None, Some(r)) => r,
@@ -734,7 +734,7 @@ pub fn bridge(a: &[Frame], b: &[Frame], orders: [Continuity; 2], crown: [f64; 2]
pub(crate) fn the_face(body: &Body) -> Result<(SurfaceEval, [[f64; 2]; 2])> {
let r = body.record();
let face = r.faces.first().ok_or_else(|| KernelError::Failed("bridge: no face".into()))?;
let surf = SurfaceEval::new(&face.surface).map_err(|e| KernelError::Failed(format!("bridge: {e}")))?;
let surf = SurfaceEval::new(face.parameters()).map_err(|e| KernelError::Failed(format!("bridge: {e}")))?;
Ok((surf, face.domain))
}
@@ -783,7 +783,7 @@ fn support_faces(r: &Brep, chain: &Chain, leave: Vec3, size: f64) -> Result<Vec<
let mut best: Option<(f64, usize)> = None;
for u in &edge.uses {
let face = u.face as usize;
let Some(surf) = r.faces.get(face).and_then(|f| SurfaceEval::new(&f.surface).ok()) else { continue };
let Some(surf) = r.faces.get(face).and_then(|f| SurfaceEval::new(f.parameters()).ok()) else { continue };
let Some(uv) = surf.project(mid, None, size * 1e-6 + 1e-9) else { continue };
let Some(n) = surf.derivs(uv[0], uv[1]).and_then(|(_, su, sv)| su.cross(sv).normalized()) else { continue };
let score = (leave - n * leave.dot(n)).len();
+1 -1
View File
@@ -885,7 +885,7 @@ pub fn planar_face_record(plane: &Plane, r: &Region2) -> Result<solidcraft_geom:
tolerance,
regions: vec![rec::Region { shells: vec![0], void: false }],
shells: vec![rec::Shell::Faces { faces: vec![rec::OrientedFace { face: 0, forward: true }], wires: vec![] }],
faces: vec![rec::Face { surface, domain: [[lo[0] - pad, hi[0] + pad], [lo[1] - pad, hi[1] + pad]], loops: face_loops }],
faces: vec![rec::Face { nurbs: None, surface, domain: [[lo[0] - pad, hi[0] + pad], [lo[1] - pad, hi[1] + pad]], loops: face_loops }],
edges,
vertices,
};
+1 -1
View File
@@ -251,7 +251,7 @@ fn measure(body: &Body, edges: &[usize], samples: usize, tolerances: ContinuityT
}
let face = |i: usize| -> Option<FaceSurface> {
let f = record.faces.get(i)?;
FaceSurface::new(&f.surface, outward.get(i).copied().unwrap_or(true))
FaceSurface::new(f.parameters(), outward.get(i).copied().unwrap_or(true))
};
let mut seams = Vec::new();
let mut unmeasured = 0usize;
+1 -1
View File
@@ -59,7 +59,7 @@ impl<'a> DistanceShape<'a> {
/// `mesh` is `body`'s mesh, within `chord` (mm) of its faces.
pub fn new(body: &'a Body, mesh: &'a Mesh, chord: f64) -> DistanceShape<'a> {
let record = body.record();
let faces: Vec<Option<SurfaceEval>> = record.faces.iter().map(|f| SurfaceEval::new(&f.surface).ok()).collect();
let faces: Vec<Option<SurfaceEval>> = record.faces.iter().map(|f| SurfaceEval::new(f.parameters()).ok()).collect();
let edges = record.edges.iter().map(|e| CurveEval::new(&e.curve).ok().map(|c| (c, e.interval))).collect();
let surfaces = faces.iter().map(|_| OnceLock::new()).collect();
let chord = if chord.is_finite() && chord > 0.0 { chord } else { 0.0 };
+3 -3
View File
@@ -266,8 +266,8 @@ impl Beside {
return Err(KernelError::Invalid("an edge of the loop lies between two faces: g1 and g2 need the one face beside it".into()));
};
let face = record.faces.get(u.face as usize).ok_or_else(|| KernelError::Failed("patch: face reference".into()))?;
let surface =
SurfaceEval::new(&face.surface).map_err(|err| KernelError::Invalid(format!("the face beside edge {e} cannot be evaluated: {err}")))?;
let surface = SurfaceEval::new(face.parameters())
.map_err(|err| KernelError::Invalid(format!("the face beside edge {e} cannot be evaluated: {err}")))?;
let outward = record
.shells
.iter()
@@ -947,7 +947,7 @@ mod tests {
fn height_over(body: &Body, x: f64, y: f64, start: &mut Option<(f64, f64)>) -> Option<f64> {
let r = record(body);
let face = &r.faces[0];
let s = SurfaceEval::new(&face.surface).ok()?;
let s = SurfaceEval::new(face.parameters()).ok()?;
let [[u0, u1], [v0, v1]] = face.domain;
let (mut u, mut v) = start.unwrap_or(((u0 + u1) / 2.0, (v0 + v1) / 2.0));
for _ in 0..30 {
+1 -1
View File
@@ -297,7 +297,7 @@ fn shell(w: &mut W, b: &Brep, si: u32, paint: &HashMap<usize, [f32; 3]>) -> Resu
let mut face_data = Vec::with_capacity(faces.len());
for of in faces {
let face = b.faces.get(of.face as usize).ok_or_else(|| failed(format!("face {}: no such face", of.face)))?;
let (s, map) = topology::surface(&face.surface).map_err(|e| failed(format!("face {}: {e}", of.face)))?;
let (s, map) = topology::surface(face.parameters()).map_err(|e| failed(format!("face {}: {e}", of.face)))?;
let keeps = map.keeps_normal();
let outer = topology::outer_loop(b, face);
let order: Vec<usize> = outer.into_iter().chain((0..face.loops.len()).filter(|l| Some(*l) != outer)).collect();
@@ -31,7 +31,7 @@ fn triangle_with_intersection_edge(support_gap: f64) -> b::Brep {
pcurve: Curve2::Spline { knots: vec![0.0, 0.0, 1.0, 1.0], points: vec![uv[k], uv[(k + 1) % 3]] },
})
.collect();
let face = b::Face { surface: plane(0.5), domain: [[0.0, 1.0], [0.0, 1.0]], loops: vec![b::Loop::Edges { edges: uses }] };
let face = b::Face { nurbs: None, surface: plane(0.5), domain: [[0.0, 1.0], [0.0, 1.0]], loops: vec![b::Loop::Edges { edges: uses }] };
let loose = b::Brep {
version: b::VERSION,
tolerance: 1e-6,
+1 -1
View File
@@ -109,7 +109,7 @@ pub fn section_frames(body: &Body, edges: &[Vec3], leave: Vec3, stations: usize)
if !chain.iter().all(|(e, _)| bounds(f, *e)) {
continue;
}
let Ok(surf) = SurfaceEval::new(&face.surface) else { continue };
let Ok(surf) = SurfaceEval::new(face.parameters()) else { continue };
let Some((_, su, sv)) = surf.project(mid, None, f64::INFINITY).and_then(|uv| surf.derivs(uv[0], uv[1])) else { continue };
let n = su.cross(sv).normalized().unwrap_or(Vec3::ZERO);
let score = (leave - n * leave.dot(n)).len();
+1 -1
View File
@@ -263,7 +263,7 @@ struct Patch {
impl Patch {
fn of(r: &b::Brep, face: usize) -> Result<Patch> {
let f = r.faces.get(face).ok_or_else(|| KernelError::Invalid(format!("no face {face}")))?;
let surface = SurfaceEval::new(&f.surface).map_err(|e| KernelError::Failed(format!("match: face {face}: {e}")))?;
let surface = SurfaceEval::new(f.parameters()).map_err(|e| KernelError::Failed(format!("match: face {face}: {e}")))?;
let [[u0, u1], [v0, v1]] = f.domain;
Ok(Patch { surface, ranges: [(u0, u1), (v0, v1)] })
}
+4 -4
View File
@@ -177,7 +177,7 @@ pub fn project_onto_face(
pub fn face_section_along(body: &Body, face: usize, at: Vec3, along: Vec3) -> Result<Vec<(brep::Curve, [f64; 2])>> {
use solidcraft_geom::brep::eval::SurfaceEval;
let f = body.record().faces.get(face).ok_or_else(|| KernelError::Invalid(format!("the body has no face {face}")))?;
let eval = SurfaceEval::new(&f.surface).map_err(|e| KernelError::Failed(format!("face section: {e}")))?;
let eval = SurfaceEval::new(f.parameters()).map_err(|e| KernelError::Failed(format!("face section: {e}")))?;
let [[u0, u1], [v0, v1]] = f.domain;
let size = body.size().max(1e-9);
let uv = eval
@@ -206,7 +206,7 @@ pub fn iso_curves(body: &Body, face: usize, at: Vec3, along_u: bool) -> Result<V
use solidcraft_geom::brep::eval::SurfaceEval;
let record = body.record();
let f = record.faces.get(face).ok_or_else(|| KernelError::Invalid(format!("the body has no face {face}")))?;
let eval = SurfaceEval::new(&f.surface).map_err(|e| KernelError::Failed(format!("iso curve: {e}")))?;
let eval = SurfaceEval::new(f.parameters()).map_err(|e| KernelError::Failed(format!("iso curve: {e}")))?;
let [[u0, u1], [v0, v1]] = f.domain;
let size = body.size().max(1e-9);
let mid = [0.5 * (u0 + u1), 0.5 * (v0 + v1)];
@@ -275,11 +275,11 @@ pub fn iso_curves(body: &Body, face: usize, at: Vec3, along_u: bool) -> Result<V
if inside(at_t(0.5 * (a + b))) {
run = Some(run.map_or((a, b), |(s, _)| (s, b)));
} else if let Some(r) = run.take() {
out.push(iso_record(&f.surface, along_u, fixed, r)?);
out.push(iso_record(f.parameters(), along_u, fixed, r)?);
}
}
if let Some(r) = run {
out.push(iso_record(&f.surface, along_u, fixed, r)?);
out.push(iso_record(f.parameters(), along_u, fixed, r)?);
}
if out.is_empty() {
return Err(KernelError::Invalid("the iso curve runs outside the face".into()));
+1
View File
@@ -137,6 +137,7 @@ fn polyhedron_record(hs: &[HalfSpace], verts: &[Vec3], faces: &[(usize, Vec<usiz
}
let w = |x: Vec3| [x.x, x.y, x.z];
out.push(rec::Face {
nurbs: None,
surface: rec::Surface::Plane { origin: w(o), u_axis: w(u), v_axis: w(v) },
domain: [[lo[0], hi_uv[0]], [lo[1], hi_uv[1]]],
loops: vec![Loop::Edges { edges }],
+1 -1
View File
@@ -1268,7 +1268,7 @@ pub(crate) fn assemble(parts: Parts, solid: bool) -> Result<Assembled> {
if record_loops.is_empty() {
return Err(fail(format!("face {f} has no loop")));
}
record_faces.push(b::Face { surface: face.surface.clone(), domain: *domain, loops: record_loops });
record_faces.push(b::Face { nurbs: None, surface: face.surface.clone(), domain: *domain, loops: record_loops });
}
let record_edges = edges
.iter()
+1 -1
View File
@@ -155,7 +155,7 @@ fn face_patch(body: &Body, face: usize) -> Result<Patch> {
let f = record.faces.get(face).ok_or_else(|| invalid(format!("the tool body has no face {face}")))?;
let forward = body.face_forward(face);
// The surface's own parameter box, unbounded where it is.
let dom = SurfaceEval::new(&f.surface).map_err(|e| invalid(format!("the split tool's surface: {e}")))?.domain();
let dom = SurfaceEval::new(f.parameters()).map_err(|e| invalid(format!("the split tool's surface: {e}")))?.domain();
let range = |k: usize| dom.range[k].map_or(ALL, |(a, b)| [a, b]);
Ok(Patch { surface: f.surface.clone(), domain: [range(0), range(1)], flip: !forward })
}
+2 -2
View File
@@ -225,7 +225,7 @@ fn planned(body: &Body, at: &[Vec3], r: &Ruled, handles: &[Option<Vec<Vec3>>]) -
*count.entry(*i).or_default() += 1;
}
}
let surfaces: Vec<Option<SurfaceEval>> = rec.faces.iter().map(|f| SurfaceEval::new(&f.surface).ok()).collect();
let surfaces: Vec<Option<SurfaceEval>> = rec.faces.iter().map(|f| SurfaceEval::new(f.parameters()).ok()).collect();
let normal = |face: usize, p: Vec3| normal_at(surfaces.get(face)?.as_ref()?, body.face_forward(face), p);
// The face each edge takes its directions from: the picked face where it borders the edge,
// carried on along the chain to the faces meeting it smoothly (a cylinder's halves); with no
@@ -1001,7 +1001,7 @@ fn face_of(mut parts: Parts, surface: b::Surface, uses: Vec<(b::Curve, [f64; 2],
}
_ => uv_bounds(&loops).ok_or_else(|| KernelError::Invalid("a ruled face encloses no area".into()))?,
};
parts.faces.push((b::Face { surface, domain, loops }, true));
parts.faces.push((b::Face { nurbs: None, surface, domain, loops }, true));
Ok(parts.canonical()?.brep)
}
+4 -4
View File
@@ -379,7 +379,7 @@ pub(crate) fn tensor_sheet(n: &NurbsSurface) -> Result<Body> {
parts.edges.push(b::Edge { curve, interval, vertices: [index(a)?, index(z)?], tolerance: parts.tolerance, uses: Vec::new() });
uses.push(b::OrientedEdge { edge: index(i)?, forward, pcurve: uv_line(pa, pz, interval) });
}
parts.faces.push((b::Face { surface: spline_surface(n), domain: n.domain(), loops: vec![b::Loop::Edges { edges: uses }] }, true));
parts.faces.push((b::Face { nurbs: None, surface: spline_surface(n), domain: n.domain(), loops: vec![b::Loop::Edges { edges: uses }] }, true));
surface_body(&parts.canonical()?.brep)
}
@@ -396,7 +396,7 @@ pub fn patch_region(plane: &Plane, region: &Region2) -> Result<Body> {
let holes = region.holes.iter().map(|h| h.ccw().reversed());
let loops = std::iter::once(region.outer.ccw()).chain(holes).map(|lp| sketch_loop(&mut parts, plane, &lp.segs)).collect::<Result<Vec<_>>>()?;
let domain = uv_box(&loops).ok_or_else(|| invalid("the region has no area"))?;
parts.faces.push((b::Face { surface: crate::freeform::plane_surface(plane), domain, loops }, true));
parts.faces.push((b::Face { nurbs: None, surface: crate::freeform::plane_surface(plane), domain, loops }, true));
let c = parts.canonical()?;
let h = record_history(&c.brep, 0, vec![record::generated(Vec::new())], |_, _, _| Vec::new());
let (body, same) = record_body(&c.brep)?;
@@ -842,7 +842,7 @@ pub fn extend(body: &Body, at: &[Vec3], distance: f64) -> Result<Body> {
let loops = vec![b::Loop::Edges { edges }];
let domain = uv_box(&loops).ok_or_else(|| invalid("the extended face has no area"))?;
let forward = face_sides(r).first().copied().unwrap_or(true);
parts.faces.push((b::Face { surface, domain, loops }, forward));
parts.faces.push((b::Face { nurbs: None, surface, domain, loops }, forward));
let c = parts.canonical()?;
let h = record_history(&c.brep, 1, vec![record::from_face(0, 0, Change::Modified)], |e, _, _| {
c.edges.get(e).and_then(|p| rim.get(*p)).map(|u| EdgeOrigin::Edge { operand: 0, edge: u.edge as usize }).into_iter().collect()
@@ -1325,7 +1325,7 @@ mod imp {
let pl = Plane { origin: at, x: u, y: n.cross(u) };
let lp = sketch_loop(&mut parts, &pl, &segs)?;
let domain = uv_box(std::slice::from_ref(&lp)).ok_or_else(|| invalid("mitre plane"))?;
parts.faces.push((b::Face { surface: crate::freeform::plane_surface(&pl), domain, loops: vec![lp] }, true));
parts.faces.push((b::Face { nurbs: None, surface: crate::freeform::plane_surface(&pl), domain, loops: vec![lp] }, true));
}
SmBrep::from_record(&parts.canonical()?.brep, true).map_err(sm("sweep mitre"))
}
+1 -1
View File
@@ -217,7 +217,7 @@ mod tests {
let face = &body.faces[u.face as usize];
let brep::Loop::Edges { edges: loop_edges } = &face.loops[u.r#loop as usize] else { panic!("an edge loop") };
let oe = &loop_edges[u.position as usize];
let Ok((s, map)) = crate::tess::surface::eval_surface(&face.surface, face.domain) else { panic!("evaluates") };
let Ok((s, map)) = crate::tess::surface::eval_surface(face.parameters(), face.domain) else { panic!("evaluates") };
for (t, p) in n.params.iter().zip(&n.points) {
let Some(q) = oe.pcurve.eval(*t) else { panic!("pcurve at {t}") };
let q = map.to_eval(q);
+2 -2
View File
@@ -135,7 +135,7 @@ pub(crate) fn plan(b: &brep::Brep, tol: f64, turn: Option<size::Turn>) -> Result
let evals: Vec<(EvalSurface, surface::Params)> = faces
.iter()
.enumerate()
.map(|(fi, f)| surface::eval_surface(&f.surface, f.domain).map_err(|e| unsupported(format!("face {fi} ({}): {e}", kind(&f.surface)))))
.map(|(fi, f)| surface::eval_surface(f.parameters(), f.domain).map_err(|e| unsupported(format!("face {fi} ({}): {e}", kind(&f.surface)))))
.collect::<Result<_>>()?;
let (evals, params): (Vec<EvalSurface>, Vec<surface::Params>) = evals.into_iter().unzip();
let curved = evals.iter().filter(|e| !matches!(e, EvalSurface::Plane { .. })).count();
@@ -160,7 +160,7 @@ pub(crate) fn plan(b: &brep::Brep, tol: f64, turn: Option<size::Turn>) -> Result
};
let loops = boundary(b, fi, face, eval, map, &nodes, &rules)?;
let mut stats = PlanStats::default();
let fp = plan_face(fi, kind(&face.surface), eval, &loops, &rules, *size, &mut stats)?;
let fp = plan_face(fi, kind(face.parameters()), eval, &loops, &rules, *size, &mut stats)?;
let fwd = forward.get(fi).copied().unwrap_or(true);
let fp =
FacePlan { flip: !fwd, triangles: if fwd { fp.triangles } else { fp.triangles.into_iter().map(|[a, b, c]| [a, c, b]).collect() }, ..fp };
+2 -2
View File
@@ -180,11 +180,11 @@ mod tests {
brep::Surface::Cylinder { .. } | brep::Surface::Cone { .. } | brep::Surface::Sphere { .. } | brep::Surface::Torus { .. } => 1,
_ => 2,
};
let (eval, map) = match eval_surface(&face.surface, face.domain) {
let (eval, map) = match eval_surface(face.parameters(), face.domain) {
Ok(e) => e,
Err(e) => panic!("not evaluable: {e} ({:?})", face.surface),
};
let Ok(reference) = SurfaceEval::new(&face.surface) else { panic!("the record's evaluator: {:?}", face.surface) };
let Ok(reference) = SurfaceEval::new(face.parameters()) else { panic!("the record's evaluator: {:?}", face.surface) };
let [[u0, u1], [v0, v1]] = face.domain;
for i in 0..=6 {
for j in 0..=6 {
+1 -1
View File
@@ -138,7 +138,7 @@ fn patch_faces(patches: &[BezierPatch]) -> Result<Parts> {
let ctrl: Vec<HPoint> = (0..=pt).flat_map(|j| (0..=ps).map(move |i| (i, j))).map(|(i, j)| p.point(i, j)).collect();
let surface = NurbsSurface::from_homogeneous([ps, pt], [knots(ps), knots(pt)], ctrl).map_err(spline_err)?;
parts.faces.push((
b::Face { surface: spline_surface(&surface), domain: [[0.0, 1.0], [0.0, 1.0]], loops: vec![b::Loop::Edges { edges: uses }] },
b::Face { nurbs: None, surface: spline_surface(&surface), domain: [[0.0, 1.0], [0.0, 1.0]], loops: vec![b::Loop::Edges { edges: uses }] },
true,
));
}
+59 -40
View File
@@ -305,22 +305,59 @@ fn frame(origin: [f64; 3], axis: [f64; 3], x: [f64; 3]) -> b::Frame {
// ---- Geometry ---------------------------------------------------------------------------
/// The schema record of each face's surface, in face order.
fn surfaces(flat: View, n_faces: usize) -> Res<Vec<b::Surface>> {
/// The schema record of each face's surface, in face order, and its NURBS form when the arrays give
/// one (`FACE_NURBS_FORM`: an analytic face with the NURBS SMLib holds it as).
fn surfaces(flat: View, n_faces: usize) -> Res<Vec<(b::Surface, Option<b::Surface>)>> {
let kinds = flat.i(FACE_SURFACE_TYPE);
if kinds.len() != n_faces {
return Err(invalid("face surface kinds"));
}
// The NURBS blocks, in face order: each NURBS face's surface and each face's NURBS form.
struct At {
j: usize,
cv: usize,
uk: usize,
vk: usize,
w: usize,
}
let next_nurbs = |at_: &mut At| -> Res<b::Surface> {
let j = at_.j;
let nu = count(at(flat.i(FACE_NURBS_U_VERTEX_COUNT), j, "NURBS face")?, "NURBS face")?;
let nv = count(at(flat.i(FACE_NURBS_V_VERTEX_COUNT), j, "NURBS face")?, "NURBS face")?;
let ou = count(at(flat.i(FACE_NURBS_U_ORDER), j, "NURBS face")?, "NURBS face")?;
let ov = count(at(flat.i(FACE_NURBS_V_ORDER), j, "NURBS face")?, "NURBS face")?;
let n = nu.checked_mul(nv).filter(|n| *n <= b::MAX_ITEMS).ok_or_else(|| invalid("NURBS face size"))?;
let slice = |v: &[f64], from: usize, len: usize| v.get(from..from + len).map(<[f64]>::to_vec).ok_or_else(|| invalid("NURBS face arrays"));
let pts = slice(flat.f(FACE_NURBS_CVS), 3 * at_.cv, 3 * n)?;
let u_knots = slice(flat.f(FACE_NURBS_U_KNOTS), at_.uk, nu + ou)?;
let v_knots = slice(flat.f(FACE_NURBS_V_KNOTS), at_.vk, nv + ov)?;
let weights = if flat.f(FACE_NURBS_WEIGHTS).is_empty() { vec![1.0; n] } else { slice(flat.f(FACE_NURBS_WEIGHTS), at_.w, n)? };
*at_ = At { j: j + 1, cv: at_.cv + n, uk: at_.uk + nu + ou, vk: at_.vk + nv + ov, w: at_.w + n };
let point = |p: usize| -> [f64; 3] { [pts[3 * p], pts[3 * p + 1], pts[3 * p + 2]] };
Ok(if weights.iter().all(|x| *x == 1.0) {
b::Surface::Spline { u_knots, v_knots, points: (0..nu).map(|r| (0..nv).map(|c| point(r * nv + c)).collect()).collect() }
} else {
let h = |p: usize| {
let (q, wt) = (point(p), weights[p]);
[q[0] * wt, q[1] * wt, q[2] * wt, wt]
};
b::Surface::RationalSpline { u_knots, v_knots, homogeneous: (0..nu).map(|r| (0..nv).map(|c| h(r * nv + c)).collect()).collect() }
})
};
let mut nurbs_at = At { j: 0, cv: 0, uk: 0, vk: 0, w: 0 };
let forms = flat.i(FACE_NURBS_FORM);
if !forms.is_empty() && forms.len() != n_faces {
return Err(invalid("face NURBS forms"));
}
let mut k = [0usize; 6];
let (mut cv, mut uk, mut vk, mut w) = (0usize, 0usize, 0usize, 0usize);
let mut out = Vec::with_capacity(n_faces);
for &kind in kinds {
for (f, &kind) in kinds.iter().enumerate() {
let slot = usize::try_from(kind).ok().filter(|s| *s < 6).ok_or_else(|| unsupported(format!("SMLib surface kind {kind}")))?;
let i = at(&k, slot, "kind")?;
if let Some(c) = k.get_mut(slot) {
*c += 1;
}
out.push(match kind {
let surface = match kind {
SURFACE_PLANE => {
let f = frame(
v3(flat.f(FACE_PLANE_ORIGIN), i, "plane")?,
@@ -368,35 +405,12 @@ fn surfaces(flat: View, n_faces: usize) -> Res<Vec<b::Surface>> {
major_radius: at(flat.f(FACE_TORUS_MAJOR_RADIUS), i, "torus")?,
minor_radius: at(flat.f(FACE_TORUS_MINOR_RADIUS), i, "torus")?,
},
SURFACE_NURBS => {
let nu = count(at(flat.i(FACE_NURBS_U_VERTEX_COUNT), i, "NURBS face")?, "NURBS face")?;
let nv = count(at(flat.i(FACE_NURBS_V_VERTEX_COUNT), i, "NURBS face")?, "NURBS face")?;
let ou = count(at(flat.i(FACE_NURBS_U_ORDER), i, "NURBS face")?, "NURBS face")?;
let ov = count(at(flat.i(FACE_NURBS_V_ORDER), i, "NURBS face")?, "NURBS face")?;
let n = nu.checked_mul(nv).filter(|n| *n <= b::MAX_ITEMS).ok_or_else(|| invalid("NURBS face size"))?;
let slice =
|a: &[f64], from: usize, len: usize| a.get(from..from + len).map(<[f64]>::to_vec).ok_or_else(|| invalid("NURBS face arrays"));
let pts = slice(flat.f(FACE_NURBS_CVS), 3 * cv, 3 * n)?;
let u_knots = slice(flat.f(FACE_NURBS_U_KNOTS), uk, nu + ou)?;
let v_knots = slice(flat.f(FACE_NURBS_V_KNOTS), vk, nv + ov)?;
let weights = if flat.f(FACE_NURBS_WEIGHTS).is_empty() { vec![1.0; n] } else { slice(flat.f(FACE_NURBS_WEIGHTS), w, n)? };
cv += n;
uk += nu + ou;
vk += nv + ov;
w += n;
let point = |p: usize| -> [f64; 3] { [pts[3 * p], pts[3 * p + 1], pts[3 * p + 2]] };
if weights.iter().all(|x| *x == 1.0) {
b::Surface::Spline { u_knots, v_knots, points: (0..nu).map(|a| (0..nv).map(|c| point(a * nv + c)).collect()).collect() }
} else {
let h = |p: usize| {
let (q, wt) = (point(p), weights[p]);
[q[0] * wt, q[1] * wt, q[2] * wt, wt]
};
b::Surface::RationalSpline { u_knots, v_knots, homogeneous: (0..nu).map(|a| (0..nv).map(|c| h(a * nv + c)).collect()).collect() }
}
}
SURFACE_NURBS => next_nurbs(&mut nurbs_at)?,
other => return Err(unsupported(format!("SMLib surface kind {other}"))),
});
};
// A NURBS face's own block came first; a form follows the analytic face's canonical entry.
let form = if kind != SURFACE_NURBS && forms.get(f) == Some(&1) { Some(next_nurbs(&mut nurbs_at)?) } else { None };
out.push((surface, form));
}
Ok(out)
}
@@ -675,6 +689,8 @@ fn onto(r: [f64; 2], natural: b::Natural) -> [f64; 2] {
/// One face before renumbering: loops relative to the record's normal, as (edge, forward).
#[derive(Clone)]
struct Face {
/// The NURBS form the face's (u, v) are on (`FACE_NURBS_FORM`), when it differs from `surface`.
nurbs: Option<b::Surface>,
surface: b::Surface,
domain: [[f64; 2]; 2],
/// SMLib's natural normal is the record's.
@@ -809,11 +825,13 @@ fn native_ordered(arrays: &Arrays) -> Res<(b::Brep, Report, Order)> {
return Err(invalid("face ranges"));
}
let mut faces = Vec::with_capacity(n_faces);
for (f, (surface, loops)) in surfaces.into_iter().zip(&face_loops).enumerate() {
for (f, ((surface, nurbs), loops)) in surfaces.into_iter().zip(&face_loops).enumerate() {
let r = range.get(4 * f..4 * f + 4).ok_or_else(|| invalid("face range"))?;
let natural = surface.natural();
// A face with a NURBS form is parameterised on it: its range and trims are that NURBS's.
let params = nurbs.as_ref().unwrap_or(&surface);
let natural = params.natural();
let domain = [onto([r[0], r[2]], natural[0]), onto([r[1], r[3]], natural[1])];
let spline = matches!(surface, b::Surface::Spline { .. } | b::Surface::RationalSpline { .. });
let spline = matches!(params, b::Surface::Spline { .. } | b::Surface::RationalSpline { .. });
let mut out = Vec::with_capacity(loops.len());
let mut records = Vec::with_capacity(loops.len());
let mut given = Vec::with_capacity(loops.len());
@@ -843,7 +861,7 @@ fn native_ordered(arrays: &Arrays) -> Res<(b::Brep, Report, Order)> {
out.push(Loop::Edges(l));
given.push(g);
}
faces.push(Face { surface, domain, agrees: true, loops: out, records, given });
faces.push(Face { nurbs, surface, domain, agrees: true, loops: out, records, given });
}
for face in &mut faces {
if outer_area(face, &edges, largest)? < 0.0 {
@@ -1154,7 +1172,8 @@ fn canonical(parts: Parts, n_vertices: usize, flat: View, tolerances: &Tolerance
Loop::Vertex(v) => Ok(Loop::Vertex(*v)),
})
.collect::<Res<_>>()?;
let mut curves = pcurves(&face.surface, face.domain, &renumbered, &face.given, &edge_records, tolerance)?.into_iter();
let mut curves =
pcurves(face.nurbs.as_ref().unwrap_or(&face.surface), face.domain, &renumbered, &face.given, &edge_records, tolerance)?.into_iter();
let mut loops = Vec::with_capacity(renumbered.len());
for (li, l) in renumbered.iter().enumerate() {
loops.push(match l {
@@ -1170,7 +1189,7 @@ fn canonical(parts: Parts, n_vertices: usize, flat: View, tolerances: &Tolerance
}
});
}
face_records.push(b::Face { surface: face.surface.clone(), domain: face.domain, loops });
face_records.push(b::Face { nurbs: face.nurbs.clone(), surface: face.surface.clone(), domain: face.domain, loops });
}
let positions = flat.f(VERTEX_POSITION);
let vertices = vertex_order
@@ -1461,7 +1480,7 @@ fn outer_area(face: &Face, edges: &[Edge], tolerance: f64) -> Res<f64> {
let records: Vec<b::Edge> =
edges.iter().map(|e| b::Edge { curve: e.curve.clone(), interval: e.interval, vertices: e.vertices, tolerance, uses: Vec::new() }).collect();
let given = face.given.first().map(std::slice::from_ref).unwrap_or(&[]);
let curves = pcurves(&face.surface, face.domain, &[Loop::Edges(outer.clone())], given, &records, tolerance)?;
let curves = pcurves(face.nurbs.as_ref().unwrap_or(&face.surface), face.domain, &[Loop::Edges(outer.clone())], given, &records, tolerance)?;
let mut pts: Vec<[f64; 2]> = Vec::new();
for (pc, &(e, forward)) in curves.first().map(Vec::as_slice).unwrap_or(&[]).iter().zip(outer) {
let edge = edges.get(e as usize).ok_or_else(|| invalid("edge"))?;
+1
View File
@@ -37,6 +37,7 @@ pub const FACE_NURBS_V_VERTEX_COUNT: i32 = 29;
pub const FACE_NURBS_U_ORDER: i32 = 30;
pub const FACE_NURBS_V_ORDER: i32 = 31;
pub const SOURCE: i32 = 32;
pub const FACE_NURBS_FORM: i32 = 33;
pub const TOLERANCE: i32 = 101;
pub const EXTENT: i32 = 102;
pub const FACE_RANGE: i32 = 103;
+38 -3
View File
@@ -169,11 +169,25 @@ fn laid_out(brep: &b::Brep, trusted: bool, analytic: bool) -> R<Arrays> {
let intervals: Vec<([f64; 2], f64)> = brep.edges.iter().map(|e| turned(&e.curve, e.interval)).collect();
// Faces, loops and edgeuses, with the surfaces and, on B-spline faces, the trims.
let any_spline_face = brep.faces.iter().any(|f| matches!(f.surface, b::Surface::Spline { .. } | b::Surface::RationalSpline { .. }));
let any_spline_face =
brep.faces.iter().any(|f| f.nurbs.is_some() || matches!(f.surface, b::Surface::Spline { .. } | b::Surface::RationalSpline { .. }));
let any_form = brep.faces.iter().any(|f| f.nurbs.is_some());
for f in &brep.faces {
a.i(flat::FACE_LOOP_COUNT).push(int(f.loops.len())?);
a.i(flat::FACE_TRIM_TYPE).push(0);
let spline = surface(&mut a, &f.surface, f.domain, analytic)?;
// A face with a NURBS form: what it is (its analytic entry), then the NURBS it is
// parameterised on, whose range and trims these are (FACE_NURBS_FORM).
let spline = match &f.nurbs {
Some(n) => {
surface(&mut a, &f.surface, f.domain, analytic)?;
nurbs_block(&mut a, n)?;
true
}
None => surface(&mut a, &f.surface, f.domain, analytic)?,
};
if any_form {
a.i(flat::FACE_NURBS_FORM).push(i64::from(f.nurbs.is_some()));
}
for l in &f.loops {
match l {
b::Loop::Vertex { vertex } => {
@@ -243,8 +257,13 @@ fn net(a: &mut Arrays, rows: impl Iterator<Item = ([f64; 3], f64)>) {
}
fn nurbs_face(a: &mut Arrays, u_knots: &[f64], v_knots: &[f64], nu: usize, nv: usize) -> R<()> {
let order = |k: &[f64], n: usize| k.len().checked_sub(n).filter(|o| *o >= 2).ok_or(Unsupported::Invalid("spline order"));
a.i(flat::FACE_SURFACE_TYPE).push(i64::from(flat::SURFACE_NURBS));
nurbs_sizes(a, u_knots, v_knots, nu, nv)
}
/// A NURBS block's sizes, orders and knots.
fn nurbs_sizes(a: &mut Arrays, u_knots: &[f64], v_knots: &[f64], nu: usize, nv: usize) -> R<()> {
let order = |k: &[f64], n: usize| k.len().checked_sub(n).filter(|o| *o >= 2).ok_or(Unsupported::Invalid("spline order"));
a.i(flat::FACE_NURBS_U_VERTEX_COUNT).push(int(nu)?);
a.i(flat::FACE_NURBS_V_VERTEX_COUNT).push(int(nv)?);
a.i(flat::FACE_NURBS_U_ORDER).push(int(order(u_knots, nu)?)?);
@@ -254,6 +273,22 @@ fn nurbs_face(a: &mut Arrays, u_knots: &[f64], v_knots: &[f64], nu: usize, nv: u
Ok(())
}
/// The NURBS block of a face's NURBS form, with no surface kind or range of its own.
fn nurbs_block(a: &mut Arrays, s: &b::Surface) -> R<()> {
match s {
b::Surface::Spline { u_knots, v_knots, points } => {
nurbs_sizes(a, u_knots, v_knots, points.len(), points.first().map_or(0, Vec::len))?;
net(a, points.iter().flatten().map(|p| (*p, 1.0)));
}
b::Surface::RationalSpline { u_knots, v_knots, homogeneous } => {
nurbs_sizes(a, u_knots, v_knots, homogeneous.len(), homogeneous.first().map_or(0, Vec::len))?;
net(a, homogeneous.iter().flatten().map(|h| ([h[0] / h[3], h[1] / h[3], h[2] / h[3]], h[3])));
}
_ => return Err(Unsupported::Invalid("NURBS form is not a NURBS")),
}
Ok(())
}
/// The face's surface and range; true for a B-spline surface (which carries trims).
fn surface(a: &mut Arrays, s: &b::Surface, domain: [[f64; 2]; 2], analytic_planes: bool) -> R<bool> {
let [[u0, u1], [v0, v1]] = domain;
+2 -1
View File
@@ -49,6 +49,7 @@ fn polyhedron(corners: &[[f64; 3]], cycles: &[&[usize]]) -> Record {
});
}
faces.push(Face {
nurbs: None,
surface: Surface::Plane { origin: o, u_axis: x, v_axis: y },
domain: [[lo[0], hi[0]], [lo[1], hi[1]]],
loops: vec![Loop::Edges { edges }],
@@ -132,7 +133,7 @@ fn a_plane_on_other_axes_goes_as_smlibs_plane() {
// non-unit and check it still goes as SMLib's plane, the volume unchanged.
let c = [[0., 0., 0.], [6., 0., 0.], [0., 6., 0.], [0., 0., 6.]];
let mut r = polyhedron(&c, &[&[0, 2, 1], &[0, 1, 3], &[1, 2, 3], &[2, 0, 3]]);
if let Some(Face { surface: Surface::Plane { u_axis, v_axis, .. }, domain, loops }) = r.faces.get_mut(0) {
if let Some(Face { nurbs: None, surface: Surface::Plane { u_axis, v_axis, .. }, domain, loops }) = r.faces.get_mut(0) {
*u_axis = scale(*u_axis, 2.0);
*v_axis = scale(*v_axis, 2.0);
*domain = domain.map(|d| d.map(|x| x / 2.0));
+1 -1
View File
@@ -883,7 +883,7 @@ impl<'b, 'a> Builder<'b, 'a> {
}
}
let domain = *domains.get(f as usize).ok_or("face reference")?;
faces.push(Face { surface: face.surface.clone(), domain, loops });
faces.push(Face { nurbs: None, surface: face.surface.clone(), domain, loops });
}
let mut out_shells = Vec::with_capacity(shells.len());
for s in &shells {
+53 -3
View File
@@ -454,7 +454,14 @@ impl BrepWriter<'_, '_, '_> {
if let Some(Some(s)) = self.surfaces.get(f as usize) {
return Ok(s.clone());
}
let (s, map) = surface(&face_of(self.b, f)?.surface).map_err(|m| format!("face {f}: {m}"))?;
let face = face_of(self.b, f)?;
let (s, map) = surface(&face.surface).map_err(|m| format!("face {f}: {m}"))?;
// A face on its NURBS form is written as the analytic surface it is, with no pcurves
// (STEP's are in the angle); only which way its normal points carries over.
let map = match &face.nurbs {
Some(n) => form_map(&face.surface, n, face.domain, map).ok_or_else(|| format!("face {f}: its NURBS form has no normal"))?,
None => map,
};
let s = (Arc::new(s), map);
if let Some(slot) = self.surfaces.get_mut(f as usize) {
*slot = Some(s.clone());
@@ -499,14 +506,21 @@ impl BrepWriter<'_, '_, '_> {
let b = self.b;
let mut associated = Vec::with_capacity(edge.uses.len());
for u in &edge.uses {
let Some(Loop::Edges { edges }) = face_of(b, u.face)?.loops.get(u.r#loop as usize) else {
let face = face_of(b, u.face)?;
if face.nurbs.is_some() {
continue;
}
let Some(Loop::Edges { edges }) = face.loops.get(u.r#loop as usize) else {
return Err(format!("face {}: loop {} is no edge loop", u.face, u.r#loop));
};
let used = edges.get(u.position as usize).ok_or_else(|| format!("face {}: no use at {}", u.face, u.position))?;
let (surface, map) = self.surface(u.face)?;
associated.push(Associated::Pcurve(Pcurve { surface, curve: Arc::new(pcurve(&used.pcurve, map)?) }));
}
let seam = matches!(edge.uses.as_slice(), [a, b] if a.face == b.face);
if associated.is_empty() {
return Ok(self.g.curve(&curve_3d));
}
let seam = associated.len() == 2 && matches!(edge.uses.as_slice(), [a, b] if a.face == b.face);
let kind = if seam { SurfaceCurveKind::Seam } else { SurfaceCurveKind::Surface };
Ok(self.g.curve(&Curve::SurfaceCurve { kind, curve_3d: Arc::new(curve_3d), associated, master: PreferredRep::Curve3d }))
}
@@ -575,3 +589,39 @@ fn signed_area(ring: &[[f64; 2]]) -> f64 {
let n = ring.len();
0.5 * (0..n).filter_map(|i| Some((ring.get(i)?, ring.get((i + 1) % n)?))).map(|(p, q)| p[0] * q[1] - q[0] * p[1]).sum::<f64>()
}
/// The map of a face on its NURBS form `n`: no pcurve goes through it, so it only says whether
/// the face's natural normal (the NURBS's, which the loops follow) is the STEP surface's. The
/// record's cylinder, cone, sphere and torus have their natural normal pointing away from the
/// axis (the sphere's centre, the torus's centre circle); `None` when the NURBS has no normal at
/// its domain's centre.
fn form_map(analytic: &BSurface, n: &BSurface, domain: [[f64; 2]; 2], map: Map) -> Option<Map> {
let eval = solidcraft_geom::brep::eval::SurfaceEval::new(n).ok()?;
let (p, su, sv) = eval.derivs(0.5 * (domain[0][0] + domain[0][1]), 0.5 * (domain[1][0] + domain[1][1]))?;
let normal = su.cross(sv);
let away = |f: &Frame| {
let o = solidcraft_geom::Vec3::new(f.origin[0], f.origin[1], f.origin[2]);
let z = solidcraft_geom::Vec3::new(f.x_axis[0], f.x_axis[1], f.x_axis[2]).cross(solidcraft_geom::Vec3::new(
f.y_axis[0],
f.y_axis[1],
f.y_axis[2],
));
let d = p - o;
(o, z, d - z * d.dot(z))
};
let outward = match analytic {
BSurface::Cylinder { frame, .. } | BSurface::Cone { frame, .. } => away(frame).2,
BSurface::Sphere { frame, .. } => p - away(frame).0,
BSurface::Torus { frame, major_radius, .. } => {
let (o, _, radial) = away(frame);
p - (o + radial.normalized()? * *major_radius)
}
_ => return None,
};
let along = normal.dot(outward);
if !(along.abs() > 0.0) {
return None;
}
let keeps = (along > 0.0) == map.keeps_normal();
Some(Map { m: [[1.0, 0.0], [0.0, if keeps { 1.0 } else { -1.0 }]] })
}
+9
View File
@@ -143,6 +143,15 @@ its producer holds it (truck's `RevolutedCurve` under a `Processor`): nothing is
baked into the profile, and `forward: false` swaps u and v, which reverses the
natural normal.
A face on a `cylinder`, `cone`, `sphere` or `torus` may also carry `nurbs`, a
`spline` or `rational_spline`: the NURBS form it is parameterised on, as SMLib
holds it (every SMLib analytic surface is a B-spline surface, and its trims are
in that NURBS's parameters, not the angle). The face's domain and pcurves are
then in the NURBS's (u, v), its natural normal is the NURBS's, and `surface`
only says what the face is (axis, radius). Pcurves on such a face are SMLib's
own trims, with nothing fitted. A STEP writer writes the analytic surface and
the face's edges without pcurves.
### Parameter ranges
An edge interval and each side of a face domain are finite with min < max. In a