mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 04:52:10 +00:00
Merge pull request #1479 from brett19/fix/revolve-inside-out-cylinder
fix: revolve inside-out cylinder keeps its axis (fixes #1463)
This commit is contained in:
@@ -195,6 +195,8 @@ pub struct UpstreamFixes {
|
||||
pub flat_corners_test_bits: bool,
|
||||
/// `SmCone::RebuildSTEPFromNURBParameters` gives a cylinder the mean of near-equal radii (semi-angle 0) (WR-385, #713).
|
||||
pub cylinder_rebuild_mean_radius: bool,
|
||||
/// `SmCylinder::IsNurbSurfaceCylinder` gives an inside-out NURB (its iso circles turning against the line through their centres) the frame `IsNurbSurfaceCone` gives one: Z against the NURB's linear direction, origin at its top-knot circle; upstream keeps the bottom-knot circle and the NURB's direction, so the cylinder's Z is reversed and its STEP evaluation is mirrored in height (WR-496, #1463).
|
||||
pub nurb_cylinder_inside_out_frame: bool,
|
||||
/// `SweepProfilesAlongPath` asks for caps only when the profile closes (WR-400, #837).
|
||||
pub planar_path_uncapped_open_profiles: bool,
|
||||
/// A rotational sweep (whole or partial turn) makes each line and circular arc of its profile its cylinder, cone, sphere or torus, not only a one-curve profile's (WR-448, #545; SolidCraft change, tangency-labs/smlib 0b143f7).
|
||||
|
||||
@@ -67,7 +67,9 @@
|
||||
//! where a `SER` call returns its status (a truthy result without cylinder) and
|
||||
//! `Some` where upstream returns `TRUE` with a cylinder; its NURB is a copy of the
|
||||
//! tested one, as upstream. A tested surface without NURB is `SM_ERR_NULL_POINTER`
|
||||
//! (upstream copies a null NURB).
|
||||
//! (upstream copies a null NURB). With the opt-in
|
||||
//! `Kernel::fixes.nurb_cylinder_inside_out_frame` (WR-496, #1463) an inside-out
|
||||
//! NURB gets `IsNurbSurfaceCone`'s frame (top-knot circle, reversed Z).
|
||||
//! - **Recursion.** None of these functions recurses.
|
||||
//! - **Debug code.** The `SM_DEBUG_CODE` checks and drawing are omitted.
|
||||
|
||||
@@ -755,7 +757,11 @@ impl Cone {
|
||||
/// corner|)` for distances and radii (upstream default scale `1`). The new bounded
|
||||
/// cylinder ([`Cone::is_cylinder`]) keeps a copy of the tested NURB as its own, its
|
||||
/// polar converter rebuilt from it and its generator reparameterised to the
|
||||
/// linear direction's NURB interval.
|
||||
/// linear direction's NURB interval. Upstream keeps an inside-out NURB's frame
|
||||
/// (origin the bottom-knot circle, Z along the linear direction), which reverses
|
||||
/// the cylinder's axis; with `k.fixes.nurb_cylinder_inside_out_frame` (WR-496,
|
||||
/// #1463) the frame is the one [`Self::is_nurb_surface_cone`] gives it: origin the
|
||||
/// top-knot circle, Z against the linear direction.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
@@ -851,6 +857,17 @@ impl Cone {
|
||||
|
||||
let inside_out = z_axis.dot(bot.frame.z_axis()) < 0.0;
|
||||
|
||||
// Not upstream (WR-496, #1463, opt-in): an inside-out NURB's STEP frame points
|
||||
// against the NURB's linear direction from its top-knot circle, as
|
||||
// IsNurbSurfaceCone builds it; upstream ("Do some correction here") keeps the
|
||||
// bottom-knot circle and the NURB's direction, reversing the cylinder's Z
|
||||
let flip = inside_out && k.fixes.nurb_cylinder_inside_out_frame;
|
||||
let (origin, z_axis) = if flip {
|
||||
(center_top, -z_axis)
|
||||
} else {
|
||||
(center_bot, z_axis)
|
||||
};
|
||||
|
||||
let y_axis = z_axis.cross(x_axis);
|
||||
// Try to eliminate some numerical inaccurracies (the top radius is not used
|
||||
// further)
|
||||
@@ -860,7 +877,7 @@ impl Cone {
|
||||
}
|
||||
|
||||
let mut cylinder = Self::new_cylinder_bounded(
|
||||
center_bot,
|
||||
origin,
|
||||
x_axis,
|
||||
y_axis,
|
||||
bot.radius,
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
//!
|
||||
//! Deviations: not in upstream. The surface is laid out as `sm_CreateRotSrf`'s B-spline
|
||||
//! surface of revolution: `u` the turn (`0` at the curve, rising with the rotation about
|
||||
//! the axis), `v` the curve from its start to its end. It is only returned when its
|
||||
//! the axis), `v` the curve from its start to its end; for a line falling along the axis,
|
||||
//! the reversed line's (`v` from its end), its cylinder or cone built from the low end
|
||||
//! and never inside out, as 9853e54's `AnalyticSurface` (#1463). It is only returned when its
|
||||
//! corners, the curve's start tangent and the turn's sense agree with that layout, so a
|
||||
//! caller can swap it in for the B-spline and keep the trims and loop the B-spline
|
||||
//! layout gives. Any other curve, or a construction that fails, gives `None` and the
|
||||
@@ -29,6 +31,7 @@ use crate::geom::geom_utility::p1::{line_closest_point, line_point_distance};
|
||||
use crate::geom::vector3d::{Point3, Vec3};
|
||||
use crate::kernel::Kernel;
|
||||
use crate::math::{EFF_ZERO, PI};
|
||||
use crate::nurbs::b_spline_curve::BSplineCurve;
|
||||
use crate::nurbs::b_spline_surface::SurfParamType;
|
||||
use crate::nurbs::b_spline_surface_n_lib::p2::EvaluateFlags;
|
||||
use crate::surface::cone::Cone;
|
||||
@@ -98,20 +101,32 @@ pub fn analytic_revolution(
|
||||
0.0
|
||||
};
|
||||
let tol = tol.min(FIT_REL * (1.0 + size));
|
||||
let Some(Some(candidate)) =
|
||||
let Some(Some((candidate, reversed))) =
|
||||
ignored(candidate(k, curve, base_pt, axis, angle_deg, tol, axis_tol))?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
let made;
|
||||
let reference = if let Some(surface) = reference {
|
||||
surface
|
||||
} else {
|
||||
let Some(surface) = ignored(create_rot_srf(k, curve, base_pt, axis, angle_deg))? else {
|
||||
return Ok(None);
|
||||
};
|
||||
made = Surface::from(surface);
|
||||
&made
|
||||
let reference = match reference {
|
||||
Some(surface) if !reversed => surface,
|
||||
_ => {
|
||||
// a line falling along the axis: laid out as the reversed line's B-spline
|
||||
let made_srf = if reversed {
|
||||
let Some(line) =
|
||||
ignored(BSplineCurve::create_line_segment(3, ends.end, ends.start))?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
create_rot_srf(k, &Curve::from(line), base_pt, axis, angle_deg)
|
||||
} else {
|
||||
create_rot_srf(k, curve, base_pt, axis, angle_deg)
|
||||
};
|
||||
let Some(surface) = ignored(made_srf)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
made = Surface::from(surface);
|
||||
&made
|
||||
}
|
||||
};
|
||||
let expected = ignored(Layout::of(k, reference, base_pt, axis))?;
|
||||
let actual = ignored(Layout::of(k, &candidate, base_pt, axis))?;
|
||||
@@ -227,7 +242,8 @@ fn in_half_plane(
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// The analytic surface for the curve's shape, before the layout test.
|
||||
/// The analytic surface for the curve's shape, before the layout test, and whether it
|
||||
/// is laid out as the reversed curve's B-spline (a line falling along the axis).
|
||||
fn candidate(
|
||||
k: &Kernel,
|
||||
curve: &Curve,
|
||||
@@ -236,7 +252,7 @@ fn candidate(
|
||||
angle_deg: f64,
|
||||
tol: f64,
|
||||
axis_tol: f64,
|
||||
) -> Result<Option<Surface>> {
|
||||
) -> Result<Option<(Surface, bool)>> {
|
||||
let ends = curve.ends(k)?;
|
||||
let (start_dist, _) = line_point_distance(base_pt, axis, ends.start);
|
||||
let (end_dist, _) = line_point_distance(base_pt, axis, ends.end);
|
||||
@@ -262,20 +278,25 @@ fn candidate(
|
||||
if !in_half_plane(k, curve, base_pt, x, y, tol)? {
|
||||
return Ok(None);
|
||||
}
|
||||
// the generator line rises along the axis from the origin; inside out runs
|
||||
// the NURB from the top, so that v always runs from the curve's start
|
||||
let (low_h, bot, top, inside_out) = if height > 0.0 {
|
||||
(start_h, start_dist, end_dist, false)
|
||||
// the generator line rises along the axis from the origin at the curve's low
|
||||
// end, never inside out (#1463: tangency-labs/smlib 9853e54's AnalyticSurface;
|
||||
// an inside-out cylinder's NURB is recognised with its axis reversed by
|
||||
// IsNurbSurfaceCylinder). A curve falling along the axis is laid out as the
|
||||
// reversed curve's B-spline (v from its end).
|
||||
let reversed = height < 0.0;
|
||||
let (low_h, bot, top) = if reversed {
|
||||
(end_h, end_dist, start_dist)
|
||||
} else {
|
||||
(end_h, end_dist, start_dist, true)
|
||||
(start_h, start_dist, end_dist)
|
||||
};
|
||||
let inside_out = false;
|
||||
let origin = base_pt + axis * low_h;
|
||||
let mut cone = if (start_dist - end_dist).abs() <= tol {
|
||||
Cone::new_cylinder_bounded(
|
||||
origin,
|
||||
x,
|
||||
y,
|
||||
start_dist,
|
||||
bot,
|
||||
0.0,
|
||||
angle_deg,
|
||||
height.abs(),
|
||||
@@ -299,7 +320,7 @@ fn candidate(
|
||||
)
|
||||
};
|
||||
cone.make_nurb(k)?;
|
||||
return Ok(Some(Surface::from(cone)));
|
||||
return Ok(Some((Surface::from(cone), reversed)));
|
||||
}
|
||||
|
||||
let Some((frame, radius, arc_start, arc_end)) = curve.is_arc(k, 8, tol)? else {
|
||||
@@ -374,7 +395,7 @@ fn candidate(
|
||||
},
|
||||
)?;
|
||||
sphere.make_nurb(k)?;
|
||||
return Ok(Some(Surface::from(sphere)));
|
||||
return Ok(Some((Surface::from(sphere), false)));
|
||||
}
|
||||
|
||||
// a torus: its minor circle is the arc's, centred off the axis
|
||||
@@ -412,7 +433,7 @@ fn candidate(
|
||||
},
|
||||
)?;
|
||||
torus.make_nurb(k)?;
|
||||
Ok(Some(Surface::from(torus)))
|
||||
Ok(Some((Surface::from(torus), false)))
|
||||
}
|
||||
|
||||
/// What a surface of revolution's natural domain shows of its layout.
|
||||
|
||||
@@ -54,10 +54,12 @@
|
||||
//! [`RotationalSweepGeometry::set_analytic_revolutions`] (off by default, so the
|
||||
//! default follows `c9c5979`; `PrimitiveCreation`'s partial rotational sweep sets it
|
||||
//! from `Kernel::fixes.revolve_analytic_profile_faces`), `EdgeSweepHigher` gives a line or circular arc its
|
||||
//! cylinder, cone, sphere or torus in the B-spline surface's layout instead of
|
||||
//! cylinder, cone, sphere or torus in the B-spline surface's layout (a line
|
||||
//! falling along the axis: the reversed line's, its cylinder or cone built from the
|
||||
//! low end and never inside out, as 9853e54's `AnalyticSurface`, #1463) instead of
|
||||
//! `CopyAndAddAnalytics` of the B-spline. The trims are the analytic surface's own
|
||||
//! boundary, in the loop order and orientation the B-spline's give; the normal is
|
||||
//! still the B-spline's, taken before the copy as upstream.
|
||||
//! boundary; the normal is the analytic surface's, taken before the copy as the
|
||||
//! fork does (`AnalyticSurface` replaces `sm_CreateRotSrf`).
|
||||
//! - **Recursion.** None.
|
||||
|
||||
use crate::curve::curve::p8::CurveIntersectOptions;
|
||||
@@ -873,6 +875,28 @@ impl RotationalSweepGeometry {
|
||||
};
|
||||
let mut new_surf = Surface::from(new_surf);
|
||||
|
||||
// SolidCraft change (tangency-labs/smlib 0b143f7, 9853e54): the cylinder,
|
||||
// cone, sphere or torus the curve sweeps, in place of the B-spline before its
|
||||
// normal is taken (AnalyticSurface precedes it), which CopyAndAddAnalytics
|
||||
// does not give
|
||||
let analytic = if self.base.make_analytics && self.analytic_revolutions && !is_plane {
|
||||
analytic_revolution(
|
||||
k,
|
||||
&copied_orig_edge_curve,
|
||||
self.base_pt,
|
||||
self.axis,
|
||||
self.angle,
|
||||
eps,
|
||||
Some(&new_surf),
|
||||
)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let is_analytic = analytic.is_some();
|
||||
if let Some(surface) = analytic {
|
||||
new_surf = surface;
|
||||
}
|
||||
|
||||
let start_uv = new_surf.natural_uv_domain_with_nurb(k)?.min();
|
||||
let normal = new_surf.evaluate_normal(k, start_uv, Side::Left, Side::Left)?;
|
||||
out.normal = Some(normal);
|
||||
@@ -891,28 +915,9 @@ impl RotationalSweepGeometry {
|
||||
}
|
||||
|
||||
// Create corresponding analytics if requested
|
||||
if self.base.make_analytics {
|
||||
// SolidCraft change (tangency-labs/smlib 0b143f7): the cylinder, cone,
|
||||
// sphere or torus in the B-spline's layout, which CopyAndAddAnalytics
|
||||
// does not give
|
||||
let analytic = if self.analytic_revolutions && !is_plane {
|
||||
analytic_revolution(
|
||||
k,
|
||||
&copied_orig_edge_curve,
|
||||
self.base_pt,
|
||||
self.axis,
|
||||
self.angle,
|
||||
eps,
|
||||
Some(&new_surf),
|
||||
)?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
new_surf = match analytic {
|
||||
Some(surface) => surface,
|
||||
// Copy pNewSurf, when possible as an analytic surface
|
||||
None => new_surf.copy_and_add_analytics(k)?,
|
||||
};
|
||||
if self.base.make_analytics && !is_analytic {
|
||||
// Copy pNewSurf, when possible as an analytic surface
|
||||
new_surf = new_surf.copy_and_add_analytics(k)?;
|
||||
}
|
||||
|
||||
// eStartTrimCurveParam fixes which of the umin/vmin line of the surface
|
||||
@@ -1465,7 +1470,29 @@ mod tests {
|
||||
n => n,
|
||||
};
|
||||
assert_eq!(analytic, expected, "{name}");
|
||||
// the same loop: roles, orientations, tolerances and the normal
|
||||
let (n_new, n_old) = (new.normal.unwrap(), old.normal.unwrap());
|
||||
if matches!(name, "cone" | "drill point") {
|
||||
// #1463: a line falling along the axis sweeps its cone from the low
|
||||
// end, never inside out (fork 9853e54), laid out as the reversed
|
||||
// line's B-spline: the loop runs the other way round, its normal
|
||||
// the analytic surface's, against the B-spline's
|
||||
if let SurfaceKind::Cone(c) = surface.kind() {
|
||||
assert!(!c.inside_out(), "{name}");
|
||||
}
|
||||
assert!(
|
||||
(n_new + n_old).length() < 1e-12,
|
||||
"{name}: {n_new:?} {n_old:?}"
|
||||
);
|
||||
assert_eq!(new.trim_curves.len(), old.trim_curves.len(), "{name}");
|
||||
assert_eq!(new.face_tol, old.face_tol, "{name}");
|
||||
continue;
|
||||
}
|
||||
// the same loop: roles, orientations, tolerances and the normal (the
|
||||
// analytic surface's, to rounding)
|
||||
assert!(
|
||||
(n_new - n_old).length() < 1e-12,
|
||||
"{name}: {n_new:?} {n_old:?}"
|
||||
);
|
||||
assert_eq!(new.trim_curves.len(), old.trim_curves.len(), "{name}");
|
||||
assert_eq!(new.uv_trim_curves.len(), old.uv_trim_curves.len(), "{name}");
|
||||
assert_eq!(new.orients, old.orients, "{name}");
|
||||
@@ -1484,7 +1511,6 @@ mod tests {
|
||||
),
|
||||
"{name}"
|
||||
);
|
||||
assert_eq!(new.normal, old.normal, "{name}");
|
||||
assert_eq!(new.face_tol, old.face_tol, "{name}");
|
||||
// each 3D trim has the B-spline trim's ends and lies on the new surface
|
||||
for (i, (c, d)) in new.trim_curves.iter().zip(&old.trim_curves).enumerate() {
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
//! Regression tests for tangency-labs/solidcore#1463: a revolve's inside-out cylinder
|
||||
//! comes back from its NURBS with its axis reversed.
|
||||
//!
|
||||
//! Two defects combine (WR-496):
|
||||
//! - `Cone::is_nurb_surface_cylinder` (`SmCylinder::IsNurbSurfaceCylinder`) keeps the
|
||||
//! bottom-knot circle and the NURB's linear direction for an inside-out NURB, so the
|
||||
//! recognised cylinder's Z is reversed and its STEP evaluation is mirrored in height.
|
||||
//! Upstream does the same; `fixes.nurb_cylinder_inside_out_frame` (opt-in) builds the
|
||||
//! frame as `is_nurb_surface_cone` does.
|
||||
//! - With `fixes.revolve_analytic_profile_faces`, the revolve made an inside-out
|
||||
//! cylinder (or cone) for a profile line running against the axis. Fork commit
|
||||
//! tangency-labs/smlib 9853e54, which that flag ports, builds them from the low end
|
||||
//! upwards, never inside out.
|
||||
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::panic,
|
||||
clippy::indexing_slicing
|
||||
)] // test helpers
|
||||
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::geom::attribute::{AttributeStore, ObjectKey};
|
||||
use solidcore::mass::integrator::properties::{brep_volume, PropertyAttributes};
|
||||
use solidcore::nurbs::b_spline_curve::BSplineCurve;
|
||||
use solidcore::nurbs::b_spline_surface_n_lib::p2::EvaluateFlags;
|
||||
use solidcore::surface::cone::Cone;
|
||||
use solidcore::surface::surface::SurfaceKind;
|
||||
use solidcore::sweep::api_brep::create_rotational_sweep;
|
||||
use solidcore::topology::brep::p2::is_manifold_solid;
|
||||
use solidcore::{Kernel, Point3, Vec3};
|
||||
|
||||
/// `SmCone::AssertValid` #3: `((P - foot) x dP/d(sweep)) . Z`, positive for a valid cone.
|
||||
fn test3(c: &Cone) -> f64 {
|
||||
let can = c.canonical();
|
||||
let (o, z) = (can.origin.origin(), can.origin.z_axis());
|
||||
let base = c.rev().base();
|
||||
let uv = base.natural_uv_domain().evaluate(0.45678, 0.45678);
|
||||
let d = base.evaluate(uv, 1, 1, EvaluateFlags::default()).unwrap();
|
||||
let sweep = if can.swap_uv { d[0][1] } else { d[1][0] };
|
||||
(d[0][0] - (o + z * (d[0][0] - o).dot(z)))
|
||||
.cross(sweep)
|
||||
.dot(z)
|
||||
}
|
||||
|
||||
/// The largest distance between the cone's STEP and NURB evaluations over its domain.
|
||||
fn step_nurb_gap(k: &Kernel, c: &Cone) -> f64 {
|
||||
let domain = c.rev().base().natural_uv_domain();
|
||||
let mut worst: f64 = 0.0;
|
||||
for (s, t) in [(0.4567, 0.7654), (0.1, 0.9), (0.9, 0.1), (0.5, 0.5)] {
|
||||
let uv = domain.evaluate(s, t);
|
||||
let step = c.evaluate_point(k, uv).unwrap();
|
||||
let nurb = c.rev().base().evaluate_point(uv).unwrap();
|
||||
worst = worst.max((step - nurb).length());
|
||||
}
|
||||
worst
|
||||
}
|
||||
|
||||
/// Radius 8 about +Z, height 22, its NURB's linear direction from the top (inside out).
|
||||
fn inside_out_cylinder(k: &Kernel) -> Cone {
|
||||
let mut c = Cone::new_cylinder_bounded(
|
||||
Point3::new(0.0, 0.0, 0.0),
|
||||
Vec3::new(1.0, 0.0, 0.0),
|
||||
Vec3::new(0.0, 1.0, 0.0),
|
||||
8.0,
|
||||
0.0,
|
||||
360.0,
|
||||
22.0,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
c.make_nurb(k).unwrap();
|
||||
c
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_1463_inside_out_nurb_cylinder_keeps_its_axis() {
|
||||
let mut k = Kernel::default();
|
||||
let c = inside_out_cylinder(&k);
|
||||
let made = test3(&c);
|
||||
assert!(made > 0.0, "the made cylinder is valid: {made}");
|
||||
|
||||
// the fix on: the cylinder it was given
|
||||
k.fixes.nurb_cylinder_inside_out_frame = true;
|
||||
let r = Cone::is_nurb_surface_cylinder(&k, c.rev().base(), 1.0)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let can = r.canonical();
|
||||
assert!(r.inside_out());
|
||||
assert!(
|
||||
(can.origin.origin() - Vec3::ZERO).length() < 1e-12,
|
||||
"{:?}",
|
||||
can.origin.origin()
|
||||
);
|
||||
assert!(
|
||||
(can.origin.z_axis() - Vec3::Z).length() < 1e-12,
|
||||
"{:?}",
|
||||
can.origin.z_axis()
|
||||
);
|
||||
assert!((r.base_radius() - 8.0).abs() < 1e-12);
|
||||
assert!(
|
||||
(test3(&r) - made).abs() < 1e-9 * made,
|
||||
"{} {made}",
|
||||
test3(&r)
|
||||
);
|
||||
let gap = step_nurb_gap(&k, &r);
|
||||
assert!(gap < 1e-12, "STEP vs NURB {gap}");
|
||||
|
||||
// the fix off: upstream's reversed axis from the bottom-knot circle (z = 22)
|
||||
k.fixes.nurb_cylinder_inside_out_frame = false;
|
||||
let r = Cone::is_nurb_surface_cylinder(&k, c.rev().base(), 1.0)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let can = r.canonical();
|
||||
assert!(r.inside_out());
|
||||
assert!((can.origin.origin() - Vec3::new(0.0, 0.0, 22.0)).length() < 1e-12);
|
||||
assert!((can.origin.z_axis() + Vec3::Z).length() < 1e-12);
|
||||
assert!(
|
||||
(test3(&r) + made).abs() < 1e-9 * made,
|
||||
"{} {made}",
|
||||
test3(&r)
|
||||
);
|
||||
assert!(step_nurb_gap(&k, &r) > 1.0);
|
||||
}
|
||||
|
||||
fn seg(a: Vec3, b: Vec3) -> Curve {
|
||||
Curve::from(BSplineCurve::create_line_segment(3, a, b).unwrap())
|
||||
}
|
||||
|
||||
/// The cones of a revolve of the rig's profile (sweeps/726: 8..30 x 0..22 about Z),
|
||||
/// either winding, turned `angle` degrees, scaled by `scale`; the revolve must be a
|
||||
/// manifold solid of the profile's swept volume.
|
||||
fn revolve_cones(k: &mut Kernel, reverse: bool, angle: f64, scale: f64) -> Vec<Cone> {
|
||||
let p = |x: f64, z: f64| Vec3::new(x * scale, 0.0, z * scale);
|
||||
let mut pts = [
|
||||
p(8.0, 0.0),
|
||||
p(30.0, 0.0),
|
||||
p(30.0, 22.0),
|
||||
p(18.0, 22.0),
|
||||
p(18.0, 10.0),
|
||||
p(8.0, 10.0),
|
||||
];
|
||||
if reverse {
|
||||
pts.reverse();
|
||||
}
|
||||
let curves: Vec<Curve> = (0..pts.len())
|
||||
.map(|i| seg(pts[i], pts[(i + 1) % pts.len()]))
|
||||
.collect();
|
||||
let refs: Vec<Option<&Curve>> = curves.iter().map(Some).collect();
|
||||
let mut b =
|
||||
create_rotational_sweep(k, &refs, Vec3::ZERO, Vec3::Z, angle, angle < 360.0).unwrap();
|
||||
assert!(is_manifold_solid(&b).unwrap(), "{reverse} {angle} {scale}");
|
||||
// the integral of x over the profile: 10 (30^2 - 8^2) / 2 + 12 (30^2 - 18^2) / 2
|
||||
let want = angle.to_radians() * 7636.0 * scale.powi(3);
|
||||
let store = AttributeStore::default();
|
||||
let attrs = PropertyAttributes {
|
||||
store: &store,
|
||||
key_of: &|_| ObjectKey(0),
|
||||
};
|
||||
let got = brep_volume(k, &mut b, 1e-6, &attrs).unwrap();
|
||||
assert!(
|
||||
(got - want).abs() <= 1e-6 * want,
|
||||
"{reverse} {angle} {scale}: volume {got} {want}"
|
||||
);
|
||||
b.face_list()
|
||||
.iter()
|
||||
.filter_map(
|
||||
|&f| match b.face(f).unwrap().surface.as_ref().unwrap().kind() {
|
||||
SurfaceKind::Cone(c) => Some((**c).clone()),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_1463_revolved_cylinders_are_never_inside_out() {
|
||||
for (reverse, angle, scale) in [
|
||||
(false, 360.0, 1.0),
|
||||
(true, 360.0, 1.0),
|
||||
(false, 180.0, 1.0),
|
||||
(true, 180.0, 1.0),
|
||||
(true, 90.0, 1e-3),
|
||||
(false, 270.0, 1e3),
|
||||
] {
|
||||
let case = format!("reverse {reverse}, {angle} deg, scale {scale}");
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.revolve_analytic_profile_faces = true;
|
||||
let cones = revolve_cones(&mut k, reverse, angle, scale);
|
||||
assert_eq!(cones.len(), 3, "{case}");
|
||||
for c in &cones {
|
||||
assert!(c.is_cylinder_class(), "{case}");
|
||||
// fork 9853e54: built from the low end upwards along the revolve's axis
|
||||
assert!(!c.inside_out(), "{case}: an inside-out cylinder");
|
||||
assert!(
|
||||
(c.canonical().origin.z_axis() - Vec3::Z).length() < 1e-12,
|
||||
"{case}"
|
||||
);
|
||||
let made = test3(c);
|
||||
assert!(made > 0.0, "{case}: the made cylinder {made}");
|
||||
// the C++ reader's round trip, with upstream's IsNurbSurfaceCylinder and
|
||||
// with the fix: the same cylinder
|
||||
for fix in [false, true] {
|
||||
k.fixes.nurb_cylinder_inside_out_frame = fix;
|
||||
let r = Cone::is_nurb_surface_cylinder(&k, c.rev().base(), 1.0)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let gap = step_nurb_gap(&k, &r);
|
||||
assert!(gap < 1e-12 * (1.0 + 30.0 * scale), "{case} {fix}: {gap}");
|
||||
assert!(
|
||||
(test3(&r) - made).abs() <= 1e-9 * made,
|
||||
"{case} {fix}: {} {made}",
|
||||
test3(&r)
|
||||
);
|
||||
let (a, b) = (r.canonical().origin, c.canonical().origin);
|
||||
assert!((a.z_axis() - b.z_axis()).length() < 1e-12, "{case} {fix}");
|
||||
}
|
||||
k.fixes.nurb_cylinder_inside_out_frame = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
Updates: WR-448
|
||||
|
||||
The fork commit is public now: tangency-labs/smlib 9853e54 ("a revolved curve is its
|
||||
analytic surface, per profile curve", cherry-pick of 0b143f7's change).
|
||||
Its `SmRotationalSweepGeometry::AnalyticSurface` builds a line's cylinder or cone from
|
||||
the low end of the line along the axis upwards with `CreateCanonical`. It is never
|
||||
inside out. The port laid out a line falling along the axis as an inside-out
|
||||
cylinder or cone, v running from the curve's start. `IsNurbSurfaceCylinder` then
|
||||
recognised that cylinder's NURB with its axis reversed (WR-496, tangency-labs/solidcore#1463).
|
||||
|
||||
With `fixes.revolve_analytic_profile_faces`, `sweep::analytic_revolution` now builds a
|
||||
falling line's cylinder or cone from its low end, not inside out. It is laid out as
|
||||
the B-spline surface of revolution of the reversed line, and checked against that
|
||||
B-spline. `RotationalSweepGeometry::edge_sweep_higher` swaps the analytic surface in
|
||||
before taking the normal, as the fork does (`AnalyticSurface` replaces
|
||||
`sm_CreateRotSrf` there). The loop then comes from the analytic surface's boundary,
|
||||
and the edge-orientation test handles the reversed side.
|
||||
Spheres and tori keep the port's layout, which still uses `inside_out` for a meridian
|
||||
that falls. `IsNurbSurfaceSphere` and `IsNurbSurfaceTorus` have no reported defect for
|
||||
those.
|
||||
@@ -0,0 +1,10 @@
|
||||
#### WR-496. `SmCylinder::IsNurbSurfaceCylinder` reverses an inside-out NURB's axis
|
||||
- **Severity:** Wrong result (a cylinder whose STEP evaluation is mirrored in height against its own NURB: `SmCone::AssertValid` #3 and `SmSurfOfRevolution` #8/#9/#10 fail, and edges miss the surface, `SmBrep` #4 Bad Gap).
|
||||
- **Area:** Surfaces (cones and cylinders)
|
||||
- **Location:** `source/SMLib/src/SmCylinder.cpp`, `SmCylinder::IsNurbSurfaceCylinder` (def. `:233`): the Z axis is `centre(top-knot iso) - centre(bottom-knot iso)` and the origin the bottom-knot circle; when the iso circles turn the other way about that Z it sets `bInsideOut = TRUE` ("Do some correction here", `:320`) and keeps the frame.
|
||||
- **What happens:** an inside-out NURB (its linear direction against the circles' right-hand axis) comes back as a cylinder with Z reversed, from the wrong end. `SmCone::IsNurbSurfaceCone` handles the same NURB correctly: Z against the linear direction, origin at the top-knot circle.
|
||||
- **Trigger:** tangency-labs/solidcore#1463: `SmCylinder(origin 0, X, Y, r 8, 0..360, h 22, inside out)`, its NURB made and recognised: origin z = 22, Z = -Z, test #3 = -7.861 (the made cylinder's +7.861), STEP vs NURB 11.68. A flat-format round trip of any inside-out cylinder (SolidCraft's `AnalyticOfNurbs`) hits it.
|
||||
- **How found:** SolidCraft rig cases (sweeps/726, sweeps/4008, sweeps/1866), issue #1463.
|
||||
- **Confirmed:** by reading the pinned source; the Rust port matches it (difftest `surface.cone_p2` cases `issue1463_cyl_*`, inside-out NURBs with the fix off). The fork (tangency-labs/smlib 5afca53) has the same code.
|
||||
- **Fix:** for an inside-out NURB, build the frame as `IsNurbSurfaceCone` does (origin the top-knot circle, Z its negation).
|
||||
- **Rust port:** opt-in `Kernel::fixes.nurb_cylinder_inside_out_frame` (default `false`, upstream's frame) in `Cone::is_nurb_surface_cylinder`, `crates/solidcore/src/surface/cone/p2.rs`. Test `crates/solidcore/tests/issue_1463_revolve_inside_out_cylinder.rs`. Separately, the opt-in `fixes.revolve_analytic_profile_faces` (WR-448) no longer makes inside-out cylinders or cones: like tangency-labs/smlib 9853e54's `SmRotationalSweepGeometry::AnalyticSurface`, a line falling along the axis is built from its low end. Fixes tangency-labs/solidcore#1463.
|
||||
Reference in New Issue
Block a user