mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-11 19:22:01 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c441805f0 | |||
| 9f171f9d35 | |||
| ccd2be8de2 | |||
| 8dfc641c5b | |||
| 6b5dc2d0a6 | |||
| 50833c3cdc | |||
| 79facfc8af | |||
| 0cf51cd24e | |||
| fd364af620 | |||
| 5570bdb6a9 | |||
| 73412a9a62 | |||
| 265a69e87a |
@@ -480,7 +480,7 @@ fn output_checks(c: &mut Checks, want: &Value, out: &str, results: &[Value], rel
|
||||
|
||||
/// The word a `surface_types` check names a face's surface by, as the record holds it: an
|
||||
/// analytic kind, `ball_patch` (a corner's patch on a sphere), `bspline` (a B-spline or NURBS
|
||||
/// surface) or `revolution` (a revolved curve).
|
||||
/// surface), `revolution` (a revolved curve) or `blend` (a round by its definition, on its net).
|
||||
fn surface_word(s: &solidcore_record::brep::Surface) -> &'static str {
|
||||
use solidcore_record::brep::Surface as S;
|
||||
match s {
|
||||
@@ -492,6 +492,7 @@ fn surface_word(s: &solidcore_record::brep::Surface) -> &'static str {
|
||||
S::Torus { .. } => "torus",
|
||||
S::Spline { .. } | S::RationalSpline { .. } => "bspline",
|
||||
S::PlacedRevolution { .. } => "revolution",
|
||||
S::Blend { .. } => "blend",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
//! A body's face may save `surface` as a `blend` {definition}: a round by its definition (a
|
||||
//! procedural blend's, or a round of an exact class's whose net has no analytic label), always
|
||||
//! with its `nurbs`, the net. An older design has these faces as plain NURBS faces and keeps them
|
||||
//! so. The last release does not read a model cache with this kind (a cache miss: the design is
|
||||
//! evaluated again) and warns about the newer format number.
|
||||
@@ -114,6 +114,7 @@ impl SurfaceEval {
|
||||
}
|
||||
// A label with no (u, v) of its own: its face evaluates its NURBS form.
|
||||
Surface::BallPatch { .. } => Err(unsupported("a ball patch's label (evaluate its NURBS form)")),
|
||||
Surface::Blend { .. } => Err(unsupported("a blend's label (evaluate its NURBS form)")),
|
||||
Surface::Spline { u_knots, v_knots, points } => {
|
||||
let rows: Vec<Vec<[f64; 4]>> = points.iter().map(|r| r.iter().map(|p| [p[0], p[1], p[2], 1.0]).collect()).collect();
|
||||
eval(nurbs(u_knots, v_knots, &rows)?)
|
||||
|
||||
@@ -68,8 +68,9 @@ impl Rigid {
|
||||
Frame { origin: self.point(f.origin), x_axis: self.direction(f.x_axis), y_axis: self.direction(f.y_axis) }
|
||||
}
|
||||
|
||||
fn curve(&self, c: &Curve) -> Curve {
|
||||
match c {
|
||||
/// The curve moved; `None` for one on a blend by its definition ([`moved`]).
|
||||
fn curve(&self, c: &Curve) -> Option<Curve> {
|
||||
Some(match c {
|
||||
Curve::Line { start, end } => Curve::Line { start: self.point(*start), end: self.point(*end) },
|
||||
Curve::Circle { frame, radius } => Curve::Circle { frame: self.frame(frame), radius: *radius },
|
||||
Curve::Ellipse { frame, radii } => Curve::Ellipse { frame: self.frame(frame), radii: *radii },
|
||||
@@ -79,15 +80,17 @@ impl Rigid {
|
||||
Curve::RationalSpline { knots: knots.clone(), homogeneous: homogeneous.iter().map(|h| self.homogeneous(*h)).collect() }
|
||||
}
|
||||
Curve::SurfaceIntersection { surfaces, leader, tolerance } => Curve::SurfaceIntersection {
|
||||
surfaces: [Box::new(self.surface(&surfaces[0])), Box::new(self.surface(&surfaces[1]))],
|
||||
leader: Box::new(self.curve(leader)),
|
||||
surfaces: [Box::new(self.surface(&surfaces[0])?), Box::new(self.surface(&surfaces[1])?)],
|
||||
leader: Box::new(self.curve(leader)?),
|
||||
tolerance: *tolerance,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn surface(&self, s: &Surface) -> Surface {
|
||||
match s {
|
||||
/// The surface moved; `None` for a blend by its definition, which the kernel moves with its
|
||||
/// net ([`moved`]).
|
||||
fn surface(&self, s: &Surface) -> Option<Surface> {
|
||||
Some(match s {
|
||||
Surface::Plane { origin, u_axis, v_axis } => {
|
||||
Surface::Plane { origin: self.point(*origin), u_axis: self.direction(*u_axis), v_axis: self.direction(*v_axis) }
|
||||
}
|
||||
@@ -122,7 +125,9 @@ impl Rigid {
|
||||
matrix: compose(&self.matrix(), matrix),
|
||||
forward: *forward,
|
||||
},
|
||||
}
|
||||
// Moving a definition is the kernel's: its supports, leader and seeds move with it.
|
||||
Surface::Blend { .. } => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,20 +143,28 @@ fn compose(a: &[[f64; 4]; 4], b: &[[f64; 4]; 4]) -> [[f64; 4]; 4] {
|
||||
}
|
||||
|
||||
/// This record moved by `motion` (module docs): topology, parameters and pcurves unchanged.
|
||||
pub fn moved(brep: &Brep, motion: &Rigid) -> Brep {
|
||||
/// `None` for a record with a blend by its definition: moving its supports, leader and seeds is
|
||||
/// the kernel's, which moves the definition with its net (its transform).
|
||||
pub fn moved(brep: &Brep, motion: &Rigid) -> Option<Brep> {
|
||||
let mut out = brep.clone();
|
||||
for f in &mut out.faces {
|
||||
f.surface = motion.surface(&f.surface);
|
||||
f.nurbs = f.nurbs.as_ref().map(|n| motion.surface(n));
|
||||
f.surface = motion.surface(&f.surface)?;
|
||||
f.nurbs = match &f.nurbs {
|
||||
Some(n) => Some(motion.surface(n)?),
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
for e in &mut out.edges {
|
||||
e.curve = motion.curve(&e.curve);
|
||||
e.nurbs = e.nurbs.as_ref().map(|n| motion.curve(n));
|
||||
e.curve = motion.curve(&e.curve)?;
|
||||
e.nurbs = match &e.nurbs {
|
||||
Some(n) => Some(motion.curve(n)?),
|
||||
None => None,
|
||||
};
|
||||
}
|
||||
for v in &mut out.vertices {
|
||||
v.point = motion.point(v.point);
|
||||
}
|
||||
out
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// A linear map that keeps handedness (rows of a matrix with positive determinant) and a
|
||||
@@ -245,7 +258,8 @@ impl Affine {
|
||||
| Surface::Sphere { .. }
|
||||
| Surface::BallPatch { .. }
|
||||
| Surface::Torus { .. }
|
||||
| Surface::PlacedRevolution { .. } => return None,
|
||||
| Surface::PlacedRevolution { .. }
|
||||
| Surface::Blend { .. } => return None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ use serde::Serialize;
|
||||
|
||||
use crate::section::CurveAccuracy;
|
||||
use curve::{C3, as_circle};
|
||||
use solidcore_record::brep::Brep;
|
||||
use solidcore_record::brep::{Brep, Surface};
|
||||
use solidcore_record::{Seg2, Vec2, Vec3};
|
||||
use surface::{Analytic, Hit, S3};
|
||||
use trim::Trim;
|
||||
@@ -405,12 +405,19 @@ fn anchor(b: &Brep) -> Vec3 {
|
||||
}
|
||||
|
||||
/// The body's records with every position moved by `-d`: points, control points and NURBS forms,
|
||||
/// a placed revolution through its matrix; regions, parameters and pcurves as they are.
|
||||
/// a placed revolution through its matrix; regions, parameters and pcurves as they are. A blend
|
||||
/// by its definition is drawn as its net, its NURBS form: the definition is no line of the drawing.
|
||||
fn shifted(b: &Brep, d: Vec3) -> Brep {
|
||||
crate::brep::placement::moved(
|
||||
b,
|
||||
&crate::brep::placement::Rigid { rotation: [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]], translation: [-d.x, -d.y, -d.z] },
|
||||
)
|
||||
let mut nets = b.clone();
|
||||
for f in &mut nets.faces {
|
||||
if matches!(f.surface, Surface::Blend { .. })
|
||||
&& let Some(net) = f.nurbs.take()
|
||||
{
|
||||
f.surface = net;
|
||||
}
|
||||
}
|
||||
let motion = crate::brep::placement::Rigid { rotation: [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]], translation: [-d.x, -d.y, -d.z] };
|
||||
crate::brep::placement::moved(&nets, &motion).unwrap_or(nets)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
|
||||
@@ -143,6 +143,8 @@ impl S3 {
|
||||
}
|
||||
Surface::RationalSpline { u_knots, v_knots, homogeneous } => S3::Nurbs(Box::new(Nurbs::new(u_knots, v_knots, homogeneous)?)),
|
||||
Surface::PlacedRevolution { .. } => placed_revolution(face)?,
|
||||
// A label with no surface of its own: its face is drawn as its NURBS form.
|
||||
Surface::Blend { .. } => return Err(HlrError::Unsupported("a blend's label (draw its NURBS form)")),
|
||||
label => S3::bounded(label, form_box(face, label)?)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -443,7 +443,7 @@ impl SurfaceKind {
|
||||
S::Cone { .. } => SurfaceKind::Cone,
|
||||
S::Sphere { .. } | S::BallPatch { .. } => SurfaceKind::Sphere,
|
||||
S::Torus { .. } => SurfaceKind::Torus,
|
||||
S::Spline { .. } | S::RationalSpline { .. } | S::PlacedRevolution { .. } => SurfaceKind::Freeform,
|
||||
S::Spline { .. } | S::RationalSpline { .. } | S::PlacedRevolution { .. } | S::Blend { .. } => SurfaceKind::Freeform,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,6 +347,7 @@ fn iso_record(surface: &brep::Surface, along_u: bool, fixed: f64, (a, b): (f64,
|
||||
}
|
||||
// A label: the face's (u, v) are its NURBS form's, whose rows are its iso curves.
|
||||
Surface::BallPatch { .. } => return Err(KernelError::Invalid("an iso curve of a ball patch's label (take its NURBS form's)".into())),
|
||||
Surface::Blend { .. } => return Err(KernelError::Invalid("an iso curve of a blend's label (take its NURBS form's)".into())),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ pub(crate) fn surf_of(b: &Brep, face: usize) -> Option<Surf> {
|
||||
let (o, a) = axes(frame)?;
|
||||
Surf::Torus { o, a, big: *major_radius, h: 0.0, r: *minor_radius, convex: out }
|
||||
}
|
||||
Surface::Spline { .. } | Surface::RationalSpline { .. } | Surface::PlacedRevolution { .. } => return None,
|
||||
Surface::Spline { .. } | Surface::RationalSpline { .. } | Surface::PlacedRevolution { .. } | Surface::Blend { .. } => return None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -176,7 +176,8 @@ pub fn transform_matrix(body: &Body, m: [[f64; 4]; 4]) -> Result<Body> {
|
||||
}
|
||||
let sources = body.transformed_sources(m)?;
|
||||
let exact = match Rigid::of_matrix(&m) {
|
||||
Some(motion) => Some(solidcraft_geom::brep::placement::moved(body.record(), &motion)),
|
||||
// (a body with a blend by its definition is moved by the kernel, the definition with it)
|
||||
Some(motion) => solidcraft_geom::brep::placement::moved(body.record(), &motion),
|
||||
None => solidcraft_geom::brep::placement::mapped(body.record(), &m),
|
||||
};
|
||||
let (placed, sources, paint) = match exact {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
//! What surface a face lies on (plane, cylinder, cone, sphere, torus), for construction
|
||||
//! geometry: axes through round faces, centres of spheres and tori.
|
||||
|
||||
use solidcore_ops::face_surface::{Exact, FaceSurface as ExactSurface};
|
||||
use solidcore_record::Vec3;
|
||||
use solidcore_record::brep::{Brep, Curve};
|
||||
use solidcore_record::brep::{Brep, Curve, Surface};
|
||||
use solidcraft_geom::UnitVec3;
|
||||
use solidcraft_geom::brep::eval::CurveEval;
|
||||
use solidcraft_geom::prelude::*;
|
||||
@@ -50,8 +51,17 @@ pub fn face_surface(b: &Body, face: usize) -> Option<FaceSurface> {
|
||||
surface_of(b.record(), face)
|
||||
}
|
||||
|
||||
/// The analytic surface of face `face` of the record `b`, as the record holds it.
|
||||
/// The analytic surface of face `face` of the record `b`, as the record holds it; a round kept
|
||||
/// as its definition is the torus that definition gives, when it is one.
|
||||
pub(crate) fn surface_of(b: &Brep, face: usize) -> Option<FaceSurface> {
|
||||
if matches!(b.faces.get(face)?.surface, Surface::Blend { .. }) {
|
||||
return match *ExactSurface::of(b, face).ok()?.exact() {
|
||||
Exact::Torus { centre, axis, major, minor } => {
|
||||
Some(FaceSurface::Torus { center: Vec3::from(centre), axis: Vec3::from(axis).unit()?, major, minor })
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
Some(match surf_of(b, face)? {
|
||||
Surf::Plane { n, d } => FaceSurface::Plane { normal: n.unit()?, point: n * d },
|
||||
Surf::Cylinder { o, a, r, .. } => FaceSurface::Cylinder { origin: o, axis: a.unit()?, radius: r },
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
//! A round's definition crosses through the body's record: the round on a bore's rim (a plane
|
||||
//! square to a cylinder's axis, the exact circle class) is written as its definition with its net,
|
||||
//! read back and rebuilt as the same record, and a later operation on the body builds.
|
||||
|
||||
use solidcore_record::Vec3;
|
||||
use solidcore_record::brep::{Brep, Surface};
|
||||
use solidcraft_kernel::*;
|
||||
|
||||
/// The faces of a record that are blends by their definition.
|
||||
fn blends(record: &Brep) -> Vec<usize> {
|
||||
record.faces.iter().enumerate().filter(|(_, f)| matches!(f.surface, Surface::Blend { .. })).map(|(i, _)| i).collect()
|
||||
}
|
||||
|
||||
fn rounded_bore() -> Body {
|
||||
let block = box_solid(Vec3::ZERO, Vec3::new(30.0, 30.0, 10.0)).unwrap();
|
||||
let bore = cylinder(Vec3::new(15.0, 15.0, 0.0), Vec3::Z, 5.0, 10.0).unwrap();
|
||||
let drilled = boolean(&block, &bore, BoolOp::Cut).unwrap().unwrap();
|
||||
fillet(&drilled, &[Vec3::new(20.0, 15.0, 10.0)], 0.3).unwrap()
|
||||
}
|
||||
|
||||
/// The round's faces are blends by their definition, each with its net as its form; the record
|
||||
/// written, read back and rebuilt in the kernel is the same record.
|
||||
#[test]
|
||||
fn a_rounds_definition_survives_save_reopen_and_rebuild() {
|
||||
let body = rounded_bore();
|
||||
let record = brep_record(&body).unwrap();
|
||||
let faces = blends(&record);
|
||||
assert!(!faces.is_empty(), "the round is written by its definition");
|
||||
for &i in &faces {
|
||||
assert!(matches!(record.faces[i].nurbs, Some(Surface::Spline { .. } | Surface::RationalSpline { .. })), "face {i} has its net");
|
||||
}
|
||||
let reopened = decode_native_body(&encode_native_body(&body).unwrap()).unwrap();
|
||||
assert_eq!(brep_record(&reopened).unwrap(), record, "saved and opened, number for number");
|
||||
let rebuilt = body_from_smlib(&smlib_body(&reopened).unwrap()).unwrap();
|
||||
let again = brep_record(&rebuilt).unwrap();
|
||||
assert_eq!(blends(&again), faces, "rebuilt, the same faces are blends");
|
||||
for &i in &faces {
|
||||
assert_eq!(again.faces[i].surface, record.faces[i].surface, "face {i}'s definition, unchanged");
|
||||
}
|
||||
}
|
||||
|
||||
/// A body whose round came back from its record takes later operations: a shell open at the top.
|
||||
#[test]
|
||||
fn a_body_rebuilt_from_its_record_takes_a_shell() {
|
||||
let body = rounded_bore();
|
||||
let reopened = decode_native_body(&encode_native_body(&body).unwrap()).unwrap();
|
||||
let rebuilt = body_from_smlib(&smlib_body(&reopened).unwrap()).unwrap();
|
||||
let shelled = shell(&rebuilt, &[Vec3::new(2.0, 2.0, 0.0)], 1.0).unwrap();
|
||||
assert!(shelled.validity().is_empty(), "{:?}", shelled.validity());
|
||||
}
|
||||
@@ -25,6 +25,7 @@ mod boolean_tests;
|
||||
mod convert;
|
||||
#[cfg(test)]
|
||||
mod convert_tests;
|
||||
pub(crate) mod definition;
|
||||
pub(crate) mod dump;
|
||||
pub(crate) mod file;
|
||||
pub(crate) mod history;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
//! A body's blend faces and their record's definitions, through the modelling layer
|
||||
//! (`solidcore_ops::blend_definition`): read off the body's faces for its record, and given back to
|
||||
//! the faces of a body built from a record, rebuilt and their nets certified again. The flat
|
||||
//! arrays carry only the nets.
|
||||
|
||||
use solidcore_record::brep::blend::BlendDefinition;
|
||||
|
||||
use super::{Body, with_body};
|
||||
use crate::{Error, Result};
|
||||
|
||||
impl super::dump::Answer for solidcore_ops::Result<Vec<Option<BlendDefinition>>> {}
|
||||
impl super::dump::Answer for solidcore_ops::Result<()> {}
|
||||
|
||||
/// The modelling layer's refusal, as this crate's.
|
||||
fn refused(e: solidcore_ops::Error) -> Error {
|
||||
Error::Solidcore(e.to_string())
|
||||
}
|
||||
|
||||
/// Each face's definition, in the body's face order (the arrays'), where its surface carries one a
|
||||
/// record keeps.
|
||||
pub(crate) fn definitions(body: &Body) -> Result<Vec<Option<BlendDefinition>>> {
|
||||
with_body("blend_definition::definitions", body, |k, b| Ok(solidcore_ops::blend_definition::definitions(k, b)))?.map_err(refused)
|
||||
}
|
||||
|
||||
/// Give the faces `faces` names (by their index in the body's face order) their definitions back.
|
||||
pub(crate) fn adopt(body: &Body, faces: &[(usize, &BlendDefinition)]) -> Result<()> {
|
||||
with_body("blend_definition::adopt", body, |k, b| Ok(solidcore_ops::blend_definition::adopt(k, b, faces)))?.map_err(refused)
|
||||
}
|
||||
@@ -101,7 +101,10 @@ impl Brep {
|
||||
/// [`Brep::to_native`], with what the mapping kept as SMLib wrote it.
|
||||
pub fn to_native_report(&self) -> std::result::Result<(b::Brep, Report), ExportError> {
|
||||
let arrays = self.arrays().map_err(ExportError::Smlib)?;
|
||||
let r = native(&arrays);
|
||||
let r = native_ordered(&arrays).and_then(|(mut brep, report, order)| {
|
||||
self.with_definitions(&mut brep, &order)?;
|
||||
Ok((brep, report))
|
||||
});
|
||||
if let Ok((record, _)) = &r {
|
||||
crate::trace::exported(self, record);
|
||||
}
|
||||
@@ -114,7 +117,10 @@ impl Brep {
|
||||
/// `GetEdges`), the order its history uses.
|
||||
pub fn to_native_ordered(&self) -> std::result::Result<(b::Brep, Order), ExportError> {
|
||||
let arrays = self.arrays().map_err(ExportError::Smlib)?;
|
||||
let r = native_ordered(&arrays).map(|(brep, _, order)| (brep, order));
|
||||
let r = native_ordered(&arrays).and_then(|(mut brep, _, order)| {
|
||||
self.with_definitions(&mut brep, &order)?;
|
||||
Ok((brep, order))
|
||||
});
|
||||
if let Ok((record, _)) = &r {
|
||||
crate::trace::exported(self, record);
|
||||
}
|
||||
@@ -122,6 +128,29 @@ impl Brep {
|
||||
}
|
||||
}
|
||||
|
||||
impl Brep {
|
||||
/// The record's blend faces as their definitions: each face of this body whose surface carries
|
||||
/// a definition a record keeps (a procedural blend, a round of an exact class whose net has no
|
||||
/// analytic label) is written as that definition, with its net, the face's NURBS, as its form.
|
||||
/// The arrays carry the net alone; `order` places this body's faces in the record.
|
||||
fn with_definitions(&self, record: &mut b::Brep, order: &Order) -> Res<()> {
|
||||
let definitions = crate::core::definition::definitions(self.core()).map_err(ExportError::Smlib)?;
|
||||
for (i, definition) in definitions.into_iter().enumerate() {
|
||||
let Some(definition) = definition else { continue };
|
||||
let at = if order.faces.is_empty() { i } else { *order.faces.get(i).ok_or_else(|| invalid("a face the order does not place"))? };
|
||||
let face = record.faces.get_mut(at).ok_or_else(|| invalid("a face the record does not have"))?;
|
||||
let net = face.nurbs.as_ref().unwrap_or(&face.surface);
|
||||
if !matches!(net, b::Surface::Spline { .. } | b::Surface::RationalSpline { .. }) {
|
||||
continue;
|
||||
}
|
||||
let net = face.nurbs.take().unwrap_or_else(|| face.surface.clone());
|
||||
face.surface = b::Surface::Blend { definition: Box::new(definition) };
|
||||
face.nurbs = Some(net);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// What the mapping could not put in its own kind, or turned.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Report {
|
||||
|
||||
@@ -188,7 +188,8 @@ fn laid_out(brep: &b::Brep, trusted: bool, analytic: bool, bare: &HashSet<(u32,
|
||||
// parameterised on, whose range and trims these are (FACE_NURBS_FORM). A revolution
|
||||
// (SMLib has no class for one) is that form alone, a NURBS face. A ball patch is its form
|
||||
// too, as a NURBS face's block under its own kind, with its sphere as its definition.
|
||||
let form_is_face = f.nurbs.is_some() && matches!(f.surface, b::Surface::PlacedRevolution { .. } | b::Surface::BallPatch { .. });
|
||||
let form_is_face =
|
||||
f.nurbs.is_some() && matches!(f.surface, b::Surface::PlacedRevolution { .. } | b::Surface::BallPatch { .. } | b::Surface::Blend { .. });
|
||||
let spline = match (&f.nurbs, &f.surface) {
|
||||
(Some(n), b::Surface::BallPatch { center, radius }) => ball_patch(&mut a, n, *center, *radius, f.domain)?,
|
||||
(Some(n), _) if form_is_face => surface(&mut a, n, f.domain, analytic)?,
|
||||
@@ -401,6 +402,7 @@ fn surface(a: &mut Arrays, s: &b::Surface, domain: [[f64; 2]; 2], analytic_plane
|
||||
}
|
||||
b::Surface::PlacedRevolution { profile, origin, axis, matrix, forward } => revolution(a, profile, *origin, *axis, matrix, *forward, domain)?,
|
||||
b::Surface::BallPatch { .. } => return Err(Unsupported::Invalid("a ball patch without its NURBS form")),
|
||||
b::Surface::Blend { .. } => return Err(Unsupported::Invalid("a blend without its NURBS form")),
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
@@ -667,20 +669,47 @@ mod tests;
|
||||
|
||||
impl crate::Brep {
|
||||
/// A body from flat arrays.
|
||||
/// A body built from `record` (its faces in the record's order, each blend face on its net),
|
||||
/// with its blend faces given their definitions back, rebuilt and their nets certified again
|
||||
/// (`solidcore_ops::blend_definition::adopt`). A net its definition does not certify refuses
|
||||
/// the body.
|
||||
fn adopt(self, record: &b::Brep) -> crate::Result<crate::Brep> {
|
||||
let faces: Vec<(usize, &solidcore_record::brep::blend::BlendDefinition)> = record
|
||||
.faces
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, f)| match &f.surface {
|
||||
b::Surface::Blend { definition } => Some((i, definition.as_ref())),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
if faces.is_empty() {
|
||||
return Ok(self);
|
||||
}
|
||||
if crate::core::counts(self.core())?[0] != record.faces.len() {
|
||||
return Err(crate::Error::Record(Unsupported::Invalid("the body built has another number of faces than its record")));
|
||||
}
|
||||
crate::core::definition::adopt(self.core(), &faces)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub(crate) fn from_arrays(a: &Arrays) -> crate::Result<crate::Brep> {
|
||||
crate::core::from_arrays(a).map(crate::Brep::made_core)
|
||||
}
|
||||
|
||||
/// The SMLib B-rep of a schema 4 record. `trusted` as in [`arrays`].
|
||||
pub fn from_record(brep: &b::Brep, trusted: bool) -> crate::Result<crate::Brep> {
|
||||
let r = arrays(brep, trusted).map_err(crate::Error::Record).and_then(|a| crate::Brep::from_arrays(&a));
|
||||
let r = arrays(brep, trusted).map_err(crate::Error::Record).and_then(|a| crate::Brep::from_arrays(&a)).and_then(|body| body.adopt(brep));
|
||||
crate::trace::imported(&r, brep, trusted, false);
|
||||
r
|
||||
}
|
||||
|
||||
/// [`crate::Brep::from_record`] through [`arrays_keeping_splines`].
|
||||
pub fn from_record_keeping_splines(brep: &b::Brep, trusted: bool) -> crate::Result<crate::Brep> {
|
||||
let r = arrays_keeping_splines(brep, trusted).map_err(crate::Error::Record).and_then(|a| crate::Brep::from_arrays(&a));
|
||||
let r = arrays_keeping_splines(brep, trusted)
|
||||
.map_err(crate::Error::Record)
|
||||
.and_then(|a| crate::Brep::from_arrays(&a))
|
||||
.and_then(|body| body.adopt(brep));
|
||||
crate::trace::imported(&r, brep, trusted, true);
|
||||
r
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ impl Surface {
|
||||
Surface::nurbs(degree, nu, nv, u_knots, v_knots, &flat, Some(&weights))
|
||||
}
|
||||
// A ball patch's label has no (u, v): its face's NURBS form is the surface.
|
||||
S::PlacedRevolution { .. } | S::BallPatch { .. } => Err(Error::Unsupported),
|
||||
S::PlacedRevolution { .. } | S::BallPatch { .. } | S::Blend { .. } => Err(Error::Unsupported),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -141,6 +141,7 @@ Natural normals are ∂S/∂u × ∂S/∂v.
|
||||
| `sphere` {frame, radius} | origin + r (cos v (cos u x + sin u y) + sin v z) | longitude, periodic | latitude, [−π/2, π/2] |
|
||||
| `torus` {frame, major_radius, minor_radius} | origin + (R + r cos v)(cos u x + sin u y) + r sin v z | longitude, periodic | meridian angle, periodic |
|
||||
| `ball_patch` {center, radius} | its `nurbs`: a label with no (u, v) of its own | its form's | its form's |
|
||||
| `blend` {definition} | its `nurbs`: a round by its definition, with no (u, v) of its own | its form's | its form's |
|
||||
| `spline` {u_knots, v_knots, points} | tensor-product B-spline, `points[i][j]` with i along u | u knot domain | v knot domain |
|
||||
| `rational_spline` {u_knots, v_knots, homogeneous} | the same, homogeneous coefficients | u knot domain | v knot domain |
|
||||
| `placed_revolution` {profile, origin, axis, matrix, forward} | M · R(angle) · profile(s): the profile revolved about origin and unit axis by the right-hand rule, then mapped by the column-major affine matrix M | `forward`: s, the profile's parameter; else the angle | `forward`: the angle, [0, 2π); else s |
|
||||
@@ -170,6 +171,18 @@ sphere. The sphere is kept because its offsets are exact and collapse at its
|
||||
centre. A STEP writer, with no entity for it, writes the net as the face's
|
||||
B-spline surface, pcurves included.
|
||||
|
||||
A `blend` face always carries `nurbs`, its net: the certified B-spline of a
|
||||
round whose definition the record keeps (`solidcore_record::brep::blend`): a
|
||||
procedural blend's definition (supports, ball, section, leader and seeds, its
|
||||
bound and the map from the net's parameters to the definition's), or a round of
|
||||
an exact class's whose net has no analytic label of its own (a surface of
|
||||
revolution, or the rational classes' B-spline), with that net's kind. The net is
|
||||
stored once, as the face's form; a support that is itself a blend keeps its own
|
||||
net in the definition. The kernel builds the face on its net, makes the
|
||||
definition from the record and certifies the net against it again before the
|
||||
face is an operand, refusing a net it does not certify. The flat arrays carry
|
||||
the net alone. A STEP writer writes the net as the face's B-spline surface.
|
||||
|
||||
### Parameter ranges
|
||||
|
||||
An edge interval and each side of a face domain are finite with min < max. In a
|
||||
|
||||
+414
@@ -0,0 +1,414 @@
|
||||
//! A face's blend definition, the record's ([`r::BlendDefinition`]) and the kernel's
|
||||
//! ([`kb::FaceDefinition`]), both ways: the same records, field by field. The kernel reads a
|
||||
//! definition off a face ([`solidcore::surface::procedural_blend::convert::face_definition`]) and
|
||||
//! gives it back, rebuilt and its net certified again
|
||||
//! ([`solidcore::surface::procedural_blend::convert::adopt_definitions`]).
|
||||
//!
|
||||
//! Recursion: a support that is itself a blend is copied with its own supports, at most
|
||||
//! three definitions deep: a record's validation and the kernel's own records bound it.
|
||||
|
||||
use solidcore::surface::procedural_blend::record as kb;
|
||||
use solidcore_record::brep::blend as r;
|
||||
|
||||
/// The record's definition as the kernel's.
|
||||
#[must_use]
|
||||
pub fn blend(d: &r::BlendDefinition) -> kb::FaceDefinition {
|
||||
match d {
|
||||
r::BlendDefinition::Procedural(d) => {
|
||||
kb::FaceDefinition::Procedural(Box::new(definition(d)))
|
||||
}
|
||||
r::BlendDefinition::ExactRound(d) => {
|
||||
kb::FaceDefinition::ExactRound(Box::new(kb::ExactRoundDefinition {
|
||||
class: class(d.class),
|
||||
supports: [support(&d.supports[0]), support(&d.supports[1])],
|
||||
radius: d.radius,
|
||||
order: d.order,
|
||||
leader: d.leader.as_ref().map(curve),
|
||||
inputs: kb::ClassInputsRecord {
|
||||
uv: d.inputs.uv,
|
||||
offsets: d.inputs.offsets,
|
||||
centre: d.inputs.centre,
|
||||
tol: d.inputs.tol,
|
||||
ball_on_collapsed: d.inputs.ball_on_collapsed,
|
||||
radii_can_change: d.inputs.radii_can_change,
|
||||
},
|
||||
chamfer: d.chamfer.as_ref().map(chamfer),
|
||||
leader_map: kb::LeaderMapRecord {
|
||||
a: d.leader_map.a,
|
||||
b: d.leader_map.b,
|
||||
along_v: d.leader_map.along_v,
|
||||
},
|
||||
net_kind: net_kind(d.net_kind),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The kernel's definition as the record's.
|
||||
#[must_use]
|
||||
pub fn record_blend(d: kb::FaceDefinition) -> r::BlendDefinition {
|
||||
match d {
|
||||
kb::FaceDefinition::Procedural(d) => {
|
||||
r::BlendDefinition::Procedural(Box::new(record_definition(*d)))
|
||||
}
|
||||
kb::FaceDefinition::ExactRound(d) => {
|
||||
let d = *d;
|
||||
let [s0, s1] = d.supports;
|
||||
r::BlendDefinition::ExactRound(Box::new(r::ExactRoundDefinition {
|
||||
class: record_class(d.class),
|
||||
supports: [record_support(s0), record_support(s1)],
|
||||
radius: d.radius,
|
||||
order: d.order,
|
||||
leader: d.leader.map(record_curve),
|
||||
inputs: r::ClassInputsRecord {
|
||||
uv: d.inputs.uv,
|
||||
offsets: d.inputs.offsets,
|
||||
centre: d.inputs.centre,
|
||||
tol: d.inputs.tol,
|
||||
ball_on_collapsed: d.inputs.ball_on_collapsed,
|
||||
radii_can_change: d.inputs.radii_can_change,
|
||||
},
|
||||
chamfer: d.chamfer.map(record_chamfer),
|
||||
leader_map: r::LeaderMapRecord {
|
||||
a: d.leader_map.a,
|
||||
b: d.leader_map.b,
|
||||
along_v: d.leader_map.along_v,
|
||||
},
|
||||
net_kind: record_net_kind(d.net_kind),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn definition(d: &r::BlendDefinitionRecord) -> kb::BlendDefinitionRecord {
|
||||
kb::BlendDefinitionRecord {
|
||||
supports: [support(&d.supports[0]), support(&d.supports[1])],
|
||||
sides: d.sides,
|
||||
radius: match &d.radius {
|
||||
r::RadiusRecord::Constant { offsets } => {
|
||||
kb::RadiusRecord::Constant { offsets: *offsets }
|
||||
}
|
||||
r::RadiusRecord::Law(law) => kb::RadiusRecord::Law(curve(law)),
|
||||
},
|
||||
order: d.order,
|
||||
thumbweight: d.thumbweight,
|
||||
leader: curve(&d.leader),
|
||||
seeds: [curve(&d.seeds[0]), curve(&d.seeds[1])],
|
||||
bound: d.bound,
|
||||
map: kb::ParameterMapRecord {
|
||||
a: d.map.a,
|
||||
b: d.map.b,
|
||||
swapped: d.map.swapped,
|
||||
},
|
||||
chamfer: d.chamfer.as_ref().map(chamfer),
|
||||
}
|
||||
}
|
||||
|
||||
fn record_definition(d: kb::BlendDefinitionRecord) -> r::BlendDefinitionRecord {
|
||||
let [s0, s1] = d.supports;
|
||||
let [seed0, seed1] = d.seeds;
|
||||
r::BlendDefinitionRecord {
|
||||
supports: [record_support(s0), record_support(s1)],
|
||||
sides: d.sides,
|
||||
radius: match d.radius {
|
||||
kb::RadiusRecord::Constant { offsets } => r::RadiusRecord::Constant { offsets },
|
||||
kb::RadiusRecord::Law(law) => r::RadiusRecord::Law(record_curve(law)),
|
||||
},
|
||||
order: d.order,
|
||||
thumbweight: d.thumbweight,
|
||||
leader: record_curve(d.leader),
|
||||
seeds: [record_curve(seed0), record_curve(seed1)],
|
||||
bound: d.bound,
|
||||
map: r::ParameterMapRecord {
|
||||
a: d.map.a,
|
||||
b: d.map.b,
|
||||
swapped: d.map.swapped,
|
||||
},
|
||||
chamfer: d.chamfer.map(record_chamfer),
|
||||
}
|
||||
}
|
||||
|
||||
fn support(s: &r::SupportRecord) -> kb::SupportRecord {
|
||||
match s {
|
||||
r::SupportRecord::Nurbs { kind, form } => kb::SupportRecord::Nurbs {
|
||||
kind: support_kind(*kind),
|
||||
form: surface(form),
|
||||
},
|
||||
r::SupportRecord::Blend(b) => {
|
||||
kb::SupportRecord::Blend(Box::new(kb::ProceduralBlendRecord {
|
||||
definition: definition(&b.definition),
|
||||
net: surface(&b.net),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn record_support(s: kb::SupportRecord) -> r::SupportRecord {
|
||||
match s {
|
||||
kb::SupportRecord::Nurbs { kind, form } => r::SupportRecord::Nurbs {
|
||||
kind: record_support_kind(kind),
|
||||
form: record_surface(form),
|
||||
},
|
||||
kb::SupportRecord::Blend(b) => {
|
||||
let b = *b;
|
||||
r::SupportRecord::Blend(Box::new(r::ProceduralBlendRecord {
|
||||
definition: record_definition(b.definition),
|
||||
net: record_surface(b.net),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn curve(c: &r::NurbsCurveRecord) -> kb::NurbsCurveRecord {
|
||||
kb::NurbsCurveRecord {
|
||||
dim: c.dim,
|
||||
points_3d: c.points_3d,
|
||||
degree: c.degree,
|
||||
knots: c.knots.clone(),
|
||||
points: c.points.clone(),
|
||||
weights: c.weights.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn record_curve(c: kb::NurbsCurveRecord) -> r::NurbsCurveRecord {
|
||||
r::NurbsCurveRecord {
|
||||
dim: c.dim,
|
||||
points_3d: c.points_3d,
|
||||
degree: c.degree,
|
||||
knots: c.knots,
|
||||
points: c.points,
|
||||
weights: c.weights,
|
||||
}
|
||||
}
|
||||
|
||||
fn surface(s: &r::NurbsSurfaceRecord) -> kb::NurbsSurfaceRecord {
|
||||
kb::NurbsSurfaceRecord {
|
||||
u_degree: s.u_degree,
|
||||
v_degree: s.v_degree,
|
||||
u_count: s.u_count,
|
||||
v_count: s.v_count,
|
||||
u_knots: s.u_knots.clone(),
|
||||
v_knots: s.v_knots.clone(),
|
||||
points: s.points.clone(),
|
||||
weights: s.weights.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn record_surface(s: kb::NurbsSurfaceRecord) -> r::NurbsSurfaceRecord {
|
||||
r::NurbsSurfaceRecord {
|
||||
u_degree: s.u_degree,
|
||||
v_degree: s.v_degree,
|
||||
u_count: s.u_count,
|
||||
v_count: s.v_count,
|
||||
u_knots: s.u_knots,
|
||||
v_knots: s.v_knots,
|
||||
points: s.points,
|
||||
weights: s.weights,
|
||||
}
|
||||
}
|
||||
|
||||
fn chamfer(c: &r::ChamferRecord) -> kb::ChamferRecord {
|
||||
kb::ChamferRecord {
|
||||
size: match c.size {
|
||||
r::ChamferSizeRecord::Distances(x) => kb::ChamferSizeRecord::Distances(x),
|
||||
r::ChamferSizeRecord::DistanceAngle { distance, angle } => {
|
||||
kb::ChamferSizeRecord::DistanceAngle { distance, angle }
|
||||
}
|
||||
r::ChamferSizeRecord::Chord(l) => kb::ChamferSizeRecord::Chord(l),
|
||||
},
|
||||
wedge: c.wedge,
|
||||
}
|
||||
}
|
||||
|
||||
fn record_chamfer(c: kb::ChamferRecord) -> r::ChamferRecord {
|
||||
r::ChamferRecord {
|
||||
size: match c.size {
|
||||
kb::ChamferSizeRecord::Distances(x) => r::ChamferSizeRecord::Distances(x),
|
||||
kb::ChamferSizeRecord::DistanceAngle { distance, angle } => {
|
||||
r::ChamferSizeRecord::DistanceAngle { distance, angle }
|
||||
}
|
||||
kb::ChamferSizeRecord::Chord(l) => r::ChamferSizeRecord::Chord(l),
|
||||
},
|
||||
wedge: c.wedge,
|
||||
}
|
||||
}
|
||||
|
||||
fn support_kind(k: r::SupportKindRecord) -> kb::SupportKindRecord {
|
||||
match k {
|
||||
r::SupportKindRecord::Free => kb::SupportKindRecord::Free,
|
||||
r::SupportKindRecord::Plane => kb::SupportKindRecord::Plane,
|
||||
r::SupportKindRecord::Cylinder => kb::SupportKindRecord::Cylinder,
|
||||
r::SupportKindRecord::Cone => kb::SupportKindRecord::Cone,
|
||||
r::SupportKindRecord::Sphere => kb::SupportKindRecord::Sphere,
|
||||
r::SupportKindRecord::Torus => kb::SupportKindRecord::Torus,
|
||||
}
|
||||
}
|
||||
|
||||
fn record_support_kind(k: kb::SupportKindRecord) -> r::SupportKindRecord {
|
||||
match k {
|
||||
kb::SupportKindRecord::Free => r::SupportKindRecord::Free,
|
||||
kb::SupportKindRecord::Plane => r::SupportKindRecord::Plane,
|
||||
kb::SupportKindRecord::Cylinder => r::SupportKindRecord::Cylinder,
|
||||
kb::SupportKindRecord::Cone => r::SupportKindRecord::Cone,
|
||||
kb::SupportKindRecord::Sphere => r::SupportKindRecord::Sphere,
|
||||
kb::SupportKindRecord::Torus => r::SupportKindRecord::Torus,
|
||||
}
|
||||
}
|
||||
|
||||
fn class(c: r::ExactClassRecord) -> kb::ExactClassRecord {
|
||||
match c {
|
||||
r::ExactClassRecord::Line => kb::ExactClassRecord::Line,
|
||||
r::ExactClassRecord::Circle => kb::ExactClassRecord::Circle,
|
||||
r::ExactClassRecord::Point => kb::ExactClassRecord::Point,
|
||||
r::ExactClassRecord::Chord => kb::ExactClassRecord::Chord,
|
||||
r::ExactClassRecord::Conic => kb::ExactClassRecord::Conic,
|
||||
r::ExactClassRecord::ConicPair => kb::ExactClassRecord::ConicPair,
|
||||
r::ExactClassRecord::NodalQuartic => kb::ExactClassRecord::NodalQuartic,
|
||||
}
|
||||
}
|
||||
|
||||
fn record_class(c: kb::ExactClassRecord) -> r::ExactClassRecord {
|
||||
match c {
|
||||
kb::ExactClassRecord::Line => r::ExactClassRecord::Line,
|
||||
kb::ExactClassRecord::Circle => r::ExactClassRecord::Circle,
|
||||
kb::ExactClassRecord::Point => r::ExactClassRecord::Point,
|
||||
kb::ExactClassRecord::Chord => r::ExactClassRecord::Chord,
|
||||
kb::ExactClassRecord::Conic => r::ExactClassRecord::Conic,
|
||||
kb::ExactClassRecord::ConicPair => r::ExactClassRecord::ConicPair,
|
||||
kb::ExactClassRecord::NodalQuartic => r::ExactClassRecord::NodalQuartic,
|
||||
}
|
||||
}
|
||||
|
||||
fn net_kind(k: r::ExactNetKindRecord) -> kb::ExactNetKindRecord {
|
||||
match k {
|
||||
r::ExactNetKindRecord::Plane => kb::ExactNetKindRecord::Plane,
|
||||
r::ExactNetKindRecord::Cylinder => kb::ExactNetKindRecord::Cylinder,
|
||||
r::ExactNetKindRecord::Cone => kb::ExactNetKindRecord::Cone,
|
||||
r::ExactNetKindRecord::Sphere => kb::ExactNetKindRecord::Sphere,
|
||||
r::ExactNetKindRecord::Torus => kb::ExactNetKindRecord::Torus,
|
||||
r::ExactNetKindRecord::Revolution => kb::ExactNetKindRecord::Revolution,
|
||||
r::ExactNetKindRecord::BallPatch => kb::ExactNetKindRecord::BallPatch,
|
||||
r::ExactNetKindRecord::BSpline => kb::ExactNetKindRecord::BSpline,
|
||||
}
|
||||
}
|
||||
|
||||
fn record_net_kind(k: kb::ExactNetKindRecord) -> r::ExactNetKindRecord {
|
||||
match k {
|
||||
kb::ExactNetKindRecord::Plane => r::ExactNetKindRecord::Plane,
|
||||
kb::ExactNetKindRecord::Cylinder => r::ExactNetKindRecord::Cylinder,
|
||||
kb::ExactNetKindRecord::Cone => r::ExactNetKindRecord::Cone,
|
||||
kb::ExactNetKindRecord::Sphere => r::ExactNetKindRecord::Sphere,
|
||||
kb::ExactNetKindRecord::Torus => r::ExactNetKindRecord::Torus,
|
||||
kb::ExactNetKindRecord::Revolution => r::ExactNetKindRecord::Revolution,
|
||||
kb::ExactNetKindRecord::BallPatch => r::ExactNetKindRecord::BallPatch,
|
||||
kb::ExactNetKindRecord::BSpline => r::ExactNetKindRecord::BSpline,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// A definition crosses to the kernel's record and back field for field.
|
||||
#[test]
|
||||
fn a_definition_crosses_both_ways_unchanged() {
|
||||
let form = r::NurbsSurfaceRecord {
|
||||
u_degree: 1,
|
||||
v_degree: 1,
|
||||
u_count: 2,
|
||||
v_count: 2,
|
||||
u_knots: vec![0., 0., 1., 1.],
|
||||
v_knots: vec![0., 0., 1., 1.],
|
||||
points: vec![[0., 0., 0.], [0., 1., 0.], [1., 0., 0.], [1., 1., 0.]],
|
||||
weights: Some(vec![1., 0.5, 1., 0.5]),
|
||||
};
|
||||
let curve = |dim: u32| r::NurbsCurveRecord {
|
||||
dim,
|
||||
points_3d: dim == 3,
|
||||
degree: 1,
|
||||
knots: vec![0., 0., 1., 1.],
|
||||
points: vec![[0., 0., 0.], [1., 2., 3.]],
|
||||
weights: None,
|
||||
};
|
||||
let inner = r::BlendDefinitionRecord {
|
||||
supports: [
|
||||
r::SupportRecord::Nurbs {
|
||||
kind: r::SupportKindRecord::Cylinder,
|
||||
form: form.clone(),
|
||||
},
|
||||
r::SupportRecord::Nurbs {
|
||||
kind: r::SupportKindRecord::Free,
|
||||
form: form.clone(),
|
||||
},
|
||||
],
|
||||
sides: [1., -1.],
|
||||
radius: r::RadiusRecord::Law(curve(2)),
|
||||
order: 2,
|
||||
thumbweight: 0.25,
|
||||
leader: curve(3),
|
||||
seeds: [curve(2), curve(2)],
|
||||
bound: 1e-7,
|
||||
map: r::ParameterMapRecord {
|
||||
a: [0.5, 1.],
|
||||
b: [2., -1.],
|
||||
swapped: true,
|
||||
},
|
||||
chamfer: Some(r::ChamferRecord {
|
||||
size: r::ChamferSizeRecord::DistanceAngle {
|
||||
distance: 1.,
|
||||
angle: 0.5,
|
||||
},
|
||||
wedge: 1.5,
|
||||
}),
|
||||
};
|
||||
let procedural = r::BlendDefinition::Procedural(Box::new(r::BlendDefinitionRecord {
|
||||
supports: [
|
||||
r::SupportRecord::Blend(Box::new(r::ProceduralBlendRecord {
|
||||
definition: inner.clone(),
|
||||
net: form.clone(),
|
||||
})),
|
||||
r::SupportRecord::Nurbs {
|
||||
kind: r::SupportKindRecord::Plane,
|
||||
form: form.clone(),
|
||||
},
|
||||
],
|
||||
radius: r::RadiusRecord::Constant { offsets: [2., 3.] },
|
||||
chamfer: None,
|
||||
..inner
|
||||
}));
|
||||
let exact = r::BlendDefinition::ExactRound(Box::new(r::ExactRoundDefinition {
|
||||
class: r::ExactClassRecord::Circle,
|
||||
supports: [
|
||||
r::SupportRecord::Nurbs {
|
||||
kind: r::SupportKindRecord::Plane,
|
||||
form: form.clone(),
|
||||
},
|
||||
r::SupportRecord::Nurbs {
|
||||
kind: r::SupportKindRecord::Cylinder,
|
||||
form,
|
||||
},
|
||||
],
|
||||
radius: 0.3,
|
||||
order: 1,
|
||||
leader: Some(curve(3)),
|
||||
inputs: r::ClassInputsRecord {
|
||||
uv: [[0.1, 0.2], [0.3, 0.4]],
|
||||
offsets: [0.3, -0.3],
|
||||
centre: [15., 15., 9.7],
|
||||
tol: 1e-6,
|
||||
ball_on_collapsed: false,
|
||||
radii_can_change: true,
|
||||
},
|
||||
chamfer: None,
|
||||
leader_map: r::LeaderMapRecord {
|
||||
a: 0.5,
|
||||
b: 2.,
|
||||
along_v: true,
|
||||
},
|
||||
net_kind: r::ExactNetKindRecord::Revolution,
|
||||
}));
|
||||
for d in [procedural, exact] {
|
||||
assert_eq!(record_blend(blend(&d)), d);
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
-7
@@ -31,6 +31,7 @@ use solidcore::geom::attribute::{AttributeStore, ObjectKey};
|
||||
use solidcore::geom::extent1d::Extent1d;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::surface::face_surface::face_surface;
|
||||
use solidcore::surface::procedural_blend::convert::adopt_definitions;
|
||||
use solidcore::topology::brep::enclosing::put_enclosing_shells_first;
|
||||
use solidcore::topology::brep::p10::{
|
||||
heal_made, make_topology_from_data, MakeTopologyOptions, TopologyEnv, HEALER_VERSION,
|
||||
@@ -289,6 +290,7 @@ fn build(
|
||||
edges: b.edge_list().to_vec(),
|
||||
vertices: b.vertex_list().to_vec(),
|
||||
};
|
||||
adopt(k, &mut b, record, &order.faces)?;
|
||||
if (order.faces.len(), order.edges.len(), order.vertices.len())
|
||||
!= (
|
||||
record.faces.len(),
|
||||
@@ -483,13 +485,20 @@ fn data(k: &Kernel, rec: &r::Brep, bare: &[(u32, u32, u32)]) -> R<BrepData> {
|
||||
// Faces, their loops and edgeuses.
|
||||
let mut eu = 0u64;
|
||||
for (fi, f) in rec.faces.iter().enumerate() {
|
||||
let made = face_surface(
|
||||
k,
|
||||
&crate::surface(&f.surface)?,
|
||||
f.nurbs.as_ref().map(crate::surface).transpose()?.as_ref(),
|
||||
f.domain,
|
||||
&boundary(rec, f)?,
|
||||
)?;
|
||||
// A blend face is built on its net, its NURBS form; its definition is given back once
|
||||
// the body is built ([`adopt`]).
|
||||
let made = match (&f.surface, &f.nurbs) {
|
||||
(r::Surface::Blend { .. }, Some(net)) => {
|
||||
face_surface(k, &crate::surface(net)?, None, f.domain, &[])?
|
||||
}
|
||||
_ => face_surface(
|
||||
k,
|
||||
&crate::surface(&f.surface)?,
|
||||
f.nurbs.as_ref().map(crate::surface).transpose()?.as_ref(),
|
||||
f.domain,
|
||||
&boundary(rec, f)?,
|
||||
)?,
|
||||
};
|
||||
let mut fd = FaceData::default();
|
||||
fd.zone_tol_3d = zone;
|
||||
fd.surface = int(d.surfaces.len())?;
|
||||
@@ -661,6 +670,26 @@ fn own_tolerances(d: &mut BrepData, rec: &r::Brep, zone: f64) -> R<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Give the record's blend faces their definitions back on the body built from it: each face's
|
||||
/// surface, its net, becomes the blend its definition makes, the net certified against it again
|
||||
/// ([`adopt_definitions`]); a net its definition does not certify refuses the record. `faces` are
|
||||
/// the body's faces in the record's order.
|
||||
fn adopt(k: &Kernel, b: &mut Brep, record: &r::Brep, faces: &[FaceId]) -> R<()> {
|
||||
let mut definitions = Vec::new();
|
||||
for (i, f) in record.faces.iter().enumerate() {
|
||||
if let r::Surface::Blend { definition } = &f.surface {
|
||||
let face = *faces
|
||||
.get(i)
|
||||
.ok_or(Unsupported::Invalid("a face the body does not have"))?;
|
||||
definitions.push((face, crate::blend::blend(definition)));
|
||||
}
|
||||
}
|
||||
if !definitions.is_empty() {
|
||||
adopt_definitions(k, b, ¬ify_ref, &definitions)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The curves of a ball patch face's loops, for [`face_surface`] to certify on its sphere:
|
||||
/// each edge's NURBS form, or its curve; empty for any other face, whose label is not
|
||||
/// checked against its loops.
|
||||
|
||||
+37
-5
@@ -7,9 +7,9 @@
|
||||
//! its NURBS form, proved on each other; an analytic face by its label and its NURBS form,
|
||||
//! proved (`lies_on`); a plane by its placement, its parameters its kernel ones times its
|
||||
//! scales. Nothing is sampled. A curve or surface with no NURBS form (an offset) is refused.
|
||||
//! Two things still cross as approximations: a procedural blend goes as its net and loses its
|
||||
//! definition and bound until the definition records move into the record (PR 1's last
|
||||
//! commit, still to come), and a use with no UV trim gets one the kernel makes by fitting.
|
||||
//! A blend face goes as its definition and its net ([`face_definition`]: a procedural blend, a
|
||||
//! round of an exact class whose net has no analytic label); a use with no UV trim gets one the
|
||||
//! kernel makes by fitting.
|
||||
//! - **Pcurves:** every use's pcurve is the kernel's own UV trim (made first where a use has
|
||||
//! none, by the kernel's fit), its knots on the edge record's interval ([`record_trim`]). A face's parameters in
|
||||
//! the record are its NURBS's (or a plane's, scaled), none periodic, so no loop is moved by
|
||||
@@ -35,6 +35,7 @@ use solidcore::geom::attribute::{AttributeStore, ObjectKey};
|
||||
use solidcore::geom::extent1d::Extent1d;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::surface::face_surface::record_face;
|
||||
use solidcore::surface::procedural_blend::convert::face_definition;
|
||||
use solidcore::topology::brep::p1 as bl;
|
||||
use solidcore::topology::brep_data::{BrepData, NO_OBJECT, UNDEF_ULONG};
|
||||
use solidcore::topology::brep_data_write::FromBrepEnv;
|
||||
@@ -48,6 +49,31 @@ use solidcore_record::brep as r;
|
||||
|
||||
use super::{Order, Unsupported, R};
|
||||
|
||||
/// A blend face's record: its definition as its label and its net as its NURBS form (the form the
|
||||
/// face was written with, or its surface when that is the net itself), when the kernel reads a
|
||||
/// definition off it ([`face_definition`]) and the net is a NURBS surface; otherwise the face as
|
||||
/// written.
|
||||
fn with_definition(
|
||||
k: &Kernel,
|
||||
surface: &solidcore::surface::surface::Surface,
|
||||
label: r::Surface,
|
||||
form: Option<r::Surface>,
|
||||
) -> R<(r::Surface, Option<r::Surface>)> {
|
||||
let net = form.as_ref().unwrap_or(&label);
|
||||
if !matches!(
|
||||
net,
|
||||
r::Surface::Spline { .. } | r::Surface::RationalSpline { .. }
|
||||
) {
|
||||
return Ok((label, form));
|
||||
}
|
||||
let Some(definition) = face_definition(k, surface)? else {
|
||||
return Ok((label, form));
|
||||
};
|
||||
let net = form.unwrap_or(label);
|
||||
let definition = Box::new(crate::blend::record_blend(definition));
|
||||
Ok((r::Surface::Blend { definition }, Some(net)))
|
||||
}
|
||||
|
||||
fn index(i: usize) -> R<u32> {
|
||||
u32::try_from(i).map_err(|_| Unsupported::Invalid("index"))
|
||||
}
|
||||
@@ -166,9 +192,15 @@ pub fn to_record(k: &mut Kernel, b: &mut Brep) -> R<(r::Brep, Order)> {
|
||||
.as_ref()
|
||||
.ok_or(Unsupported::Invalid("a face without a surface"))?;
|
||||
let made = record_face(k, surface, &fd.uv_domain)?;
|
||||
let (label, form) = with_definition(
|
||||
k,
|
||||
surface,
|
||||
crate::record_surface(made.surface)?,
|
||||
made.nurbs.map(crate::record_surface).transpose()?,
|
||||
)?;
|
||||
let mut face = Face {
|
||||
surface: crate::record_surface(made.surface)?,
|
||||
nurbs: made.nurbs.map(crate::record_surface).transpose()?,
|
||||
surface: label,
|
||||
nurbs: form,
|
||||
domain: made.domain,
|
||||
agrees: made.agrees,
|
||||
loops: Vec::new(),
|
||||
|
||||
+20
-11
@@ -28,12 +28,14 @@
|
||||
//! | [`Seg2`] | [`kp::Seg2`] | [`seg2`] | [`record_seg2`] |
|
||||
//! | [`Loop2`] | [`kp::Loop2`] | [`loop2`] | [`record_loop2`] |
|
||||
//! | [`Region2`] (a planar profile) | [`kp::Region2`] | [`region2`] | [`record_region2`] |
|
||||
//! | [`BlendDefinition`](solidcore_record::brep::blend::BlendDefinition) (a face's blend) | `FaceDefinition` | [`blend::blend`] | [`blend::record_blend`] |
|
||||
//!
|
||||
//! [`brep`] converts the record's B-rep to the kernel's body and back.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::disallowed_methods)]
|
||||
|
||||
pub mod blend;
|
||||
pub mod brep;
|
||||
|
||||
use solidcore::eval as k;
|
||||
@@ -183,10 +185,10 @@ pub fn curve(c: &Curve) -> Result<k::Curve, Invalid> {
|
||||
if !record_curve_fits(c, 0) {
|
||||
return Err(Invalid("geometry depth"));
|
||||
}
|
||||
Ok(curve_inner(c))
|
||||
curve_inner(c)
|
||||
}
|
||||
fn curve_inner(c: &Curve) -> k::Curve {
|
||||
match c {
|
||||
fn curve_inner(c: &Curve) -> Result<k::Curve, Invalid> {
|
||||
Ok(match c {
|
||||
Curve::Line { start, end } => k::Curve::Line {
|
||||
start: *start,
|
||||
end: *end,
|
||||
@@ -215,11 +217,11 @@ fn curve_inner(c: &Curve) -> k::Curve {
|
||||
leader,
|
||||
tolerance,
|
||||
} => k::Curve::SurfaceIntersection {
|
||||
surfaces: [Box::new(surface_inner(a)), Box::new(surface_inner(b))],
|
||||
leader: Box::new(curve_inner(leader)),
|
||||
surfaces: [Box::new(surface_inner(a)?), Box::new(surface_inner(b)?)],
|
||||
leader: Box::new(curve_inner(leader)?),
|
||||
tolerance: *tolerance,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A kernel curve as the record's; refuses geometry deeper than [`MAX_DEPTH`].
|
||||
@@ -266,10 +268,12 @@ pub fn surface(s: &Surface) -> Result<k::Surface, Invalid> {
|
||||
if !record_surface_fits(s, 0) {
|
||||
return Err(Invalid("geometry depth"));
|
||||
}
|
||||
Ok(surface_inner(s))
|
||||
surface_inner(s)
|
||||
}
|
||||
fn surface_inner(s: &Surface) -> k::Surface {
|
||||
match s {
|
||||
/// A blend label has no kernel surface of its own: its geometry is the face's NURBS form, and its
|
||||
/// definition crosses with [`blend`].
|
||||
fn surface_inner(s: &Surface) -> Result<k::Surface, Invalid> {
|
||||
Ok(match s {
|
||||
Surface::Plane {
|
||||
origin,
|
||||
u_axis,
|
||||
@@ -345,13 +349,18 @@ fn surface_inner(s: &Surface) -> k::Surface {
|
||||
matrix,
|
||||
forward,
|
||||
} => k::Surface::PlacedRevolution {
|
||||
profile: Box::new(curve_inner(profile)),
|
||||
profile: Box::new(curve_inner(profile)?),
|
||||
origin: *origin,
|
||||
axis: *axis,
|
||||
matrix: *matrix,
|
||||
forward: *forward,
|
||||
},
|
||||
}
|
||||
Surface::Blend { .. } => {
|
||||
return Err(Invalid(
|
||||
"a blend's label: its geometry is the face's NURBS form",
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// A kernel surface as the record's; refuses geometry deeper than [`MAX_DEPTH`].
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
//! A body's blend faces between its record and the kernel, body by body in the body's face order:
|
||||
//! each face's definition as the record keeps it ([`definitions`]), and the definitions a record
|
||||
//! holds given back to the faces of the body built from it, rebuilt and their nets certified again
|
||||
//! ([`adopt`]). The kernel reads and rebuilds; `solidcore-convert` copies the fields.
|
||||
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::surface::procedural_blend::convert::{adopt_definitions, face_definition};
|
||||
use solidcore::{Brep, SolidError};
|
||||
use solidcore_record::brep::blend::BlendDefinition;
|
||||
|
||||
use crate::error::{status, Error, Result};
|
||||
|
||||
/// Each face's definition, in the body's face order, where its surface carries one a record keeps:
|
||||
/// a procedural blend's, or a round of an exact class's whose net has no analytic label of its own
|
||||
/// (`face_definition`). `None` for every other face.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The kernel's, as the operation's ([`Error`]).
|
||||
pub fn definitions(k: &Kernel, b: &Brep) -> Result<Vec<Option<BlendDefinition>>> {
|
||||
let mut out = Vec::with_capacity(b.face_list().len());
|
||||
for &f in b.face_list() {
|
||||
out.push(match b.face(f).map_err(status)?.surface.as_ref() {
|
||||
Some(surface) => face_definition(k, surface)
|
||||
.map_err(status)?
|
||||
.map(solidcore_convert::blend::record_blend),
|
||||
None => None,
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Give the faces `faces` names (each by its index in the body's face order) the definitions their
|
||||
/// record holds: the face's surface, its net, becomes the blend the definition makes, the net
|
||||
/// certified against it again (`adopt_definitions`).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`Error::Argument`] for an index past the body's faces; [`Error::DefinitionNotCertified`] for a
|
||||
/// net its definition does not certify; the kernel's other refusals, as the operation's.
|
||||
pub fn adopt(k: &Kernel, b: &mut Brep, faces: &[(usize, &BlendDefinition)]) -> Result<()> {
|
||||
let ids = b.face_list().to_vec();
|
||||
for (i, d) in faces {
|
||||
let f = *ids.get(*i).ok_or(Error::Argument)?;
|
||||
let definition = [(f, solidcore_convert::blend::blend(d))];
|
||||
adopt_definitions(k, b, &solidcore::topology::loopuse::notify_ref, &definition).map_err(
|
||||
|e| match e {
|
||||
SolidError::DefinitionNotCertified { eps, .. } => {
|
||||
Error::DefinitionNotCertified { face: *i, eps }
|
||||
}
|
||||
e => status(e),
|
||||
},
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
+12
@@ -160,6 +160,15 @@ pub enum Error {
|
||||
},
|
||||
/// The application cancelled the call.
|
||||
Cancelled,
|
||||
/// A body built from a record whose blend face's net its definition does not certify (the
|
||||
/// kernel's `DefinitionNotCertified`): the face, by its index in the body's face order, and
|
||||
/// the bound ε its definition states.
|
||||
DefinitionNotCertified {
|
||||
/// The face's index in the body's face order.
|
||||
face: usize,
|
||||
/// The bound the definition states.
|
||||
eps: f64,
|
||||
},
|
||||
/// Ops was called from inside a kernel call or from the failure callback.
|
||||
Reentered,
|
||||
/// A record the conversion cannot carry to the kernel, and why.
|
||||
@@ -431,6 +440,9 @@ impl std::fmt::Display for Error {
|
||||
"the loft's guided edges were asked within {asked} of their rails; at {wires} wires the bound proved is {reached}"
|
||||
),
|
||||
Error::WallsTouch => write!(f, "the offset's walls meet"),
|
||||
Error::DefinitionNotCertified { face, eps } => {
|
||||
write!(f, "the definition of face {face} does not certify its net within {eps}")
|
||||
}
|
||||
Error::TooLarge { size, kind, .. } => {
|
||||
write!(f, "too large: the largest {kind:?} that fits is {size}")
|
||||
}
|
||||
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
//! A body face's exact surface, for an application: what the face is ([`Exact`]: its analytic
|
||||
//! label, the torus a round of the exact circle class is, a blend by its definition, or its
|
||||
//! NURBS), and on it, in the face's own (u, v), its point, its unit normal and the point of it
|
||||
//! nearest a given point from a guess. A blend is evaluated on its definition S, rebuilt from the
|
||||
//! record and its net certified again; the nearest point is a bounded Newton iteration, refused
|
||||
//! when it does not settle. The kernel computes every value (`surface::face_evaluation`); this
|
||||
//! module reads the record and converts.
|
||||
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore::surface::face_evaluation as fe;
|
||||
use solidcore::surface::face_surface::{face_surface, FaceSurface as KernelFace};
|
||||
use solidcore::surface::procedural_blend::convert::definition_surface;
|
||||
use solidcore_record::brep::{Brep, Loop, Surface};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::session::{run, Answer};
|
||||
|
||||
/// What a face's exact surface is.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Exact {
|
||||
/// An analytic surface, as the record labels it (a plane, cylinder, cone, sphere, torus, ball
|
||||
/// patch or revolution); the face's (u, v) are its NURBS form's.
|
||||
Label(Surface),
|
||||
/// A round of the exact circle class: the torus its definition gives.
|
||||
Torus {
|
||||
/// The torus's centre.
|
||||
centre: [f64; 3],
|
||||
/// Its unit axis.
|
||||
axis: [f64; 3],
|
||||
/// The major radius: the circle the ball's centre runs on.
|
||||
major: f64,
|
||||
/// The minor radius: the ball's.
|
||||
minor: f64,
|
||||
},
|
||||
/// A blend by its definition S, its net within `bound` of it (0 for an exact round).
|
||||
Blend {
|
||||
/// The net's certified bound from S.
|
||||
bound: f64,
|
||||
},
|
||||
/// A B-spline or NURBS surface: the face is its NURBS.
|
||||
Nurbs,
|
||||
}
|
||||
|
||||
/// The point of a face's surface nearest a given point.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Closest {
|
||||
/// Its (u, v), the face's.
|
||||
pub uv: [f64; 2],
|
||||
/// The point.
|
||||
pub point: [f64; 3],
|
||||
/// Its distance from the given point.
|
||||
pub distance: f64,
|
||||
}
|
||||
|
||||
/// A body face's exact surface ([`FaceSurface::of`]).
|
||||
#[derive(Debug)]
|
||||
pub struct FaceSurface {
|
||||
face: KernelFace,
|
||||
exact: Exact,
|
||||
}
|
||||
|
||||
impl Answer for [f64; 3] {}
|
||||
impl Answer for Closest {}
|
||||
|
||||
fn triple(p: solidcore::Vec3) -> [f64; 3] {
|
||||
[p.x, p.y, p.z]
|
||||
}
|
||||
|
||||
impl FaceSurface {
|
||||
/// Face `face` of `record`'s exact surface. A blend's definition is rebuilt from the record
|
||||
/// and its net, the face's NURBS form, certified against it again.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`Error::Argument`] for a face past the record's, or a record the conversion refuses;
|
||||
/// [`Error::Unsupported`] for a face whose (u, v) are its label's own (an analytic label with
|
||||
/// no NURBS form, whose parameters are not the kernel's); [`Error::DefinitionNotCertified`]
|
||||
/// for a blend whose net its definition does not certify; the kernel's refusals.
|
||||
pub fn of(record: &Brep, face: usize) -> Result<FaceSurface> {
|
||||
let f = record.faces.get(face).ok_or(Error::Argument)?;
|
||||
let conv = |s: &Surface| solidcore_convert::surface(s).map_err(|_| Error::Argument);
|
||||
// A blend is built on its net; a label on its form; a ball patch's loops, which the
|
||||
// kernel certifies on its sphere.
|
||||
let (label, form, definition) = match (&f.surface, &f.nurbs) {
|
||||
(Surface::Blend { definition }, Some(net)) => (
|
||||
conv(net)?,
|
||||
None,
|
||||
Some(solidcore_convert::blend::blend(definition)),
|
||||
),
|
||||
(Surface::Blend { .. }, None) => return Err(Error::Argument),
|
||||
(label, form) => (conv(label)?, form.as_ref().map(conv).transpose()?, None),
|
||||
};
|
||||
let mut boundary = Vec::new();
|
||||
if matches!(f.surface, Surface::BallPatch { .. }) {
|
||||
for l in &f.loops {
|
||||
if let Loop::Edges { edges } = l {
|
||||
for u in edges {
|
||||
let e = record.edges.get(u.edge as usize).ok_or(Error::Argument)?;
|
||||
boundary.push(
|
||||
solidcore_convert::curve(e.nurbs.as_ref().unwrap_or(&e.curve))
|
||||
.map_err(|_| Error::Argument)?,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let domain = f.domain;
|
||||
let labelled = !matches!(
|
||||
f.surface,
|
||||
Surface::Spline { .. } | Surface::RationalSpline { .. } | Surface::Blend { .. }
|
||||
);
|
||||
let surface_label = f.surface.clone();
|
||||
run("face_surface::of", move |k: &mut Kernel| {
|
||||
let mut made = face_surface(k, &label, form.as_ref(), domain, &boundary)?;
|
||||
if !made.trims {
|
||||
return Ok(Err(Error::Unsupported(
|
||||
"a face whose (u, v) are its label's own",
|
||||
)));
|
||||
}
|
||||
let exact = match &definition {
|
||||
Some(definition) => match definition_surface(k, definition, &made.surface)? {
|
||||
Ok(surface) => {
|
||||
made.surface = surface;
|
||||
match fe::circle_round_torus(k, &made)? {
|
||||
Some(t) => Exact::Torus {
|
||||
centre: triple(t.centre),
|
||||
axis: triple(t.axis),
|
||||
major: t.major,
|
||||
minor: t.minor,
|
||||
},
|
||||
None => Exact::Blend {
|
||||
bound: made.surface.procedural_blend().map_or(0.0, |b| b.bound()),
|
||||
},
|
||||
}
|
||||
}
|
||||
Err(eps) => {
|
||||
return Ok(Err(Error::DefinitionNotCertified { face, eps }));
|
||||
}
|
||||
},
|
||||
None if labelled => Exact::Label(surface_label),
|
||||
None => Exact::Nurbs,
|
||||
};
|
||||
Ok(Ok(FaceSurface { face: made, exact }))
|
||||
})?
|
||||
}
|
||||
|
||||
/// What the face's exact surface is.
|
||||
#[must_use]
|
||||
pub fn exact(&self) -> &Exact {
|
||||
&self.exact
|
||||
}
|
||||
|
||||
/// The face's parameter box, `[[u0, u1], [v0, v1]]`, in its own (u, v).
|
||||
#[must_use]
|
||||
pub fn domain(&self) -> [[f64; 2]; 2] {
|
||||
fe::face_domain(&self.face)
|
||||
}
|
||||
|
||||
/// The point at the face's (u, v).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The kernel's refusal of the evaluation.
|
||||
pub fn point(&self, uv: [f64; 2]) -> Result<[f64; 3]> {
|
||||
run("face_surface::point", |k| {
|
||||
fe::point(k, &self.face, uv).map(triple)
|
||||
})
|
||||
}
|
||||
|
||||
/// The unit normal at the face's (u, v), `∂S/∂u × ∂S/∂v` made unit.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`Error::Degenerate`] where the partials are parallel; the kernel's refusal of the
|
||||
/// evaluation.
|
||||
pub fn normal(&self, uv: [f64; 2]) -> Result<[f64; 3]> {
|
||||
run("face_surface::normal", |k| {
|
||||
fe::normal(k, &self.face, uv).map(triple)
|
||||
})
|
||||
}
|
||||
|
||||
/// The point of the face's surface nearest `p`, from the guess `guess` (the face's (u, v)),
|
||||
/// within the face's parameter box: a bounded Newton iteration
|
||||
/// (`face_evaluation::MAX_NEAREST_STEPS` steps).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The kernel's `NotConverged` past its steps, as [`Error::Status`]; [`Error::Degenerate`];
|
||||
/// the kernel's refusal of the evaluations.
|
||||
pub fn closest_point(&self, p: [f64; 3], guess: [f64; 2]) -> Result<Closest> {
|
||||
let p = solidcore::Vec3::new(p[0], p[1], p[2]);
|
||||
run("face_surface::closest_point", |k| {
|
||||
fe::nearest(k, &self.face, p, guess).map(|n| Closest {
|
||||
uv: n.uv,
|
||||
point: triple(n.point),
|
||||
distance: n.distance,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -48,9 +48,13 @@ pub fn curve_interval(
|
||||
/// Whether the face's NURBS form turns its natural normal against its surface's
|
||||
/// ([`solidcore::eval::label::form_turns`]). A shell's `forward` is about [`Face::parameters`],
|
||||
/// so the side the surface is used on is `forward != form_turns(face)`. `Some(false)` without a
|
||||
/// form; `None` where the form does not evaluate.
|
||||
/// form, and for a blend, whose form is its geometry; `None` where the form does not evaluate.
|
||||
#[must_use]
|
||||
pub fn form_turns(face: &Face) -> Option<bool> {
|
||||
// A blend's label has no normal of its own: its form is the face's geometry.
|
||||
if matches!(face.surface, Surface::Blend { .. }) {
|
||||
return Some(false);
|
||||
}
|
||||
let surface = solidcore_convert::surface(&face.surface).ok()?;
|
||||
let nurbs = face
|
||||
.nurbs
|
||||
|
||||
+2
@@ -30,6 +30,7 @@
|
||||
#![cfg_attr(not(test), deny(clippy::float_arithmetic))]
|
||||
|
||||
pub mod blend;
|
||||
pub mod blend_definition;
|
||||
pub mod body;
|
||||
pub mod boolean;
|
||||
pub mod cancel;
|
||||
@@ -39,6 +40,7 @@ pub mod continuity;
|
||||
pub mod curve;
|
||||
pub mod error;
|
||||
pub mod extend;
|
||||
pub mod face_surface;
|
||||
pub mod file;
|
||||
pub mod label;
|
||||
pub mod measure;
|
||||
|
||||
+5
-1
@@ -37,7 +37,11 @@ pub fn geometry(b: &Brep) -> Result<()> {
|
||||
}
|
||||
}
|
||||
for f in &b.faces {
|
||||
surface(&f.surface)?;
|
||||
// (a blend's label is its definition, which its structural validation checks and the
|
||||
// kernel certifies its net against when the face is built; its geometry is its form)
|
||||
if !matches!(f.surface, Surface::Blend { .. }) {
|
||||
surface(&f.surface)?;
|
||||
}
|
||||
if let Some(n) = &f.nurbs {
|
||||
surface(n)?;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
//! `face_surface::FaceSurface`: a body face's exact surface and its point, normal and nearest
|
||||
//! point, against closed forms.
|
||||
//!
|
||||
//! - The round of a bore's rim (the plane z = 10 square to a Z bore of radius 5, a round of 0.3):
|
||||
//! the torus whose centre circle has radius 5.3 at z = 9.7 and whose tube is 0.3. Every point
|
||||
//! is on it, every normal is its normal and every nearest point is its nearest point, to 1e-12.
|
||||
//! - The round at a boss's root (a Z boss of radius 5 on the plane z = 10, a round of 0.3): the
|
||||
//! torus of centre circle radius 5.3 at z = 10.3 and tube 0.3, the same way.
|
||||
//! - A variable round (a general procedural blend): the nearest point on its definition S of a
|
||||
//! point of its net is within the certified bound, its residual along S's normal; a point off
|
||||
//! S along its normal has that point for its nearest.
|
||||
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::panic,
|
||||
clippy::indexing_slicing,
|
||||
clippy::disallowed_methods
|
||||
)] // test
|
||||
|
||||
mod common;
|
||||
|
||||
use common::{all_fixes, edge_at, vertex_at};
|
||||
use solidcore_ops::blend::{blend, fillet_variable, Blend, Corners};
|
||||
use solidcore_ops::boolean::boolean;
|
||||
use solidcore_ops::face_surface::{Exact, FaceSurface};
|
||||
use solidcore_ops::{make_box, make_cylinder, Body, BooleanOp, Error};
|
||||
use solidcore_record::brep::{Brep as Record, Surface};
|
||||
|
||||
const TOL: f64 = 1e-12;
|
||||
|
||||
fn sub(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||||
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
|
||||
}
|
||||
|
||||
fn add(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
|
||||
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
|
||||
}
|
||||
|
||||
fn scale(a: [f64; 3], s: f64) -> [f64; 3] {
|
||||
[a[0] * s, a[1] * s, a[2] * s]
|
||||
}
|
||||
|
||||
fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
|
||||
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
|
||||
}
|
||||
|
||||
fn length(a: [f64; 3]) -> f64 {
|
||||
dot(a, a).sqrt()
|
||||
}
|
||||
|
||||
/// The record of `b` and the indices of its faces kept as their blend definition.
|
||||
fn blend_faces(b: &Body) -> (Record, Vec<usize>) {
|
||||
let (record, _) = b.to_record().unwrap();
|
||||
let faces = record
|
||||
.faces
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, f)| matches!(f.surface, Surface::Blend { .. }))
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
(record, faces)
|
||||
}
|
||||
|
||||
/// The (u, v) at fractions `(a, c)` of the face's parameter box.
|
||||
fn at(s: &FaceSurface, a: f64, c: f64) -> [f64; 2] {
|
||||
let [[u0, u1], [v0, v1]] = s.domain();
|
||||
[u0 + a * (u1 - u0), v0 + c * (v1 - v0)]
|
||||
}
|
||||
|
||||
/// The interior fractions sampled.
|
||||
const FRACTIONS: [f64; 5] = [0.1, 0.3, 0.5, 0.7, 0.9];
|
||||
|
||||
/// A torus about the Z axis through `centre`: its centre circle of radius `major` in
|
||||
/// z = `centre[2]`, its tube `minor`.
|
||||
struct Torus {
|
||||
centre: [f64; 3],
|
||||
major: f64,
|
||||
minor: f64,
|
||||
}
|
||||
|
||||
impl Torus {
|
||||
/// The point of the centre circle nearest `p` (off the axis).
|
||||
fn spine(&self, p: [f64; 3]) -> [f64; 3] {
|
||||
let d = [p[0] - self.centre[0], p[1] - self.centre[1], 0.0];
|
||||
add(self.centre, scale(d, self.major / length(d)))
|
||||
}
|
||||
|
||||
/// The point of the torus nearest `p` (off the axis and the centre circle).
|
||||
fn nearest(&self, p: [f64; 3]) -> [f64; 3] {
|
||||
let c = self.spine(p);
|
||||
let d = sub(p, c);
|
||||
add(c, scale(d, self.minor / length(d)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Every round face of `b` is the torus `t`: the exact surface it reports, and its points,
|
||||
/// normals and nearest points, to [`TOL`].
|
||||
fn check_round(b: &Body, t: &Torus) {
|
||||
let (record, faces) = blend_faces(b);
|
||||
assert!(!faces.is_empty(), "the round kept no definition");
|
||||
for face in faces {
|
||||
let s = FaceSurface::of(&record, face).unwrap();
|
||||
let Exact::Torus {
|
||||
centre,
|
||||
axis,
|
||||
major,
|
||||
minor,
|
||||
} = *s.exact()
|
||||
else {
|
||||
panic!("face {face}: {:?}, not a torus", s.exact());
|
||||
};
|
||||
assert!(length(sub(centre, t.centre)) <= TOL, "centre {centre:?}");
|
||||
assert!(
|
||||
axis[0].abs() <= TOL && axis[1].abs() <= TOL && (axis[2].abs() - 1.0).abs() <= TOL,
|
||||
"axis {axis:?}"
|
||||
);
|
||||
assert!((major - t.major).abs() <= TOL, "major {major}");
|
||||
assert!((minor - t.minor).abs() <= TOL, "minor {minor}");
|
||||
// the face's normal is the torus's, on one side throughout
|
||||
let mut side = 0.0;
|
||||
for a in FRACTIONS {
|
||||
for c in FRACTIONS {
|
||||
let uv = at(&s, a, c);
|
||||
let p = s.point(uv).unwrap();
|
||||
let off = sub(p, t.spine(p));
|
||||
assert!(
|
||||
(length(off) - t.minor).abs() <= TOL,
|
||||
"{uv:?}: {p:?} is {} off the torus",
|
||||
length(off) - t.minor
|
||||
);
|
||||
let n = s.normal(uv).unwrap();
|
||||
let m = scale(off, 1.0 / length(off));
|
||||
if side == 0.0 {
|
||||
side = dot(n, m).signum();
|
||||
}
|
||||
assert!(
|
||||
length(sub(n, scale(m, side))) <= TOL,
|
||||
"{uv:?}: normal {n:?}, the torus's {m:?}"
|
||||
);
|
||||
// a point 0.1 off along the normal, from a guess beside the point
|
||||
let q = add(p, scale(n, 0.1));
|
||||
let guess = at(&s, a + 0.04, c - 0.04);
|
||||
let near = s.closest_point(q, guess).unwrap();
|
||||
let want = t.nearest(q);
|
||||
assert!(
|
||||
length(sub(near.point, want)) <= TOL,
|
||||
"{uv:?}: nearest {:?}, the torus's {want:?}",
|
||||
near.point
|
||||
);
|
||||
assert!((near.distance - length(sub(q, want))).abs() <= TOL);
|
||||
assert!(length(sub(s.point(near.uv).unwrap(), near.point)) <= TOL);
|
||||
// the same inputs give the same answer, bit for bit
|
||||
assert_eq!(s.closest_point(q, guess).unwrap(), near);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The 30 x 30 x 10 block at the origin.
|
||||
fn plate() -> Body {
|
||||
make_box([0.0; 3], [30.0, 30.0, 10.0]).unwrap()
|
||||
}
|
||||
|
||||
/// A point of the circle of radius 5 about (15, 15) at height `z`, off any seam.
|
||||
fn on_rim(z: f64) -> [f64; 3] {
|
||||
let a = std::f64::consts::FRAC_PI_4;
|
||||
[15.0 + 5.0 * a.cos(), 15.0 + 5.0 * a.sin(), z]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bore_rims_round_is_its_torus() {
|
||||
all_fixes();
|
||||
let bore = make_cylinder([15.0, 15.0, 0.0], 5.0, 10.0).unwrap();
|
||||
let b = boolean(plate(), bore, BooleanOp::Subtract)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let e = edge_at(&b, on_rim(10.0));
|
||||
blend(&b, Blend::Round, &[e], &[0.3], &[], Corners::Filled).unwrap();
|
||||
check_round(
|
||||
&b,
|
||||
&Torus {
|
||||
centre: [15.0, 15.0, 9.7],
|
||||
major: 5.3,
|
||||
minor: 0.3,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_boss_roots_round_is_its_torus() {
|
||||
all_fixes();
|
||||
let boss = make_cylinder([15.0, 15.0, 10.0], 5.0, 5.0).unwrap();
|
||||
let b = boolean(plate(), boss, BooleanOp::Union).unwrap().unwrap();
|
||||
let e = edge_at(&b, on_rim(10.0));
|
||||
blend(&b, Blend::Round, &[e], &[0.3], &[], Corners::Filled).unwrap();
|
||||
check_round(
|
||||
&b,
|
||||
&Torus {
|
||||
centre: [15.0, 15.0, 10.3],
|
||||
major: 5.3,
|
||||
minor: 0.3,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_variable_rounds_definition_is_within_its_bound_of_its_net() {
|
||||
all_fixes();
|
||||
// the round of radius 2 to 5 along the block's edge x = 10, z = 30 (tests/blend.rs)
|
||||
let b = make_box([0.0; 3], [10.0, 20.0, 30.0]).unwrap();
|
||||
let e = edge_at(&b, [10.0, 10.0, 30.0]);
|
||||
let start = vertex_at(&b, [10.0, 0.0, 30.0]);
|
||||
fillet_variable(&b, e, start, 1, &[0.0, 0.0, 1.0, 1.0], &[2.0, 5.0]).unwrap();
|
||||
let (record, faces) = blend_faces(&b);
|
||||
assert!(!faces.is_empty(), "the round kept no definition");
|
||||
for face in faces {
|
||||
let s = FaceSurface::of(&record, face).unwrap();
|
||||
let Exact::Blend { bound } = *s.exact() else {
|
||||
panic!("face {face}: {:?}, not a procedural blend", s.exact());
|
||||
};
|
||||
assert!(bound.is_finite() && bound >= 0.0, "bound {bound}");
|
||||
// the same face as its net alone: its NURBS, in the same (u, v)
|
||||
let mut bare = record.clone();
|
||||
let f = &mut bare.faces[face];
|
||||
f.surface = f.nurbs.take().unwrap();
|
||||
let net = FaceSurface::of(&bare, face).unwrap();
|
||||
assert_eq!(*net.exact(), Exact::Nurbs);
|
||||
assert_eq!(net.domain(), s.domain());
|
||||
for a in FRACTIONS {
|
||||
for c in FRACTIONS {
|
||||
let uv = at(&s, a, c);
|
||||
// a point of the net: S's nearest is within the bound, along S's normal
|
||||
let n_point = net.point(uv).unwrap();
|
||||
let near = s.closest_point(n_point, uv).unwrap();
|
||||
assert!(
|
||||
near.distance <= bound,
|
||||
"{uv:?}: the net is {} off S, its bound {bound}",
|
||||
near.distance
|
||||
);
|
||||
let r = sub(n_point, near.point);
|
||||
let n = s.normal(near.uv).unwrap();
|
||||
let along = sub(r, scale(n, dot(r, n)));
|
||||
assert!(
|
||||
length(along) <= TOL,
|
||||
"{uv:?}: residual {r:?} is {} off S's normal",
|
||||
length(along)
|
||||
);
|
||||
// a point 0.05 off S along its normal has that point for its nearest (the
|
||||
// round's radius is 2 at least)
|
||||
let p = s.point(uv).unwrap();
|
||||
let q = add(p, scale(s.normal(uv).unwrap(), 0.05));
|
||||
let near = s.closest_point(q, at(&s, a + 0.04, c - 0.04)).unwrap();
|
||||
assert!(
|
||||
length(sub(near.point, p)) <= 1e-9,
|
||||
"{uv:?}: nearest {:?}, want {p:?}",
|
||||
near.point
|
||||
);
|
||||
assert!((near.distance - 0.05).abs() <= 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_face_past_the_records_is_refused() {
|
||||
all_fixes();
|
||||
let (record, _) = plate().to_record().unwrap();
|
||||
let past = record.faces.len();
|
||||
assert!(matches!(
|
||||
FaceSurface::of(&record, past),
|
||||
Err(Error::Argument)
|
||||
));
|
||||
}
|
||||
+19
-4
@@ -11,6 +11,7 @@
|
||||
//! bounds are its `interval`, a face's its `domain` and loops.
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod blend;
|
||||
mod curve2;
|
||||
pub mod envelope;
|
||||
pub mod history;
|
||||
@@ -301,6 +302,14 @@ pub enum Surface {
|
||||
matrix: [[f64; 4]; 4],
|
||||
forward: bool,
|
||||
},
|
||||
/// A blend by its definition: a procedural blend's definition S, or a round of an exact
|
||||
/// class's ([`blend::BlendDefinition`]). It has no (u, v) of its own: the face's `nurbs` is
|
||||
/// its net B, certified within the definition's bound of S (an exact round's net is S), and
|
||||
/// the face's (u, v) are the net's. The kernel rebuilds the definition and certifies the net
|
||||
/// again before the face is an operand.
|
||||
Blend {
|
||||
definition: Box<blend::BlendDefinition>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Why a record is refused.
|
||||
@@ -725,6 +734,10 @@ fn surface(s: &Surface, depth: usize, budget: &mut Budget) -> Result<()> {
|
||||
points(matrix, budget)?;
|
||||
curve(profile, depth + 1, budget)
|
||||
}
|
||||
Surface::Blend { definition } => {
|
||||
definition.validate().map_err(|e| Invalid(e.reason))?;
|
||||
budget.add(definition.items())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -837,12 +850,13 @@ impl Brep {
|
||||
| Surface::BallPatch { .. }
|
||||
| Surface::Torus { .. }
|
||||
| Surface::PlacedRevolution { .. }
|
||||
| Surface::Blend { .. }
|
||||
),
|
||||
"NURBS form on a surface that is not a cylinder, cone, sphere, ball patch, torus or revolution",
|
||||
"NURBS form on a surface that is not a cylinder, cone, sphere, ball patch, torus, revolution or blend",
|
||||
)?;
|
||||
}
|
||||
// A cylinder, cone, sphere, ball patch, torus or revolution is a label: its geometry is
|
||||
// its form.
|
||||
// A cylinder, cone, sphere, ball patch, torus, revolution or blend is a label: its
|
||||
// geometry is its form.
|
||||
require(
|
||||
f.nurbs.is_some()
|
||||
|| !matches!(
|
||||
@@ -853,8 +867,9 @@ impl Brep {
|
||||
| Surface::BallPatch { .. }
|
||||
| Surface::Torus { .. }
|
||||
| Surface::PlacedRevolution { .. }
|
||||
| Surface::Blend { .. }
|
||||
),
|
||||
"a cylinder, cone, sphere, ball patch, torus or revolution without its NURBS form",
|
||||
"a cylinder, cone, sphere, ball patch, torus, revolution or blend without its NURBS form",
|
||||
)?;
|
||||
let [u, v] = parameter_natural(f.parameters())?;
|
||||
interval(&f.domain[0], u)?;
|
||||
|
||||
+636
@@ -0,0 +1,636 @@
|
||||
//! A blend by its definition, as plain data: what a face's [`Surface::Blend`](super::Surface)
|
||||
//! holds. A procedural blend's definition S (docs/definitions-and-certificates.md: its supports,
|
||||
//! ball, section, leader and seeds, its certified bound ε and the map from the net's parameters to
|
||||
//! S's), or a round of an exact class's (its supports, ball, section, leader and what its class is
|
||||
//! read with). The face's `nurbs` is the net B, certified within ε of S (an exact round's net is S,
|
||||
//! ε = 0), stored once there; a support that is itself a blend is no face, and keeps its whole
|
||||
//! record, net included.
|
||||
//!
|
||||
//! The kernel holds the same records as its values (`surface::procedural_blend::record`), and
|
||||
//! `solidcore-convert` copies the fields from one to the other; the kernel's rebuild makes the
|
||||
//! definition exactly and certifies the net again before it is an operand. Nothing here
|
||||
//! evaluates, measures or compares geometry: the checks are structural ([`BlendDefinition::validate`]).
|
||||
//!
|
||||
//! Recursion: [`BlendDefinitionRecord::validate`] walks nested supports at most
|
||||
//! [`RECORD_MAX_DEPTH`] deep and refuses deeper records before descending.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// The most definitions a record nests (a round on a round on a round).
|
||||
pub const RECORD_MAX_DEPTH: u32 = 3;
|
||||
|
||||
/// A face's blend definition: a procedural blend's, or a round of an exact class's. The face's
|
||||
/// `nurbs` is its net.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum BlendDefinition {
|
||||
/// A procedural blend: its net within the definition's bound of S.
|
||||
Procedural(Box<BlendDefinitionRecord>),
|
||||
/// A round of an exact class: its net is S.
|
||||
ExactRound(Box<ExactRoundDefinition>),
|
||||
}
|
||||
|
||||
/// A NURBS curve of a definition (the leader, a seed, a radius law), as stored.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct NurbsCurveRecord {
|
||||
/// The curve's image dimension: 2 or 3.
|
||||
pub dim: u32,
|
||||
/// Whether the control points carry a z.
|
||||
pub points_3d: bool,
|
||||
/// The degree p.
|
||||
pub degree: u32,
|
||||
/// The full knot vector, `points.len() + degree + 1` knots.
|
||||
pub knots: Vec<f64>,
|
||||
/// The control points as stored: `(w x, w y, w z)` when rational, else `(x, y, z)`; z is 0
|
||||
/// when `points_3d` is false.
|
||||
pub points: Vec<[f64; 3]>,
|
||||
/// The weights, one per control point, when the curve is rational.
|
||||
pub weights: Option<Vec<f64>>,
|
||||
}
|
||||
|
||||
/// A NURBS surface (a support, or a nested blend's net), as stored.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct NurbsSurfaceRecord {
|
||||
/// The degree in u.
|
||||
pub u_degree: u32,
|
||||
/// The degree in v.
|
||||
pub v_degree: u32,
|
||||
/// The number of control points along u.
|
||||
pub u_count: u32,
|
||||
/// The number of control points along v.
|
||||
pub v_count: u32,
|
||||
/// The full u knot vector, `u_count + u_degree + 1` knots.
|
||||
pub u_knots: Vec<f64>,
|
||||
/// The full v knot vector, `v_count + v_degree + 1` knots.
|
||||
pub v_knots: Vec<f64>,
|
||||
/// The control points u-major (`points[i * v_count + j]`), as stored: weighted when rational.
|
||||
pub points: Vec<[f64; 3]>,
|
||||
/// The weights, one per control point, when the surface is rational.
|
||||
pub weights: Option<Vec<f64>>,
|
||||
}
|
||||
|
||||
/// The analytic kind of a support, a label beside its NURBS form, which is the support's one
|
||||
/// geometric truth.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SupportKindRecord {
|
||||
/// No analytic kind: a B-spline surface.
|
||||
Free,
|
||||
/// A plane.
|
||||
Plane,
|
||||
/// A cylinder.
|
||||
Cylinder,
|
||||
/// A cone.
|
||||
Cone,
|
||||
/// A sphere.
|
||||
Sphere,
|
||||
/// A torus.
|
||||
Torus,
|
||||
}
|
||||
|
||||
/// A surface the ball rolls on: a NURBS form with its analytic kind, or itself a blend by its
|
||||
/// definition.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum SupportRecord {
|
||||
/// A surface by its NURBS form, and its analytic kind.
|
||||
Nurbs {
|
||||
/// The analytic kind ([`SupportKindRecord::Free`]: none).
|
||||
kind: SupportKindRecord,
|
||||
/// The NURBS form.
|
||||
form: NurbsSurfaceRecord,
|
||||
},
|
||||
/// A procedural blend, definition and net (nested, at most [`RECORD_MAX_DEPTH`] deep in all).
|
||||
Blend(Box<ProceduralBlendRecord>),
|
||||
}
|
||||
|
||||
/// The ball's radius.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum RadiusRecord {
|
||||
/// A ball of constant offsets from the two supports.
|
||||
Constant {
|
||||
/// The offsets from S1 and S2.
|
||||
offsets: [f64; 2],
|
||||
},
|
||||
/// A ball whose radius is the x of a curve over the leader's parameter.
|
||||
Law(NurbsCurveRecord),
|
||||
}
|
||||
|
||||
/// How a chamfer's size was asked for.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub enum ChamferSizeRecord {
|
||||
/// The setbacks from the edge along supports 1 and 2.
|
||||
Distances([f64; 2]),
|
||||
/// The setback along support 1 and the chamfer's angle (radians) to it.
|
||||
DistanceAngle {
|
||||
/// The setback along support 1.
|
||||
distance: f64,
|
||||
/// The chamfer's angle to support 1.
|
||||
angle: f64,
|
||||
},
|
||||
/// The chord between the rails.
|
||||
Chord(f64),
|
||||
}
|
||||
|
||||
/// A chamfer's sizes and the wedge angle they were converted to offsets at.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ChamferRecord {
|
||||
/// The sizes as asked.
|
||||
pub size: ChamferSizeRecord,
|
||||
/// The angle between the supports the sizes were converted at.
|
||||
pub wedge: f64,
|
||||
}
|
||||
|
||||
/// The affine map from the net's (u, v) to the definition's (s, t):
|
||||
/// `(s, t) = (a[0] + b[0] p, a[1] + b[1] q)`, `(p, q)` = `(u, v)`, or `(v, u)` when swapped.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ParameterMapRecord {
|
||||
/// The offsets a.
|
||||
pub a: [f64; 2],
|
||||
/// The scales b.
|
||||
pub b: [f64; 2],
|
||||
/// Whether `(p, q)` is `(v, u)`.
|
||||
pub swapped: bool,
|
||||
}
|
||||
|
||||
/// The definition S of a rolling-ball blend.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct BlendDefinitionRecord {
|
||||
/// The supports S1, S2, by value.
|
||||
pub supports: [SupportRecord; 2],
|
||||
/// The ball's side of each support, +1 or -1 along its normal.
|
||||
pub sides: [f64; 2],
|
||||
/// The radius: constant offsets, or a law.
|
||||
pub radius: RadiusRecord,
|
||||
/// The section's order n: 0 the chord (a chamfer), 1 the ball's circle, 2 to 4 a G^n section.
|
||||
pub order: u32,
|
||||
/// The thumbweight w for n >= 2; 0 for n = 1.
|
||||
pub thumbweight: f64,
|
||||
/// The leader (centre line) the stations are solved in; it fixes s.
|
||||
pub leader: NurbsCurveRecord,
|
||||
/// The seeds: each foot's (u, v) on its support over s, the stations' Newton starts.
|
||||
pub seeds: [NurbsCurveRecord; 2],
|
||||
/// The certified bound ε: the net is within ε of S.
|
||||
pub bound: f64,
|
||||
/// The map from the net's parameters to the definition's.
|
||||
pub map: ParameterMapRecord,
|
||||
/// A chamfer's sizes (order 0): what its offsets were asked as.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub chamfer: Option<ChamferRecord>,
|
||||
}
|
||||
|
||||
/// A procedural blend: its definition S and its certified net B. A face's blend keeps its net as
|
||||
/// the face's `nurbs`; this whole record is a nested support's.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ProceduralBlendRecord {
|
||||
/// The definition S.
|
||||
pub definition: BlendDefinitionRecord,
|
||||
/// The net B, within the definition's bound of S.
|
||||
pub net: NurbsSurfaceRecord,
|
||||
}
|
||||
|
||||
/// The exact class of a round whose net is its definition.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ExactClassRecord {
|
||||
/// A straight spine: the cylinder about it.
|
||||
Line,
|
||||
/// A circle about a common axis: the section turned about it.
|
||||
Circle,
|
||||
/// One point: a piece of the ball.
|
||||
Point,
|
||||
/// A chamfer between planes: the plane through the feet of its line spine.
|
||||
Chord,
|
||||
/// A conic spine.
|
||||
Conic,
|
||||
/// One of a pair of conics.
|
||||
ConicPair,
|
||||
/// A nodal quartic spine.
|
||||
NodalQuartic,
|
||||
}
|
||||
|
||||
/// The analytic kind of an exact net, a label beside its NURBS form (the face's `nurbs`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ExactNetKindRecord {
|
||||
/// A plane.
|
||||
Plane,
|
||||
/// A cylinder.
|
||||
Cylinder,
|
||||
/// A cone.
|
||||
Cone,
|
||||
/// A sphere.
|
||||
Sphere,
|
||||
/// A torus.
|
||||
Torus,
|
||||
/// A surface of revolution: its first section turned about the axis of its circles.
|
||||
Revolution,
|
||||
/// A ball patch: a rational patch on its sphere.
|
||||
BallPatch,
|
||||
/// A B-spline surface, the form itself (the rational classes' exact surfaces).
|
||||
BSpline,
|
||||
}
|
||||
|
||||
/// What an exact round's class is read with beside its supports.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ClassInputsRecord {
|
||||
/// The `(u, v)` each support's quadric is read at.
|
||||
pub uv: [[f64; 2]; 2],
|
||||
/// The signed offset of the ball's centre from each support along its normal.
|
||||
pub offsets: [f64; 2],
|
||||
/// The ball's centre the spine is classified at.
|
||||
pub centre: [f64; 3],
|
||||
/// The tolerance placements are decided within.
|
||||
pub tol: f64,
|
||||
/// Whether a collapsed offset crossed by the other is the ball's piece.
|
||||
pub ball_on_collapsed: bool,
|
||||
/// Whether the radii can change along the edge.
|
||||
pub radii_can_change: bool,
|
||||
}
|
||||
|
||||
/// How an exact round's net runs along its leader: the leader's parameter is `a + b w`, `w` the
|
||||
/// net's u, or its v when `along_v`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub struct LeaderMapRecord {
|
||||
/// The offset a.
|
||||
pub a: f64,
|
||||
/// The scale b.
|
||||
pub b: f64,
|
||||
/// Whether the leader runs along the net's v.
|
||||
pub along_v: bool,
|
||||
}
|
||||
|
||||
/// A round of an exact class by its definition (supports, ball, section, leader and what its
|
||||
/// class is read with) and its net's analytic kind: the kernel's `ExactRoundRecord` without the
|
||||
/// net's form, which is the face's `nurbs`.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ExactRoundDefinition {
|
||||
/// The class.
|
||||
pub class: ExactClassRecord,
|
||||
/// The supports S1, S2, by value.
|
||||
pub supports: [SupportRecord; 2],
|
||||
/// The ball's radius (0 for the chord).
|
||||
pub radius: f64,
|
||||
/// The section's order: 1, the ball's circle; 0, the chord.
|
||||
pub order: u32,
|
||||
/// The leader (the centre line), where the class's construction makes one.
|
||||
pub leader: Option<NurbsCurveRecord>,
|
||||
/// What the class is read with beside the supports.
|
||||
pub inputs: ClassInputsRecord,
|
||||
/// A chamfer's sizes (the chord).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub chamfer: Option<ChamferRecord>,
|
||||
/// How the net's parameter runs along the leader.
|
||||
pub leader_map: LeaderMapRecord,
|
||||
/// The net's analytic kind.
|
||||
pub net_kind: ExactNetKindRecord,
|
||||
}
|
||||
|
||||
/// What a definition's structural check refused: the field and why.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct RecordError {
|
||||
/// The field, as a path (`definition.leader.knots`).
|
||||
pub field: &'static str,
|
||||
/// What is wrong with it.
|
||||
pub reason: &'static str,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for RecordError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(f, "{}: {}", self.field, self.reason)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RecordError {}
|
||||
|
||||
const fn refuse(field: &'static str, reason: &'static str) -> RecordError {
|
||||
RecordError { field, reason }
|
||||
}
|
||||
|
||||
fn all_finite(values: &[f64]) -> bool {
|
||||
values.iter().all(|v| v.is_finite())
|
||||
}
|
||||
|
||||
fn points_finite(points: &[[f64; 3]]) -> bool {
|
||||
points.iter().all(|p| all_finite(p))
|
||||
}
|
||||
|
||||
/// `count + degree + 1`, or `None` past `usize`.
|
||||
fn knot_count(count: usize, degree: u32) -> Option<usize> {
|
||||
let degree = usize::try_from(degree).ok()?;
|
||||
count.checked_add(degree)?.checked_add(1)
|
||||
}
|
||||
|
||||
impl NurbsCurveRecord {
|
||||
/// The structural check: a degree of at least 1, `points + degree + 1` knots, a weight per
|
||||
/// point when rational, a dimension of 2 or 3 (a 3D curve has 3D points), and every number
|
||||
/// finite.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The first [`RecordError`] met, naming `field`.
|
||||
pub fn validate(&self, field: &'static str) -> Result<(), RecordError> {
|
||||
if !(self.dim == 2 || self.dim == 3) || (self.dim == 3 && !self.points_3d) {
|
||||
return Err(refuse(field, "a curve's dimension is 2 or 3"));
|
||||
}
|
||||
if self.degree < 1 || self.points.len() <= self.degree as usize {
|
||||
return Err(refuse(
|
||||
field,
|
||||
"a curve has degree 1 or more and degree + 1 points",
|
||||
));
|
||||
}
|
||||
if knot_count(self.points.len(), self.degree) != Some(self.knots.len()) {
|
||||
return Err(refuse(field, "a curve has points + degree + 1 knots"));
|
||||
}
|
||||
if let Some(w) = &self.weights
|
||||
&& (w.len() != self.points.len() || !all_finite(w))
|
||||
{
|
||||
return Err(refuse(
|
||||
field,
|
||||
"a rational curve has a finite weight per point",
|
||||
));
|
||||
}
|
||||
if !all_finite(&self.knots) || !points_finite(&self.points) {
|
||||
return Err(refuse(field, "a curve's numbers are finite"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// How many numbers it holds, for the record's item budget.
|
||||
pub fn items(&self) -> usize {
|
||||
self.knots
|
||||
.len()
|
||||
.saturating_add(self.points.len())
|
||||
.saturating_add(self.weights.as_ref().map_or(0, Vec::len))
|
||||
}
|
||||
}
|
||||
|
||||
impl NurbsSurfaceRecord {
|
||||
/// The structural check: degrees of at least 1, `u_count * v_count` points, each direction's
|
||||
/// `count + degree + 1` knots, a weight per point when rational, and every number finite.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The first [`RecordError`] met, naming `field`.
|
||||
pub fn validate(&self, field: &'static str) -> Result<(), RecordError> {
|
||||
let (nu, nv) = (self.u_count as usize, self.v_count as usize);
|
||||
if self.u_degree < 1
|
||||
|| self.v_degree < 1
|
||||
|| nu <= self.u_degree as usize
|
||||
|| nv <= self.v_degree as usize
|
||||
{
|
||||
return Err(refuse(
|
||||
field,
|
||||
"a surface has degree 1 or more and degree + 1 points each way",
|
||||
));
|
||||
}
|
||||
if nu.checked_mul(nv) != Some(self.points.len()) {
|
||||
return Err(refuse(field, "a surface has u_count * v_count points"));
|
||||
}
|
||||
if knot_count(nu, self.u_degree) != Some(self.u_knots.len())
|
||||
|| knot_count(nv, self.v_degree) != Some(self.v_knots.len())
|
||||
{
|
||||
return Err(refuse(
|
||||
field,
|
||||
"a surface has count + degree + 1 knots each way",
|
||||
));
|
||||
}
|
||||
if let Some(w) = &self.weights
|
||||
&& (w.len() != self.points.len() || !all_finite(w))
|
||||
{
|
||||
return Err(refuse(
|
||||
field,
|
||||
"a rational surface has a finite weight per point",
|
||||
));
|
||||
}
|
||||
if !all_finite(&self.u_knots) || !all_finite(&self.v_knots) || !points_finite(&self.points)
|
||||
{
|
||||
return Err(refuse(field, "a surface's numbers are finite"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// How many numbers it holds, for the record's item budget.
|
||||
pub fn items(&self) -> usize {
|
||||
self.u_knots
|
||||
.len()
|
||||
.saturating_add(self.v_knots.len())
|
||||
.saturating_add(self.points.len())
|
||||
.saturating_add(self.weights.as_ref().map_or(0, Vec::len))
|
||||
}
|
||||
}
|
||||
|
||||
impl SupportRecord {
|
||||
fn validate_at(&self, depth: u32, field: &'static str) -> Result<(), RecordError> {
|
||||
match self {
|
||||
SupportRecord::Nurbs { form, .. } => form.validate(field),
|
||||
SupportRecord::Blend(b) => b.validate_at(depth + 1),
|
||||
}
|
||||
}
|
||||
|
||||
/// How many numbers it holds, nested blends included (call on a validated record: they nest
|
||||
/// at most [`RECORD_MAX_DEPTH`] deep).
|
||||
pub fn items(&self) -> usize {
|
||||
match self {
|
||||
SupportRecord::Nurbs { form, .. } => form.items(),
|
||||
SupportRecord::Blend(b) => b.definition.items().saturating_add(b.net.items()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn chamfer_finite(c: &ChamferRecord) -> bool {
|
||||
let sizes = match c.size {
|
||||
ChamferSizeRecord::Distances([a, b]) => [a, b, c.wedge],
|
||||
ChamferSizeRecord::DistanceAngle { distance, angle } => [distance, angle, c.wedge],
|
||||
ChamferSizeRecord::Chord(l) => [l, l, c.wedge],
|
||||
};
|
||||
all_finite(&sizes)
|
||||
}
|
||||
|
||||
impl BlendDefinitionRecord {
|
||||
/// The structural check of the definition, nested supports included: every curve and
|
||||
/// surface as their own checks, the definition's numbers finite, and supports nested at most
|
||||
/// [`RECORD_MAX_DEPTH`] deep. The kernel's rebuild checks what the numbers mean.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The first [`RecordError`] met.
|
||||
pub fn validate(&self) -> Result<(), RecordError> {
|
||||
self.validate_at(1)
|
||||
}
|
||||
|
||||
fn validate_at(&self, depth: u32) -> Result<(), RecordError> {
|
||||
if depth > RECORD_MAX_DEPTH {
|
||||
return Err(refuse(
|
||||
"definition.supports",
|
||||
"blends nest at most 3 definitions deep",
|
||||
));
|
||||
}
|
||||
for support in &self.supports {
|
||||
support.validate_at(depth, "definition.supports")?;
|
||||
}
|
||||
let numbers = [
|
||||
self.sides[0],
|
||||
self.sides[1],
|
||||
self.thumbweight,
|
||||
self.bound,
|
||||
self.map.a[0],
|
||||
self.map.a[1],
|
||||
self.map.b[0],
|
||||
self.map.b[1],
|
||||
];
|
||||
if !all_finite(&numbers) {
|
||||
return Err(refuse("definition", "a definition's numbers are finite"));
|
||||
}
|
||||
if self.chamfer.as_ref().is_some_and(|c| !chamfer_finite(c)) {
|
||||
return Err(refuse(
|
||||
"definition.chamfer",
|
||||
"a definition's numbers are finite",
|
||||
));
|
||||
}
|
||||
match &self.radius {
|
||||
RadiusRecord::Constant { offsets } => {
|
||||
if !all_finite(offsets) {
|
||||
return Err(refuse(
|
||||
"definition.radius",
|
||||
"a definition's numbers are finite",
|
||||
));
|
||||
}
|
||||
}
|
||||
RadiusRecord::Law(law) => law.validate("definition.radius")?,
|
||||
}
|
||||
self.leader.validate("definition.leader")?;
|
||||
for seed in &self.seeds {
|
||||
seed.validate("definition.seeds")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// How many numbers it holds, nested supports included (call on a validated record).
|
||||
pub fn items(&self) -> usize {
|
||||
let law = match &self.radius {
|
||||
RadiusRecord::Constant { .. } => 0,
|
||||
RadiusRecord::Law(law) => law.items(),
|
||||
};
|
||||
self.supports
|
||||
.iter()
|
||||
.map(SupportRecord::items)
|
||||
.chain([law, self.leader.items()])
|
||||
.chain(self.seeds.iter().map(NurbsCurveRecord::items))
|
||||
.fold(0usize, usize::saturating_add)
|
||||
}
|
||||
}
|
||||
|
||||
impl ProceduralBlendRecord {
|
||||
/// The structural check of a nested blend: its definition's and its net's.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The first [`RecordError`] met.
|
||||
pub fn validate(&self) -> Result<(), RecordError> {
|
||||
self.validate_at(1)
|
||||
}
|
||||
|
||||
fn validate_at(&self, depth: u32) -> Result<(), RecordError> {
|
||||
self.definition.validate_at(depth)?;
|
||||
self.net.validate("net")
|
||||
}
|
||||
}
|
||||
|
||||
impl ExactRoundDefinition {
|
||||
/// The structural check: every curve and surface as their own checks, the supports NURBS
|
||||
/// forms (an exact round's supports are faces, never blends), its numbers finite, and for the
|
||||
/// chord its sizes and order 0, for any other class order 1 and a positive radius.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The first [`RecordError`] met.
|
||||
pub fn validate(&self) -> Result<(), RecordError> {
|
||||
for support in &self.supports {
|
||||
match support {
|
||||
SupportRecord::Nurbs { form, .. } => form.validate("supports")?,
|
||||
SupportRecord::Blend(_) => {
|
||||
return Err(refuse("supports", "an exact round's supports are faces"));
|
||||
}
|
||||
}
|
||||
}
|
||||
let i = &self.inputs;
|
||||
let numbers = [
|
||||
self.radius,
|
||||
i.uv[0][0],
|
||||
i.uv[0][1],
|
||||
i.uv[1][0],
|
||||
i.uv[1][1],
|
||||
i.offsets[0],
|
||||
i.offsets[1],
|
||||
i.centre[0],
|
||||
i.centre[1],
|
||||
i.centre[2],
|
||||
i.tol,
|
||||
self.leader_map.a,
|
||||
self.leader_map.b,
|
||||
];
|
||||
if !all_finite(&numbers) {
|
||||
return Err(refuse("inputs", "an exact round's numbers are finite"));
|
||||
}
|
||||
if self.leader_map.b == 0.0 {
|
||||
return Err(refuse("leader_map", "the leader's map is singular"));
|
||||
}
|
||||
if self.class == ExactClassRecord::Chord {
|
||||
let Some(c) = &self.chamfer else {
|
||||
return Err(refuse("chamfer", "a chord keeps its chamfer's sizes"));
|
||||
};
|
||||
if self.order != 0 || self.radius != 0.0 {
|
||||
return Err(refuse(
|
||||
"order",
|
||||
"a chord's section is of order 0, with no ball",
|
||||
));
|
||||
}
|
||||
if !chamfer_finite(c) {
|
||||
return Err(refuse("chamfer", "an exact round's numbers are finite"));
|
||||
}
|
||||
} else {
|
||||
if self.radius <= 0.0 {
|
||||
return Err(refuse("radius", "the ball's radius is positive"));
|
||||
}
|
||||
if self.order != 1 || self.chamfer.is_some() {
|
||||
return Err(refuse(
|
||||
"order",
|
||||
"an exact round's section is the ball's circle",
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(leader) = &self.leader {
|
||||
leader.validate("leader")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// How many numbers it holds.
|
||||
pub fn items(&self) -> usize {
|
||||
self.supports
|
||||
.iter()
|
||||
.map(SupportRecord::items)
|
||||
.chain([self.leader.as_ref().map_or(0, NurbsCurveRecord::items)])
|
||||
.fold(0usize, usize::saturating_add)
|
||||
}
|
||||
}
|
||||
|
||||
impl BlendDefinition {
|
||||
/// The structural check of the definition ([`BlendDefinitionRecord::validate`],
|
||||
/// [`ExactRoundDefinition::validate`]).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The first [`RecordError`] met.
|
||||
pub fn validate(&self) -> Result<(), RecordError> {
|
||||
match self {
|
||||
BlendDefinition::Procedural(d) => d.validate(),
|
||||
BlendDefinition::ExactRound(r) => r.validate(),
|
||||
}
|
||||
}
|
||||
|
||||
/// How many numbers it holds, for the record's item budget (call on a validated record).
|
||||
pub fn items(&self) -> usize {
|
||||
match self {
|
||||
BlendDefinition::Procedural(d) => d.items(),
|
||||
BlendDefinition::ExactRound(r) => r.items(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+126
@@ -840,3 +840,129 @@ fn degenerate_cone_and_torus_labels_are_valid_only_with_their_form() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A small, structurally valid procedural blend definition (its numbers mean nothing: the
|
||||
/// record checks structure only).
|
||||
fn a_blend(depth: u32) -> blend::BlendDefinitionRecord {
|
||||
use blend::*;
|
||||
let form = NurbsSurfaceRecord {
|
||||
u_degree: 1,
|
||||
v_degree: 1,
|
||||
u_count: 2,
|
||||
v_count: 2,
|
||||
u_knots: vec![0., 0., 1., 1.],
|
||||
v_knots: vec![0., 0., 1., 1.],
|
||||
points: vec![[0., 0., 0.], [0., 1., 0.], [1., 0., 0.], [1., 1., 0.]],
|
||||
weights: None,
|
||||
};
|
||||
let curve = |dim: u32| NurbsCurveRecord {
|
||||
dim,
|
||||
points_3d: dim == 3,
|
||||
degree: 1,
|
||||
knots: vec![0., 0., 1., 1.],
|
||||
points: vec![[0., 0., 0.], [1., 0., 0.]],
|
||||
weights: None,
|
||||
};
|
||||
let support = if depth > 1 {
|
||||
SupportRecord::Blend(Box::new(ProceduralBlendRecord {
|
||||
definition: a_blend(depth - 1),
|
||||
net: form.clone(),
|
||||
}))
|
||||
} else {
|
||||
SupportRecord::Nurbs {
|
||||
kind: SupportKindRecord::Plane,
|
||||
form: form.clone(),
|
||||
}
|
||||
};
|
||||
BlendDefinitionRecord {
|
||||
supports: [
|
||||
support,
|
||||
SupportRecord::Nurbs {
|
||||
kind: SupportKindRecord::Plane,
|
||||
form,
|
||||
},
|
||||
],
|
||||
sides: [1., 1.],
|
||||
radius: RadiusRecord::Constant { offsets: [1., 1.] },
|
||||
order: 1,
|
||||
thumbweight: 0.,
|
||||
leader: curve(3),
|
||||
seeds: [curve(2), curve(2)],
|
||||
bound: 1e-6,
|
||||
map: ParameterMapRecord {
|
||||
a: [0., 0.],
|
||||
b: [1., 1.],
|
||||
swapped: false,
|
||||
},
|
||||
chamfer: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn blend_label(depth: u32) -> Surface {
|
||||
Surface::Blend {
|
||||
definition: Box::new(blend::BlendDefinition::Procedural(Box::new(a_blend(depth)))),
|
||||
}
|
||||
}
|
||||
|
||||
/// A blend face is its definition with its net as its NURBS form: it validates and round-trips
|
||||
/// number for number; without its net, or nested past three definitions, it is refused.
|
||||
#[test]
|
||||
fn a_blend_face_is_its_definition_on_its_net() {
|
||||
let form = Surface::Spline {
|
||||
u_knots: vec![-10., -10., 10., 10.],
|
||||
v_knots: vec![-10., -10., 10., 10.],
|
||||
points: vec![
|
||||
vec![[-10., -10., 0.], [-10., 10., 0.]],
|
||||
vec![[10., -10., 0.], [10., 10., 0.]],
|
||||
],
|
||||
};
|
||||
let mut b = sheet();
|
||||
b.faces[0].surface = blend_label(3);
|
||||
b.faces[0].nurbs = Some(form);
|
||||
b.validate().unwrap_or_else(|e| panic!("{e}"));
|
||||
let text = serde_json::to_string(&b).unwrap();
|
||||
assert!(text.contains(r#""kind":"blend""#), "{text}");
|
||||
let back: Brep = serde_json::from_str(&text).unwrap();
|
||||
assert_eq!(back, b);
|
||||
let mut bare = b.clone();
|
||||
bare.faces[0].nurbs = None;
|
||||
assert!(bare.validate().is_err(), "a blend without its net");
|
||||
let mut deep = b.clone();
|
||||
deep.faces[0].surface = blend_label(4);
|
||||
assert!(deep.validate().is_err(), "nested past three definitions");
|
||||
let mut damaged = b;
|
||||
if let Surface::Blend { definition } = &mut damaged.faces[0].surface
|
||||
&& let blend::BlendDefinition::Procedural(d) = definition.as_mut()
|
||||
{
|
||||
d.bound = f64::NAN;
|
||||
}
|
||||
assert!(
|
||||
damaged.validate().is_err(),
|
||||
"a definition's numbers are finite"
|
||||
);
|
||||
}
|
||||
|
||||
/// A reader from before the blend label refuses a record that has one, by name, rather than
|
||||
/// misreading it: its surface kinds are a closed set, and an unknown kind is an error.
|
||||
#[test]
|
||||
fn an_older_reader_refuses_a_blend_face_by_name() {
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||
#[allow(dead_code)]
|
||||
enum OlderSurface {
|
||||
Plane {
|
||||
origin: [f64; 3],
|
||||
u_axis: [f64; 3],
|
||||
v_axis: [f64; 3],
|
||||
},
|
||||
BallPatch {
|
||||
center: [f64; 3],
|
||||
radius: f64,
|
||||
},
|
||||
}
|
||||
let label = serde_json::to_value(blend_label(1)).unwrap();
|
||||
let error = serde_json::from_value::<OlderSurface>(label)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(error.contains("unknown variant `blend`"), "{error}");
|
||||
}
|
||||
|
||||
+4
-3
@@ -306,13 +306,14 @@ pub fn surface(s: &BSurface) -> R<(Surface, Map)> {
|
||||
}
|
||||
BSurface::PlacedRevolution { profile, origin, axis, matrix, forward } => revolution(profile, *origin, *axis, matrix, *forward)?,
|
||||
BSurface::BallPatch { .. } => return Err("a ball patch's label (its face is written as its NURBS form)".into()),
|
||||
BSurface::Blend { .. } => return Err("a blend's label (its face is written as its NURBS form)".into()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether a face is written as its NURBS form, pcurves and all: a ball patch, which STEP has no
|
||||
/// entity for, is the B-spline surface it is parameterised on.
|
||||
/// Whether a face is written as its NURBS form, pcurves and all: a ball patch or a blend by its
|
||||
/// definition, which STEP has no entity for, is the B-spline surface it is parameterised on.
|
||||
fn as_form(face: &Face) -> bool {
|
||||
face.nurbs.is_some() && matches!(face.surface, BSurface::BallPatch { .. })
|
||||
face.nurbs.is_some() && matches!(face.surface, BSurface::BallPatch { .. } | BSurface::Blend { .. })
|
||||
}
|
||||
|
||||
/// A record pcurve in the STEP surface's parameters: its control points through `map` (a linear
|
||||
|
||||
@@ -145,6 +145,7 @@ fn kind(s: &brep::Surface) -> &'static str {
|
||||
brep::Surface::Spline { .. } => "spline",
|
||||
brep::Surface::RationalSpline { .. } => "rational spline",
|
||||
brep::Surface::PlacedRevolution { .. } => "revolution",
|
||||
brep::Surface::Blend { .. } => "blend",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+22
-4
@@ -430,6 +430,20 @@ pub enum SolidError {
|
||||
distance: Option<f64>,
|
||||
},
|
||||
|
||||
/// A blend face's definition, read from a record, whose net it does not certify: the net is
|
||||
/// not proved within the definition's bound `eps` of S (docs/definitions-and-certificates.md
|
||||
/// §4.5), so the face is refused, never kept with a larger bound. Not upstream; its
|
||||
/// [`SolidError::sm_status`] is `None`.
|
||||
#[error("{operation}: the definition of {face} does not certify its net within {eps}")]
|
||||
DefinitionNotCertified {
|
||||
/// Operation that read the definition.
|
||||
operation: &'static str,
|
||||
/// The face.
|
||||
face: EntityRef,
|
||||
/// The bound ε the definition states.
|
||||
eps: f64,
|
||||
},
|
||||
|
||||
/// The operation needs an upstream routine whose unit is not ported yet (a
|
||||
/// plan back-edge). Returned instead of panicking or faking a result; every
|
||||
/// site carries a `// UNPORTED(<unit id>): <C++ symbol>` comment so the
|
||||
@@ -1131,6 +1145,7 @@ impl SolidError {
|
||||
| Self::FaceTrimsUnavailable { .. }
|
||||
| Self::UnsureRegion { .. }
|
||||
| Self::ExtendRefused { .. }
|
||||
| Self::DefinitionNotCertified { .. }
|
||||
| Self::Unported { .. } => None,
|
||||
}
|
||||
}
|
||||
@@ -1222,7 +1237,9 @@ impl SolidError {
|
||||
#[must_use]
|
||||
pub fn kind(&self) -> ErrorKind {
|
||||
match self {
|
||||
Self::InvalidInput { .. } => ErrorKind::InvalidInput,
|
||||
Self::InvalidInput { .. } | Self::DefinitionNotCertified { .. } => {
|
||||
ErrorKind::InvalidInput
|
||||
}
|
||||
Self::NotFound { .. } => ErrorKind::NotFound,
|
||||
Self::Degenerate { .. } => ErrorKind::Degenerate,
|
||||
Self::NotConverged { .. } => ErrorKind::NotConverged,
|
||||
@@ -1348,6 +1365,7 @@ impl SolidError {
|
||||
| Self::BlendRefused { operation, .. }
|
||||
| Self::RoundRefused { operation, .. }
|
||||
| Self::ExtendRefused { operation, .. }
|
||||
| Self::DefinitionNotCertified { operation, .. }
|
||||
| Self::PreparationRequired { operation }
|
||||
| Self::FaceTrimsUnavailable { operation, .. }
|
||||
| Self::Cancelled { operation } => operation,
|
||||
@@ -1360,9 +1378,9 @@ impl SolidError {
|
||||
#[must_use]
|
||||
pub fn entity(&self) -> Option<EntityRef> {
|
||||
match self {
|
||||
Self::NotFound { entity, .. } | Self::FaceTrimsUnavailable { entity, .. } => {
|
||||
Some(*entity)
|
||||
}
|
||||
Self::NotFound { entity, .. }
|
||||
| Self::FaceTrimsUnavailable { entity, .. }
|
||||
| Self::DefinitionNotCertified { face: entity, .. } => Some(*entity),
|
||||
Self::UnsureRegion { region, .. } => Some(EntityRef::Region(*region)),
|
||||
Self::Degenerate { entity, .. }
|
||||
| Self::Upstream { entity, .. }
|
||||
|
||||
+1
@@ -15,6 +15,7 @@ pub mod cone;
|
||||
pub mod continued;
|
||||
pub mod curve_bounded_surface;
|
||||
pub mod extend;
|
||||
pub mod face_evaluation;
|
||||
pub mod face_surface;
|
||||
pub mod from_parameters;
|
||||
pub mod implicit;
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
//! A face's surface evaluated in the face's own (u, v), the record's: its point, its unit normal,
|
||||
//! and the point of it nearest a given point, found from a guess. The surface is the face's exact
|
||||
//! one ([`FaceSurface`]: an analytic surface on its form, a blend by its definition, or its NURBS),
|
||||
//! so a blend is evaluated on its definition S, not on its net. A round of the exact circle class
|
||||
//! also has its torus read off its definition ([`circle_round_torus`]).
|
||||
//!
|
||||
//! The nearest point is a Newton iteration on the squared distance from the guess, each step the
|
||||
//! Newton step where the distance's Hessian is positive and the Gauss-Newton step otherwise,
|
||||
//! clamped to the face's parameter box, at most [`MAX_NEAREST_STEPS`] steps: it stops when a step
|
||||
//! moves (u, v) by no more than their rounding, and is refused (`NotConverged`) past the cap.
|
||||
//! Nothing is sampled, and the steps are a function of the inputs alone.
|
||||
//!
|
||||
//! Not upstream. Deviations: none (no upstream counterpart). No recursion.
|
||||
|
||||
use crate::blend::fillet_exact::{class_surface, classify_supports};
|
||||
use crate::error::{Result, SolidError};
|
||||
use crate::geom::vector2d::Point2;
|
||||
use crate::geom::vector3d::{Point3, Vec3};
|
||||
use crate::kernel::Kernel;
|
||||
use crate::nurbs::b_spline_surface_n_lib::EvaluateFlags;
|
||||
use crate::surface::face_surface::FaceSurface;
|
||||
use crate::surface::implicit::{ClassSurface, Implicit};
|
||||
use crate::surface::procedural_blend::exact::ExactClass;
|
||||
use crate::surface::procedural_blend::BlendRef;
|
||||
|
||||
const OP_NORMAL: &str = "surface::face_evaluation::normal";
|
||||
const OP_NEAREST: &str = "surface::face_evaluation::nearest";
|
||||
const OP_TORUS: &str = "surface::face_evaluation::circle_round_torus";
|
||||
|
||||
/// The most Newton steps [`nearest`] takes.
|
||||
pub const MAX_NEAREST_STEPS: u32 = 64;
|
||||
|
||||
/// The point of a face's surface nearest a given point.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Nearest {
|
||||
/// Its (u, v), the face's (the record's).
|
||||
pub uv: [f64; 2],
|
||||
/// The point.
|
||||
pub point: Point3,
|
||||
/// Its distance from the given point.
|
||||
pub distance: f64,
|
||||
/// The Newton steps taken.
|
||||
pub steps: u32,
|
||||
}
|
||||
|
||||
/// The torus a round of the exact circle class is, from its definition.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct RoundTorus {
|
||||
/// Its centre.
|
||||
pub centre: Point3,
|
||||
/// Its unit axis.
|
||||
pub axis: Vec3,
|
||||
/// The major radius: the circle the ball's centre runs on.
|
||||
pub major: f64,
|
||||
/// The minor radius: the ball's.
|
||||
pub minor: f64,
|
||||
}
|
||||
|
||||
/// The kernel's (u, v) of the face's: `[su u, sv v]` ([`FaceSurface::scale`]).
|
||||
fn kernel_uv(f: &FaceSurface, uv: [f64; 2]) -> Point2 {
|
||||
Point2::new(f.scale[0] * uv[0], f.scale[1] * uv[1])
|
||||
}
|
||||
|
||||
/// The partial `d[i][j]` of an evaluation.
|
||||
fn partial(d: &[Vec<Vec3>], i: usize, j: usize, op: &'static str) -> Result<Vec3> {
|
||||
d.get(i)
|
||||
.and_then(|row| row.get(j))
|
||||
.copied()
|
||||
.ok_or_else(|| SolidError::invalid_input(op, "a short evaluation"))
|
||||
}
|
||||
|
||||
/// The face's parameter box in its own (u, v), `[[u0, u1], [v0, v1]]`: the kernel's over the
|
||||
/// scales.
|
||||
#[must_use]
|
||||
pub fn face_domain(f: &FaceSurface) -> [[f64; 2]; 2] {
|
||||
let d = &f.uv_domain;
|
||||
let side = |a: f64, b: f64, s: f64| {
|
||||
let (a, b) = (a / s, b / s);
|
||||
[a.min(b), a.max(b)]
|
||||
};
|
||||
[
|
||||
side(d.u_min(), d.u_max(), f.scale[0]),
|
||||
side(d.v_min(), d.v_max(), f.scale[1]),
|
||||
]
|
||||
}
|
||||
|
||||
/// The point of the face's surface at its (u, v).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Those of the surface's evaluation.
|
||||
pub fn point(k: &Kernel, f: &FaceSurface, uv: [f64; 2]) -> Result<Point3> {
|
||||
f.surface.evaluate_point(k, kernel_uv(f, uv))
|
||||
}
|
||||
|
||||
/// The unit normal of the face's surface at its (u, v): `∂S/∂u × ∂S/∂v` in the face's parameters,
|
||||
/// made unit.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `Degenerate` where the partials are parallel (a pole, an apex); those of the evaluation.
|
||||
pub fn normal(k: &Kernel, f: &FaceSurface, uv: [f64; 2]) -> Result<Vec3> {
|
||||
let d = f
|
||||
.surface
|
||||
.evaluate(k, kernel_uv(f, uv), 1, 1, EvaluateFlags::default())?;
|
||||
let n = partial(&d, 1, 0, OP_NORMAL)?.cross(partial(&d, 0, 1, OP_NORMAL)?);
|
||||
// the face's parameters are the kernel's over the scales: its normal turns with their sign
|
||||
let n = if f.scale[0] * f.scale[1] < 0.0 {
|
||||
n * -1.0
|
||||
} else {
|
||||
n
|
||||
};
|
||||
n.unitize().map_err(|_| SolidError::Degenerate {
|
||||
operation: OP_NORMAL,
|
||||
entity: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The point of the face's surface nearest `p`, from the guess `guess` (the face's (u, v)), within
|
||||
/// the face's parameter box (module docs).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `NotConverged` past [`MAX_NEAREST_STEPS`]; `InvalidInput` for a point or guess not finite; those
|
||||
/// of the evaluations.
|
||||
pub fn nearest(k: &Kernel, f: &FaceSurface, p: Point3, guess: [f64; 2]) -> Result<Nearest> {
|
||||
if !(p.x.is_finite() && p.y.is_finite() && p.z.is_finite())
|
||||
|| !(guess[0].is_finite() && guess[1].is_finite())
|
||||
{
|
||||
return Err(SolidError::invalid_input(
|
||||
OP_NEAREST,
|
||||
"a point or guess not finite",
|
||||
));
|
||||
}
|
||||
let flags = EvaluateFlags::default();
|
||||
let mut uv = f.uv_domain.clamp_point(kernel_uv(f, guess));
|
||||
for step in 1..=MAX_NEAREST_STEPS {
|
||||
let d = f.surface.evaluate(k, uv, 2, 2, flags)?;
|
||||
let (s, su, sv) = (
|
||||
partial(&d, 0, 0, OP_NEAREST)?,
|
||||
partial(&d, 1, 0, OP_NEAREST)?,
|
||||
partial(&d, 0, 1, OP_NEAREST)?,
|
||||
);
|
||||
let (suu, suv, svv) = (
|
||||
partial(&d, 2, 0, OP_NEAREST)?,
|
||||
partial(&d, 1, 1, OP_NEAREST)?,
|
||||
partial(&d, 0, 2, OP_NEAREST)?,
|
||||
);
|
||||
let r = s - p;
|
||||
let (fu, fv) = (r.dot(su), r.dot(sv));
|
||||
let (guu, guv, gvv) = (su.dot(su), su.dot(sv), sv.dot(sv));
|
||||
let (a, b, c) = (guu + r.dot(suu), guv + r.dot(suv), gvv + r.dot(svv));
|
||||
let det = a * c - b * b;
|
||||
// Newton where the Hessian is positive, else Gauss-Newton (the first fundamental form)
|
||||
let (a, b, c, det) = if det > 0.0 && a > 0.0 && det.is_finite() {
|
||||
(a, b, c, det)
|
||||
} else {
|
||||
(guu, guv, gvv, guu * gvv - guv * guv)
|
||||
};
|
||||
let usable = det > 0.0 && det.is_finite();
|
||||
if !usable {
|
||||
return Err(SolidError::Degenerate {
|
||||
operation: OP_NEAREST,
|
||||
entity: None,
|
||||
});
|
||||
}
|
||||
let du = -(c * fu - b * fv) / det;
|
||||
let dv = -(a * fv - b * fu) / det;
|
||||
let next = f.uv_domain.clamp_point(Point2::new(uv.x + du, uv.y + dv));
|
||||
let moved = (next.x - uv.x).abs() <= 4.0 * f64::EPSILON * (1.0 + uv.x.abs())
|
||||
&& (next.y - uv.y).abs() <= 4.0 * f64::EPSILON * (1.0 + uv.y.abs());
|
||||
uv = next;
|
||||
if moved {
|
||||
let point = f.surface.evaluate_point(k, uv)?;
|
||||
return Ok(Nearest {
|
||||
uv: [uv.x / f.scale[0], uv.y / f.scale[1]],
|
||||
point,
|
||||
distance: (point - p).length(),
|
||||
steps: step,
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(SolidError::NotConverged {
|
||||
operation: OP_NEAREST,
|
||||
iterations: MAX_NEAREST_STEPS,
|
||||
})
|
||||
}
|
||||
|
||||
/// The torus a round of the exact circle class is (a plane square to a cylinder's axis, coaxial
|
||||
/// cones, a sphere and a coaxial cone): the class's torus its supports give, with the net certified
|
||||
/// on it within the tolerance the class was read in (`ExactRound::supports_distance`). `None` for
|
||||
/// any other surface.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `InvalidInput` for a circle-class round whose supports give no torus; those of classifying the
|
||||
/// supports and of the certificate.
|
||||
pub fn circle_round_torus(k: &Kernel, f: &FaceSurface) -> Result<Option<RoundTorus>> {
|
||||
let Some(BlendRef::Exact { round, net }) = f.surface.procedural_blend() else {
|
||||
return Ok(None);
|
||||
};
|
||||
if round.class() != ExactClass::Circle {
|
||||
return Ok(None);
|
||||
}
|
||||
let (Some(a), Some(b)) = (round.support(0), round.support(1)) else {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP_TORUS,
|
||||
"an exact round without its two supports",
|
||||
));
|
||||
};
|
||||
let blend = classify_supports(k, [a, b], round.inputs())?;
|
||||
let Some(ClassSurface::Implicit(Implicit::Torus {
|
||||
centre,
|
||||
axis,
|
||||
major,
|
||||
minor,
|
||||
})) = class_surface(&blend, round.leader(), round.leader_map())
|
||||
else {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP_TORUS,
|
||||
"a round of the circle class whose supports give no torus",
|
||||
));
|
||||
};
|
||||
round.supports_distance(k, net)?;
|
||||
Ok(Some(RoundTorus {
|
||||
centre,
|
||||
axis,
|
||||
major,
|
||||
minor,
|
||||
}))
|
||||
}
|
||||
@@ -17,10 +17,10 @@ use super::exact::{ExactClass, ExactRound};
|
||||
use super::record::{
|
||||
BezierCurveRecord, BlendDefinitionRecord, ChamferRecord, ChamferSizeRecord, ClassInputsRecord,
|
||||
CornerRecord, DraftRecord, EndCornerRecord, ExactClassRecord, ExactNetKindRecord,
|
||||
ExactNetRecord, ExactRoundRecord, LeaderMapRecord, MixedCornerRecord, MixedSideRecord,
|
||||
NurbsCurveRecord, NurbsSurfaceRecord, ParameterMapRecord, PlaneRecord, ProceduralBlendRecord,
|
||||
QuadricKindRecord, QuadricRecord, RadiusRecord, SetbackCornerRecord, SetbackRoundRecord,
|
||||
SupportKindRecord, SupportRecord, SweptCornerRecord,
|
||||
ExactNetRecord, ExactRoundDefinition, ExactRoundRecord, FaceDefinition, LeaderMapRecord,
|
||||
MixedCornerRecord, MixedSideRecord, NurbsCurveRecord, NurbsSurfaceRecord, ParameterMapRecord,
|
||||
PlaneRecord, ProceduralBlendRecord, QuadricKindRecord, QuadricRecord, RadiusRecord,
|
||||
SetbackCornerRecord, SetbackRoundRecord, SupportKindRecord, SupportRecord, SweptCornerRecord,
|
||||
};
|
||||
use super::record::{EndOrderRecord, MotionRecord, SectionBlendRecord, SectionFrameRecord};
|
||||
use super::{BlendDefinitionInput, BlendRef, ProceduralBlend, BLEND_MAX_DEPTH};
|
||||
@@ -626,6 +626,165 @@ pub fn exact_rebuild(k: &Kernel, rec: &ExactRoundRecord) -> Result<Surface> {
|
||||
Ok(net)
|
||||
}
|
||||
|
||||
const OP_ADOPT: &str = "surface::procedural_blend::convert::adopt_definitions";
|
||||
|
||||
/// The definition a face's surface carries, without its net, when a body's record keeps it as
|
||||
/// the face's blend: a procedural blend's, or a round of an exact class's whose net has no
|
||||
/// analytic label of its own (a surface of revolution, or the rational classes' B-spline). A
|
||||
/// round whose net is an analytic surface (a plane, cylinder, cone, sphere or torus) or a ball
|
||||
/// patch keeps that label in the record, and its definition is not written; nor is a definition
|
||||
/// whose supports a record cannot hold (`Unsupported`, [`to_record`]), which then goes as its
|
||||
/// net. `None` for any other surface.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Those of [`to_record`] and [`exact_to_record`] other than `Unsupported`.
|
||||
pub fn face_definition(k: &Kernel, surface: &Surface) -> Result<Option<FaceDefinition>> {
|
||||
let unrecorded = |e: SolidError| match e {
|
||||
SolidError::Unsupported { .. } => Ok(None),
|
||||
other => Err(other),
|
||||
};
|
||||
if let SurfaceKind::ProceduralBlend(pb) = surface.kind() {
|
||||
return match to_record(k, pb) {
|
||||
Ok(rec) => Ok(Some(FaceDefinition::Procedural(Box::new(rec.definition)))),
|
||||
Err(e) => unrecorded(e),
|
||||
};
|
||||
}
|
||||
let Some(BlendRef::Exact { net, .. }) = surface.procedural_blend() else {
|
||||
return Ok(None);
|
||||
};
|
||||
if !matches!(
|
||||
net.kind(),
|
||||
SurfaceKind::SurfOfRevolution(_) | SurfaceKind::BSpline(_)
|
||||
) {
|
||||
return Ok(None);
|
||||
}
|
||||
let rec = match exact_to_record(k, surface) {
|
||||
Ok(rec) => rec,
|
||||
Err(e) => return unrecorded(e),
|
||||
};
|
||||
Ok(Some(FaceDefinition::ExactRound(Box::new(
|
||||
ExactRoundDefinition {
|
||||
class: rec.class,
|
||||
supports: rec.supports,
|
||||
radius: rec.radius,
|
||||
order: rec.order,
|
||||
leader: rec.leader,
|
||||
inputs: rec.inputs,
|
||||
chamfer: rec.chamfer,
|
||||
leader_map: rec.leader_map,
|
||||
net_kind: rec.net.kind,
|
||||
},
|
||||
))))
|
||||
}
|
||||
|
||||
/// The surface a face's definition makes on its net `net` (the face's surface as built from its
|
||||
/// record, a NURBS surface): the definition made from the record exactly ([`from_record`],
|
||||
/// [`exact_from_record`]) and the net certified against it again. `Ok(Err(eps))`, with the bound
|
||||
/// the definition states, where the net is not certified: the definition and its net disagree.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// `InvalidInput` for a net that is not a NURBS surface; those of the rebuilds; a cancel as it is.
|
||||
pub fn definition_surface(
|
||||
k: &Kernel,
|
||||
definition: &FaceDefinition,
|
||||
net: &Surface,
|
||||
) -> Result<std::result::Result<Surface, f64>> {
|
||||
let Some(form) = as_b_spline(k, net)? else {
|
||||
return Err(SolidError::invalid_input(
|
||||
OP_ADOPT,
|
||||
"a blend face's net is not a NURBS surface",
|
||||
));
|
||||
};
|
||||
let net = surface_record(&form)?;
|
||||
// (a cancel ends the operation; any other failure of the certificate refuses the net)
|
||||
let certified = |r: Result<f64>, eps: f64| match r {
|
||||
Ok(_) => Ok(Ok(())),
|
||||
Err(e) if e.is_cancelled() => Err(e),
|
||||
Err(_) => Ok(Err(eps)),
|
||||
};
|
||||
Ok(match definition {
|
||||
FaceDefinition::Procedural(d) => {
|
||||
let pb = from_record(
|
||||
k,
|
||||
&ProceduralBlendRecord {
|
||||
definition: (**d).clone(),
|
||||
net,
|
||||
},
|
||||
)?;
|
||||
certified(pb.recertify(k), d.bound)?.map(|()| Surface::from(pb))
|
||||
}
|
||||
FaceDefinition::ExactRound(d) => {
|
||||
let rec = ExactRoundRecord {
|
||||
class: d.class,
|
||||
supports: d.supports.clone(),
|
||||
radius: d.radius,
|
||||
order: d.order,
|
||||
leader: d.leader.clone(),
|
||||
inputs: d.inputs,
|
||||
chamfer: d.chamfer,
|
||||
leader_map: d.leader_map,
|
||||
net: ExactNetRecord {
|
||||
kind: d.net_kind,
|
||||
form: net,
|
||||
},
|
||||
};
|
||||
let made = exact_from_record(k, &rec)?;
|
||||
let proved = match made.procedural_blend() {
|
||||
Some(blend) => certified(blend.recertify(k), 0.0)?,
|
||||
None => Ok(()),
|
||||
};
|
||||
proved.map(|()| made)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Give each face of `faces` its definition back: its net is the face's surface as it was
|
||||
/// built (a NURBS surface), the definition is made from the record exactly ([`from_record`],
|
||||
/// [`exact_from_record`]) and the net certified against it again, and the face's surface becomes
|
||||
/// the blend (`SetSurface`; `key_of` names a face as the body's notifications do). A net its
|
||||
/// definition does not certify refuses the face ([`SolidError::DefinitionNotCertified`]): a
|
||||
/// record's blend is never an operand until it is certified (docs/definitions-and-certificates.md
|
||||
/// §7 inv. 8).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`SolidError::DefinitionNotCertified`] naming the first face refused; `InvalidInput` for a
|
||||
/// face without a NURBS surface; those of the rebuilds and of `SetSurface`. A cancel passes
|
||||
/// through as it is.
|
||||
pub fn adopt_definitions(
|
||||
k: &Kernel,
|
||||
b: &mut crate::topology::Brep,
|
||||
key_of: &dyn Fn(EntityRef) -> crate::geom::object::ObjectRef,
|
||||
faces: &[(FaceId, FaceDefinition)],
|
||||
) -> Result<()> {
|
||||
for (f, definition) in faces {
|
||||
let face = *f;
|
||||
let made = {
|
||||
let surface =
|
||||
b.face(face)?.surface.as_ref().ok_or_else(|| {
|
||||
SolidError::invalid_input(OP_ADOPT, "a face without a surface")
|
||||
})?;
|
||||
definition_surface(k, definition, surface)?
|
||||
};
|
||||
let made = made.map_err(|eps| SolidError::DefinitionNotCertified {
|
||||
operation: OP_ADOPT,
|
||||
face: EntityRef::Face(face),
|
||||
eps,
|
||||
})?;
|
||||
crate::topology::face::p1::set_surface(
|
||||
k,
|
||||
b,
|
||||
face,
|
||||
key_of(EntityRef::Face(face)),
|
||||
made,
|
||||
true,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A chamfer's sizes as their record (#2486).
|
||||
fn chamfer_record(c: ChamferDefinition) -> ChamferRecord {
|
||||
ChamferRecord {
|
||||
|
||||
@@ -569,6 +569,44 @@ impl ExactRoundRecord {
|
||||
}
|
||||
}
|
||||
|
||||
/// A face's blend definition without its net, which is the face's surface: what a body's
|
||||
/// record keeps of a procedural blend's face or of a round of an exact class's
|
||||
/// (`solidcore-record`'s `brep::blend::BlendDefinition` holds the same fields, and
|
||||
/// `solidcore-convert` copies them). [`super::convert::face_definition`] reads one off a face,
|
||||
/// [`super::convert::adopt_definitions`] gives it back, rebuilt and re-certified.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum FaceDefinition {
|
||||
/// A procedural blend's definition S.
|
||||
Procedural(Box<BlendDefinitionRecord>),
|
||||
/// A round of an exact class's definition.
|
||||
ExactRound(Box<ExactRoundDefinition>),
|
||||
}
|
||||
|
||||
/// An [`ExactRoundRecord`] without its net's form, which is the face's surface: the definition
|
||||
/// and the net's analytic kind.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct ExactRoundDefinition {
|
||||
/// The class.
|
||||
pub class: ExactClassRecord,
|
||||
/// The supports S1, S2, by value.
|
||||
pub supports: [SupportRecord; 2],
|
||||
/// The ball's radius (0 for the chord).
|
||||
pub radius: f64,
|
||||
/// The section's order: 1, the ball's circle; 0, the chord.
|
||||
pub order: u32,
|
||||
/// The leader (the centre line), where the class's construction makes one.
|
||||
pub leader: Option<NurbsCurveRecord>,
|
||||
/// What the class is read with beside the supports.
|
||||
pub inputs: ClassInputsRecord,
|
||||
/// A chamfer's sizes (the chord, #2486).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub chamfer: Option<ChamferRecord>,
|
||||
/// How the net's parameter runs along the leader.
|
||||
pub leader_map: LeaderMapRecord,
|
||||
/// The net's analytic kind.
|
||||
pub net_kind: ExactNetKindRecord,
|
||||
}
|
||||
|
||||
/// A face of a blend corner's (#2488): its corner kind's definition and its net.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum CornerRecord {
|
||||
|
||||
@@ -115,7 +115,7 @@ the frozen truck-geometry PRs, a `kernel-paused` PR), for the report's coverage
|
||||
| `volume_mm3`, `area_mm2` as `[min, max]` | An inclusive range, where analysis only bounds the measure: a cut removes at most its tool's volume, a join adds at most the tool's. `method` derives both ends. |
|
||||
| `continuity` | `{order, edges}`: every seam `edges` names reaches `order` (`g0` … `g4`), as Continuity Analysis (`inspect.continuity`) measures it: from the faces' exact surfaces, at its default 33 stations a seam and its default tolerances (1 µm, 1°, 0.01 / mm, 0.001 / mm², 0.0001 / mm³), never loosened per case. A failure names the worst seam (lowest order; of those, the one furthest past the next order's tolerance), its faces, the order it reaches and that next order's worst value. `edges`: `blend` (default), every seam of a face a fillet or chamfer made (a round, a chamfer face or a corner patch) with the faces either side of the edge it replaced, and every seam of a corner patch with the rounds it joins: what the blend is built to meet (not where a round runs into an end face, nor where two rounds meet without a patch: that seam is the input's, tangent only where the rounded edges were); `corner`, those of them on a corner patch (a blend made from several replaced edges); or a list of points `[[x,y,z], …]`, the edges nearest them, as Continuity Analysis picks them. `exact: true`: every seam reaches `order` and not the next (a case built to an order proves the measurement reads that order, no more and no less). A constant round meets its faces G1 (curvature jumps by 1 / r), a `curvature` round G2, a chamfer G0; a `corner: g2` patch is built G2 to the rounds and faces round it. |
|
||||
| `continuity` (older form) | `{near, within_mm, clear_mm, curvature_jump_per_mm}`: over every tangent seam lying wholly within `within_mm` of `near` (a corner's patch and its neighbours), the curvature jump across it (the faces' normal curvatures square to the seam, from their exact surfaces, at 65 stations a seam) is at most `curvature_jump_per_mm` at every station at least `clear_mm` along the seam from a vertex where two of those seams turn (where a neighbour's own G1 seam ends, which no surface is G2 to both sides of). A rolling ball's corner jumps 1 / r everywhere. |
|
||||
| `surface_types` | The set of kinds the faces' surfaces are, over every body, as the records hold them: `plane`, `cylinder`, `cone`, `sphere`, `torus`, `ball_patch` (a corner's patch on a sphere, its NURBS net), `bspline` (a B-spline or NURBS surface) or `revolution` (a revolved curve). Exact: none missing, none extra. |
|
||||
| `surface_types` | The set of kinds the faces' surfaces are, over every body, as the records hold them: `plane`, `cylinder`, `cone`, `sphere`, `torus`, `ball_patch` (a corner's patch on a sphere, its NURBS net), `bspline` (a B-spline or NURBS surface), `revolution` (a revolved curve) or `blend` (a round by its definition, its NURBS net its geometry). Exact: none missing, none extra. |
|
||||
| `splines` | `[{degree: [p, q], spans?: [m, n]}, …]`: one entry per B-spline face, each matching one face (directions in either order); `spans`, the distinct knot intervals each way, only where the construction fixes them. |
|
||||
| `boundary_edges` | The edges only one face uses (a sheet's free boundary), over every body, as the records hold them; exact or `[min, max]`. |
|
||||
| `flat_pattern` | A sheet metal case's flat pattern, from the script's last `sheet.flat_pattern` result: `size_mm` ([w, h] of the outline), `area_mm2` (the outline's area less the cut-outs', relative tolerance as volume) and `bend_lines` (each `[[x, y], [x, y]]`, a bend's centre line in flat coordinates, matched one for one, either way round, and as many as the result has), positions within the box-diagonal tolerance. |
|
||||
|
||||
Reference in New Issue
Block a user