Compare commits

...

12 Commits

Author SHA1 Message Date
jyviko 3c441805f0 A face's exact surface for applications: point, normal and nearest point
solidcore-ops gains face_surface::FaceSurface: for a body face, its exact
surface (its analytic label, else its blend definition rebuilt from the
record and recertified on its net, else its NURBS), and on it, in the
face's own (u, v), point, normal and closest_point. A round of the exact
circle class reports the torus its supports give, certified on its net.
closest_point is a bounded Newton iteration (64 steps, Gauss-Newton where
the Hessian is not positive, clamped to the face's box), refused with
NotConverged past the cap; nothing is sampled or timed.

The kernel computes every value (surface::face_evaluation); adopting a
definition and FaceSurface::of share procedural_blend::convert's
definition_surface. The kernel crate's face_surface returns a round kept
as its definition as its torus through ops, with no kernel paths.

Tests against closed forms: the round of a bore's rim and of a boss's root
are their tori to 1e-12 (point, normal, nearest point); a variable round's
definition is within its certified bound of its net, the residual along
its normal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
2026-11-11 13:23:22 -05:00
jyviko 9f171f9d35 Blend definition record: CI's compile error and clippy findings
- geom `placement::moved`: a face's or edge's NURBS form is moved by a match (the motion of a
  form is an `Option`, so `map(..).transpose()` was the wrong tool).
- The definition enums box their variants (`brep::blend::BlendDefinition` in the record,
  `FaceDefinition` in the kernel): the definitions are large and of different sizes.
- solidcore-record `brep::blend`: the weight checks are one `if let` chain each.
- solidcore `procedural_blend::convert`: an unused operation name goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
2026-11-11 11:35:33 -05:00
jyviko ccd2be8de2 smlib, solidcore-ops: a body's blend definitions through body-level ops calls only
The application reaches a body's blend faces through the modelling layer alone, adding no kernel
path: `solidcore_ops::blend_definition::definitions(k, body)` walks the body's faces in their
order and reads each face's definition, and `adopt(k, body, &[(index, definition)])` gives faces
named by that index their definitions back, an index past the faces refused as
`Error::Argument`. A net its definition does not certify reaches the application as the new
`solidcore_ops::Error::DefinitionNotCertified { face, eps }`, the face by its index; other kernel
refusals as the operation's. crates/smlib/src/core/definition.rs now calls ops alone and imports
nothing from `solidcore::`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
2026-11-11 10:43:05 -05:00
jyviko 8dfc641c5b Native B-rep: the blend face, and its design-format note
docs/native-brep.md gains the `blend` {definition} surface row and its paragraph: the face's
`nurbs` is its net, stored once; the kernel builds the face on its net and certifies it against
the definition before it is an operand. A pending design-format change says what an older design
and the last release do with it: as the ball patch before it, no SCBREP bump (an older reader
refuses the unknown kind by name, a model-cache miss; solidcore-record's test shows it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
2026-11-11 10:37:29 -05:00
jyviko 6b5dc2d0a6 geom, kernel, cli: a blend face is its definition on its net
A blend label is the face's definition; its geometry is its NURBS form.
- The record's rigid motion (`placement::moved`) refuses a record with one (`None`): moving a
  definition's supports, leader and seeds is the kernel's, and `transform_matrix` then moves the
  body through the kernel's transform, which moves the definition with its net. The affine map
  (`mapped`) refuses it as it refuses every labelled face.
- Hidden lines draw a blend face as its net: the definition is dropped there, for the drawing
  only (`hlr::shifted`).
- The record evaluator, iso curves, the face kinds (freeform) and the offset's analytic surfaces
  (none) read it as its form, as a ball patch's label is read.
- The rig names it `blend` in `surface_types` (tests/kernel-cases/README.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
2026-11-11 10:37:29 -05:00
jyviko 50833c3cdc smlib: a body's blend faces cross to and from its record with their definitions
The flat arrays still carry a blend face as its net alone; the definition crosses beside them
through the modelling layer (`solidcore_ops::blend_definition`). Exporting a body
(`to_native_report`, `to_native_ordered`) writes each face whose surface carries a definition a
record keeps (a procedural blend, a round of an exact class whose net has no analytic label) as
`Surface::Blend` with its net as its form. Importing a record (`from_record`,
`from_record_keeping_splines`) builds a blend face on its net, then gives it its definition
back, rebuilt and the net certified again; a net its definition does not certify refuses the
body (`DefinitionNotCertified`), and so does a body the build gave another number of faces. The
record → arrays mapping sends a blend face as its NURBS form, and record SSI refuses a blend
label as it refuses a ball patch's.

Tests: the round on a bore's rim is written by its definition with its net, survives save and
reopen number for number, comes back from a rebuild with the same definitions, and the rebuilt
body takes a shell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
2026-11-11 10:37:29 -05:00
jyviko 79facfc8af solidcore-ops: a body's blend faces between its record and the kernel
`blend_definition::record_definition` reads a face's definition off its surface as a record keeps
it, and `blend_definition::adopt` gives the faces of a body built from a record their definitions
back, rebuilt and their nets certified again: the modelling layer's calls for a client that
builds bodies its own way (through flat arrays) and needs its blend faces to cross with their
definitions, through the kernel and solidcore-convert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
2026-11-11 10:37:29 -05:00
jyviko 0cf51cd24e solidcore-ops: a blend face's geometry is its form
A blend label is the face's definition, with no surface or normal of its own: the record's
geometry check reads such a face by its form (its definition is checked structurally and
certified by the kernel when the face is built), and `label::form_turns` says its form turns
nothing, as for a face with no form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
2026-11-11 10:37:28 -05:00
jyviko fd364af620 solidcore-tess, solidcore-step: a blend face is meshed and written as its net
A blend label is the face's definition; its geometry is its NURBS form. The mesher names the kind
"blend" in its errors, and the STEP writer writes the face as its NURBS form, pcurves and all, as
it writes a ball patch: STEP has no entity for the definition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
2026-11-11 10:37:28 -05:00
jyviko 5570bdb6a9 solidcore-convert: a blend face's definition crosses between the record and the kernel
`blend::blend` and `blend::record_blend` copy a face's blend definition field by field between
the record's `BlendDefinition` and the kernel's `FaceDefinition`. A body written as its record
(`to_record`) writes a blend face as its definition with its net as the NURBS form, wherever the
kernel reads one off it; a record built as the kernel's body (`to_kernel`) builds a blend face on
its net, then gives it its definition back, rebuilt and the net certified again
(`adopt_definitions`), refusing the record for a net its definition does not certify. The
record's blend label has no kernel surface of its own: `surface` refuses it by name, as the
curves and surfaces holding one do.

Test: a procedural definition with a nested blend support, and an exact round's, cross both ways
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
2026-11-11 10:37:28 -05:00
jyviko 73412a9a62 solidcore: a face's blend definition read off for its record, and given back certified
`convert::face_definition` reads the definition a face's surface carries, without its net (the
face's surface is the net): 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, the rational classes' B-spline). A round
on an analytic surface or a ball patch keeps that label in a record, as before, and a definition
whose supports a record cannot hold goes as its net. `FaceDefinition` and
`ExactRoundDefinition` (an `ExactRoundRecord` without its net's form) are the plain data.

`convert::adopt_definitions` gives faces their definitions back: the net is the face's NURBS
surface as built, the definition is made from the record exactly and the net certified against
it again before the face's surface becomes the blend. A net its definition does not certify
refuses the face with the new `SolidError::DefinitionNotCertified { face, eps }`, never kept with
a larger bound; a cancel passes through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
2026-11-11 10:37:28 -05:00
jyviko 265a69e87a solidcore-record: a face's surface may be a blend by its definition
`Surface::Blend { definition }`: a procedural blend's definition S, or a round of an exact
class's (`brep::blend::BlendDefinition`), with the face's `nurbs` its net B, stored once there.
The records are the kernel's plain data, mirrored here as `eval::geometry` mirrors the record's
surfaces: `BlendDefinitionRecord` and the records it holds, `ProceduralBlendRecord` for a support
that is itself a blend (no face, so it keeps its net), and `ExactRoundDefinition`, the kernel's
`ExactRoundRecord` without the net's form. Validation is structural, as the kernel's records':
every curve and surface, finite numbers, nesting at most three definitions deep, the items
counted against the record's budget, and a blend label needs its NURBS form.

Tests: a blend face validates and round-trips number for number, and is refused without its
net, nested too deep or with a number not finite; a reader whose surface kinds predate the label
refuses it by name ("unknown variant `blend`") rather than misreading it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DtL5M2MmcDrkcWhQeHS2K8
2026-11-11 10:37:28 -05:00
40 changed files with 2528 additions and 74 deletions
+2 -1
View File
@@ -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.
+1
View File
@@ -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)?)
+29 -15
View File
@@ -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,
})
}
+13 -6
View File
@@ -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)]
+2
View File
@@ -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)?)?,
})
}
+1 -1
View File
@@ -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,
}
}
}
+1
View File
@@ -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())),
})
}
+1 -1
View File
@@ -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,
})
}
+2 -1
View File
@@ -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 {
+12 -2
View File
@@ -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());
}
+1
View File
@@ -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;
+28
View File
@@ -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)
}
+31 -2
View File
@@ -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 {
+32 -3
View File
@@ -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
}
+1 -1
View File
@@ -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),
}
}
+13
View File
@@ -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
View File
@@ -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
View File
@@ -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, &notify_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
View File
@@ -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
View File
@@ -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`].
+56
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+1 -1
View File
@@ -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. |