mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-06 03:51:08 +00:00
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:
@@ -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<_>>()?;
|
||||
|
||||
@@ -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, ®ion, h, 1.0, -dr)?]
|
||||
vec![kernel::extrude_tapered(&base, ®ion, h, 1.0, -dr, kernel::DraftCorner::default())?]
|
||||
} else {
|
||||
kernel::extrude(&base, &[region], 0.0, h)?
|
||||
};
|
||||
|
||||
@@ -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, ®ion, 20.0, 1.0, -taper).unwrap();
|
||||
let f = extrude_tapered(&Plane::XY, ®ion, 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);
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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 @@
|
||||
1c8e4154320a4b14332ab21269a5a28e5d64d325
|
||||
815fe9f0f9b832bda08a584632178eb9db8af030
|
||||
|
||||
@@ -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
@@ -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"),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user