mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-11 19:22:01 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c01f857aa | |||
| 649b1d8fd9 |
+1
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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, _, _| {
|
||||
|
||||
+92
-3
@@ -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, §ions, &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::*;
|
||||
|
||||
+275
@@ -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
@@ -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
@@ -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() {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
);
|
||||
}
|
||||
Vendored
+5
-5
@@ -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())
|
||||
|
||||
|
Reference in New Issue
Block a user