smlib: drafted extrudes keep their corners and curve sweeps keep the profile where it lies on solidcore (solidcore#1449, #1450)

Wires the two solidcore fixes into the smlib crate's solidcore port of the capi sweeps:

- solidcore#1449: `fixes()` turns on `taper_tangent_corner_planes`. `extrude`'s draft
  no longer refuses a sharp corner under the curvature rule (or the tangent rule with a
  profile of lines and arcs) as not supported: it calls
  `create_taper_extrude_with_corner` with the rule (curvature -> CurveExtension,
  tangent -> LinearExtension, round -> FilletCorner), and a refused corner is the
  capi's `Error::Corner { why, at }` (Apart, WallCloses, NotExact at the corner's
  point), as the C++ path reports `SmTaperCornerRefusal` and `pOptRefusedCorner`.
  The tangent/sharp/has_corner checks that only fed the refusal are gone.
- solidcore#1450: a sweep along a path that is neither straight nor arcs about one
  axis runs `create_curve_sweep_with_placements` with `profile_at_path_start` (and
  `translational_sweep` for a translating frame), and `placed` takes the first,
  middle and last placements. As in the capi's `smlib_sweep_placed`, round a closed
  path the ends are not capped (the caps would lie on each other inside the solid)
  and the faces are stitched, a lamina left being not solid.

The tests the two issues held back run again (extrude_square_drafted_curvature,
extrude_draft_wall_closes, sweep_along_a_bent_path), with a new case round a closed
rounded rectangle (Pappus). The module note drops the two refusals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MNXLXkZLQ6T7TSi4wswcmk
This commit is contained in:
Brett Lawson
2026-10-30 01:58:56 +01:00
parent c8adedaec2
commit 744f7574d9
2 changed files with 87 additions and 70 deletions
+62 -67
View File
@@ -15,12 +15,6 @@
//!
//! What the capi asks of SMLib code that solidcore does not have is [`Error::Unsupported`],
//! refused where the capi would first reach it, so every error before it is the C++ path's:
//! - a drafted extrusion with a sharp corner under [`DraftCorner::Curvature`] (fork 54148db), or
//! under [`DraftCorner::Tangent`] with a profile of lines and arcs only (fork e8f23e0): the
//! fork's `sm_CreateCurvatureKeepingWalls` and its `SmTaperCornerRefusal`;
//! - a sweep along a path that is neither straight nor (turning with it) arcs about one axis:
//! SM_API's curve sweep with `bProfileAtPathStart` (fork 2dd71a0) and its placements (fork
//! 5d3939e);
//! - a planar section of a face (`SmBrep::CreatePlanarSectionCurves`).
//!
//! The screw sweeps are the capi's own: it builds their walls from SMLib's B-spline surfaces,
@@ -41,17 +35,21 @@ use solidcore::geom::tol::USE_DEFAULT;
use solidcore::geom::vector3d::{Point3, Vec3};
use solidcore::heal::stitch::p1::api_stitch_into_solid;
use solidcore::kernel::Kernel;
use solidcore::math::EFF_ZERO_SQRT;
use solidcore::nurbs::b_spline_curve::{BSplineCurve, BSplineCurveForm, CanonicalData, CurveCanonical, KnotType};
use solidcore::nurbs::b_spline_surface::{BSplineSurface, BSplineSurfaceForm, CanonicalSurfaceData};
use solidcore::nurbs::b_spline_surface_n_lib::p2::OrientType;
use solidcore::primitive::primitive_creation::SweepOptions;
use solidcore::surface::cone::Cone;
use solidcore::surface::surface::Surface;
use solidcore::sweep::api_brep::{create_linear_sweep, create_rotational_sweep, create_taper_extrude};
use solidcore::sweep::api_brep::{
CornerRefusal, OffsetCornerType, TaperCornerRefusal, TaperExtrude, create_curve_sweep_with_placements, create_linear_sweep,
create_rotational_sweep, create_taper_extrude_with_corner,
};
use solidcore::sweep::api_surfaces::create_skin_primitive;
use solidcore::topology::brep::p1::{ParallelProjectionOptions, create_parallel_projection_curves};
use solidcore::topology::brep::p2::{ArenaNotifyState, is_manifold_solid};
use solidcore::topology::brep::p3::new_brep;
use solidcore::topology::brep::p4::stitch_faces;
use solidcore::topology::brep::p5::{StitchAndOrientOptions, stitch_and_orient};
use solidcore::topology::brep::p8::{
FaceBuildEnv, create_face_from_surface, create_faces_with_curves, create_planar_face_with_3d_curves, create_planar_faces_with_3d_curves,
@@ -61,7 +59,7 @@ use solidcore::topology::{Brep, FaceId};
use super::blend::status;
use super::offset::{face_miss, pick_faces};
use super::{Body, answer, run, with_body};
use super::{Body, run, with_body};
use crate::sweeps::{Affine, Curve as Profile, DraftCorner, Ends, MAX_CURVES, MAX_STATION_CURVES, MAX_STATIONS, SweepFrame};
use crate::{Error, Result, ffi};
@@ -298,44 +296,6 @@ pub(crate) fn planar_faces(loops: &[Profile]) -> Result<Body> {
})
}
/// The tangent of `c` at `t`, from the left or the right.
fn tangent(k: &Kernel, c: &Curve, t: f64, from_left: bool) -> solidcore::Result<Option<Vec3>> {
Ok(c.evaluate(k, t, 1, from_left, true)?.get(1).copied())
}
/// Whether the angle about `n` from `a` to `b` is a sharp corner (`sm_DraftHasCorner`: more than
/// `SM_EFF_ZERO_SQRT`); an angle whose measure returns a status is none, as there ([`answer`]).
fn sharp(n: Vec3, a: Option<Vec3>, b: Option<Vec3>) -> solidcore::Result<bool> {
Ok(match (a, b) {
(Some(a), Some(b)) => answer(n.ccw_angle_between(a, b))?.is_some_and(|x| x.abs() > EFF_ZERO_SQRT),
_ => false,
})
}
/// `sm_DraftHasCorner`, on the profile as given (one closed loop, in order): whether it has a
/// sharp corner, where one curve meets the next or (a NURBS curve's own) at an inner knot of full
/// multiplicity, where SMLib's composite curve splits it.
fn has_corner(k: &Kernel, curves: &[Curve], n: Vec3) -> solidcore::Result<bool> {
let count = curves.len();
for (i, b) in curves.iter().enumerate() {
let Some(a) = curves.get((i + count - 1) % count) else { continue };
let (ia, ib) = (a.natural_interval(), b.natural_interval());
if sharp(n, tangent(k, a, ia.max(), true)?, tangent(k, b, ib.min(), false)?)? {
return Ok(true);
}
if let CurveKind::BSpline(s) = b.kind() {
let c = s.canonical()?;
let inner = c.knots.iter().zip(&c.knot_multiplicities).skip(1).take(c.knots.len().saturating_sub(2));
for (&u, &m) in inner {
if m >= c.degree && sharp(n, tangent(k, b, u, true)?, tangent(k, b, u, false)?)? {
return Ok(true);
}
}
}
}
Ok(false)
}
/// `smlib_extrude`: the planar `profile` extruded `distance` along `direction`, straight
/// (`SmApiCreateLinearSweep`) or drafted (`SmApiCreateTaperExtrude`).
pub(crate) fn extrude(profile: &[Profile], direction: [f64; 3], distance: f64, draft: f64, corner: DraftCorner, cap: bool) -> Result<Body> {
@@ -367,27 +327,35 @@ pub(crate) fn extrude(profile: &[Profile], direction: [f64; 3], distance: f64, d
}
let height = if along > 0.0 { distance } else { -distance };
let angle = draft * 180.0 / PI;
// In `CreateTaperExtrude`, a profile with a sharp corner keeps its walls' curvature
// (the curvature rule) or continues lines' and arcs' walls as their tangent planes (the
// tangent rule, lines and arcs only): the fork's `sm_CreateCurvatureKeepingWalls`
// (54148db, e8f23e0), which solidcore does not have. Without a sharp corner the three
// rules give the same walls, which the offset method builds.
let exact = curves.iter().all(|c| matches!(c.kind(), CurveKind::Line(_) | CurveKind::Ellipse(_)));
let fork_only = match corner {
DraftCorner::Curvature => true,
DraftCorner::Tangent => exact,
DraftCorner::Round => false,
// The fork's corner rules (54148db, e8f23e0): the curvature rule continues each wall as
// itself, the tangent rule a profile of lines and arcs as their tangent planes
// (`fixes.taper_tangent_corner_planes`), and a corner either cannot close is refused,
// with why and where (`SmTaperCornerRefusal`, `pOptRefusedCorner`), as the capi passes it on.
let rule = match corner {
DraftCorner::Curvature => OffsetCornerType::CurveExtension,
DraftCorner::Tangent => OffsetCornerType::LinearExtension,
DraftCorner::Round => OffsetCornerType::FilletCorner,
};
if fork_only && has_corner(k, &curves, normal)? {
return Ok(Err(Error::Unsupported));
}
match create_taper_extrude(k, &refs, height, angle, if cap { 3 } else { 0 }, corner == DraftCorner::Round) {
Ok(r) => finish(r, cap),
match create_taper_extrude_with_corner(k, &refs, height, angle, if cap { 3 } else { 0 }, rule) {
Ok(TaperExtrude::Solid(r)) => finish(r, cap),
Ok(TaperExtrude::Refused(r)) => Ok(Err(refused(r))),
Err(e) => Ok(Err(status(e))),
}
})
}
/// A refused draft corner as the capi reports it: the reason's status with the corner's point,
/// [`Error::Corner`] when the point is finite and the status's own error otherwise.
fn refused(r: CornerRefusal) -> Error {
let (why, code) = match r.reason {
TaperCornerRefusal::Apart => (crate::sweeps::CornerRefusal::Apart, ffi::sweeps::ERR_CORNER_APART),
TaperCornerRefusal::WallCloses => (crate::sweeps::CornerRefusal::WallCloses, ffi::sweeps::ERR_WALL_CLOSES),
TaperCornerRefusal::NotExact => (crate::sweeps::CornerRefusal::NotExact, ffi::trim::ERR_NOT_EXACT),
};
let at = [r.corner.x, r.corner.y, r.corner.z];
if at.iter().all(|x| x.is_finite()) { Error::Corner { why, at } } else { crate::error(code) }
}
/// `smlib_revolve`: the planar `profile` turned `angle` radians about the axis through `origin`
/// along `axis` (`SmApiCreateRotationalSweep`).
pub(crate) fn revolve(profile: &[Profile], origin: [f64; 3], axis: [f64; 3], angle: f64, cap: bool) -> Result<Body> {
@@ -546,12 +514,39 @@ pub(crate) fn sweep_placed(profile: &[Profile], path: &[Profile], frame: SweepFr
}
return finish(r, cap);
}
if let Err(e) = joined(&along)? {
return Ok(Err(e));
let path_curve = match joined(&along)? {
Ok(b) => Curve::from(b),
Err(e) => return Ok(Err(e)),
};
// Round a closed path the profile comes back onto itself: the walls close on each other
// there, where caps would lie on top of each other inside the solid.
let (Some(head), Some(tail)) = (along.first(), along.last()) else { return Ok(Err(Error::Argument)) };
let closed = (ends(k, head)?.0 - ends(k, tail)?.1).length() <= 1e-6;
// `SmApiCreateCurveSweep` with `bProfileAtPathStart` (fork 2dd71a0), so the profile is
// swept where it lies, and the placements it reports (fork 5d3939e).
let options = SweepOptions {
cap_ends: cap && !closed,
translational_sweep: frame == SweepFrame::Translate,
profile_at_path_start: true,
..SweepOptions::default()
};
let r = match create_curve_sweep_with_placements(k, &refs, Some(&path_curve), None, None, Some(&options)) {
Ok(r) => r,
Err(e) => return Ok(Err(status(e))),
};
for (m, p) in placed.iter_mut().zip(&r.placements) {
*m = motion(p.x_axis(), p.y_axis(), p.z_axis(), p.origin());
}
// `SmApiCreateCurveSweep` with `bProfileAtPathStart` (fork 2dd71a0) and the placements it
// reports (fork 5d3939e): solidcore's curve sweep has neither.
Ok(Err(Error::Unsupported))
let mut b = r.sweep.brep;
if closed {
let tol = b.tolerance();
match stitch_faces(k, &mut b, tol, None, false) {
Ok(s) if s.lamina_edges != 0 => return Ok(Err(Error::NotSolid)),
Ok(_) => b.refresh_cached_topology(),
Err(e) => return Ok(Err(status(e))),
}
}
finish(b, cap)
})?;
Ok((body, placed))
}
+25 -3
View File
@@ -171,6 +171,27 @@ fn case(name: &str) -> Case {
),
Expect::About(PI * (10.0 + 10.0 * FRAC_PI_2), 1e-4),
),
// A 4 x 3 rectangle round a closed 60 x 40 rounded rectangle (corner radius 8), from the
// middle of its bottom line: no caps, the walls closing on each other. Pappus: A L, with
// A = 12 and L = 2 (44 + 24) + 4 (π/2) 8.
"sweep_round_a_closed_path" => {
let arc = |c: [f64; 3], r: [f64; 3]| Curve::Arc { center: c, axis: Z, reference: r, radius: 8.0, start: 0.0, end: FRAC_PI_2 };
let path = vec![
line([0.0, -20.0, 0.0], [22.0, -20.0, 0.0]),
arc([22.0, -12.0, 0.0], [0.0, -1.0, 0.0]),
line([30.0, -12.0, 0.0], [30.0, 12.0, 0.0]),
arc([22.0, 12.0, 0.0], [1.0, 0.0, 0.0]),
line([22.0, 20.0, 0.0], [-22.0, 20.0, 0.0]),
arc([-22.0, 12.0, 0.0], [0.0, 1.0, 0.0]),
line([-30.0, 12.0, 0.0], [-30.0, -12.0, 0.0]),
arc([-22.0, -12.0, 0.0], [-1.0, 0.0, 0.0]),
line([-22.0, -20.0, 0.0], [0.0, -20.0, 0.0]),
];
c(
Op::Sweep(polygon(&[[0.0, -22.0, -1.5], [0.0, -18.0, -1.5], [0.0, -18.0, 1.5], [0.0, -22.0, 1.5]]), path, SweepFrame::Follow, true),
Expect::About(12.0 * (2.0 * (44.0 + 24.0) + 16.0 * PI), 1e-4),
)
}
// The frustum of a square pyramid: side 20 to 10 over 10.
"loft_two_squares" => {
c(Op::Loft(vec![square(0.0, 0.0, 20.0, 0.0), square(5.0, 5.0, 10.0, 10.0)], 1, true), Expect::Volume(frustum(400.0, 100.0, 10.0)))
@@ -384,14 +405,15 @@ cases!(
extrude_rectangle,
extrude_circle,
extrude_square_drafted_round,
extrude_square_drafted_curvature: ignore = "solidcore#1449: no curvature-keeping draft corners, fork 54148db",
extrude_square_drafted_curvature,
extrude_circle_drafted,
extrude_draft_wall_closes: ignore = "solidcore#1449: no curvature-keeping draft corners nor SmTaperCornerRefusal, fork 54148db",
extrude_draft_wall_closes,
revolve_rectangle_full,
revolve_rectangle_quarter,
sweep_circle_straight,
sweep_circle_along_an_arc,
sweep_along_a_bent_path: ignore = "solidcore#1450: no curve sweep keeping the profile at the path's start, fork 2dd71a0 / 5d3939e",
sweep_along_a_bent_path,
sweep_round_a_closed_path,
loft_two_squares,
loft_two_circles,
loft_to_point_pyramid,