Compare commits

...

2 Commits

Author SHA1 Message Date
bherbruck 7c01f857aa ops: a controlled sweep through the kernel's stations, its walls named by the skin's trace (#13664)
`sweep::controlled::sweep_controlled(profile, profile_normal, path, laws)` sweeps closed profile loops along a path of lines, arcs and helices by laws (orientation perpendicular, parallel or by a direction; twist; end scale; up to two guide rails, fitted by scale or stretch), capped at both ends, and returns which faces are the start and end caps and which profile curve each wall was swept from.

Every number is the kernel's: where the stations go (`sweep::stations::fractions`, SolidCraft's fit convention), each station's frame and map (`sweep::stations::stations`), and the skin through them. The walls are named by the kernel's trace of its own skin (`create_skin_primitive_with_sources`, #15062), not by the skin's face order, which is not the profile's (a square's wall 0 held its fourth side): each wall must hold one curve of each section, the same profile curve throughout, or the sweep is refused by name. A curve with a kink of its own sweeps a wall on each side of it.

SolidCraft's limits on the laws (four turns of twist, end scale 0.01 to 100, the least crossing angle and sine) live here and reach the kernel as its laws' limits; the kernel refuses only what has no sweep.

Refused, by name: a path with corners and a closed path (each its own construction: #13639, #13638). The cubic skin's distance from the law surface between stations is not certified (exact where the sections vary linearly along a straight path, as an end scale does; #13637).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 13:51:26 -05:00
bherbruck 649b1d8fd9 skin: say which profile curves each side face was swept from, traced through the construction (tangency-labs/solidcore#3139)
`create_skin_primitive_with_sources` builds the skin `create_skin_primitive` builds, bit for bit, and returns, for each side face, the indices of the curves it was swept from.

The trace runs through the construction:
1. Each curve's edge in its planar profile face.
2. That edge's segment of the loop's joined B-spline (`create_by_joining_with_breaks`).
3. The segment's span of knots.
4. The knots each face spans across the sections after the G1 split.

These are compared exactly, with no tolerance and no geometry. The answer holds where every section's joined curve has the same degree and knots: the skin's compatibility step is then the identity, which the trace checks against the surface's own knots. Otherwise it returns `SkinSources::NotTraced { why }`: open profiles, sections that are not knot-identical, or knots the skin moved. A controlled sweep's sections are affine images of one profile, so it is always traced. The default paths are unchanged, and no golden moves.

Written by w2949 in the swarm's batch tree (e271549a87), carried onto main by w2695: two hunks re-placed on main's typed errors (the b_spline_curve p2 module doc, the skin's layer-count check). Review fixes (w2878): a failed read or a construction that stopped before the side faces now has its own reason (JoinedCurveUnreadable, SurfaceKnotsUnreadable, FaceDomainUnreadable, SideFacesNotReached) instead of borrowing another's; the planar faces' edge-to-curve zip is count-checked (a mismatch records no edge: EdgeUntraced, never a wrong wall); the duplicate api_surfaces table row is dropped; every test body is compared bit for bit plain against traced; SkinMovedKnots and EdgeUntraced are unit-tested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 11:46:06 -05:00
16 changed files with 1622 additions and 43 deletions
+1
View File
@@ -71,6 +71,7 @@ use crate::{Error, Result};
/// A call's own refusal, else the value: as the kernel's calls answer inside [`run`].
type Out<T> = std::result::Result<T, Error>;
pub mod controlled;
pub mod railed;
/// A coordinate no larger than this (mm, 1000 km), so that the kernel's tolerances stay
+388
View File
@@ -0,0 +1,388 @@
//! The controlled sweep: a profile swept along a path of lines, arcs and helices by laws
//! (orientation, twist, end scale, up to two guide rails), skinned through the kernel's
//! stations ([`solidcore::sweep::stations`]), and which faces it made from which profile curve
//! and which are its caps, from the kernel's skin rather than any geometry.
//!
//! Every number is the kernel's: where the stations go ([`stations::fractions`], SolidCraft's
//! fit convention), each station's frame and the skin. The skin is a cubic interpolation through
//! the stations; its distance from the law surface between them is not certified (exact where
//! the sections vary linearly along a straight path, as an end scale along a line does;
//! #13637).
//!
//! Refused, by name: a path with corners (#13639) and a closed path
//! (#13638), each its own construction.
use solidcore::kernel::Kernel;
use solidcore::math::Vec3;
use solidcore::sweep::api_surfaces::{create_skin_primitive_with_sources, NotTraced, SkinSources};
use solidcore::sweep::stations::{
self, Laws, Orientation as KOrientation, Path, Piece as KPiece, RailFit as KRailFit, Stop,
};
use solidcore::topology::{Brep, FaceId};
use super::{bspline, parse, Affine, Curve, KCurve, Made};
use crate::body::{finish, Body, Expect};
use crate::error::status;
use crate::session::run;
use crate::{Error, Result};
/// SolidCraft's limits on a controlled sweep's laws: conventions of the app, not geometry
/// (the kernel refuses only what has no sweep; `solidcore::sweep::stations` module docs).
/// The most twist, four turns (radians, `8 pi`).
pub const MAX_TWIST: f64 = 25.132_741_228_718_345;
/// The smallest end scale (SolidCraft's).
pub const MIN_SCALE: f64 = 0.01;
/// The largest end scale (SolidCraft's).
pub const MAX_SCALE: f64 = 100.0;
/// The least angle a path meets a parallel profile's plane at (radians, 5 degrees;
/// SolidCraft's).
pub const MIN_PARALLEL_CROSSING: f64 = 0.087_266_462_599_716_47;
/// The least sine of the angle between the path and a direction orientation (SolidCraft's).
pub const MIN_DIRECTION_ACROSS: f64 = 1e-3;
/// One piece of a controlled sweep's path.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Piece {
/// From `a` to `b`.
Line {
/// Its start.
a: [f64; 3],
/// Its end.
b: [f64; 3],
},
/// From `start`, `angle` radians right-handed about the unit `axis` through `centre`.
Arc {
/// A point of the axis.
centre: [f64; 3],
/// The axis (unit).
axis: [f64; 3],
/// Its start.
start: [f64; 3],
/// The angle run (radians, positive).
angle: f64,
},
/// As [`Piece::Arc`], rising `rise` along the axis per radian.
Helix {
/// A point of the axis.
centre: [f64; 3],
/// The axis (unit).
axis: [f64; 3],
/// Its start.
start: [f64; 3],
/// The angle run (radians, positive).
angle: f64,
/// The rise per radian.
rise: f64,
},
}
/// How the profile turns as it follows the path ([`solidcore::sweep::stations::Orientation`]).
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum Orientation {
/// Square to the path, carried by its rotation-minimizing frame.
#[default]
Perpendicular,
/// As drawn.
Parallel,
/// Square to the path, its turn fixed by a direction.
Direction([f64; 3]),
}
/// How guide rails size the profile.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum RailFit {
/// Evenly.
#[default]
Scale,
/// Only across to the rail.
Stretch,
}
/// A controlled sweep's laws. The default is a plain sweep.
#[derive(Clone, Debug, PartialEq)]
pub struct ControlledSweep {
/// How the profile turns.
pub orientation: Orientation,
/// The twist over the path (radians).
pub twist: f64,
/// The profile's scale at the end.
pub end_scale: f64,
/// Up to two guide rails, each one curve.
pub rails: Vec<Curve>,
/// How the rails size the profile.
pub rail_fit: RailFit,
}
impl Default for ControlledSweep {
fn default() -> Self {
ControlledSweep {
orientation: Orientation::Perpendicular,
twist: 0.0,
end_scale: 1.0,
rails: Vec::new(),
rail_fit: RailFit::Scale,
}
}
}
/// Which faces of a controlled sweep came from where, in the body's record indices.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SweptFaces {
/// The start cap's faces.
pub start: Vec<usize>,
/// The end cap's faces.
pub end: Vec<usize>,
/// Each wall face, in the skin's order, with the index (into the profile) of the curve it
/// was swept from, as the kernel's skin traced it (`create_skin_primitive_with_sources`).
/// A curve with a kink of its own sweeps a wall on each side of it.
pub walls: Vec<(usize, usize)>,
}
fn v3(p: [f64; 3]) -> Vec3 {
Vec3::new(p[0], p[1], p[2])
}
fn p3(v: Vec3) -> [f64; 3] {
[v.x, v.y, v.z]
}
fn kernel_piece(p: &Piece) -> KPiece {
match *p {
Piece::Line { a, b } => KPiece::Line { a: v3(a), b: v3(b) },
Piece::Arc {
centre,
axis,
start,
angle,
} => KPiece::Arc {
centre: v3(centre),
axis: v3(axis),
start: v3(start),
angle,
},
Piece::Helix {
centre,
axis,
start,
angle,
rise,
} => KPiece::Helix {
centre: v3(centre),
axis: v3(axis),
start: v3(start),
angle,
rise,
},
}
}
/// The body's record index of each of `ids`.
fn indices(order: &[FaceId], ids: &[FaceId]) -> Option<Vec<usize>> {
ids.iter()
.map(|f| order.iter().position(|g| g == f))
.collect()
}
/// The profile `profile` (closed loops in the plane whose normal is `profile_normal`) swept
/// along `path` by `laws`, capped at both ends, and which faces came from where.
///
/// # Errors
/// [`Error::Argument`] for no path pieces, a profile or rail the curves refuse, or more than
/// two rails; the kernel's refusal ([`Error::Status`]) of a twist or end scale past
/// SolidCraft's limits ([`MAX_TWIST`], [`MIN_SCALE`], [`MAX_SCALE`], passed to it);
/// [`Error::Unsupported`] for a path with corners or a closed path, and for a skin the kernel
/// does not trace to its profile curves (a controlled sweep's sections are affine images of
/// one profile, so it always is: `create_skin_primitive_with_sources`) or a wall not swept
/// from one curve of each section; the kernel's refusals of the path, the laws and
/// the rails ([`Error::Status`]), with SolidCraft's least crossing angle and sine
/// ([`MIN_PARALLEL_CROSSING`], [`MIN_DIRECTION_ACROSS`]); those of `finish`.
pub fn sweep_controlled(
profile: &[Curve],
profile_normal: [f64; 3],
path: &[Piece],
laws: &ControlledSweep,
) -> Result<(Body, SweptFaces)> {
if path.is_empty() || laws.rails.len() > 2 {
return Err(Error::Argument);
}
let made = run(
"sweep_controlled",
|k| -> solidcore::Result<Result<(Brep, Faces)>> {
build(k, profile, profile_normal, path, laws)
},
)?;
let (brep, faces) = made?;
let body = Body::new(brep);
let (_, order) = body.to_record()?;
let start = indices(&order.faces, &faces.start);
let end = indices(&order.faces, &faces.end);
let (Some(start), Some(end)) = (start, end) else {
return Err(Error::Exception);
};
let mut walls = Vec::new();
for (f, curve) in faces.walls {
let Some(i) = order.faces.iter().position(|g| *g == f) else {
return Err(Error::Exception);
};
walls.push((i, curve));
}
Ok((body, SweptFaces { start, end, walls }))
}
/// The kernel's face ids of a controlled sweep's caps and walls.
struct Faces {
start: Vec<FaceId>,
end: Vec<FaceId>,
walls: Vec<(FaceId, usize)>,
}
fn build(
k: &mut Kernel,
profile: &[Curve],
profile_normal: [f64; 3],
path: &[Piece],
laws: &ControlledSweep,
) -> solidcore::Result<Result<(Brep, Faces)>> {
let made: Made = match parse(k, profile) {
Ok(m) => m,
Err(e) => return Ok(Err(e)),
};
let mut rails: Vec<KCurve> = Vec::new();
for rail in &laws.rails {
let rail_made = match parse(k, std::slice::from_ref(rail)) {
Ok(m) => m,
Err(e) => return Ok(Err(e)),
};
let [c] = rail_made.curves.as_slice() else {
return Ok(Err(Error::Argument));
};
rails.push(c.clone());
}
let kpath = Path {
pieces: path.iter().map(kernel_piece).collect(),
};
if !kpath.corner_fractions()?.is_empty() {
return Ok(Err(Error::Unsupported(
"a controlled sweep along a path with corners (its runs meet at mitres: #13639)",
)));
}
if kpath.is_closed()? {
return Ok(Err(Error::Unsupported(
"a controlled sweep round a closed path (its last section on its first: #13638)",
)));
}
let klaws = Laws {
orientation: match laws.orientation {
Orientation::Perpendicular => KOrientation::Perpendicular,
Orientation::Parallel => KOrientation::Parallel,
Orientation::Direction(d) => KOrientation::Direction(v3(d)),
},
twist: laws.twist,
end_scale: laws.end_scale,
rails: rails.iter().collect(),
rail_fit: match laws.rail_fit {
RailFit::Scale => KRailFit::Scale,
RailFit::Stretch => KRailFit::Stretch,
},
min_crossing: MIN_PARALLEL_CROSSING,
min_across: MIN_DIRECTION_ACROSS,
max_twist: MAX_TWIST,
end_scale_within: [MIN_SCALE, MAX_SCALE],
};
let fractions = stations::fractions(&kpath, &klaws)?;
let at: Vec<Stop> = fractions.into_iter().map(Stop::At).collect();
let st = stations::stations(k, &kpath, v3(profile_normal), &klaws, &at)?;
let Some(first) = st.first() else {
return Ok(Err(Error::Argument));
};
let mut sections = Vec::new();
let mut counts = Vec::new();
for s in &st {
let map = Affine {
m: s.m.map(p3),
t: p3(s.translation(first)),
};
let t = map.transform();
for c in &made.curves {
let Some(b) = bspline(c)? else {
return Ok(Err(Error::Argument));
};
match b.affine_image(&t) {
Ok((image, _)) => sections.push(KCurve::from(image)),
Err(e) => return Ok(Err(status(e))),
}
}
counts.push(made.curves.len());
}
let degree = st.len().saturating_sub(1).min(3);
let refs: Vec<Option<&KCurve>> = sections.iter().map(Some).collect();
let (skin, sources) = match create_skin_primitive_with_sources(k, &counts, &refs, degree, true)
{
Ok(r) => r,
Err(e) => return Ok(Err(status(e))),
};
let traced = match sources {
SkinSources::Traced(walls) => walls,
SkinSources::NotTraced { why } => return Ok(Err(Error::Unsupported(not_traced(why)))),
};
// each wall holds one curve of each section, the same profile curve throughout: the
// indices c, c + n, c + 2 n, ... of the sections' curves (n the profile's)
let n = made.curves.len();
let mut walls = Vec::new();
for (f, curves) in traced {
let Some(&c) = curves.first() else {
return Ok(Err(Error::Unsupported(
"a wall the skin traced to no profile curve",
)));
};
let one_curve = c < n
&& curves.len() == st.len()
&& curves.iter().enumerate().all(|(s, &ci)| ci == c + s * n);
if !one_curve {
return Ok(Err(Error::Unsupported(
"a wall not swept from one curve of each section",
)));
}
walls.push((f, c));
}
let faces = Faces {
start: skin.start_faces,
end: skin.end_faces,
walls,
};
Ok(match finish(k, skin.brep, Expect::SOLID)? {
Ok(b) => Ok((b, faces)),
Err(e) => Err(e),
})
}
/// The refusal for a skin the kernel did not trace, by its reason.
fn not_traced(why: NotTraced) -> &'static str {
match why {
NotTraced::OpenProfiles => "a skin not traced to its profile curves: open profiles",
NotTraced::SectionsNotKnotIdentical => {
"a skin not traced to its profile curves: sections whose knots differ"
}
NotTraced::SkinMovedKnots => {
"a skin not traced to its profile curves: its knots are not the sections'"
}
NotTraced::FaceOffKnots => {
"a skin not traced to its profile curves: a wall's bounds are not its knots"
}
NotTraced::EdgeUntraced => {
"a skin not traced to its profile curves: a profile edge with no recorded curve"
}
NotTraced::JoinedCurveUnreadable => {
"a skin not traced to its profile curves: a section's curve could not be read"
}
NotTraced::SurfaceKnotsUnreadable => {
"a skin not traced to its profile curves: its knots could not be read"
}
NotTraced::FaceDomainUnreadable => {
"a skin not traced to its profile curves: a wall's domain could not be read"
}
NotTraced::SideFacesNotReached => {
"a skin not traced to its profile curves: its walls were not made"
}
}
}
+249
View File
@@ -0,0 +1,249 @@
//! `sweep::controlled::sweep_controlled` (#13664): a controlled sweep through the kernel's
//! stations, and which faces came from where, against closed forms.
//!
//! - a square of side 2 about the path, swept 10 along a line with end scale `k`: its sections
//! vary linearly, which the cubic skin reproduces, so the volume is the frustum's
//! `A L (1 + k + k^2) / 3`;
//! - its faces: one start cap in z = 0, one end cap in z = L, and a wall per profile line
//! holding that line;
//! - twisted a quarter turn and scaled, the end cap's corners are the start's turned and scaled;
//! - one straight rail diverging from the path scales it as end scale `(d_0 + s L) / d_0`;
//! - a path with a corner and a closed path are refused, named; so are a twist and end scale
//! past SolidCraft's limits, which live here, not in the kernel.
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)] // test helpers
use solidcore_ops::sweep::controlled::{sweep_controlled, ControlledSweep, Piece, SweptFaces};
use solidcore_ops::sweep::Curve;
use solidcore_ops::{Body, Error};
use solidcore_record::brep::{Brep as Record, Loop};
use std::f64::consts::{FRAC_PI_2, PI};
const L: f64 = 10.0;
/// The square of side 2 about the origin in z = 0, counter-clockwise from (-1, -1).
fn square() -> Vec<Curve> {
let c = [[-1.0, -1.0], [1.0, -1.0], [1.0, 1.0], [-1.0, 1.0]];
(0..4)
.map(|i| Curve::Line {
start: [c[i][0], c[i][1], 0.0],
end: [c[(i + 1) % 4][0], c[(i + 1) % 4][1], 0.0],
})
.collect()
}
fn up(len: f64) -> Vec<Piece> {
vec![Piece::Line {
a: [0.0, 0.0, 0.0],
b: [0.0, 0.0, len],
}]
}
fn swept(laws: &ControlledSweep) -> (Body, SweptFaces) {
sweep_controlled(&square(), [0.0, 0.0, 1.0], &up(L), laws).unwrap()
}
/// The points of the vertices of face `f`'s loops.
fn face_points(r: &Record, f: usize) -> Vec<[f64; 3]> {
let mut out = Vec::new();
for lp in &r.faces[f].loops {
if let Loop::Edges { edges } = lp {
for oe in edges {
for v in r.edges[oe.edge as usize].vertices {
out.push(r.vertices[v as usize].point);
}
}
}
}
out
}
fn has(points: &[[f64; 3]], p: [f64; 3]) -> bool {
points
.iter()
.any(|q| (0..3).all(|i| (q[i] - p[i]).abs() < 1e-9))
}
#[test]
fn a_scaled_square_is_the_frustum() {
for k in [1.0, 0.5, 2.0, 3.0] {
let laws = ControlledSweep {
end_scale: k,
..ControlledSweep::default()
};
let (b, _) = swept(&laws);
let v = b.volume(1e-10).unwrap();
let want = 4.0 * L * (1.0 + k + k * k) / 3.0;
assert!((v - want).abs() <= 1e-9 * want, "k {k}: {v} vs {want}");
}
}
#[test]
fn its_faces_are_the_caps_and_a_wall_per_profile_line() {
let (b, faces) = swept(&ControlledSweep::default());
let (r, _) = b.to_record().unwrap();
assert_eq!(faces.start.len(), 1, "{faces:?}");
assert_eq!(faces.end.len(), 1, "{faces:?}");
assert_eq!(faces.walls.len(), 4, "{faces:?}");
assert_eq!(r.faces.len(), 6);
let at = |f: usize| face_points(&r, f);
assert!(at(faces.start[0]).iter().all(|p| p[2].abs() < 1e-12));
assert!(at(faces.end[0]).iter().all(|p| (p[2] - L).abs() < 1e-12));
for (i, c) in square().iter().enumerate() {
let Curve::Line { start, end } = c else {
panic!("a line");
};
let wall = faces
.walls
.iter()
.find(|(_, curve)| *curve == i)
.expect("a wall for each line")
.0;
let points = at(wall);
assert!(
has(&points, *start) && has(&points, *end),
"line {i}: {points:?}"
);
}
}
/// A profile given out of order, one side reversed: each wall still holds its own line (the
/// kernel's trace, not the skin's face order).
#[test]
fn a_shuffled_profile_s_walls_are_its_own_lines() {
let mut profile = square();
profile.swap(0, 2);
profile.swap(1, 3);
if let Curve::Line { start, end } = &mut profile[1] {
std::mem::swap(start, end);
}
let (b, faces) = sweep_controlled(
&profile,
[0.0, 0.0, 1.0],
&up(L),
&ControlledSweep::default(),
)
.unwrap();
let (r, _) = b.to_record().unwrap();
assert_eq!(faces.walls.len(), 4, "{faces:?}");
let mut seen: Vec<usize> = faces.walls.iter().map(|&(_, c)| c).collect();
seen.sort_unstable();
assert_eq!(seen, [0, 1, 2, 3], "{faces:?}");
for &(wall, i) in &faces.walls {
let Curve::Line { start, end } = profile[i] else {
panic!("a line");
};
let points = face_points(&r, wall);
// the wall runs from its line at the start to the line moved up L at the end
let up_l = |p: [f64; 3]| [p[0], p[1], p[2] + L];
assert!(
has(&points, start) && has(&points, end) && has(&points, up_l(start)),
"line {i}: {points:?}"
);
}
}
#[test]
fn a_twisted_scaled_square_ends_turned_and_scaled() {
let (twist, k) = (FRAC_PI_2, 2.0);
let laws = ControlledSweep {
twist,
end_scale: k,
..ControlledSweep::default()
};
let (b, faces) = swept(&laws);
let (r, _) = b.to_record().unwrap();
let end = face_points(&r, faces.end[0]);
for (x, y) in [(-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)] {
// a quarter turn: sin 1, cos 0 (ops computes no trigonometry, docs/layers.md)
let (s, c) = (1.0, 0.0);
let want = [k * (x * c - y * s), k * (x * s + y * c), L];
assert!(has(&end, want), "{want:?} in {end:?}");
}
}
#[test]
fn a_diverging_rail_scales_as_its_distance() {
let (d0, slope) = (4.0, 0.1);
let rail = Curve::Line {
start: [d0, 0.0, 0.0],
end: [d0 + slope * L, 0.0, L],
};
let laws = ControlledSweep {
rails: vec![rail],
..ControlledSweep::default()
};
let (b, _) = swept(&laws);
let k = (d0 + slope * L) / d0;
let want = 4.0 * L * (1.0 + k + k * k) / 3.0;
let v = b.volume(1e-10).unwrap();
assert!((v - want).abs() <= 1e-9 * want, "{v} vs {want}");
}
#[test]
fn a_cornered_or_closed_path_is_refused() {
let corner = vec![
Piece::Line {
a: [0.0, 0.0, 0.0],
b: [0.0, 0.0, L],
},
Piece::Line {
a: [0.0, 0.0, L],
b: [L, 0.0, L],
},
];
let err = sweep_controlled(
&square(),
[0.0, 0.0, 1.0],
&corner,
&ControlledSweep::default(),
)
.unwrap_err();
assert!(
matches!(err, Error::Unsupported(w) if w.contains("corners")),
"{err:?}"
);
let closed = vec![Piece::Arc {
centre: [-20.0, 0.0, 0.0],
axis: [0.0, 1.0, 0.0],
start: [0.0, 0.0, 0.0],
angle: 2.0 * PI,
}];
let err = sweep_controlled(
&square(),
[0.0, 0.0, 1.0],
&closed,
&ControlledSweep::default(),
)
.unwrap_err();
assert!(
matches!(err, Error::Unsupported(w) if w.contains("closed")),
"{err:?}"
);
}
/// SolidCraft's limits are the ops entry's, passed to the kernel's laws: five turns of twist
/// and an end scale past 0.01 to 100 are refused, though the kernel's own defaults take them.
#[test]
fn solidcraft_s_limits_refuse_here() {
use solidcore_ops::sweep::controlled::{MAX_SCALE, MAX_TWIST, MIN_SCALE};
let refused = |laws: ControlledSweep| {
let got = sweep_controlled(&square(), [0.0, 0.0, 1.0], &up(L), &laws);
assert!(matches!(got, Err(Error::Status(_))), "{laws:?}: {got:?}");
};
refused(ControlledSweep {
twist: 5.0 * 2.0 * PI,
..ControlledSweep::default()
});
for end_scale in [0.5 * MIN_SCALE, 2.0 * MAX_SCALE] {
refused(ControlledSweep {
end_scale,
..ControlledSweep::default()
});
}
assert_eq!(MAX_TWIST.to_bits(), (8.0 * PI).to_bits(), "four turns");
}
+28 -3
View File
@@ -16,11 +16,13 @@
//! | [`CompositeCurve::create_offsets_of_many_curves`], [`OffsetParams`], [`TrimmedOffsets`], [`OffsetPiece`] | `CreateOffsetsOfManyCurves` (static) |
//! | [`CompositeCurve::intersect_and_trim_offsets`] | `IntersectAndTrimOffsets` (static) |
//! | [`CompositeCurve::create_trimmed_offsets`], [`OffsetDirection`] | `CreateTrimmedOffsets`, `SmOffsetDirectionType` |
//! | [`CompositeCurve::make_composite_nurb`] | `MakeCompositeNurb` |
//! | [`CompositeCurve::make_composite_nurb`], [`CompositeCurve::make_composite_nurb_with_breaks`] | `MakeCompositeNurb` |
//! | [`CompositeCurve::read_from_db`] | `SmCompositeCurve::ReadFromDB` (static) |
//! | [`CompositeCurve::write_to_db`] | `SmCompositeCurve::WriteToDB` |
//!
//! Deviations:
//! - **Join breaks** (not upstream). [`CompositeCurve::make_composite_nurb_with_breaks`]
//! also returns each segment's span in the joined curve (tangency-labs/solidcore#3139).
//! - **Attributes.** A [`Curve`] carries no attribute list, so the offsetters pass each
//! curve with its offset map attribute as an [`OffsetPiece`]. Copies and trims keep
//! the attribute (upstream's `SM_AB_COPY` behaviour copies it with `MakeCopy`), and
@@ -858,6 +860,22 @@ impl CompositeCurve {
/// Those of the join; [`SolidError::InvalidInput`] for a segment that is not an
/// `SmBSplineCurve` (upstream joins a `NULL` curve).
pub fn make_composite_nurb(&mut self) -> Result<BSplineCurve> {
self.composite_nurb(false).map(|(joined, _)| joined)
}
/// [`CompositeCurve::make_composite_nurb`], also returning where each segment lies
/// in the joined curve ([`BSplineCurve::create_by_joining_with_breaks`]: segment `i`
/// spans `[breaks[i], breaks[i + 1]]`, each value one of the curve's knots).
///
/// # Errors
///
/// Those of [`CompositeCurve::make_composite_nurb`].
pub fn make_composite_nurb_with_breaks(&mut self) -> Result<(BSplineCurve, Vec<f64>)> {
self.composite_nurb(true)
}
/// `MakeCompositeNurb`, with the join's breaks when asked (empty otherwise).
fn composite_nurb(&mut self, with_breaks: bool) -> Result<(BSplineCurve, Vec<f64>)> {
// get arrays of composite curve segments and their senses
let mut bsplines = Vec::with_capacity(self.segments.len());
let mut senses = Vec::with_capacity(self.segments.len());
@@ -880,7 +898,14 @@ impl CompositeCurve {
}
// create the joined curve
let joined = BSplineCurve::create_by_joining(&mut bsplines, Some(&senses))?;
let (joined, breaks) = if with_breaks {
BSplineCurve::create_by_joining_with_breaks(&mut bsplines, Some(&senses))?
} else {
(
BSplineCurve::create_by_joining(&mut bsplines, Some(&senses))?,
Vec::new(),
)
};
// the join raised the B-spline segments in place
for (s, b) in self.segments.iter_mut().zip(bsplines) {
@@ -888,7 +913,7 @@ impl CompositeCurve {
*x = b;
}
}
Ok(joined)
Ok((joined, breaks))
}
/// `CreateTrimmedOffsets`: [`Self::create_offsets_of_many_curves`] of this curve
+42 -1
View File
@@ -11,7 +11,7 @@
//! | [`BSplineCurve::calculate_bounding_box`], [`BoundingBoxRequest`], [`BoundingBoxes`] | `CalculateBoundingBox` |
//! | [`BSplineCurve::is_planar`], [`Planar`] | `SmCurve::IsPlanar` |
//! | [`BSplineCurve::create_analytic_curve`], [`AnalyticCurve`] | `CreateAnalyticCurve` |
//! | [`BSplineCurve::create_by_joining`] | `CreateByJoining` |
//! | [`BSplineCurve::create_by_joining`], [`BSplineCurve::create_by_joining_with_breaks`] | `CreateByJoining` |
//! | [`BSplineCurve::create_point_curve`] | `CreatePointCurve` |
//! | [`BSplineCurve::create_simple_offset`] | `CreateSimpleOffset` |
//! | [`BSplineCurve::has_repeated_end_control_points`] | `HasRepeatedEndControlPoints` |
@@ -43,6 +43,9 @@
//! `p1`). `CalculateBoundingBox`'s unused `bExpandBox` flag is dropped and the boxes
//! asked for come back in [`BoundingBoxes`] (fresh boxes, as a caller passing new
//! `SmExtent3d` / `SmPseudoBox` / `SmPolarBox` objects gets).
//! - **Join breaks** (not upstream). [`BSplineCurve::create_by_joining_with_breaks`]
//! also returns the knot at which each segment ends, read off the knot vector as it is
//! built (the skin's face sources, tangency-labs/solidcore#3139).
//! - **Missing NURB.** Where upstream dereferences the `NULL` NURB of a curve built
//! without one (`GetOrCreateGwNurbPointer` cannot make it for a plain B-spline), the
//! port returns `SM_ERR` ([`BSplineFailure::NoNurbsForm`]). That includes
@@ -868,6 +871,34 @@ impl BSplineCurve {
/// different dimension or degree; `SM_ERR` without a NURB, for a zero end weight or
/// when degree elevation or the canonical construction fails.
pub fn create_by_joining(segments: &mut [Self], senses: Option<&[bool]>) -> Result<Self> {
Self::join(segments, senses, None)
}
/// [`BSplineCurve::create_by_joining`], also returning where each segment lies in
/// the joined curve: the joined curve's first knot, then each segment's last knot,
/// in segment order (`segments.len() + 1` values). Segment `i` spans
/// `[breaks[i], breaks[i + 1]]`. Each value is one of the joined curve's knots, the
/// very value its knot vector holds. The curve is the one
/// [`BSplineCurve::create_by_joining`] makes.
///
/// # Errors
///
/// Those of [`BSplineCurve::create_by_joining`].
pub fn create_by_joining_with_breaks(
segments: &mut [Self],
senses: Option<&[bool]>,
) -> Result<(Self, Vec<f64>)> {
let mut breaks = Vec::new();
let joined = Self::join(segments, senses, Some(&mut breaks))?;
Ok((joined, breaks))
}
/// `CreateByJoining`, recording the segments' breaks in `breaks` when asked.
fn join(
segments: &mut [Self],
senses: Option<&[bool]>,
mut breaks: Option<&mut Vec<f64>>,
) -> Result<Self> {
let mut ctrl_pts: Vec<Point3> = Vec::new();
let mut weights: Vec<f64> = Vec::new();
let mut knot_mult: Vec<usize> = Vec::new();
@@ -966,6 +997,9 @@ impl BSplineCurve {
}
ctrl_pts.push(last_point);
knots.push(first_of(&seg_knots)?);
if let Some(b) = breaks.as_mut() {
b.push(first_of(&seg_knots)?);
}
knot_mult.push(
*seg_knot_mult
.first()
@@ -1086,6 +1120,13 @@ impl BSplineCurve {
}
ctrl_pts.push(pt);
}
if let Some(b) = breaks.as_mut() {
b.push(
*knots
.last()
.ok_or_else(|| invalid(OP_JOIN, "empty joined data"))?,
);
}
}
// Clamp that last knot
@@ -4,7 +4,7 @@
//!
//! | Rust | Upstream |
//! |------|----------|
//! | [`PrimitiveCreation::create_skin_primitive`] | nine-argument `CreateSkinPrimitive` |
//! | [`PrimitiveCreation::create_skin_primitive`], [`PrimitiveCreation::create_skin_primitive_traced`] | nine-argument `CreateSkinPrimitive` |
//! | `skin_primitive_internal` (private) | `sm_CreateSkinPrimitiveInternal` |
//!
//! Deviations: only the ownership-reporting overload is in scope, so the internal
@@ -13,7 +13,9 @@
//! takes every curve of an open profile (upstream reports a lone curve that the
//! composite builder leaves out as not transferred; see `plan/findings.md`).
//! Profile counts past the curve list and missing curves (upstream out-of-range
//! and null reads) are errors. No recursion.
//! and null reads) are errors. No recursion. Not upstream (tangency-labs/solidcore#3139):
//! [`PrimitiveCreation::create_skin_primitive_traced`] also traces each side face's
//! profile curves (`p3::skin_sources`), building the same skin.
use super::PrimitiveCreationAccess;
use crate::curve::composite_curve::CompositeCurve;
@@ -22,6 +24,7 @@ use crate::error::{Result, SolidError};
use crate::math::{max2, min2, BIG_DOUBLE, EFF_ZERO_DEG, EFF_ZERO_SQRT};
use crate::nurbs::b_spline_surface::p1::SurfParamType;
use crate::primitive::primitive_creation::p1::PrimitiveCreation;
use crate::primitive::primitive_creation::p3::skin_sources::SkinTrace;
use crate::sweep::topology_sweep::access::FaceRef;
use crate::topology::{BrepId, FaceId};
@@ -65,6 +68,45 @@ impl PrimitiveCreation {
cap_ends,
[start_faces, side_faces, end_faces],
transferred,
None,
)
}
/// [`PrimitiveCreation::create_skin_primitive`], also tracing which profile curves
/// each side face was swept from into `trace` (not upstream:
/// [`crate::primitive::primitive_creation::p3::skin_sources`],
/// tangency-labs/solidcore#3139). The B-rep and faces are those
/// `create_skin_primitive` makes; the trace adds no failure.
///
/// # Errors
///
/// Those of [`PrimitiveCreation::create_skin_primitive`].
#[allow(clippy::too_many_arguments)] // upstream's arguments and the trace
pub fn create_skin_primitive_traced(
&mut self,
a: &mut impl PrimitiveCreationAccess,
brep: BrepId,
curves_in_each_profile: &[usize],
curves: &mut [Option<Curve>],
approx_tol: f64,
degree: usize,
cap_ends: bool,
[start_faces, side_faces, end_faces]: [&mut Vec<FaceId>; 3],
transferred: &mut Vec<bool>,
trace: &mut SkinTrace,
) -> Result<()> {
skin_primitive_internal(
self,
a,
brep,
curves_in_each_profile,
curves,
approx_tol,
degree,
cap_ends,
[start_faces, side_faces, end_faces],
transferred,
Some(trace),
)
}
}
@@ -81,6 +123,7 @@ fn skin_primitive_internal(
cap_ends: bool,
[start_faces, side_faces, end_faces]: [&mut Vec<FaceId>; 3],
transferred: &mut Vec<bool>,
trace: Option<&mut SkinTrace>,
) -> Result<()> {
// empty entity maps
primitive.init_entity_maps();
@@ -126,6 +169,7 @@ fn skin_primitive_internal(
cap_ends,
[start_faces, side_faces, end_faces],
transferred,
trace,
);
a.delete_brep(scratch);
result
@@ -148,6 +192,7 @@ fn skin_with_scratch(
mut cap_ends: bool,
[start_faces, side_faces, end_faces]: [&mut Vec<FaceId>; 3],
transferred: &mut [bool],
mut trace: Option<&mut SkinTrace>,
) -> Result<()> {
a.set_editing(scratch, true)?;
@@ -181,7 +226,15 @@ fn skin_with_scratch(
.collect();
let held: Vec<bool> = profile.iter().map(Option::is_some).collect();
let infinite = a.infinite_region(scratch)?;
let status = a.planar_faces((scratch, infinite), &mut profile, tol, &mut profile_faces);
let status = a
.planar_faces_with_edges((scratch, infinite), &mut profile, tol, &mut profile_faces)
.map(|edges| {
// (the trace: each edge with its curve's index among all the curves)
if let Some(t) = trace.as_deref_mut() {
t.edge_curves
.extend(edges.into_iter().map(|(e, i)| (e, profile_start + i)));
}
});
// A lower-level error can occur after one or more curves have already
// transferred to lower-level topology: record those transfers.
@@ -278,6 +331,9 @@ fn skin_with_scratch(
// when data was found to be a sequence of loft curves
if open_profiles {
if let Some(t) = trace.as_deref_mut() {
t.open_profiles = true;
}
// don't make cap faces
cap_ends = false;
// clear out the stale input pointers
@@ -287,7 +343,9 @@ fn skin_with_scratch(
}
// loft the loops on a sequence of faces or a sequence of loft curves
primitive.create_skin_from_faces(a, brep, &faces, &nurbs, approx_tol, degree, side_faces)?;
primitive.create_skin_from_faces_traced(
a, brep, &faces, &nurbs, approx_tol, degree, side_faces, trace,
)?;
// When asked - create start and stop cap faces
if cap_ends {
@@ -9,7 +9,7 @@
//! | [`PrimitiveCreation::create_partial_rotational_sweep`] | partial `CreateRotationalSweep` |
//! | [`PrimitiveCreation::create_swung_primitive`] | `CreateSwungPrimitive` |
//! | [`PrimitiveCreation::create_curve_sweep`] | `CreateCurveSweep` |
//! | [`PrimitiveCreation::create_skin_from_faces`] | `CreateSkinFromFaces` |
//! | [`PrimitiveCreation::create_skin_from_faces`], [`PrimitiveCreation::create_skin_from_faces_traced`] | `CreateSkinFromFaces` |
//! | [`PrimitiveCreation::create_blend_primitive`] | `CreateBlendPrimitive` |
//! | [`ruled_surface_from_offset`] | `sm_CreateRuledSurfaceFromOffsetCurve` |
//!
@@ -25,6 +25,7 @@ mod access;
mod construct;
mod geometry;
mod ruled;
pub mod skin_sources;
mod sweeps;
pub use super::p1::PrimitiveCreation;
@@ -50,9 +50,27 @@ pub trait PrimitiveAccess: SweepAccess {
tolerance: f64,
faces: &mut Vec<FaceId>,
) -> Result<()> {
self.planar_faces_with_edges(region, curves, tolerance, faces)
.map(drop)
}
/// [`PrimitiveAccess::planar_faces`], also returning the edge made from each curve
/// with that curve's slot in `curves` (not upstream: the skin's face sources,
/// tangency-labs/solidcore#3139). The faces are those `planar_faces` makes.
fn planar_faces_with_edges(
&mut self,
region: RegionRef,
curves: &mut Vec<Option<Curve>>,
tolerance: f64,
faces: &mut Vec<FaceId>,
) -> Result<Vec<(EdgeId, usize)>> {
let slots: Vec<usize> = curves
.iter()
.enumerate()
.filter_map(|(i, c)| c.as_ref().map(|_| i))
.collect();
let input: Vec<Curve> = curves.iter().flatten().cloned().collect();
*faces = with_face_env(self, region.0, |kernel, model, fenv| {
crate::topology::brep::p8::create_planar_faces_with_3d_curves(
let (made, edges) = with_face_env(self, region.0, |kernel, model, fenv| {
crate::topology::brep::p8::create_planar_faces_with_3d_curves_and_edges(
kernel,
model,
Some(region.1),
@@ -61,11 +79,16 @@ pub trait PrimitiveAccess: SweepAccess {
fenv,
)
})?;
*faces = made;
// consumed on success: the B-rep owns every curve, each slot stays empty
for slot in curves.iter_mut() {
*slot = None;
}
Ok(())
// (the tracing adds no failure: an edge whose curve has no slot is left out)
Ok(edges
.into_iter()
.filter_map(|(e, i)| slots.get(i).map(|&slot| (e, slot)))
.collect())
}
/// Create one planar face from its boundary curves
/// (`SmBrep::CreatePlanarFaceWith3DCurves`).
@@ -206,6 +229,16 @@ pub trait PrimitiveAccess: SweepAccess {
crate::topology::face::p3::create_composites(kernel, model, face.1)
})
}
/// [`PrimitiveAccess::face_composites`], each composite with the edge of each
/// segment (not upstream: the skin's face sources, tangency-labs/solidcore#3139).
fn face_composites_with_edges(
&mut self,
face: FaceRef,
) -> Result<Vec<(CompositeCurve, Vec<EdgeId>)>> {
self.with_brep_mut(face.0, |kernel, model, _, _| {
crate::topology::face::p3::create_composites_with_edges(kernel, model, face.1)
})
}
/// Face's normal bounding box for the rotational axis check.
fn primitive_face_box(&mut self, face: FaceRef) -> Result<Extent3d> {
self.with_brep_mut(face.0, |kernel, model, _, _| {
@@ -56,6 +56,8 @@ use crate::surface::surface::{
use crate::sweep::topology_sweep::access::{FaceRef, RegionRef};
use crate::topology::{BrepId, EdgeuseId, FaceId};
use super::skin_sources::{layer_sources, LoopSpans, NotTraced, SkinSources, SkinTrace};
const OP: &str = "primitive::primitive_creation::p3";
pub(super) fn target(primitive: &PrimitiveCreation, brep: BrepId) -> Result<RegionRef> {
@@ -220,6 +222,31 @@ impl PrimitiveCreation {
approx_tol: f64,
degree: usize,
new_faces: &mut Vec<FaceId>,
) -> Result<()> {
self.create_skin_from_faces_traced(
a, brep, faces, profiles, approx_tol, degree, new_faces, None,
)
}
/// [`PrimitiveCreation::create_skin_from_faces`], also tracing which profile curves
/// each new face was swept from into `trace` when given (not upstream:
/// [`super::skin_sources`], tangency-labs/solidcore#3139). The faces and surfaces are
/// those `create_skin_from_faces` makes; the trace adds no failure.
///
/// # Errors
///
/// Those of [`PrimitiveCreation::create_skin_from_faces`].
#[allow(clippy::too_many_arguments)] // upstream's arguments and the trace
pub fn create_skin_from_faces_traced(
&mut self,
a: &mut impl PrimitiveAccess,
brep: BrepId,
faces: &[FaceRef],
profiles: &[BSplineCurve],
approx_tol: f64,
degree: usize,
new_faces: &mut Vec<FaceId>,
trace: Option<&mut SkinTrace>,
) -> Result<()> {
self.init_entity_maps();
new_faces.clear();
@@ -227,6 +254,9 @@ impl PrimitiveCreation {
let mut skin_curves = Vec::new();
let mut layers = Vec::new();
let mut layer_count = 0;
// (the trace: each section loop's spans, by layer, and the first reason it fails)
let mut loop_spans: Vec<(usize, LoopSpans)> = Vec::new();
let mut untraced: Option<NotTraced> = None;
for (i, &face) in faces.iter().enumerate() {
let domain = crate::topology::face::p2::uv_domain(a.brep(face.0)?, face.1)?;
face_surface(a, face)?.evaluate_normal(
@@ -235,7 +265,7 @@ impl PrimitiveCreation {
Side::Left,
Side::Left,
)?;
let composites = a.face_composites(face)?;
let composites = a.face_composites_with_edges(face)?;
if i == 0 {
layer_count = composites.len();
} else if layer_count != composites.len() {
@@ -244,8 +274,15 @@ impl PrimitiveCreation {
"the faces have different numbers of layers",
));
}
for (j, mut composite) in composites.into_iter().enumerate() {
skin_curves.push(composite.make_composite_nurb()?);
for (j, (mut composite, edges)) in composites.into_iter().enumerate() {
let (joined, breaks) = composite.make_composite_nurb_with_breaks()?;
if trace.is_some() && untraced.is_none() {
match LoopSpans::of(&joined, breaks, edges) {
Ok(spans) => loop_spans.push((j, spans)),
Err(_) => untraced = Some(NotTraced::JoinedCurveUnreadable),
}
}
skin_curves.push(joined);
layers.push(j);
}
}
@@ -253,7 +290,9 @@ impl PrimitiveCreation {
skin_curves.extend_from_slice(profiles);
layer_count = 1;
layers.resize(skin_curves.len(), 0);
untraced = Some(NotTraced::OpenProfiles);
}
let mut traced: Vec<(FaceId, Vec<usize>)> = Vec::new();
let mut made = Vec::new();
for layer in 0..layer_count {
let curves: Vec<_> = skin_curves
@@ -272,6 +311,10 @@ impl PrimitiveCreation {
..Default::default()
},
)?;
// (the trace: the surface's knots across the sections, V)
let surface_knots = trace
.is_some()
.then(|| surface.knots(SurfParamType::V, None).map(|(k, _)| k));
let surface = Surface::from(surface);
// (a B-spline surface just made: never lazy)
let domain = surface.natural_uv_domain_if_built();
@@ -282,6 +325,34 @@ impl PrimitiveCreation {
p,
)?)?;
new_faces.extend_from_slice(&made);
if trace.is_some() && untraced.is_none() {
let sections: Vec<&LoopSpans> = loop_spans
.iter()
.filter(|(l, _)| *l == layer)
.map(|(_, s)| s)
.collect();
let found = match (surface_knots, face_v_intervals(&*a, brep, &made)) {
(Some(Ok(knots)), Some(intervals)) => {
let edge_curves = trace
.as_deref()
.map_or(&[][..], |t| t.edge_curves.as_slice());
layer_sources(edge_curves, &sections, &knots, &intervals)
}
(Some(Err(_)) | None, _) => Err(NotTraced::SurfaceKnotsUnreadable),
(_, None) => Err(NotTraced::FaceDomainUnreadable),
};
match found {
Ok(mut layer_faces) => traced.append(&mut layer_faces),
Err(why) => untraced = Some(why),
}
}
}
if let Some(t) = trace {
let why = untraced.or_else(|| t.open_profiles.then_some(NotTraced::OpenProfiles));
t.sources = Some(match why {
Some(why) => SkinSources::NotTraced { why },
None => SkinSources::Traced(traced),
});
}
Ok(())
}
@@ -734,6 +805,24 @@ fn sweep_ends_coincide(
Ok(true)
}
/// Each of `faces`' V interval: its surface's natural domain across the sections
/// (`None` when one cannot be read; the trace then reports it, the skin is unchanged).
fn face_v_intervals(
a: &impl PrimitiveAccess,
brep: BrepId,
faces: &[FaceId],
) -> Option<Vec<(FaceId, f64, f64)>> {
let k = a.kernel(brep).ok()?;
let b = a.brep(brep).ok()?;
let mut out = Vec::new();
for &f in faces {
let surface = crate::topology::face::p2::surface(b, f).ok()??;
let domain = surface.natural_uv_domain_with_nurb(k).ok()?;
out.push((f, domain.min().y, domain.max().y));
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -0,0 +1,275 @@
//! Which profile curves each side face of a skin was swept from, traced through the
//! skin's construction (not upstream: tangency-labs/solidcore#3139).
//!
//! Upstream: none. The trace follows `CreateSkinPrimitive` and `CreateSkinFromFaces`
//! (`source/SMLib/src/SmPrimitiveCreation.cpp`) without changing what they build.
//!
//! | Rust | Upstream |
//! |-------------------|----------|
//! | [`SkinSources`] | (none) |
//! | [`NotTraced`] | (none) |
//! | [`SkinTrace`] | (none) |
//!
//! How the trace runs. Each closed profile becomes a planar face: the edge made from
//! each profile curve is recorded with the curve's index. Each loop of a profile face
//! becomes one composite curve whose segments are the loop's edges, joined into one
//! B-spline; the join records the knot at which each segment ends, so segment `i` spans
//! `[breaks[i], breaks[i + 1]]` of the joined curve. The skin interpolates the sections'
//! control points, so where every section's joined curve has the same degree and the
//! same knots (bit for bit), its compatibility step changes nothing and the skinned
//! surface's knots across the sections (V) are the sections' knots. The surface is then
//! split at those knots into faces, and a face holds a curve when the face's V interval
//! and the curve's segment span overlap. Every comparison is between knot values the
//! construction itself carried, compared exactly: no tolerance and no geometry.
//!
//! Where a condition for that does not hold, the trace reports [`NotTraced`] and why,
//! never an estimate; the skin is built the same either way.
//!
//! Deviations: not upstream (the whole module).
use crate::error::Result;
use crate::nurbs::b_spline_curve::p1::BSplineCurve;
use crate::topology::{EdgeId, FaceId};
/// Why a skin's side faces were not traced to their profile curves.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NotTraced {
/// The profiles are open curves, lofted as one composite each; the composite
/// builder does not report which curve each of its segments is.
OpenProfiles,
/// The sections' joined curves differ in degree or knots, so making them compatible
/// can move a curve's ends across the skin (knots within a thousandth of the range
/// are merged).
SectionsNotKnotIdentical,
/// The skinned surface's knots across the sections are not the sections' knots.
SkinMovedKnots,
/// A side face's bounds across the sections are not knots of its surface.
FaceOffKnots,
/// A profile edge has no recorded curve, or a loop has not one edge per segment.
EdgeUntraced,
/// A section loop's joined curve's degree or knots could not be read.
JoinedCurveUnreadable,
/// The skinned surface's knots across the sections could not be read.
SurfaceKnotsUnreadable,
/// A side face's surface or its domain across the sections could not be read.
FaceDomainUnreadable,
/// The construction stopped before it made the side faces, so there was nothing to
/// trace.
SideFacesNotReached,
}
/// Which profile curves each side face of a skin was swept from.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SkinSources {
/// Each side face, in the order of the skin's side faces, with the indices (into
/// the skin's curves, every profile's) of the curves it was swept from, ascending:
/// profile by profile, and within a profile in the caller's order. The faces split
/// at the skin's knots of continuity G1 or less, judged on the knots: each join of two
/// curves is one, tangent or not, so a face holds one curve of each section, and a
/// curve with a kink of its own appears on several faces.
Traced(Vec<(FaceId, Vec<usize>)>),
/// Not traced, and why.
NotTraced {
/// Why.
why: NotTraced,
},
}
/// What the skin's construction records for [`SkinSources`] when asked.
#[derive(Debug, Default)]
pub struct SkinTrace {
/// The edge made from each profile curve, with the curve's index.
pub(crate) edge_curves: Vec<(EdgeId, usize)>,
/// The profiles are lofted as open composites.
pub(crate) open_profiles: bool,
/// The outcome, once the side faces are made.
pub(crate) sources: Option<SkinSources>,
}
impl SkinTrace {
/// The outcome: [`SkinSources::NotTraced`] with [`NotTraced::SideFacesNotReached`]
/// when the side faces were never reached.
#[must_use]
pub fn into_sources(self) -> SkinSources {
self.sources.unwrap_or(SkinSources::NotTraced {
why: NotTraced::SideFacesNotReached,
})
}
}
/// One loop of one section: its joined curve's knots and its segments.
#[derive(Debug)]
pub(crate) struct LoopSpans {
degree: usize,
knots: Vec<f64>,
multiplicities: Vec<usize>,
breaks: Vec<f64>,
edges: Vec<EdgeId>,
}
impl LoopSpans {
/// The loop of `joined`, with the join's `breaks` and the edge of each segment.
pub(crate) fn of(joined: &BSplineCurve, breaks: Vec<f64>, edges: Vec<EdgeId>) -> Result<Self> {
let (knots, multiplicities) = joined.knots(None)?;
Ok(Self {
degree: joined.degree()?,
knots,
multiplicities,
breaks,
edges,
})
}
}
/// Bit-for-bit equality of two knot lists.
fn same_values(a: &[f64], b: &[f64]) -> bool {
a.len() == b.len() && a.iter().zip(b).all(|(x, y)| x.to_bits() == y.to_bits())
}
/// Whether `v` is one of `knots`, bit for bit.
fn is_knot(knots: &[f64], v: f64) -> bool {
knots.iter().any(|k| k.to_bits() == v.to_bits())
}
/// The sources of one layer's faces: `sections` the layer's loop in each section,
/// `surface_knots` the skinned surface's distinct knots across the sections, `faces`
/// each face made from it with its V interval.
pub(crate) fn layer_sources(
edge_curves: &[(EdgeId, usize)],
sections: &[&LoopSpans],
surface_knots: &[f64],
faces: &[(FaceId, f64, f64)],
) -> std::result::Result<Vec<(FaceId, Vec<usize>)>, NotTraced> {
let first = *sections.first().ok_or(NotTraced::EdgeUntraced)?;
for s in sections.iter().copied() {
if s.degree != first.degree
|| !same_values(&s.knots, &first.knots)
|| s.multiplicities != first.multiplicities
{
return Err(NotTraced::SectionsNotKnotIdentical);
}
}
if !same_values(surface_knots, &first.knots) {
return Err(NotTraced::SkinMovedKnots);
}
// each section's segments: (span start, span end, curve)
let mut spans: Vec<(f64, f64, usize)> = Vec::new();
for s in sections.iter().copied() {
if s.breaks.len() != s.edges.len() + 1 {
return Err(NotTraced::EdgeUntraced);
}
for (i, e) in s.edges.iter().enumerate() {
let curve = edge_curves
.iter()
.find(|(edge, _)| edge == e)
.map(|&(_, c)| c)
.ok_or(NotTraced::EdgeUntraced)?;
let (Some(&a), Some(&b)) = (s.breaks.get(i), s.breaks.get(i + 1)) else {
return Err(NotTraced::EdgeUntraced);
};
spans.push((a, b, curve));
}
}
let mut out = Vec::new();
for &(face, v0, v1) in faces {
if !is_knot(surface_knots, v0) || !is_knot(surface_knots, v1) {
return Err(NotTraced::FaceOffKnots);
}
let mut curves: Vec<usize> = spans
.iter()
.filter(|&&(a, b, _)| v0.max(a) < v1.min(b))
.map(|&(_, _, c)| c)
.collect();
curves.sort_unstable();
curves.dedup();
out.push((face, curves));
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn spans(knots: &[f64], breaks: &[f64], edges: &[u32]) -> LoopSpans {
LoopSpans {
degree: 1,
knots: knots.to_vec(),
multiplicities: vec![1; knots.len()],
breaks: breaks.to_vec(),
edges: edges.iter().map(|&e| EdgeId::from_index(e)).collect(),
}
}
#[test]
fn faces_take_the_curves_whose_spans_they_cover() {
let knots = [0.0, 1.0, 2.0, 3.0];
let edge_curves = vec![
(EdgeId::from_index(7), 2),
(EdgeId::from_index(8), 0),
(EdgeId::from_index(9), 1),
];
let s = spans(&knots, &knots, &[7, 8, 9]);
let f = |i: u32| FaceId::from_index(i);
// one face over the first two segments, one over the third
let got = layer_sources(
&edge_curves,
&[&s],
&knots,
&[(f(0), 0.0, 2.0), (f(1), 2.0, 3.0)],
)
.unwrap();
assert_eq!(got, vec![(f(0), vec![0, 2]), (f(1), vec![1])]);
}
#[test]
fn different_section_knots_are_not_traced() {
let a = spans(&[0.0, 1.0, 2.0], &[0.0, 1.0, 2.0], &[1, 2]);
let b = spans(&[0.0, 1.5, 2.0], &[0.0, 1.5, 2.0], &[3, 4]);
let got = layer_sources(&[], &[&a, &b], &[0.0, 1.0, 2.0], &[]);
assert_eq!(got, Err(NotTraced::SectionsNotKnotIdentical));
}
#[test]
fn a_face_bound_off_the_knots_is_not_traced() {
let knots = [0.0, 1.0, 2.0];
let s = spans(&knots, &knots, &[1, 2]);
let edge_curves = vec![(EdgeId::from_index(1), 0), (EdgeId::from_index(2), 1)];
let got = layer_sources(
&edge_curves,
&[&s],
&knots,
&[(FaceId::from_index(0), 0.0, 1.5)],
);
assert_eq!(got, Err(NotTraced::FaceOffKnots));
}
#[test]
fn surface_knots_other_than_the_sections_are_not_traced() {
let knots = [0.0, 1.0, 2.0];
let s = spans(&knots, &knots, &[1, 2]);
let edge_curves = vec![(EdgeId::from_index(1), 0), (EdgeId::from_index(2), 1)];
// the surface's V knots with a knot the sections do not have
let got = layer_sources(&edge_curves, &[&s], &[0.0, 0.5, 1.0, 2.0], &[]);
assert_eq!(got, Err(NotTraced::SkinMovedKnots));
}
#[test]
fn an_edge_with_no_recorded_curve_is_not_traced() {
let knots = [0.0, 1.0, 2.0];
let s = spans(&knots, &knots, &[1, 2]);
// edge 2 has no curve recorded
let edge_curves = vec![(EdgeId::from_index(1), 0)];
let got = layer_sources(&edge_curves, &[&s], &knots, &[]);
assert_eq!(got, Err(NotTraced::EdgeUntraced));
// and a loop with a break too few for its edges
let short = spans(&knots, &[0.0, 2.0], &[1, 2]);
let got = layer_sources(
&[(EdgeId::from_index(1), 0), (EdgeId::from_index(2), 1)],
&[&short],
&knots,
&[],
);
assert_eq!(got, Err(NotTraced::EdgeUntraced));
}
}
+85 -13
View File
@@ -10,6 +10,7 @@
//! | `validate_skin_primitive_inputs` (private) | `SmApiValidateSkinPrimitiveInputs` (static) |
//! | `copy_skin_primitive_curves` (private) | `SmApiCopySkinPrimitiveCurves` (static) |
//! | [`create_skin_primitive`], [`SkinPrimitive`] | `SmApiCreateSkinPrimitive` |
//! | [`create_skin_primitive_with_sources`], [`SkinSources`], [`NotTraced`] | (not upstream: #3139) |
//! | [`create_skin_from_faces`], [`SkinFromFaces`] | `SmApiCreateSkinFromFaces` |
//! | [`create_extrude`] | `SmApiCreateExtrude` |
//! | [`create_sweep`] | `SmApiCreateSweep` |
@@ -32,6 +33,10 @@
//! - **Statuses.** `SM_ERR_INVALID_INPUT` is [`SolidError::InvalidInput`]; `SM_ERR` is
//! [`SolidError::Upstream`] with [`crate::error::SM_ERR`]. Errors of the called
//! constructors pass through unchanged.
//! - **Skin sources** (not upstream, tangency-labs/solidcore#3139).
//! [`create_skin_primitive_with_sources`] builds the skin [`create_skin_primitive`]
//! builds and also says which profile curves each side face was swept from, traced
//! through the construction ([`SkinSources`]).
//! - [`create_extrude`] makes a 2D curve's NURB 3D in place, as upstream (the curve
//! is the caller's, taken `&mut`).
//! - The B-rep entry points ([`create_skin_primitive`], [`create_skin_from_faces`],
@@ -51,6 +56,8 @@ use crate::nurbs::b_spline_surface::{BSplineSurface, SurfParamType};
use crate::nurbs::b_spline_surface_n_lib::p2::SkinnedSurfaceOptions;
use crate::nurbs::knots::MAX_DEGREE;
use crate::primitive::primitive_creation::model::PrimitiveModel;
use crate::primitive::primitive_creation::p3::skin_sources::SkinTrace;
pub use crate::primitive::primitive_creation::p3::skin_sources::{NotTraced, SkinSources};
use crate::primitive::primitive_creation::p3::RepeatedSweepOptions;
use crate::surface::surface::Surface;
use crate::topology::{Brep, FaceId};
@@ -166,6 +173,57 @@ pub fn create_skin_primitive(
curves_3d: &[Option<&Curve>],
degree: usize,
cap_ends: bool,
) -> Result<SkinPrimitive> {
skin_primitive(k, curves_in_each_profile, curves_3d, degree, cap_ends, None)
}
/// [`create_skin_primitive`], also returning which of `curves_3d` each side face was
/// swept from (not upstream: tangency-labs/solidcore#3139). The skin is the one
/// [`create_skin_primitive`] makes, bit for bit.
///
/// The sources are traced through the construction, never matched by geometry
/// ([`crate::primitive::primitive_creation::p3::skin_sources`]): each curve's edge in
/// its profile's planar face, that edge's segment of the loop's joined B-spline, the
/// segment's span of knots, and the knots each side face spans across the sections.
/// [`SkinSources::Traced`] lists each side face, in [`SkinPrimitive::side_faces`]
/// order, with the indices into `curves_3d` of its curves, ascending (profile by
/// profile, and within a profile in the caller's order). The faces split at the
/// skin's knots of continuity G1 or less, judged on the knots: each join of two curves
/// is one, tangent or not (a rounded square's eight curves make eight walls), and a
/// curve with a kink of its own spans several. [`SkinSources::NotTraced`] says why there is no answer: open
/// profiles, or sections whose joined curves differ in degree or knots (the skin's
/// compatibility step can then move a curve's ends; see [`NotTraced`]).
///
/// # Errors
///
/// Those of [`create_skin_primitive`]; the trace adds none.
pub fn create_skin_primitive_with_sources(
k: &mut Kernel,
curves_in_each_profile: &[usize],
curves_3d: &[Option<&Curve>],
degree: usize,
cap_ends: bool,
) -> Result<(SkinPrimitive, SkinSources)> {
let mut trace = SkinTrace::default();
let skin = skin_primitive(
k,
curves_in_each_profile,
curves_3d,
degree,
cap_ends,
Some(&mut trace),
)?;
Ok((skin, trace.into_sources()))
}
/// `SmApiCreateSkinPrimitive`, tracing the side faces' curves into `trace` when given.
fn skin_primitive(
k: &mut Kernel,
curves_in_each_profile: &[usize],
curves_3d: &[Option<&Curve>],
degree: usize,
cap_ends: bool,
trace: Option<&mut SkinTrace>,
) -> Result<SkinPrimitive> {
validate_skin_primitive_inputs(curves_in_each_profile, curves_3d, degree)?;
@@ -177,19 +235,33 @@ pub fn create_skin_primitive(
PrimitiveModel::build_new(k, |pc, model| {
let (mut start, mut side, mut end) = (Vec::new(), Vec::new(), Vec::new());
let mut transferred = Vec::new();
let status = pc.create_skin_primitive(
model,
PrimitiveModel::MAIN,
curves_in_each_profile,
&mut curves,
0.0,
degree,
cap_ends,
&mut start,
&mut side,
&mut end,
&mut transferred,
);
let status = match trace {
None => pc.create_skin_primitive(
model,
PrimitiveModel::MAIN,
curves_in_each_profile,
&mut curves,
0.0,
degree,
cap_ends,
&mut start,
&mut side,
&mut end,
&mut transferred,
),
Some(t) => pc.create_skin_primitive_traced(
model,
PrimitiveModel::MAIN,
curves_in_each_profile,
&mut curves,
0.0,
degree,
cap_ends,
[&mut start, &mut side, &mut end],
&mut transferred,
t,
),
};
match status {
Err(e) if e.is_unported_or_cancelled() => return Err(e),
// Ownership is indeterminate for copies without a report entry.
+43 -2
View File
@@ -5,7 +5,7 @@
//! | Rust | Upstream |
//! |----------------------------------------|-------------------------------------------|
//! | [`create_faces_with_curves`] | `SmBrep::CreateFacesWithCurves` |
//! | [`create_planar_faces_with_3d_curves`] | `SmBrep::CreatePlanarFacesWith3DCurves` |
//! | [`create_planar_faces_with_3d_curves`], [`create_planar_faces_with_3d_curves_and_edges`] | `SmBrep::CreatePlanarFacesWith3DCurves` |
//! | [`create_planar_face_with_3d_curves`] | `SmBrep::CreatePlanarFaceWith3DCurves` |
//! | [`create_face_in_region_from_surface`] | `SmBrep::CreateFaceInRegionFromSurface` |
//! | [`create_face_from_surface`] | `SmBrep::CreateFaceFromSurface` |
@@ -131,6 +131,9 @@ pub struct NewFaces {
pub shell: Option<ShellId>,
/// `rResultingFaces` / `rNewFaces`.
pub faces: Vec<FaceId>,
/// (not upstream; [`create_faces_with_curves`] only) The edge made from each curve,
/// in the order of the curves given.
pub curve_edges: Vec<Option<EdgeId>>,
}
fn invalid(reason: &'static str) -> SolidError {
@@ -467,6 +470,31 @@ pub fn create_planar_faces_with_3d_curves(
tol_3d: f64,
fenv: &mut FaceBuildEnv<'_, '_>,
) -> Result<Vec<FaceId>> {
create_planar_faces_with_3d_curves_and_edges(k, b, region, curves, tol_3d, fenv)
.map(|(faces, _)| faces)
}
/// The faces [`create_planar_faces_with_3d_curves_and_edges`] makes, and the edge made
/// from each curve with that curve's index.
pub type PlanarFacesAndEdges = (Vec<FaceId>, Vec<(EdgeId, usize)>);
/// [`create_planar_faces_with_3d_curves`], also returning the edge made from each
/// curve with that curve's index in `curves` (not upstream: the skin's face sources,
/// tangency-labs/solidcore#3139). The faces are those
/// [`create_planar_faces_with_3d_curves`] makes.
///
/// # Errors
///
/// Those of [`create_planar_faces_with_3d_curves`]; the edges add none (a curve that
/// made no edge is left out).
pub fn create_planar_faces_with_3d_curves_and_edges(
k: &mut Kernel,
b: &mut Brep,
region: Option<RegionId>,
curves: &[Curve],
tol_3d: f64,
fenv: &mut FaceBuildEnv<'_, '_>,
) -> Result<PlanarFacesAndEdges> {
let _ = tol_3d; // SM_REF1(d3DToleranceArg)
// Check for region default.
@@ -492,7 +520,19 @@ pub fn create_planar_faces_with_3d_curves(
Some(plane.plane),
fenv,
)?;
Ok(made.faces)
// one entry per ordered curve, in order (create_faces_with_curves pushes one per curve);
// were a count ever to differ, no edge is recorded, so the trace says `EdgeUntraced`
// rather than give a wall another curve's index
let edges = if made.curve_edges.len() == plane.ordered_curves.len() {
made.curve_edges
.iter()
.zip(&plane.ordered_curves)
.filter_map(|(&e, &ci)| e.map(|e| (e, ci)))
.collect()
} else {
Vec::new()
};
Ok((made.faces, edges))
}
// ---- CreateFacesWithCurves ------------------------------------------------------------
@@ -665,6 +705,7 @@ pub fn create_faces_with_curves(
&mut made,
Some(&mut *fenv.env),
)?;
out.curve_edges.push(made.edge);
// A new face must be created when the loop exhausts:
if last_edge && made.face.is_none() {
+4
View File
@@ -2840,6 +2840,9 @@ pub struct FaceCurves {
/// consecutive edge uses of a loop end at one vertex at two parameters (a pole), so
/// they stand for no edge of the face.
pub filled_gaps: Vec<usize>,
/// (not upstream) The edge of each curve of `curves_3d`, in the same order (filled
/// only when 3D curves are asked for).
pub edges: Vec<EdgeId>,
}
/// `CreateCurvesFromFace`: copies of the face's loop curves (3D and/or UV) of the face
@@ -2965,6 +2968,7 @@ pub fn create_curves_from_face(
copy.trim(k, &mut ivl, TrimMode::Notify)?;
if let Some(list) = out.curves_3d.as_mut() {
list.push(copy);
out.edges.push(e);
}
}
}
+26 -8
View File
@@ -7,7 +7,7 @@
//! | Rust | Upstream |
//! |----------------------------------------------|-------------------------------------------|
//! | [`compute_loop_orientation`], [`compute_loop_orientation_3d`] | `SmFace::ComputeLoopOrientation` |
//! | [`create_composites`] | `SmFace::CreateComposites` |
//! | [`create_composites`], [`create_composites_with_edges`] | `SmFace::CreateComposites` |
//! | [`create_uv_trim_curves`], [`create_uv_trim_curves_notified`], [`UvTrimCurveGaps`] | `SmFace::CreateUVTrimCurves` |
//! | [`face_intersect`], [`FaceIntersectOptions`], [`FaceIntersection`], [`IntersectObject`] | `SmFace::FaceIntersect` |
//! | [`notify`] | `SmFace::Notify` |
@@ -545,18 +545,37 @@ pub fn notify(
/// edge loops); [`SolidError::InvalidInput`] when the loop counts exceed the curves or
/// orientations returned (upstream reads past them).
pub fn create_composites(k: &Kernel, b: &mut Brep, f: FaceId) -> Result<Vec<CompositeCurve>> {
Ok(create_composites_with_edges(k, b, f)?
.into_iter()
.map(|(composite, _)| composite)
.collect())
}
/// [`create_composites`], each composite with the edge of each of its segments, in
/// segment order (not upstream: the skin's face sources, tangency-labs/solidcore#3139).
///
/// # Errors
///
/// Those of [`create_composites`].
pub fn create_composites_with_edges(
k: &Kernel,
b: &mut Brep,
f: FaceId,
) -> Result<Vec<(CompositeCurve, Vec<EdgeId>)>> {
// init output
let mut composites = Vec::new();
// Create List of Curves from Face Loops
let fc = face2::create_curves_from_face(k, b, f, OrientType::Same, true, false)?;
let mut curves = fc.curves_3d.unwrap_or_default().into_iter();
let mut edges = fc.edges.into_iter();
// for every loop
let mut count = 0usize;
for &num_crv in &fc.curve_loops {
let mut senses = Vec::new();
let mut loop_curves = Vec::new();
let mut loop_edges = Vec::new();
// for every edge in the loop
for _ in 0..num_crv {
@@ -573,16 +592,15 @@ pub fn create_composites(k: &Kernel, b: &mut Brep, f: FaceId) -> Result<Vec<Comp
// build Curve and Senses array
loop_curves.push(curve);
senses.push(orient != OrientType::Opposite);
// (the edges add no failure: a caller checks there is one per segment)
loop_edges.extend(edges.next());
}
// build CompositeCurve
composites.push(CompositeCurve::with_curves(
3,
loop_curves,
true,
Some(&senses),
None,
)?);
composites.push((
CompositeCurve::with_curves(3, loop_curves, true, Some(&senses), None)?,
loop_edges,
));
}
// all done
+284
View File
@@ -0,0 +1,284 @@
//! tangency-labs/solidcore#3139: a skin says which profile curves each side face was
//! swept from, traced through its construction (`create_skin_primitive_with_sources`).
//!
//! Each wall is checked by closed form: where a wall holds one curve of each of two
//! sections, its surface at the start of the loft (u min) and across the middle of its
//! profile span passes through that curve's midpoint in the first section, and at the
//! end of the loft (u max) through the matching curve's midpoint in the second.
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
use solidcore::curve::curve::Curve;
use solidcore::math::Point3;
use solidcore::nurbs::b_spline_curve::BSplineCurve;
use solidcore::primitive::api_curves::create_arc;
use solidcore::sweep::api_surfaces::{
create_skin_primitive, create_skin_primitive_with_sources, NotTraced, SkinPrimitive,
SkinSources,
};
use solidcore::topology::face::p2 as face2;
use solidcore::topology::FaceId;
use solidcore::Kernel;
fn line(p: [f64; 3], q: [f64; 3]) -> Curve {
let p = Point3::new(p[0], p[1], p[2]);
let q = Point3::new(q[0], q[1], q[2]);
Curve::from(BSplineCurve::create_line_segment(3, p, q).unwrap())
}
/// The square's sides at height `z`, scaled by `s` about the axis, in the order given
/// by `order`; `reversed` sides run backwards.
fn square(z: f64, s: f64, order: [usize; 4], reversed: &[usize]) -> Vec<Curve> {
let c = [[-1.0, -1.0], [1.0, -1.0], [1.0, 1.0], [-1.0, 1.0]];
order
.iter()
.map(|&i| {
let (a, b) = (c[i], c[(i + 1) % 4]);
let (a, b) = if reversed.contains(&i) {
(b, a)
} else {
(a, b)
};
line([s * a[0], s * a[1], z], [s * b[0], s * b[1], z])
})
.collect()
}
fn midpoint(k: &Kernel, c: &Curve) -> Point3 {
let ivl = c.natural_interval();
c.evaluate_point(k, 0.5 * (ivl.min() + ivl.max())).unwrap()
}
fn skin(
k: &mut Kernel,
counts: &[usize],
curves: &[Curve],
cap: bool,
) -> (SkinPrimitive, SkinSources) {
let refs: Vec<Option<&Curve>> = curves.iter().map(Some).collect();
// every body: the traced skin is the plain one, bit for bit (each on a fresh kernel)
let plain = create_skin_primitive(&mut Kernel::default(), counts, &refs, 1, cap).unwrap();
let (again, _) =
create_skin_primitive_with_sources(&mut Kernel::default(), counts, &refs, 1, cap).unwrap();
same_skin(&plain, &again);
create_skin_primitive_with_sources(k, counts, &refs, 1, cap).unwrap()
}
/// The two skins have the same faces, in the same roles, on the same surfaces bit for bit
/// (Debug prints each f64 exactly).
fn same_skin(plain: &SkinPrimitive, traced: &SkinPrimitive) {
assert_eq!(plain.start_faces, traced.start_faces);
assert_eq!(plain.side_faces, traced.side_faces);
assert_eq!(plain.end_faces, traced.end_faces);
let faces = solidcore::topology::brep::p1::faces(&plain.brep, None).unwrap();
assert_eq!(
faces,
solidcore::topology::brep::p1::faces(&traced.brep, None).unwrap()
);
for f in faces {
let a = format!("{:?}", face2::surface(&plain.brep, f).unwrap());
let b = format!("{:?}", face2::surface(&traced.brep, f).unwrap());
assert_eq!(a, b, "{f:?}");
}
}
fn traced(s: SkinSources) -> Vec<(FaceId, Vec<usize>)> {
match s {
SkinSources::Traced(walls) => walls,
SkinSources::NotTraced { why } => panic!("not traced: {why:?}"),
}
}
/// A wall holding curve `a` of the first section and `b` of the second passes through
/// their midpoints at its two ends, across the middle of its span.
fn wall_runs_from(k: &Kernel, skin: &SkinPrimitive, f: FaceId, a: &Curve, b: &Curve) {
let surface = face2::surface(&skin.brep, f).unwrap().unwrap();
let d = surface.natural_uv_domain_with_nurb(k).unwrap();
for (u, c) in [(0.0, a), (1.0, b)] {
let p = surface.evaluate_point(k, d.evaluate(u, 0.5)).unwrap();
let m = midpoint(k, c);
assert!(
p.distance(m) <= 1e-9 * (1.0 + m.length()),
"{f:?} at u {u}: {p:?} vs {m:?}"
);
}
}
#[test]
fn a_shuffled_square_s_walls_are_its_sides() {
let mut k = Kernel::default();
// sides 2, 0, 3, 1, side 3 reversed; the second section the same, scaled (a frustum)
let mut curves = square(0.0, 1.0, [2, 0, 3, 1], &[3]);
curves.extend(square(5.0, 0.5, [2, 0, 3, 1], &[3]));
let (made, sources) = skin(&mut k, &[4, 4], &curves, true);
let walls = traced(sources);
assert_eq!(walls.len(), 4, "{walls:?}");
assert_eq!(
walls.iter().map(|(f, _)| *f).collect::<Vec<_>>(),
made.side_faces
);
let mut seen = Vec::new();
for (f, curves_of) in &walls {
// one curve of each section, the same side of the square
let [a, b] = curves_of[..] else {
panic!("{f:?}: {curves_of:?}")
};
assert_eq!(b, a + 4, "{f:?}: {curves_of:?}");
wall_runs_from(&k, &made, *f, &curves[a], &curves[b]);
seen.push(a);
}
seen.sort_unstable();
assert_eq!(seen, [0, 1, 2, 3]);
}
#[test]
fn the_traced_skin_is_the_skin() {
let mut curves = square(0.0, 1.0, [2, 0, 3, 1], &[3]);
curves.extend(square(5.0, 0.5, [2, 0, 3, 1], &[3]));
let refs: Vec<Option<&Curve>> = curves.iter().map(Some).collect();
let plain = create_skin_primitive(&mut Kernel::default(), &[4, 4], &refs, 1, true).unwrap();
let (traced, _) =
create_skin_primitive_with_sources(&mut Kernel::default(), &[4, 4], &refs, 1, true)
.unwrap();
same_skin(&plain, &traced);
}
#[test]
fn sections_with_different_knots_are_not_traced() {
let mut k = Kernel::default();
// the second section's top side is two lines: five curves, knots 0..5 against 0..4
let mut curves = square(0.0, 1.0, [0, 1, 2, 3], &[]);
curves.extend([
line([-1.0, -1.0, 5.0], [1.0, -1.0, 5.0]),
line([1.0, -1.0, 5.0], [1.0, 1.0, 5.0]),
line([1.0, 1.0, 5.0], [0.0, 1.0, 5.0]),
line([0.0, 1.0, 5.0], [-1.0, 1.0, 5.0]),
line([-1.0, 1.0, 5.0], [-1.0, -1.0, 5.0]),
]);
let (made, sources) = skin(&mut k, &[4, 5], &curves, true);
assert!(!made.side_faces.is_empty());
assert_eq!(
sources,
SkinSources::NotTraced {
why: NotTraced::SectionsNotKnotIdentical
}
);
}
#[test]
fn a_kinked_curve_spans_a_wall_per_kink() {
let mut k = Kernel::default();
// each section's square is one closed degree-1 B-spline: one curve, four corners
let closed = |z: f64| {
let pts = [
[-1.0, -1.0],
[1.0, -1.0],
[1.0, 1.0],
[-1.0, 1.0],
[-1.0, -1.0],
]
.map(|[x, y]| Point3::new(x, y, z));
let mut sides: Vec<BSplineCurve> = (0..4)
.map(|i| BSplineCurve::create_line_segment(3, pts[i], pts[i + 1]).unwrap())
.collect();
Curve::from(BSplineCurve::create_by_joining(&mut sides, None).unwrap())
};
let curves = [closed(0.0), closed(5.0)];
let (made, sources) = skin(&mut k, &[1, 1], &curves, true);
let walls = traced(sources);
assert_eq!(walls.len(), 4, "{walls:?}");
assert_eq!(made.side_faces.len(), 4);
for (_, curves_of) in &walls {
assert_eq!(curves_of, &[0, 1]);
}
}
#[test]
fn a_rounded_square_s_tangent_joins_still_split_walls() {
// The faces split at knots of continuity G1 or less judged on the knots, so each
// join (all tangent here) is a split: eight curves, eight walls, each one curve's.
let mut k = Kernel::default();
// sides along x = +-1, y = +-1 between the corner arcs of radius 0.5
let section = |z: f64| -> Vec<Curve> {
let r = 0.5;
let e = 1.0 - r;
let arc = |cx: f64, cy: f64, a0: f64| {
Curve::from(create_arc(Point3::new(cx, cy, z), r, a0, a0 + 90.0).unwrap())
};
vec![
line([-e, -1.0, z], [e, -1.0, z]),
arc(e, -e, 270.0),
line([1.0, -e, z], [1.0, e, z]),
arc(e, e, 0.0),
line([e, 1.0, z], [-e, 1.0, z]),
arc(-e, e, 90.0),
line([-1.0, e, z], [-1.0, -e, z]),
arc(-e, -e, 180.0),
]
};
let mut curves = section(0.0);
curves.extend(section(5.0));
let (made, sources) = skin(&mut k, &[8, 8], &curves, true);
let walls = traced(sources);
assert_eq!(walls.len(), 8, "{walls:?}");
assert_eq!(made.side_faces.len(), 8);
let mut seen = Vec::new();
for (f, curves_of) in &walls {
let [a, b] = curves_of[..] else {
panic!("{f:?}: {curves_of:?}")
};
assert_eq!(b, a + 8, "{f:?}: {curves_of:?}");
wall_runs_from(&k, &made, *f, &curves[a], &curves[b]);
seen.push(a);
}
seen.sort_unstable();
assert_eq!(seen, (0..8).collect::<Vec<_>>(), "{walls:?}");
}
#[test]
fn a_holed_square_s_inner_walls_are_the_hole_s_sides() {
let mut k = Kernel::default();
let section = |z: f64| {
let mut c = square(z, 2.0, [0, 1, 2, 3], &[]);
c.extend(square(z, 0.5, [1, 3, 0, 2], &[]));
c
};
let mut curves = section(0.0);
curves.extend(section(5.0));
let (made, sources) = skin(&mut k, &[8, 8], &curves, true);
let walls = traced(sources);
assert_eq!(walls.len(), 8, "{walls:?}");
let mut seen = Vec::new();
for (f, curves_of) in &walls {
let [a, b] = curves_of[..] else {
panic!("{f:?}: {curves_of:?}")
};
assert_eq!(b, a + 8, "{f:?}: {curves_of:?}");
wall_runs_from(&k, &made, *f, &curves[a], &curves[b]);
seen.push(a);
}
seen.sort_unstable();
assert_eq!(seen, (0..8).collect::<Vec<_>>());
}
#[test]
fn open_profiles_are_not_traced() {
let mut k = Kernel::default();
let curves = [
line([0.0, 0.0, 0.0], [1.0, 0.0, 0.0]),
line([1.0, 0.0, 0.0], [1.0, 1.0, 0.0]),
line([0.0, 0.0, 5.0], [1.0, 0.0, 5.0]),
line([1.0, 0.0, 5.0], [1.0, 1.0, 5.0]),
];
let (_, sources) = skin(&mut k, &[2, 2], &curves, false);
assert_eq!(
sources,
SkinSources::NotTraced {
why: NotTraced::OpenProfiles
}
);
}
+5 -5
View File
@@ -409,8 +409,8 @@ crates/solidcore/src/curve/composite_curve/p1.rs trim_last_break Vec::insert 1 p
crates/solidcore/src/curve/composite_curve/p2.rs CompositeCurve::create_offsets_of_many_curves with_capacity 1 proven Vec::with_capacity(curves.len()) n = curves.len(), an existing slice of references; bool is smaller
crates/solidcore/src/curve/composite_curve/p2.rs CompositeCurve::create_offsets_of_many_curves with_capacity 2 proven Vec::with_capacity(comp.segments.len()) n = comp.segments.len(), an existing Vec of segments, each holding a Curve
crates/solidcore/src/curve/composite_curve/p2.rs CompositeCurve::intersect_and_trim_offsets windows 1 proven windows(2) constant non-zero size
crates/solidcore/src/curve/composite_curve/p2.rs CompositeCurve::make_composite_nurb with_capacity 1 proven Vec::with_capacity(self.segments.len()) n = self.segments.len(), segments each holding a Curve, which holds a BSplineCurve inline
crates/solidcore/src/curve/composite_curve/p2.rs CompositeCurve::make_composite_nurb with_capacity 2 proven Vec::with_capacity(self.segments.len()) n = self.segments.len(), an existing Vec of segments; bool is smaller
crates/solidcore/src/curve/composite_curve/p2.rs CompositeCurve::composite_nurb with_capacity 1 proven Vec::with_capacity(self.segments.len()) n = self.segments.len(), segments each holding a Curve, which holds a BSplineCurve inline
crates/solidcore/src/curve/composite_curve/p2.rs CompositeCurve::composite_nurb with_capacity 2 proven Vec::with_capacity(self.segments.len()) n = self.segments.len(), an existing Vec of segments; bool is smaller
crates/solidcore/src/curve/crv_in_volume.rs CrvInVolume::compute_breaks Vec::insert 1 proven insert(jj, class_bndry) returns Err when jj > knots.len()
crates/solidcore/src/curve/crv_in_volume.rs CrvInVolume::compute_breaks Vec::insert 2 proven insert(jj.min(mults.len()), 2) index is min(.., mults.len())
crates/solidcore/src/curve/crv_on_surf.rs CrvOnSurf::knots_with_status Vec::insert 1 proven insert(jj, class_bndry) returns Err when jj > knots.len()
@@ -616,9 +616,9 @@ crates/solidcore/src/nurbs/b_spline_curve/p1.rs BSplineCurve::control_polygon wi
crates/solidcore/src/nurbs/b_spline_curve/p1.rs BSplineCurve::from_data copy_from_slice 1 open copy_from_slice(src_u)
crates/solidcore/src/nurbs/b_spline_curve/p1.rs BSplineCurve::from_data copy_from_slice 2 open copy_from_slice(src_pw)
crates/solidcore/src/nurbs/b_spline_curve/p2.rs BSplineCurve::calculate_bounding_box_onto int-div 1 proven num_points / smp_cnt num_points >= 1 is checked; smp_cnt = num_points.min(7)
crates/solidcore/src/nurbs/b_spline_curve/p2.rs BSplineCurve::create_by_joining Vec::insert 1 proven insert(1, mid) seg_pts.get(1) succeeds before insertion; inserting at 1 only grows it
crates/solidcore/src/nurbs/b_spline_curve/p2.rs BSplineCurve::create_by_joining Vec::insert 2 proven insert(1, mid1) seg_pts.get(1) succeeds before insertion; inserting at 1 only grows it
crates/solidcore/src/nurbs/b_spline_curve/p2.rs BSplineCurve::create_by_joining Vec::insert 3 proven insert(2, mid2) seg_pts.get(1) succeeds before insertion; inserting at 1 only grows it
crates/solidcore/src/nurbs/b_spline_curve/p2.rs BSplineCurve::join Vec::insert 1 proven insert(1, mid) seg_pts.get(1) succeeds before insertion; inserting at 1 only grows it
crates/solidcore/src/nurbs/b_spline_curve/p2.rs BSplineCurve::join Vec::insert 2 proven insert(1, mid1) seg_pts.get(1) succeeds before insertion; inserting at 1 only grows it
crates/solidcore/src/nurbs/b_spline_curve/p2.rs BSplineCurve::join Vec::insert 3 proven insert(2, mid2) seg_pts.get(1) succeeds before insertion; inserting at 1 only grows it
crates/solidcore/src/nurbs/b_spline_curve/p2.rs BSplineCurve::read_from_db int-div 1 proven u64::MAX / (4 * 32) constant non-zero divisor
crates/solidcore/src/nurbs/b_spline_curve/p3.rs BSplineCurve::assert_no_periodic_jump windows 1 proven windows(2) constant non-zero size
crates/solidcore/src/nurbs/b_spline_curve/p3.rs BSplineCurve::assert_valid_with with_capacity 1 open Vec::with_capacity(solutions.len())
1 # Panic-prone calls in library code that no deny lint catches (#2490). Written by
409 crates/solidcore/src/curve/composite_curve/p2.rs
410 crates/solidcore/src/curve/composite_curve/p2.rs
411 crates/solidcore/src/curve/composite_curve/p2.rs
412 crates/solidcore/src/curve/composite_curve/p2.rs
413 crates/solidcore/src/curve/composite_curve/p2.rs
414 crates/solidcore/src/curve/crv_in_volume.rs
415 crates/solidcore/src/curve/crv_in_volume.rs
416 crates/solidcore/src/curve/crv_on_surf.rs
616 crates/solidcore/src/nurbs/b_spline_curve/p1.rs
617 crates/solidcore/src/nurbs/b_spline_curve/p1.rs
618 crates/solidcore/src/nurbs/b_spline_curve/p2.rs
619 crates/solidcore/src/nurbs/b_spline_curve/p2.rs
620 crates/solidcore/src/nurbs/b_spline_curve/p2.rs
621 crates/solidcore/src/nurbs/b_spline_curve/p2.rs
622 crates/solidcore/src/nurbs/b_spline_curve/p2.rs
623 crates/solidcore/src/nurbs/b_spline_curve/p3.rs
624 crates/solidcore/src/nurbs/b_spline_curve/p3.rs