kernel: a tapered extrusion closes its corners by a rule, preserving curvature by default

extrude_tapered(_between) take a DraftCorner: PreserveCurvature (each wall
continues as itself, a line's plane or an arc's cone, until it meets its
neighbour; SMLib's SM_OC_CURVE_EXTENSION), Tangent (the offsets continued
along their end tangents) or Round (an arc about the corner). A corner
the rule cannot close is refused with its point and why
(solidcraft_smlib::Error::Corner). draft_corners reports, per sharp
corner of a profile, what each rule gives: how far each offset curve runs
on or is trimmed, whether the corner cuts inside the exact offset, or why
the rule refuses it. SMLib pinned at the fork's draft-corners head.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017itGh5fpMEousnZ91cYcVt
This commit is contained in:
jyviko
2026-10-22 09:11:24 -04:00
parent 0414f5a7a1
commit 384c5866f1
19 changed files with 405 additions and 50 deletions
+3 -2
View File
@@ -4180,7 +4180,8 @@ fn hole_tools_and_prisms(
let r_end = r * 0.9;
let len = (r_top - r_end) / half.tan();
let top_plane = plane.offset(top);
tools.push((crate::lineage::part_tool(kernel::extrude_tapered(&top_plane, &disc(r_top), len, -1.0, -half)?, "csink"), None));
let csink = kernel::extrude_tapered(&top_plane, &disc(r_top), len, -1.0, -half, kernel::DraftCorner::default())?;
tools.push((crate::lineage::part_tool(csink, "csink"), None));
tools.extend(bore(tip_angle.as_ref())?);
}
}
@@ -4559,7 +4560,7 @@ fn feature_tools_with_constructor(
.map(|r| {
let roles =
crate::lineage::ProfileRoles { sketch: Some(&ss), region: Some(r), caps: caps.clone(), thin: thin_roles.as_ref() };
let body = kernel::extrude_tapered_between(&start_plane, r, 0.0, len, 0.0, sign, taper)?;
let body = kernel::extrude_tapered_between(&start_plane, r, 0.0, len, 0.0, sign, taper, kernel::DraftCorner::default())?;
Ok(roles.tool(body, None))
})
.collect::<Result<_>>()?;
+6 -3
View File
@@ -1946,8 +1946,11 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
// The post: a (tapered) cylinder standing on the face.
let base = Plane::new(position.0, x, z.cross(x)).ok_or_else(|| DocError::Invalid("boss frame".into()))?;
let disc = Region2 { outer: Loop2::circle(Vec2::ZERO, r0), holes: Vec::new() };
let post =
if dr.abs() > 1e-12 { vec![kernel::extrude_tapered(&base, &disc, h, 1.0, -dr)?] } else { kernel::extrude(&base, &[disc], 0.0, h)? };
let post = if dr.abs() > 1e-12 {
vec![kernel::extrude_tapered(&base, &disc, h, 1.0, -dr, kernel::DraftCorner::default())?]
} else {
kernel::extrude(&base, &[disc], 0.0, h)?
};
let mut tools: Vec<Traced> = post.into_iter().map(|b| crate::lineage::part_tool(b, "post")).collect();
if let Some(n) = ribs {
let n = val(vals, n, Kind::Unitless)?.round();
@@ -2145,7 +2148,7 @@ pub(super) fn plastic_eval(doc: &Document, vals: &BTreeMap<String, Value>, f: &F
let region = Region2 { outer: shape(0.0), holes };
let base = Plane::new(position.0, x, y).ok_or_else(|| DocError::Invalid("rest frame".into()))?;
let tool = if dr.abs() > 1e-12 {
vec![kernel::extrude_tapered(&base, &region, h, 1.0, -dr)?]
vec![kernel::extrude_tapered(&base, &region, h, 1.0, -dr, kernel::DraftCorner::default())?]
} else {
kernel::extrude(&base, &[region], 0.0, h)?
};
+25 -7
View File
@@ -5,6 +5,7 @@
use solidcraft_geom::{Loop2, Plane, Region2, Seg2, Vec2, Vec3};
use crate::body::Body;
use crate::draft_corners::DraftCorner;
use crate::numbered::{self, NumberedLoop, NumberedRegion};
use crate::sweep_path::RailFit;
use crate::{KernelError, Result};
@@ -25,6 +26,11 @@ fn oriented(r: &Region2) -> Result<NumberedRegion> {
numbered_region(r).map(|n| n.oriented())
}
/// The outer loop a tapered extrusion takes: counterclockwise, collinear lines merged.
pub(crate) fn oriented_outer(r: &Region2) -> Result<Loop2> {
Ok(oriented(r)?.map_loops(|l| l.merged_lines(continues_straight)).outer.lp)
}
pub(crate) fn region_ok(r: &Region2) -> Result<()> {
if r.area().abs() < 1e-9 {
return Err(KernelError::Invalid("profile has no area".into()));
@@ -746,9 +752,10 @@ fn with_start(s: &Seg2, x: Vec2) -> Seg2 {
const MEET_SAMPLES: usize = 48;
/// Extrude one region by `length` along `dir_sign` × the plane normal with a taper angle
/// (radians; positive grows the profile).
pub fn extrude_tapered(plane: &Plane, region: &Region2, length: f64, dir_sign: f64, taper: f64) -> Result<Body> {
extrude_tapered_between(plane, region, 0.0, length, 0.0, dir_sign, taper)
/// (radians; positive grows the profile), its sharp corners closed by `corner`
/// ([`crate::draft_corners`] says what each rule gives at each corner).
pub fn extrude_tapered(plane: &Plane, region: &Region2, length: f64, dir_sign: f64, taper: f64, corner: DraftCorner) -> Result<Body> {
extrude_tapered_between(plane, region, 0.0, length, 0.0, dir_sign, taper, corner)
}
/// [`extrude_tapered`] from `from` to `to` along `dir_sign` × the plane normal, the region
@@ -757,7 +764,17 @@ pub fn extrude_tapered(plane: &Plane, region: &Region2, length: f64, dir_sign: f
/// grown by the taper. With a `foot` (a height, less than `to - from`), the sides stand
/// straight for that height above `from` and only then slope, up to the same top: a face the
/// solid stands on can then be pushed into the body it joins, as a prism's can.
pub fn extrude_tapered_between(plane: &Plane, region: &Region2, from: f64, to: f64, foot: f64, dir_sign: f64, taper: f64) -> Result<Body> {
#[allow(clippy::too_many_arguments)]
pub fn extrude_tapered_between(
plane: &Plane,
region: &Region2,
from: f64,
to: f64,
foot: f64,
dir_sign: f64,
taper: f64,
corner: DraftCorner,
) -> Result<Body> {
let (from, to, foot, taper) = (finite(from, "start")?, finite(to, "distance")?, finite(foot, "foot")?, finite(taper, "taper")?);
if to - from <= 1e-6 {
return Err(KernelError::Invalid("extrude distance is zero".into()));
@@ -769,12 +786,13 @@ pub fn extrude_tapered_between(plane: &Plane, region: &Region2, from: f64, to: f
return Err(KernelError::Invalid("taper must be between −90° and 90°".into()));
}
region_ok(region)?;
smlib_tapered(plane, region, from, to, foot, dir_sign.signum(), taper)
smlib_tapered(plane, region, from, to, foot, dir_sign.signum(), taper, corner)
}
/// [`extrude_tapered_between`] through SMLib's draft extrusion: one loop (SMLib's takes no
/// holes), its section at `from` the profile offset by the taper, after any straight foot.
fn smlib_tapered(plane: &Plane, region: &Region2, from: f64, to: f64, foot: f64, sign: f64, taper: f64) -> Result<Body> {
#[allow(clippy::too_many_arguments)]
fn smlib_tapered(plane: &Plane, region: &Region2, from: f64, to: f64, foot: f64, sign: f64, taper: f64, corner: DraftCorner) -> Result<Body> {
if !region.holes.is_empty() {
return Err(crate::smlib_sweeps::refuse("draft extrusion of a profile with holes (SMLib's takes one loop)"));
}
@@ -787,7 +805,7 @@ fn smlib_tapered(plane: &Plane, region: &Region2, from: f64, to: f64, foot: f64,
let segs = segs.ok_or_else(|| crate::smlib_sweeps::refuse("draft extrusion that starts before its profile where the taper closes a side"))?;
NumberedLoop { lp: Loop2 { segs }, from: outer.from }
};
let draft = (taper != 0.0).then_some(crate::smlib_sweeps::Draft { taper, foot });
let draft = (taper != 0.0).then_some(crate::smlib_sweeps::Draft { taper, foot, corner });
crate::smlib_sweeps::extrude(plane, &crate::smlib_sweeps::pieces([&bottom]), Some(region), from * sign, to - from, sign, draft)
}
@@ -167,7 +167,7 @@ fn boss_foot_fills() {
fn cone_rim() {
let taper = 10f64.to_radians();
let region = Region2 { outer: Loop2::circle_from(Vec2::ZERO, 10.0, 0.0), holes: vec![] };
let f = extrude_tapered(&Plane::XY, &region, 20.0, 1.0, -taper).unwrap();
let f = extrude_tapered(&Plane::XY, &region, 20.0, 1.0, -taper, crate::DraftCorner::default()).unwrap();
let base = volume(&f);
let top = 10.0 - 20.0 * taper.tan();
let rim = Vec3::new(top, 0.0, 20.0);
+1 -1
View File
@@ -328,7 +328,7 @@ fn round_faces_of_every_kind_are_divided_by_the_angle() {
let bodies = [
("sphere", crate::sphere(Vec3::ZERO, 4.0).unwrap()),
("torus", crate::torus(Vec3::ZERO, 6.0, 2.0).unwrap()),
("cone", crate::extrude_tapered(&Plane::XY, &disc, 6.0, 1.0, -0.3).unwrap()),
("cone", crate::extrude_tapered(&Plane::XY, &disc, 6.0, 1.0, -0.3, crate::DraftCorner::default()).unwrap()),
];
// The most the surface normal turns along one side of a triangle of a curved face.
let turn = |b: &Body, m: &Mesh| {
+224
View File
@@ -0,0 +1,224 @@
//! How a tapered extrusion closes its profile's sharp corners ([`DraftCorner`]), and what each
//! rule gives at each corner of a profile ([`draft_corners`]), so a dialog can offer the others
//! where the chosen one is a poor fit.
//!
//! At the far end the profile is offset by the run of its sides (`distance · tan taper`). Where
//! a corner turns away from that offset the offset curves part, and the rule closes the gap:
//! [`DraftCorner::PreserveCurvature`] continues each curve as itself (a line along its line, an
//! arc along its circle) to where the two cross, [`DraftCorner::Tangent`] continues each straight
//! along its end tangent, [`DraftCorner::Round`] closes it with an arc about the corner. Where the
//! corner turns into the offset the offset curves overlap and every rule trims them where they
//! cross.
use serde::{Deserialize, Serialize};
use solidcraft_geom::{Region2, Seg2, Vec2};
use crate::Result;
/// How a tapered extrusion closes the gap its offset opens at a sharp corner of the profile.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DraftCorner {
/// Each wall continues as itself (a line's plane, an arc's cone) until it meets its
/// neighbour: every section is the profile's curves offset and continued as themselves.
#[default]
PreserveCurvature,
/// Each offset curve continues straight along its end tangent (a flat face each side).
Tangent,
/// An arc about the corner (a cone about it).
Round,
}
impl DraftCorner {
/// The three rules, in the order a dialog offers them.
pub const ALL: [DraftCorner; 3] = [DraftCorner::PreserveCurvature, DraftCorner::Tangent, DraftCorner::Round];
}
/// Why a rule cannot close a corner.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CornerRefusal {
/// The continued curves do not meet (two parallel lines, two circles that part).
Apart,
/// A curve at the corner is neither a line nor an arc: its wall has no exact continuation.
NotExact,
/// A wall at the corner closes before the far end (its arc shrinks to its centre).
WallCloses,
}
/// What one rule gives at a corner, at the far end of the extrusion.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct CornerFill {
/// How far the offset curve before the corner and the one after it run on past their ends
/// to close it (negative: trimmed back where they overlap). For [`DraftCorner::Round`] the
/// arc's length is split between the two.
pub extension: [f64; 2],
/// Whether the corner cuts inside the exact offset of the corner itself (its far point is
/// nearer the profile's corner than the offset distance): the notch preserving curvature
/// makes where a curve bends back toward the solid.
pub inward: bool,
}
/// What one rule gives at a corner, or why it cannot close it.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct CornerOption {
pub rule: DraftCorner,
pub fill: std::result::Result<CornerFill, CornerRefusal>,
}
/// One sharp corner of a profile and what each rule gives there.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CornerReport {
/// The corner on the profile, in the sketch's coordinates.
pub at: Vec2,
/// Whether the offset opens a gap there (the rules differ) or the offset curves overlap
/// (every rule trims them where they cross).
pub gap: bool,
/// [`DraftCorner::ALL`] in order.
pub options: [CornerOption; 3],
}
/// An offset curve at the far end as its rule continues it: a line or a circle, or neither.
#[derive(Clone, Copy, Debug)]
enum Carrier {
Line { p: Vec2, d: Vec2 },
Circle { c: Vec2, r: f64, ccw: bool },
Free,
}
/// The sharp corners of `region`'s outer loop (the loop a tapered extrusion takes) and what each
/// rule gives at each, for an extrusion `distance` long whose sides run out by `taper` radians
/// (positive grows the profile, as [`crate::extrude_tapered`] takes it).
pub fn draft_corners(region: &Region2, distance: f64, taper: f64) -> Result<Vec<CornerReport>> {
crate::build::region_ok(region)?;
if !(distance.is_finite() && taper.is_finite()) {
return Err(crate::KernelError::Invalid("taper or distance is not finite".into()));
}
let outer = crate::build::oriented_outer(region)?;
let segs = &outer.segs;
let d = distance * taper.tan();
let n = segs.len();
let mut out = Vec::new();
for k in 0..n {
let (Some(a), Some(b)) = (segs.get((k + n - 1) % n), segs.get(k)) else { continue };
let (ta, tb) = (a.end_tangent(), b.start_tangent());
let turn = ta.cross(tb).atan2(ta.dot(tb));
if turn.abs() <= 1e-9 {
continue;
}
let p = b.start();
// The outer loop runs counterclockwise: growing moves each curve to its right.
let (ca, cb) = (carrier(a, d), carrier(b, d));
let (ea, eb) = (a.end() + right(ta) * d, b.start() + right(tb) * d);
// A left turn opens a gap when the profile grows, a right turn when it shrinks.
let gap = (turn > 0.0) == (d > 0.0);
let curvature = continued(ca, cb, ea, eb, p, d);
let options = if gap {
[
CornerOption { rule: DraftCorner::PreserveCurvature, fill: curvature },
CornerOption { rule: DraftCorner::Tangent, fill: tangent(ea, ta, eb, tb, p, d) },
CornerOption {
rule: DraftCorner::Round,
fill: Ok(CornerFill { extension: [0.5 * d.abs() * turn.abs(), 0.5 * d.abs() * turn.abs()], inward: false }),
},
]
} else {
// Overlapping offsets: every rule trims them where they cross.
let trim = curvature.map(|f| CornerFill { inward: false, ..f });
DraftCorner::ALL.map(|rule| CornerOption { rule, fill: if rule == DraftCorner::PreserveCurvature { curvature } else { trim } })
};
out.push(CornerReport { at: p, gap, options });
}
Ok(out)
}
fn right(t: Vec2) -> Vec2 {
let t = t.normalized().unwrap_or(t);
Vec2::new(t.y, -t.x)
}
/// `s` offset by `d` to its right, as the curve its far-end section continues along.
fn carrier(s: &Seg2, d: f64) -> Carrier {
match *s {
Seg2::Line { a, b } => match (b - a).normalized() {
Some(t) => Carrier::Line { p: a + right(t) * d, d: t },
None => Carrier::Free,
},
Seg2::Arc { center, radius, sweep, .. } => {
let ccw = sweep > 0.0;
// Right of a counterclockwise arc is away from its centre.
Carrier::Circle { c: center, r: if ccw { radius + d } else { radius - d }, ccw }
}
_ => Carrier::Free,
}
}
/// The tangent rule: the end tangents' crossing.
fn tangent(ea: Vec2, ta: Vec2, eb: Vec2, tb: Vec2, p: Vec2, d: f64) -> std::result::Result<CornerFill, CornerRefusal> {
let (ta, tb) = (ta.normalized().unwrap_or(ta), tb.normalized().unwrap_or(tb));
let den = ta.cross(tb);
if den.abs() < 1e-12 {
return Err(CornerRefusal::Apart);
}
let s = (eb - ea).cross(tb) / den;
let x = ea + ta * s;
let t = (x - eb).dot(tb);
Ok(CornerFill { extension: [s, -t], inward: x.dist(p) < d.abs() - 1e-9 })
}
/// The curvature rule: the continued curves' crossing nearest the corner.
fn continued(ca: Carrier, cb: Carrier, ea: Vec2, eb: Vec2, p: Vec2, d: f64) -> std::result::Result<CornerFill, CornerRefusal> {
for c in [ca, cb] {
match c {
Carrier::Free => return Err(CornerRefusal::NotExact),
Carrier::Circle { r, .. } if r <= 0.0 => return Err(CornerRefusal::WallCloses),
_ => {}
}
}
let mut cands: Vec<Vec2> = Vec::with_capacity(2);
match (ca, cb) {
(Carrier::Line { p: pa, d: da }, Carrier::Line { p: pb, d: db }) => {
let den = da.cross(db);
if den.abs() >= 1e-12 {
cands.push(pa + da * ((pb - pa).cross(db) / den));
}
}
(Carrier::Line { p: l, d: dl }, Carrier::Circle { c, r, .. }) | (Carrier::Circle { c, r, .. }, Carrier::Line { p: l, d: dl }) => {
let w = l - c;
let bq = dl.dot(w);
let disc = bq * bq - (w.len2() - r * r);
if disc >= 0.0 {
let root = disc.sqrt();
cands.extend([l + dl * (-bq - root), l + dl * (-bq + root)]);
}
}
(Carrier::Circle { c: c1, r: r1, .. }, Carrier::Circle { c: c2, r: r2, .. }) => {
let e = c2 - c1;
let dd = e.len();
if dd > 1e-12 {
let e = e / dd;
let along = (r1 * r1 - r2 * r2 + dd * dd) / (2.0 * dd);
let h2 = r1 * r1 - along * along;
if h2 >= 0.0 {
let h = h2.sqrt();
cands.extend([c1 + e * along + e.perp() * h, c1 + e * along - e.perp() * h]);
}
}
}
_ => {}
}
let Some(x) = cands.into_iter().min_by(|u, v| u.dist(p).total_cmp(&v.dist(p))) else { return Err(CornerRefusal::Apart) };
let extension = [run(ca, ea, x), -run(cb, eb, x)];
Ok(CornerFill { extension, inward: x.dist(p) < d.abs() - 1e-9 })
}
/// How far along a carrier, the way the profile runs, from `from` to `x`.
fn run(c: Carrier, from: Vec2, x: Vec2) -> f64 {
match c {
Carrier::Line { d, .. } => (x - from).dot(d),
Carrier::Circle { c, r, ccw } => {
let turn = (from - c).cross(x - c).atan2((from - c).dot(x - c));
r * if ccw { turn } else { -turn }
}
Carrier::Free => 0.0,
}
}
+2 -2
View File
@@ -290,8 +290,8 @@ fn collinear_profile_lines_taper_into_one_side_face() {
let whole = polygon(&[(0.0, 0.0), (20.0, 0.0), (20.0, 10.0), (0.0, 10.0)]);
let taper = 5f64.to_radians();
for (from, to, foot) in [(0.0, 5.0, 0.0), (-1.0, 5.0, 1.0)] {
let body = crate::extrude_tapered_between(&Plane::XY, &split, from, to, foot, 1.0, taper).unwrap();
let reference = crate::extrude_tapered_between(&Plane::XY, &whole, from, to, foot, 1.0, taper).unwrap();
let body = crate::extrude_tapered_between(&Plane::XY, &split, from, to, foot, 1.0, taper, crate::DraftCorner::default()).unwrap();
let reference = crate::extrude_tapered_between(&Plane::XY, &whole, from, to, foot, 1.0, taper, crate::DraftCorner::default()).unwrap();
assert_eq!(body.face_count(), reference.face_count(), "{from}..{to} foot {foot}: as many faces as one piece");
let (v, w) = (crate::measure(&body).unwrap().volume, crate::measure(&reference).unwrap().volume);
assert!((v - w).abs() < 1e-9 * w, "{v} vs {w}");
+2
View File
@@ -34,6 +34,7 @@ mod coplanar_tests;
mod corner_g2;
mod delete_face;
mod distance;
mod draft_corners;
mod emboss;
#[cfg(test)]
mod face_source_tests;
@@ -167,6 +168,7 @@ pub use continuity::{
pub use corner_g2::{CornerPatch, corner_g2_patch, seam_error};
pub use delete_face::delete_faces;
pub use distance::{BodyDistance, DistanceShape, body_distance};
pub use draft_corners::{CornerFill, CornerOption, CornerRefusal, CornerReport, DraftCorner, draft_corners};
pub use emboss::{cylinder_emboss_anchor, emboss_cylinder, emboss_cylinder_at, has_cylinder_emboss_wall, infer_cylinder_emboss};
pub use fair::{
FairCurve, FairNet, FairOptions, FairReport, MAX_FAIR_ITERATIONS, MAX_FAIR_POINTS, fair_bspline_curve, fair_bspline_surface, fair_curve,
+1 -1
View File
@@ -309,7 +309,7 @@ fn revolve_with_an_edge_on_the_axis() {
#[test]
fn tapered_extrusion() {
let r = Region2 { outer: rotated(&rect(0.0, 0.0, 20.0, 10.0).reversed(), 1), holes: vec![] };
let b = crate::extrude_tapered(&Plane::XY, &r, 10.0, 1.0, 0.1).unwrap();
let b = crate::extrude_tapered(&Plane::XY, &r, 10.0, 1.0, 0.1, crate::DraftCorner::default()).unwrap();
let want: Vec<Vec<FaceOrigin>> = points(&b)
.iter()
.map(|pts| {
+17 -5
View File
@@ -66,6 +66,8 @@ pub(crate) struct Draft {
/// A height (less than the distance) the walls first stand straight for, before they slope
/// on to the same far end ([`crate::extrude_tapered_between`]'s foot).
pub foot: f64,
/// How the sharp corners close.
pub corner: crate::DraftCorner,
}
pub(crate) use imp::{
@@ -81,7 +83,7 @@ pub(crate) fn refuse(what: &str) -> KernelError {
mod imp {
use std::collections::BTreeSet;
use solidcraft_smlib::sweeps::{Affine, Curve, Ends, SweepFrame};
use solidcraft_smlib::sweeps::{Affine, Curve, DraftCorner, Ends, SweepFrame};
use solidcraft_smlib::{Brep, CheckLevel};
use solidcraft_geom::Vec2;
@@ -95,6 +97,14 @@ mod imp {
KernelError::Failed(format!("SMLib {what}: {e}"))
}
fn corner(c: crate::DraftCorner) -> DraftCorner {
match c {
crate::DraftCorner::PreserveCurvature => DraftCorner::Curvature,
crate::DraftCorner::Tangent => DraftCorner::Tangent,
crate::DraftCorner::Round => DraftCorner::Round,
}
}
fn p(v: Vec3) -> [f64; 3] {
[v.x, v.y, v.z]
}
@@ -380,8 +390,9 @@ mod imp {
}
let knee = plane.offset(from + sign.signum() * foot);
let lean = -(distance * d.taper.tan() / (distance - foot)).atan();
let prism = Brep::extrude(&c, p(dir), foot, 0.0, true).map_err(|e| failed("extrude", e))?;
let slope = Brep::extrude(&curves(&knee, pieces)?, p(dir), distance - foot, lean, true).map_err(|e| failed("extrude", e))?;
let prism = Brep::extrude(&c, p(dir), foot, 0.0, DraftCorner::default(), true).map_err(|e| failed("extrude", e))?;
let slope =
Brep::extrude(&curves(&knee, pieces)?, p(dir), distance - foot, lean, corner(d.corner), true).map_err(|e| failed("extrude", e))?;
let brep = prism.boolean(&slope, solidcraft_smlib::BooleanOp::Union).map_err(|e| failed("extrude", e))?;
let brep = brep.ok_or_else(|| failed("extrude", "nothing is left"))?;
let body = body_of(&brep, "extrude")?;
@@ -405,7 +416,8 @@ mod imp {
}
// SMLib's draft is positive inward; the kernel's taper grows the profile.
let lean = draft.map_or(0.0, |d| -d.taper);
let brep = Brep::extrude(&c, p(dir), distance, lean, true).map_err(|e| failed("extrude", e))?;
let rule = draft.map_or(DraftCorner::default(), |d| corner(d.corner));
let brep = Brep::extrude(&c, p(dir), distance, lean, rule, true).map_err(|e| failed("extrude", e))?;
let body = body_of(&brep, "extrude")?;
// Half-way along a straight wall lies over its segment; a drafted one has moved.
let inside = if draft.is_none() { seeds(&base, pieces, |q| q + dir * (distance * 0.5)) } else { Vec::new() };
@@ -601,7 +613,7 @@ mod imp {
let h = size * 0.5;
let c = [at + (u + v) * h, at + (v - u) * h, at - (u + v) * h, at + (u - v) * h];
let square: Vec<Curve> = (0..4).map(|i| Curve::Line { start: p(c[i]), end: p(c[(i + 1) % 4]) }).collect();
Brep::extrude(&square, p(normal), size, 0.0, true).map_err(|e| failed("mitre", e))
Brep::extrude(&square, p(normal), size, 0.0, DraftCorner::default(), true).map_err(|e| failed("mitre", e))
}
fn combined(a: &Brep, b: &Brep, op: solidcraft_smlib::BooleanOp, what: &str) -> Result<Brep> {
+20 -2
View File
@@ -61,7 +61,16 @@ fn an_extrusion_names_its_caps_and_walls_and_keeps_their_sources() {
// A drafted disc: a frustum, its wall the circle's.
let disc = region(Loop2::circle(Vec2::ZERO, 5.0));
let taper = -5f64.to_radians();
let b = smlib_sweeps::extrude(&Plane::XY, &pieces_of(&disc), Some(&disc), 0.0, 4.0, 1.0, Some(Draft { taper, foot: 0.0 })).unwrap();
let b = smlib_sweeps::extrude(
&Plane::XY,
&pieces_of(&disc),
Some(&disc),
0.0,
4.0,
1.0,
Some(Draft { taper, foot: 0.0, corner: crate::DraftCorner::default() }),
)
.unwrap();
let r2 = 5.0 + 4.0 * taper.tan();
close(volume(&b), PI * 4.0 / 3.0 * (25.0 + r2 * r2 + 5.0 * r2), 1e-3, "drafted disc");
assert!(origins(&b).iter().filter(|o| o.contains("Profile")).count() >= 1);
@@ -223,7 +232,16 @@ fn a_drafted_extrusion_stands_on_a_straight_foot() {
// the prism 100 and the frustum from 10 to 10 + 2 x 10 tan 5 over 9.
let sq = region(Loop2::polygon(&[Vec2::new(0.0, 0.0), Vec2::new(10.0, 0.0), Vec2::new(10.0, 10.0), Vec2::new(0.0, 10.0)]));
let taper = 5f64.to_radians();
let b = smlib_sweeps::extrude(&Plane::XY, &pieces_of(&sq), Some(&sq), 0.0, 10.0, 1.0, Some(Draft { taper, foot: 1.0 })).unwrap();
let b = smlib_sweeps::extrude(
&Plane::XY,
&pieces_of(&sq),
Some(&sq),
0.0,
10.0,
1.0,
Some(Draft { taper, foot: 1.0, corner: crate::DraftCorner::default() }),
)
.unwrap();
let (a1, s2) = (100.0, 10.0 + 2.0 * 10.0 * taper.tan());
let a2 = s2 * s2;
close(volume(&b), 100.0 + 9.0 / 3.0 * (a1 + a2 + (a1 * a2).sqrt()), 1e-9, "footed draft");
+1 -1
View File
@@ -1178,7 +1178,7 @@ fn countersink_exports_one_cone() {
// A 90° countersink, radius 4 at the top face, down from 1 mm above it to radius 1.8, and the
// 2 mm drill through.
let disc = Region2 { outer: Loop2::circle(Vec2::ZERO, 5.0), holes: vec![] };
let cone = crate::extrude_tapered(&Plane::XY.offset(5.0), &disc, 3.2, -1.0, -FRAC_PI_4).unwrap();
let cone = crate::extrude_tapered(&Plane::XY.offset(5.0), &disc, 3.2, -1.0, -FRAC_PI_4, crate::DraftCorner::default()).unwrap();
let drill = cylinder(Vec3::new(0.0, 0.0, -1.0), Vec3::Z, 2.0, 6.0).unwrap();
let b = boolean(&boolean(&plate, &cone, BoolOp::Cut).unwrap().unwrap(), &drill, BoolOp::Cut).unwrap().unwrap();
let text = step_export(&[&b], "t").unwrap();
+5 -1
View File
@@ -1266,7 +1266,11 @@ mod imp {
}
pub(super) fn extruded(base: &Plane, chain: &[Numbered], n: Vec3, distance: f64) -> Result<Brep> {
checked(&SmBrep::extrude(&curves(base, chain)?, a3(n), distance, 0.0, false).map_err(sm("surface extrude"))?, "surface extrude")
checked(
&SmBrep::extrude(&curves(base, chain)?, a3(n), distance, 0.0, solidcraft_smlib::sweeps::DraftCorner::default(), false)
.map_err(sm("surface extrude"))?,
"surface extrude",
)
}
pub(super) fn revolved(plane: &Plane, chain: &[Numbered], origin: Vec3, axis: Vec3, angle: f64) -> Result<Brep> {
+17 -13
View File
@@ -442,7 +442,7 @@ fn tapered_extrudes() {
outer: Loop2::polygon(&[Vec2::new(-20.0, -20.0), Vec2::new(20.0, -20.0), Vec2::new(20.0, 20.0), Vec2::new(-20.0, 20.0)]),
holes: vec![],
};
let b = extrude_tapered(&Plane::XY, &sq, 30.0, 1.0, 10f64.to_radians()).unwrap();
let b = extrude_tapered(&Plane::XY, &sq, 30.0, 1.0, 10f64.to_radians(), crate::DraftCorner::default()).unwrap();
let top = 40.0 + 2.0 * 30.0 * 10f64.to_radians().tan();
let v = 30.0 / 3.0 * (1600.0 + top * top + 40.0 * top);
let m = measure(&b).unwrap();
@@ -450,13 +450,13 @@ fn tapered_extrudes() {
assert_eq!(m.merged.faces, 6);
// Inward taper on a circle (a cone frustum), downward.
let c = Region2 { outer: Loop2::circle(Vec2::ZERO, 10.0), holes: vec![] };
let b = extrude_tapered(&Plane::XY, &c, 10.0, -1.0, -20f64.to_radians()).unwrap();
let b = extrude_tapered(&Plane::XY, &c, 10.0, -1.0, -20f64.to_radians(), crate::DraftCorner::default()).unwrap();
let r2 = 10.0 - 10.0 * 20f64.to_radians().tan();
let v = PI * 10.0 / 3.0 * (100.0 + r2 * r2 + 10.0 * r2);
let m = measure(&b).unwrap();
assert!(rel(m.volume, v) < 5e-4, "{} vs {v}", m.volume);
assert!(m.bbox.max.z < 1e-6 && m.bbox.min.z > -10.0 - 1e-6, "{:?}", m.bbox);
assert!(extrude_tapered(&Plane::XY, &c, 10.0, 1.0, -60f64.to_radians()).is_err(), "closes the profile");
assert!(extrude_tapered(&Plane::XY, &c, 10.0, 1.0, -60f64.to_radians(), crate::DraftCorner::default()).is_err(), "closes the profile");
}
/// A slot (two lines and two half-circles, tangent) tapered inward: every edge moves in by
@@ -472,7 +472,7 @@ fn tapered_extrude_of_lines_and_arcs() {
Seg2::Arc { center: Vec2::ZERO, radius: r, start: PI / 2.0, sweep: PI },
],
};
let b = extrude_tapered(&Plane::XY, &Region2 { outer: slot, holes: vec![] }, h, 1.0, -a).unwrap();
let b = extrude_tapered(&Plane::XY, &Region2 { outer: slot, holes: vec![] }, h, 1.0, -a, crate::DraftCorner::default()).unwrap();
let (a0, p, t) = (2.0 * r * l + PI * r * r, 2.0 * l + 2.0 * PI * r, a.tan());
let v = a0 * h - p * t * h * h / 2.0 + PI * t * t * h.powi(3) / 3.0;
let m = measure(&b).unwrap();
@@ -486,7 +486,7 @@ fn tapered_extrude_of_lines_and_arcs() {
Seg2::Line { a: Vec2::new(0.0, r), b: Vec2::ZERO },
],
};
let e = extrude_tapered(&Plane::XY, &Region2 { outer: quarter, holes: vec![] }, h, 1.0, a).unwrap_err();
let e = extrude_tapered(&Plane::XY, &Region2 { outer: quarter, holes: vec![] }, h, 1.0, a, crate::DraftCorner::default()).unwrap_err();
assert!(e.to_string().contains("arc meets another edge at a corner"), "{e}");
}
@@ -511,7 +511,7 @@ fn tapered_extrude_of_arcs_meeting_at_corners() {
Seg2::Arc { center: Vec2::new(c, 0.0), radius: w0, start: at(c, x, -y), sweep: 2.0 * at(c, x, y) },
],
};
let b = extrude_tapered(&Plane::XY, &Region2 { outer: lobed, holes: vec![] }, h, -1.0, -a).unwrap();
let b = extrude_tapered(&Plane::XY, &Region2 { outer: lobed, holes: vec![] }, h, -1.0, -a, crate::DraftCorner::default()).unwrap();
// Union of the circle and two lobes: the circle plus each lobe less its lens with the circle.
let area = |d: f64| {
let (r, w) = (r0 - d, w0 - d);
@@ -543,7 +543,7 @@ fn tapered_extrude_of_conics() {
Seg2::Conic { a: p(0.0, -r), apex: p(r, -r), b: p(r, 0.0), w },
],
};
let b = extrude_tapered(&Plane::XY, &Region2 { outer: disc, holes: vec![] }, h, 1.0, -a).unwrap();
let b = extrude_tapered(&Plane::XY, &Region2 { outer: disc, holes: vec![] }, h, 1.0, -a, crate::DraftCorner::default()).unwrap();
let r1 = r - h * a.tan();
let v = PI * h / 3.0 * (r * r + r1 * r1 + r * r1);
let m = measure(&b).unwrap();
@@ -569,7 +569,7 @@ fn tapered_extrude_of_cubics_is_close_to_the_true_offset() {
],
};
let (area, perimeter) = (disc.signed_area().abs(), disc.segs.iter().map(|s| s.length()).sum::<f64>());
let b = extrude_tapered(&Plane::XY, &Region2 { outer: disc, holes: vec![] }, h, 1.0, -a).unwrap();
let b = extrude_tapered(&Plane::XY, &Region2 { outer: disc, holes: vec![] }, h, 1.0, -a, crate::DraftCorner::default()).unwrap();
let t = a.tan();
let v = area * h - perimeter * t * h * h / 2.0 + PI * t * t * h.powi(3) / 3.0;
let m = measure(&b).unwrap();
@@ -584,7 +584,7 @@ fn tapered_extrude_swallows_a_short_tip() {
let (h, a) = (2.0, 5f64.to_radians());
let d = h * a.tan();
let pts = [Vec2::new(0.0, -2.0), Vec2::new(20.0, -0.01), Vec2::new(20.0, 0.01), Vec2::new(0.0, 2.0)];
let b = extrude_tapered(&Plane::XY, &Region2 { outer: Loop2::polygon(&pts), holes: vec![] }, h, 1.0, -a).unwrap();
let b = extrude_tapered(&Plane::XY, &Region2 { outer: Loop2::polygon(&pts), holes: vec![] }, h, 1.0, -a, crate::DraftCorner::default()).unwrap();
// The top: the three long lines moved in by d (to their left, the loop running
// counter-clockwise), meeting each other.
let moved = |p: Vec2, q: Vec2| {
@@ -613,7 +613,7 @@ fn tapered_extrude_between_with_a_foot() {
holes: vec![],
};
let (from, to, foot) = (-2.0, 10.0, 0.5);
let b = extrude_tapered_between(&Plane::XY, &sq, from, to, foot, 1.0, a).unwrap();
let b = extrude_tapered_between(&Plane::XY, &sq, from, to, foot, 1.0, a, crate::DraftCorner::default()).unwrap();
let side = |z: f64| 40.0 + 2.0 * z * a.tan();
let (s0, s1, rise) = (side(from), side(to), to - from - foot);
let v = foot * s0 * s0 + rise / 3.0 * (s0 * s0 + s1 * s1 + s0 * s1);
@@ -621,7 +621,10 @@ fn tapered_extrude_between_with_a_foot() {
assert!(rel(m.volume, v) < 1e-6, "{} vs {v}", m.volume);
assert!((m.bbox.min.z - from).abs() < 1e-6 && (m.bbox.max.z - to).abs() < 1e-6, "{:?}", m.bbox);
assert_eq!(m.merged.faces, 10);
assert!(extrude_tapered_between(&Plane::XY, &sq, from, to, 7.0, 1.0, a).is_err(), "a foot taller than half the solid");
assert!(
extrude_tapered_between(&Plane::XY, &sq, from, to, 7.0, 1.0, a, crate::DraftCorner::default()).is_err(),
"a foot taller than half the solid"
);
}
/// A taper steeper than a profile's details is refused, and says why: a slot narrower than the
@@ -638,10 +641,11 @@ fn tapered_extrude_refuses_a_taper_steeper_than_the_details() {
Vec2::new(4.9, 10.0),
Vec2::new(0.0, 10.0),
]);
let e = extrude_tapered(&Plane::XY, &Region2 { outer: slot, holes: vec![] }, 2.0, 1.0, 10f64.to_radians()).unwrap_err();
let e = extrude_tapered(&Plane::XY, &Region2 { outer: slot, holes: vec![] }, 2.0, 1.0, 10f64.to_radians(), crate::DraftCorner::default())
.unwrap_err();
assert!(e.to_string().contains("the taper closes the profile"), "{e}");
let ring = Region2 { outer: Loop2::circle(Vec2::ZERO, 5.0), holes: vec![Loop2::circle(Vec2::ZERO, 0.3).reversed()] };
let e = extrude_tapered(&Plane::XY, &ring, 2.0, 1.0, 10f64.to_radians()).unwrap_err();
let e = extrude_tapered(&Plane::XY, &ring, 2.0, 1.0, 10f64.to_radians(), crate::DraftCorner::default()).unwrap_err();
assert!(e.to_string().contains("the taper closes the profile"), "{e}");
}
+1 -1
View File
@@ -1 +1 @@
1c8e4154320a4b14332ab21269a5a28e5d64d325
815fe9f0f9b832bda08a584632178eb9db8af030
+14 -3
View File
@@ -20,6 +20,15 @@ pub const CURVE_MAX_CURVES: usize = 4096;
pub const CURVE_MAX_DEGREE: usize = 25;
pub const CURVE_MAX_POINTS: usize = 100_000;
/// How a drafted extrusion closes a sharp corner (`SMLIB_DRAFT_CORNER_*`).
pub const DRAFT_CORNER_CURVATURE: i32 = 0;
pub const DRAFT_CORNER_TANGENT: i32 = 1;
pub const DRAFT_CORNER_ROUND: i32 = 2;
/// The continued curves do not meet at the corner (`SMLIB_ERR_CORNER_APART`).
pub const ERR_CORNER_APART: i32 = -1108;
/// A wall closes before the far end (`SMLIB_ERR_WALL_CLOSES`).
pub const ERR_WALL_CLOSES: i32 = -1109;
pub const SWEEP_FOLLOW: i32 = 0;
pub const SWEEP_TRANSLATE: i32 = 1;
@@ -64,8 +73,10 @@ unsafe extern "C" {
direction: *const f64,
distance: f64,
draft: f64,
corner: i32,
cap: i32,
out: *mut *mut RawBrep,
out_corner: *mut f64,
) -> i32;
fn smlib_revolve(
kinds: *const i64,
@@ -188,9 +199,9 @@ pub fn planar_faces(loops: &List) -> Result<Handle, i32> {
}
#[cfg(feature = "native")]
pub fn extrude(profile: &List, direction: &[f64; 3], distance: f64, draft: f64, cap: bool) -> Result<Handle, i32> {
pub fn extrude(profile: &List, direction: &[f64; 3], distance: f64, draft: f64, corner: i32, cap: bool, at: &mut [f64; 3]) -> Result<Handle, i32> {
let (k, nk, r, nr) = raw(profile)?;
new_brep(|out| unsafe { smlib_extrude(k, nk, r, nr, direction.as_ptr(), distance, draft, i32::from(cap), out) })
new_brep(|out| unsafe { smlib_extrude(k, nk, r, nr, direction.as_ptr(), distance, draft, corner, i32::from(cap), out, at.as_mut_ptr()) })
}
#[cfg(feature = "native")]
@@ -330,7 +341,7 @@ pub fn planar_faces(_: &List) -> Result<Handle, i32> {
}
#[cfg(not(feature = "native"))]
pub fn extrude(_: &List, _: &[f64; 3], _: f64, _: f64, _: bool) -> Result<Handle, i32> {
pub fn extrude(_: &List, _: &[f64; 3], _: f64, _: f64, _: i32, _: bool, _: &mut [f64; 3]) -> Result<Handle, i32> {
Err(NOT_BUILT)
}
+13 -1
View File
@@ -34,7 +34,7 @@ pub use measure::{MEASURE_ACCURACY, MassProperties};
pub use ops::{Blend, BooleanOp, ChamferSecond, MAX_POINTS};
/// Why an SMLib call failed.
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq)]
pub enum Error {
/// SMLib's own status (1001 `SM_ERR`, 1007 `SM_ERR_INVALID_INPUT`, …).
Status(i32),
@@ -58,6 +58,8 @@ pub enum Error {
/// The answer is not exact in the input's geometry (a box that would wrap round its
/// surface, an analytic surface whose speed across a side varies along it).
NotExact,
/// A drafted extrusion could not close the corner (or wall) at `at` on its profile, for `why`.
Corner { why: sweeps::CornerRefusal, at: [f64; 3] },
/// A C++ exception (most likely out of memory) was caught at the boundary.
Exception,
/// The C API is a different version from the one this crate was written against.
@@ -82,6 +84,16 @@ impl std::fmt::Display for Error {
Error::Unsupported => write!(f, "not supported yet: SMLib cannot build this exactly"),
Error::NoEffect => write!(f, "SMLib made no change: it reported success, but the operation did nothing"),
Error::NotExact => write!(f, "SMLib cannot build this exactly in the input's geometry"),
Error::Corner { why, at } => {
let at = format!("({:.4}, {:.4}, {:.4})", at[0], at[1], at[2]);
match why {
sweeps::CornerRefusal::Apart => write!(f, "the draft's continued curves do not meet at the corner at {at}"),
sweeps::CornerRefusal::WallCloses => write!(f, "the draft closes the wall at {at} before the far end"),
sweeps::CornerRefusal::NotExact => {
write!(f, "the draft cannot continue the walls at the corner at {at} exactly (only a line's or an arc's wall continues)")
}
}
}
Error::Exception => write!(f, "SMLib raised an internal exception"),
Error::Version(v) => write!(f, "SMLib C API version {v}, expected {}", ffi::ABI_VERSION),
Error::NotBuilt => write!(f, "not supported yet: this build has no SMLib"),
+48 -2
View File
@@ -34,6 +34,32 @@ pub enum Curve {
},
}
/// How a drafted [`Brep::extrude`] closes the gap its offset opens at a sharp corner of the
/// profile. Where the offsets overlap instead, all three trim them where they cross.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum DraftCorner {
/// Each wall continues as itself (a line's plane, an arc's cone) until it meets its
/// neighbour: every section is the profile's curves offset and continued as themselves.
#[default]
Curvature,
/// Each offset curve continues straight along its end tangent: a flat face each side.
Tangent,
/// An arc about the corner: a cone about it.
Round,
}
/// Why [`DraftCorner::Curvature`] could not close a corner ([`Error::Corner`]).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CornerRefusal {
/// The continued curves do not meet (two parallel lines, two circles that part).
Apart,
/// A wall closes before the far end: its neighbours meet across it, or its arc shrinks to its
/// centre.
WallCloses,
/// A curve there is neither a line nor an arc: its wall has no exact continuation.
NotExact,
}
/// How the profile is carried along a [`Brep::sweep`] path.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SweepFrame {
@@ -141,10 +167,30 @@ impl Brep {
/// The planar `profile` extruded `distance` along the unit `direction` (square to its plane).
/// `draft` (radians) leans the walls: positive inward, negative outward; a drafted profile is
/// one closed loop, its curves in order, and `direction` is its normal (either way round).
pub fn extrude(profile: &[Curve], direction: [f64; 3], distance: f64, draft: f64, cap: bool) -> Result<Brep> {
/// `corner` closes a drafted profile's sharp corners; a corner it cannot close is
/// [`Error::Corner`] (or [`Error::NotExact`] where a curve there is neither a line nor an
/// arc, under [`DraftCorner::Curvature`]).
pub fn extrude(profile: &[Curve], direction: [f64; 3], distance: f64, draft: f64, corner: DraftCorner, cap: bool) -> Result<Brep> {
init()?;
let l = List::of(profile)?;
ffi::sweeps::extrude(&l.raw(), &direction, distance, draft, cap).map(Brep).map_err(error)
let rule = match corner {
DraftCorner::Curvature => ffi::sweeps::DRAFT_CORNER_CURVATURE,
DraftCorner::Tangent => ffi::sweeps::DRAFT_CORNER_TANGENT,
DraftCorner::Round => ffi::sweeps::DRAFT_CORNER_ROUND,
};
let mut at = [f64::NAN; 3];
ffi::sweeps::extrude(&l.raw(), &direction, distance, draft, rule, cap, &mut at).map(Brep).map_err(|s| {
let why = match s {
ffi::sweeps::ERR_CORNER_APART => Some(CornerRefusal::Apart),
ffi::sweeps::ERR_WALL_CLOSES => Some(CornerRefusal::WallCloses),
ffi::trim::ERR_NOT_EXACT => Some(CornerRefusal::NotExact),
_ => None,
};
match why {
Some(why) if at.iter().all(|x| x.is_finite()) => Error::Corner { why, at },
_ => error(s),
}
})
}
/// The planar `profile` turned `angle` radians (0 < angle <= 2 pi) right-handed about the
+4 -4
View File
@@ -27,11 +27,11 @@ fn circle(center: [f64; 3], radius: f64) -> Curve {
#[test]
fn extrusions_straight_and_drafted() {
let rect = polygon(&[[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [10.0, 6.0, 0.0], [0.0, 6.0, 0.0]]);
let b = Brep::extrude(&rect, [0.0, 0.0, 1.0], 5.0, 0.0, true).unwrap();
let b = Brep::extrude(&rect, [0.0, 0.0, 1.0], 5.0, 0.0, DraftCorner::default(), true).unwrap();
assert_close(solid_volume(&b, "prism"), 300.0, 1e-9, "prism");
// A disc drafted 5 degrees in over 4 mm: a frustum of radii 5 and 5 - 4 tan 5.
let r2 = 5.0 - 4.0 * 5f64.to_radians().tan();
let b = Brep::extrude(&[circle([0.0; 3], 5.0)], [0.0, 0.0, 1.0], 4.0, 5f64.to_radians(), true).unwrap();
let b = Brep::extrude(&[circle([0.0; 3], 5.0)], [0.0, 0.0, 1.0], 4.0, 5f64.to_radians(), DraftCorner::default(), true).unwrap();
assert_close(solid_volume(&b, "frustum"), PI * 4.0 / 3.0 * (25.0 + r2 * r2 + 5.0 * r2), 1e-6, "drafted disc");
}
@@ -114,9 +114,9 @@ fn sweeps_through_stations() {
#[test]
fn hostile_curve_lists_are_refused() {
let nan = [Curve::Line { start: [0.0, f64::NAN, 0.0], end: [1.0, 0.0, 0.0] }];
assert_eq!(Brep::extrude(&nan, [0.0, 0.0, 1.0], 1.0, 0.0, true).err(), Some(Error::Argument));
assert_eq!(Brep::extrude(&nan, [0.0, 0.0, 1.0], 1.0, 0.0, DraftCorner::default(), true).err(), Some(Error::Argument));
let bad = [Curve::Nurbs { degree: 3, points: vec![[0.0, 0.0, 0.0, 1.0]; 2], knots: vec![0.0; 6] }];
assert_eq!(Brep::extrude(&bad, [0.0, 0.0, 1.0], 1.0, 0.0, true).err(), Some(Error::Argument));
assert_eq!(Brep::extrude(&bad, [0.0, 0.0, 1.0], 1.0, 0.0, DraftCorner::default(), true).err(), Some(Error::Argument));
assert_eq!(Brep::loft(&[vec![circle([0.0; 3], 1.0)]], 1, true).err(), Some(Error::Argument));
let sq = polygon(&[[0.0; 3], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0]]);
assert_eq!(Brep::revolve(&sq, [0.0; 3], [0.0, 1.0, 0.0], 7.0, true).err(), Some(Error::Argument));