mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 04:52:10 +00:00
Merge pull request #1492 from brett19/fix/revolve-torus-seam-inside-out
fix: revolved torus/sphere never inside out, torus keeps its flag, one-circle revolve seam at the profile (fixes #1490, #1491)
This commit is contained in:
@@ -267,6 +267,8 @@ pub struct UpstreamFixes {
|
||||
pub new_edge_vertex_tolerance_covers_gap: bool,
|
||||
/// A torus rebuilt from its NURB measures its generating interval signed (WR-408, #1112).
|
||||
pub torus_signed_gen_curve_interval: bool,
|
||||
/// The `SmTorus` constructor sets `m_bInsideOut` from its `bInsideOut` (upstream only orients the generating circle with it), and a torus made, rebuilt or read with this fix keeps the flag in its copies (upstream's copy constructor resets it), so the surface and its generator agree (`SmSurfOfRevolution` Test #11 `"InsideOut GenCurve"`) (WR-500, #1490).
|
||||
pub torus_keeps_inside_out: bool,
|
||||
/// `SmTorus::IsNurbSurfaceTorus` measures the major radius from the torus origin (the minor circle's centre projected onto the common line), not from the first sweep circle's centre, which lies off the equator when the NURB's tube does not start there (WR-501, #1470; tangency-labs/smlib@6136a04).
|
||||
pub torus_major_radius_from_origin: bool,
|
||||
/// `SmTorus::CreateIsoParametricCurve` takes a minor circle's direction from its midpoint when its start lies on the axis, not from the start point again, so a horn torus's minor iso circles get a direction and an analytic domain (WR-504, #1514; tangency-labs/smlib#40).
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
//! a whole turn gives each other curve its cylinder, cone, sphere or torus
|
||||
//! ([`analytic_revolution`]) before falling back to the `SmSurfOfRevolution`, and a
|
||||
//! partial turn sweeps with [`RotationalSweepGeometry::set_analytic_revolutions`] on.
|
||||
//! With the fix a whole turn also skips the one-curve special case
|
||||
//! (`sm_CreateAnalyticRotationalSweep`), as fork 9853e54 deletes it: a one-curve profile
|
||||
//! goes through the same per-curve rule, so a whole circle's torus has its seam at the
|
||||
//! profile (#1491).
|
||||
|
||||
use super::{construct::target, geometry::bspline, PrimitiveAccess};
|
||||
use crate::curve::curve::Curve;
|
||||
@@ -315,7 +319,12 @@ impl PrimitiveCreation {
|
||||
self.init_entity_maps();
|
||||
let _ = target(self, brep)?;
|
||||
a.set_editing(brep, true)?;
|
||||
if let [Some(curve)] = curves {
|
||||
// Not upstream (#1491, with `fixes.revolve_analytic_profile_faces`): fork
|
||||
// 9853e54 deletes the one-curve special case (sm_CreateAnalyticRotationalSweep),
|
||||
// whose whole-circle torus puts its seam a quarter turn from the profile; the
|
||||
// per-curve rule below covers a one-curve profile too
|
||||
let one_curve_special_case = !a.kernel(brep)?.fixes.revolve_analytic_profile_faces;
|
||||
if let (true, [Some(curve)]) = (one_curve_special_case, &mut *curves) {
|
||||
if ignored(a.with_primitive_brep(brep, |k, b| {
|
||||
analytic_rotational_sweep(k, b, curve, axis_base, axis)
|
||||
}))?
|
||||
|
||||
@@ -801,6 +801,7 @@ impl Surface {
|
||||
minor_radius: s.minor_radius,
|
||||
// The copy constructor resets this flag; an evaluation copy must not.
|
||||
inside_out: s.inside_out,
|
||||
keeps_inside_out: s.keeps_inside_out,
|
||||
})),
|
||||
SurfaceKind::SurfOfRevolution(s) => {
|
||||
SurfaceKind::SurfOfRevolution(Box::new(s.clone_for_evaluation(depth + 1)?))
|
||||
|
||||
@@ -55,7 +55,10 @@
|
||||
//! default, `Clone` (and [`Torus::copy`]) resets [`Torus::inside_out`] to `false`.
|
||||
//! The constructor also leaves it `false` (its `bInsideOut` only orients the minor
|
||||
//! circle); only [`Torus::rebuild_step_from_nurb_parameters`] and
|
||||
//! [`Torus::read_from_db`] set it.
|
||||
//! [`Torus::read_from_db`] set it. With the opt-in
|
||||
//! `Kernel::fixes.torus_keeps_inside_out` (WR-500, #1490) the constructor sets it
|
||||
//! from `bInsideOut` and a torus made, rebuilt or read that way keeps it in its
|
||||
//! copies (`SmSurfOfRevolution` Test #11 compares it with the generator's).
|
||||
//! - **Arguments.** Out-params become return values; `SmContext` arguments and the
|
||||
//! arguments upstream marks `NotUsed` (`EvaluateSTEP`'s side flags,
|
||||
//! `CreateIsoParametricCurve`'s approximation tolerance, `ReadFromDB`'s type) are
|
||||
@@ -165,17 +168,23 @@ pub struct Torus {
|
||||
pub(crate) minor_radius: f64,
|
||||
/// `m_bInsideOut`.
|
||||
pub(crate) inside_out: bool,
|
||||
/// Not upstream (WR-500, #1490): made, rebuilt or read with
|
||||
/// `Kernel::fixes.torus_keeps_inside_out`, so the copy keeps
|
||||
/// [`Self::inside_out`].
|
||||
pub(crate) keeps_inside_out: bool,
|
||||
}
|
||||
|
||||
/// The copy constructor: copies the surface of revolution and the radii; the
|
||||
/// inside-out flag keeps its default (`false`), as upstream.
|
||||
/// inside-out flag keeps its default (`false`), as upstream, unless the torus keeps it
|
||||
/// (`Kernel::fixes.torus_keeps_inside_out`, WR-500).
|
||||
impl Clone for Torus {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
base: self.base.clone(),
|
||||
major_radius: self.major_radius,
|
||||
minor_radius: self.minor_radius,
|
||||
inside_out: false,
|
||||
inside_out: self.keeps_inside_out && self.inside_out,
|
||||
keeps_inside_out: self.keeps_inside_out,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,6 +221,7 @@ impl Torus {
|
||||
major_radius: UNDEF_DOUBLE,
|
||||
minor_radius: UNDEF_DOUBLE,
|
||||
inside_out: false,
|
||||
keeps_inside_out: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,12 +285,15 @@ impl Torus {
|
||||
};
|
||||
|
||||
// (the self-intersection test of a major radius below the minor radius only
|
||||
// warns upstream)
|
||||
// warns upstream); not upstream (WR-500, opt-in): the surface is inside out
|
||||
// with its generator
|
||||
let keeps_inside_out = k.fixes.torus_keeps_inside_out;
|
||||
Ok(Self {
|
||||
base,
|
||||
major_radius,
|
||||
minor_radius,
|
||||
inside_out: false,
|
||||
inside_out: keeps_inside_out && opts.inside_out,
|
||||
keeps_inside_out,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1337,6 +1350,8 @@ impl Torus {
|
||||
self.major_radius = maj_rad;
|
||||
self.minor_radius = min_rad;
|
||||
self.inside_out = inside_out;
|
||||
// not upstream (WR-500, opt-in): its copies keep the flag
|
||||
self.keeps_inside_out |= k.fixes.torus_keeps_inside_out;
|
||||
self.base.planar_generator = true;
|
||||
self.base.position = ref_frame;
|
||||
self.base.swap_uv = swap_uv;
|
||||
@@ -1412,6 +1427,8 @@ impl Torus {
|
||||
pub fn read_from_db(k: &Kernel, db: &mut DatabaseReader, version: u64) -> Result<Self> {
|
||||
// init output object
|
||||
let mut torus = Self::empty();
|
||||
// not upstream (WR-500, opt-in): its copies keep the flag read
|
||||
torus.keeps_inside_out = k.fixes.torus_keeps_inside_out;
|
||||
|
||||
if db.file_type() == FileType::Ascii {
|
||||
if let Some(s) = db.in_stream_mut() {
|
||||
|
||||
@@ -18,13 +18,14 @@
|
||||
//! 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; 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
|
||||
//! and never inside out, as 9853e54's `AnalyticSurface` (#1463); likewise an arc
|
||||
//! falling round its circle, its sphere or torus never inside out (#1490). 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
|
||||
//! caller keeps the upstream surface. No recursion.
|
||||
|
||||
use crate::curve::curve::Curve;
|
||||
use crate::curve::curve::{Curve, CurveKind};
|
||||
use crate::error::{ignored, Result};
|
||||
use crate::geom::extent2d::Extent2d;
|
||||
use crate::geom::geom_utility::p1::{line_closest_point, line_point_distance};
|
||||
@@ -110,14 +111,13 @@ pub fn analytic_revolution(
|
||||
let reference = match reference {
|
||||
Some(surface) if !reversed => surface,
|
||||
_ => {
|
||||
// a line falling along the axis: laid out as the reversed line's B-spline
|
||||
// a curve falling along the axis or round its circle: laid out as the
|
||||
// reversed curve's B-spline
|
||||
let made_srf = if reversed {
|
||||
let Some(line) =
|
||||
ignored(BSplineCurve::create_line_segment(3, ends.end, ends.start))?
|
||||
else {
|
||||
let Some(back) = ignored(reversed_curve(k, curve, ends.start, ends.end))? else {
|
||||
return Ok(None);
|
||||
};
|
||||
create_rot_srf(k, &Curve::from(line), base_pt, axis, angle_deg)
|
||||
create_rot_srf(k, &back, base_pt, axis, angle_deg)
|
||||
} else {
|
||||
create_rot_srf(k, curve, base_pt, axis, angle_deg)
|
||||
};
|
||||
@@ -242,8 +242,22 @@ fn in_half_plane(
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// `curve` run the other way (from `end` to `start`): a line its segment, another
|
||||
/// curve a copy with its parameterisation reversed.
|
||||
fn reversed_curve(k: &Kernel, curve: &Curve, start: Point3, end: Point3) -> Result<Curve> {
|
||||
if matches!(curve.kind(), CurveKind::Line(_)) {
|
||||
return Ok(Curve::from(BSplineCurve::create_line_segment(
|
||||
3, end, start,
|
||||
)?));
|
||||
}
|
||||
let mut back = curve.clone();
|
||||
let _ = back.reverse_parameterization(k, &curve.natural_interval())?;
|
||||
Ok(back)
|
||||
}
|
||||
|
||||
/// 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).
|
||||
/// is laid out as the reversed curve's B-spline (a line falling along the axis, an arc
|
||||
/// falling round its circle).
|
||||
fn candidate(
|
||||
k: &Kernel,
|
||||
curve: &Curve,
|
||||
@@ -372,12 +386,15 @@ fn candidate(
|
||||
};
|
||||
let (start_lat, end_lat) = (latitude(ends.start), latitude(ends.end));
|
||||
// the meridian rises from x towards the axis (x cross axis is -y)
|
||||
// #1490: never inside out (fork 9853e54 builds the sphere over its latitude
|
||||
// range); a falling meridian is laid out as the reversed arc's B-spline
|
||||
let rising = frame.z_axis().dot(-y) > 0.0;
|
||||
let (low, high, inside_out) = if rising {
|
||||
(start_lat, end_lat, false)
|
||||
let (low, high) = if rising {
|
||||
(start_lat, end_lat)
|
||||
} else {
|
||||
(end_lat, start_lat, true)
|
||||
(end_lat, start_lat)
|
||||
};
|
||||
let inside_out = false;
|
||||
if high - low <= EFF_ZERO {
|
||||
return Ok(None);
|
||||
}
|
||||
@@ -395,7 +412,7 @@ fn candidate(
|
||||
},
|
||||
)?;
|
||||
sphere.make_nurb(k)?;
|
||||
return Ok(Some((Surface::from(sphere), false)));
|
||||
return Ok(Some((Surface::from(sphere), !rising)));
|
||||
}
|
||||
|
||||
// a torus: its minor circle is the arc's, centred off the axis
|
||||
@@ -411,12 +428,16 @@ fn candidate(
|
||||
let d = p - centre;
|
||||
d.dot(axis).atan2(d.dot(x)) * to_deg
|
||||
};
|
||||
// #1490: never inside out (fork 9853e54's `CreateCanonical(.., FALSE, FALSE, ..)`
|
||||
// over the tube angle range from its low end); an arc falling round its minor
|
||||
// circle is laid out as the reversed arc's B-spline
|
||||
let rising = frame.z_axis().dot(-y) > 0.0;
|
||||
let (start, inside_out) = if rising {
|
||||
(minor_angle(ends.start), false)
|
||||
let start = if rising {
|
||||
minor_angle(ends.start)
|
||||
} else {
|
||||
(minor_angle(ends.end), true)
|
||||
minor_angle(ends.end)
|
||||
};
|
||||
let inside_out = false;
|
||||
let (low, high) = tube_range(start, sweep);
|
||||
let mut torus = Torus::new(
|
||||
k,
|
||||
@@ -433,7 +454,7 @@ fn candidate(
|
||||
},
|
||||
)?;
|
||||
torus.make_nurb(k)?;
|
||||
Ok(Some((Surface::from(torus), false)))
|
||||
Ok(Some((Surface::from(torus), !rising)))
|
||||
}
|
||||
|
||||
/// What a surface of revolution's natural domain shows of its layout.
|
||||
|
||||
@@ -1471,16 +1471,23 @@ mod tests {
|
||||
};
|
||||
assert_eq!(analytic, expected, "{name}");
|
||||
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}");
|
||||
}
|
||||
// #1463, #1490: never inside out (fork 9853e54)
|
||||
let inside_out = match surface.kind() {
|
||||
SurfaceKind::Cone(c) => c.inside_out(),
|
||||
SurfaceKind::Sphere(s) => s.inside_out(),
|
||||
SurfaceKind::Torus(t) => t.inside_out(),
|
||||
_ => false,
|
||||
};
|
||||
assert!(!inside_out, "{name}");
|
||||
if (n_new - n_old).length() >= 1e-12 {
|
||||
// a curve falling along the axis or round its circle is laid out as the
|
||||
// reversed curve's B-spline: the loop runs the other way round, its
|
||||
// normal the analytic surface's at the curve's other end
|
||||
assert!(
|
||||
(n_new + n_old).length() < 1e-12,
|
||||
matches!(
|
||||
name,
|
||||
"cone" | "drill point" | "fillet" | "minor circle from the bottom"
|
||||
),
|
||||
"{name}: {n_new:?} {n_old:?}"
|
||||
);
|
||||
assert_eq!(new.trim_curves.len(), old.trim_curves.len(), "{name}");
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
//! Regression tests for tangency-labs/solidcore#1490: a revolve's torus is built inside
|
||||
//! out, and its flag is lost (`SmSurfOfRevolution` Test #11 `"InsideOut GenCurve"`).
|
||||
//!
|
||||
//! - With `fixes.revolve_analytic_profile_faces`, `analytic_revolution` made an arc
|
||||
//! falling round its minor circle an inside-out torus (and a falling meridian an
|
||||
//! inside-out sphere). Fork tangency-labs/smlib 9853e54 builds them with
|
||||
//! `CreateCanonical(.., FALSE, FALSE, ..)`, never inside out.
|
||||
//! - Upstream's `SmTorus` constructor never sets `m_bInsideOut` and its copy
|
||||
//! constructor resets it, so an inside-out torus fails Test #11 (WR-500); the opt-in
|
||||
//! `fixes.torus_keeps_inside_out` keeps the flag.
|
||||
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::panic,
|
||||
clippy::indexing_slicing
|
||||
)] // test helpers
|
||||
|
||||
use solidcore::curve::curve::{Curve, CurveKind};
|
||||
use solidcore::geom::assert_array::{AssertArray, ObjectKey};
|
||||
use solidcore::geom::attribute::AttributeStore;
|
||||
use solidcore::geom::axis2_placement::Axis2Placement;
|
||||
use solidcore::geom::extent2d::Extent2d;
|
||||
use solidcore::mass::integrator::properties::{brep_volume, PropertyAttributes};
|
||||
use solidcore::nurbs::b_spline_curve::{BSplineCurve, NurbCircleParam};
|
||||
use solidcore::surface::surface::{Surface, SurfaceKind};
|
||||
use solidcore::surface::torus::{Torus, TorusOptions};
|
||||
use solidcore::sweep::api_brep::create_rotational_sweep;
|
||||
use solidcore::{Kernel, Point3, Vec3};
|
||||
|
||||
/// Whether `SmSurfOfRevolution` Test #11 fails on `s`.
|
||||
fn test11_fails(k: &Kernel, s: &Surface, what: &str) -> bool {
|
||||
let mut list = AssertArray::new();
|
||||
let _ = s
|
||||
.assert_valid(k, Some(&mut list), 0, ObjectKey(1))
|
||||
.unwrap_or_else(|e| panic!("{what}: {e:?}"));
|
||||
list.reports().iter().any(|r| r.test_index == 11 && !r.ok)
|
||||
}
|
||||
|
||||
/// The torus's and its generating circle's inside-out flags.
|
||||
fn flags(t: &Torus) -> (bool, bool) {
|
||||
let gen = match t.base().gen_curve().unwrap().kind() {
|
||||
CurveKind::Ellipse(e) => e.inside_out(),
|
||||
_ => panic!("the generator is a circle"),
|
||||
};
|
||||
(t.inside_out(), gen)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_1490_inside_out_torus_keeps_its_flag() {
|
||||
for fix in [false, true] {
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.torus_keeps_inside_out = fix;
|
||||
// as the rig runs: the recognised torus keeps the tested NURB (SetFromGwNurb,
|
||||
// whose rebuild sets the flag)
|
||||
k.fixes.found_torus_sphere_keep_tested_nurb = true;
|
||||
let mut t = Torus::new(
|
||||
&k,
|
||||
Point3::new(0.0, 0.0, 0.0),
|
||||
Vec3::new(1.0, 0.0, 0.0),
|
||||
Vec3::new(0.0, 1.0, 0.0),
|
||||
Extent2d::from_bounds(0.0, -90.0, 270.0, 90.0),
|
||||
20.0,
|
||||
4.0,
|
||||
TorusOptions {
|
||||
swap_uv: false,
|
||||
inside_out: true,
|
||||
circle_nurb: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
t.make_nurb(&k).unwrap();
|
||||
// made, copied, recognised from its NURB and the recognised torus's copy
|
||||
let found = Torus::is_nurb_surface_torus(&k, t.base().base(), 1.0)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let (copy, found_copy) = (t.clone(), found.clone());
|
||||
let cases = [
|
||||
("made", t),
|
||||
("copy", copy),
|
||||
("recognised", found),
|
||||
("recognised copy", found_copy),
|
||||
];
|
||||
for (name, s) in cases {
|
||||
// upstream: the constructor leaves the surface's flag FALSE, only the
|
||||
// rebuild sets it, and copies reset it
|
||||
let kept = fix || name == "recognised";
|
||||
assert_eq!(flags(&s), (kept, true), "{name} fix {fix}");
|
||||
assert_eq!(
|
||||
test11_fails(&k, &Surface::from(s), &format!("{name} {fix}")),
|
||||
!kept,
|
||||
"{name} fix {fix}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn line(a: Vec3, b: Vec3) -> Curve {
|
||||
Curve::from(BSplineCurve::create_line_segment(3, a, b).unwrap())
|
||||
}
|
||||
|
||||
/// The rig's sweeps/4007 profile: the line x = 14, z -14..14 and the half circle of
|
||||
/// radius 14 about (14, 0, 0) through (28, 0, 0), wound either way, turned 270 degrees
|
||||
/// about Z: the horn torus R = r = 14.
|
||||
fn horn_torus(k: &mut Kernel, down: bool) -> solidcore::Brep {
|
||||
let (top, bottom) = (Vec3::new(14.0, 0.0, 14.0), Vec3::new(14.0, 0.0, -14.0));
|
||||
let centre = Vec3::new(14.0, 0.0, 0.0);
|
||||
// the arc from the top round to the bottom (down) or from the bottom up
|
||||
let x = if down { Vec3::Z } else { -Vec3::Z };
|
||||
let arc = Curve::from(
|
||||
BSplineCurve::create_circle_segment(
|
||||
3,
|
||||
&Axis2Placement::new(centre, x, Vec3::X),
|
||||
14.0,
|
||||
0.0,
|
||||
180.0,
|
||||
NurbCircleParam::Quadratic,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let side = if down {
|
||||
line(bottom, top)
|
||||
} else {
|
||||
line(top, bottom)
|
||||
};
|
||||
create_rotational_sweep(
|
||||
k,
|
||||
&[Some(&side), Some(&arc)],
|
||||
Vec3::ZERO,
|
||||
Vec3::Z,
|
||||
270.0,
|
||||
true,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_1490_revolved_torus_is_never_inside_out() {
|
||||
for down in [false, true] {
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.revolve_analytic_profile_faces = true;
|
||||
let mut b = horn_torus(&mut k, down);
|
||||
let tori: Vec<Torus> = b
|
||||
.face_list()
|
||||
.iter()
|
||||
.filter_map(
|
||||
|&f| match b.face(f).unwrap().surface.as_ref().unwrap().kind() {
|
||||
SurfaceKind::Torus(t) => Some((**t).clone()),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(tori.len(), 1, "down {down}");
|
||||
let t = &tori[0];
|
||||
// fork 9853e54: never inside out, and so the surface and generator agree
|
||||
assert_eq!(flags(t), (false, false), "down {down}");
|
||||
assert!(
|
||||
!test11_fails(&k, &Surface::from(t.clone()), "revolve"),
|
||||
"down {down}"
|
||||
);
|
||||
// Pappus: half disc pi 14^2 / 2 at 14 + 4 14 / (3 pi) from the axis, 3/2 pi
|
||||
let area = std::f64::consts::PI * 196.0 / 2.0;
|
||||
let centroid = 14.0 + 56.0 / (3.0 * std::f64::consts::PI);
|
||||
let want = area * centroid * 1.5 * std::f64::consts::PI;
|
||||
let store = AttributeStore::default();
|
||||
let attrs = PropertyAttributes {
|
||||
store: &store,
|
||||
key_of: &|_| solidcore::geom::attribute::ObjectKey(0),
|
||||
};
|
||||
let got = brep_volume(&k, &mut b, 1e-6, &attrs).unwrap();
|
||||
assert!(
|
||||
(got - want).abs() <= 1e-6 * want,
|
||||
"down {down}: {got} {want}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_1490_revolved_sphere_is_never_inside_out() {
|
||||
// a quarter disc of radius 3 (rim, arc, axis), wound either way, turned 270 degrees
|
||||
for down in [false, true] {
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.revolve_analytic_profile_faces = true;
|
||||
let (o, rim, pole) = (
|
||||
Vec3::ZERO,
|
||||
Vec3::new(3.0, 0.0, 0.0),
|
||||
Vec3::new(0.0, 0.0, 3.0),
|
||||
);
|
||||
// the meridian from the rim up to the pole, or from the pole down
|
||||
let (x, y) = if down {
|
||||
(Vec3::Z, Vec3::X)
|
||||
} else {
|
||||
(Vec3::X, Vec3::Z)
|
||||
};
|
||||
let arc = Curve::from(
|
||||
BSplineCurve::create_circle_segment(
|
||||
3,
|
||||
&Axis2Placement::new(o, x, y),
|
||||
3.0,
|
||||
0.0,
|
||||
90.0,
|
||||
NurbCircleParam::Quadratic,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let curves = if down {
|
||||
[line(o, pole), arc, line(rim, o)]
|
||||
} else {
|
||||
[line(o, rim), arc, line(pole, o)]
|
||||
};
|
||||
let refs: Vec<Option<&Curve>> = curves.iter().map(Some).collect();
|
||||
let mut b =
|
||||
create_rotational_sweep(&mut k, &refs, Vec3::ZERO, Vec3::Z, 270.0, true).unwrap();
|
||||
let spheres: Vec<bool> = b
|
||||
.face_list()
|
||||
.iter()
|
||||
.filter_map(
|
||||
|&f| match b.face(f).unwrap().surface.as_ref().unwrap().kind() {
|
||||
SurfaceKind::Sphere(s) => Some(s.inside_out()),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(spheres, [false], "down {down}");
|
||||
let want = 2.0 / 3.0 * std::f64::consts::PI * 27.0 * 0.75;
|
||||
let store = AttributeStore::default();
|
||||
let attrs = PropertyAttributes {
|
||||
store: &store,
|
||||
key_of: &|_| solidcore::geom::attribute::ObjectKey(0),
|
||||
};
|
||||
let got = brep_volume(&k, &mut b, 1e-6, &attrs).unwrap();
|
||||
assert!(
|
||||
(got - want).abs() <= 1e-6 * want,
|
||||
"down {down}: {got} {want}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
//! Regression test for tangency-labs/solidcore#1491: a full-turn revolve of one whole
|
||||
//! circle went through upstream's one-curve special case
|
||||
//! (`sm_CreateAnalyticRotationalSweep`), whose torus takes X from the circle's normal,
|
||||
//! so its seam lies a quarter turn from the profile. Fork tangency-labs/smlib 9853e54
|
||||
//! deletes that case; with `fixes.revolve_analytic_profile_faces` the circle goes
|
||||
//! through the per-curve rule, its torus's X radial into the profile's half-plane.
|
||||
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::panic,
|
||||
clippy::indexing_slicing
|
||||
)] // test helpers
|
||||
|
||||
use solidcore::boolean::api_brep::boolean_union;
|
||||
use solidcore::curve::curve::Curve;
|
||||
use solidcore::curve::ellipse::Ellipse;
|
||||
use solidcore::geom::attribute::AttributeStore;
|
||||
use solidcore::geom::axis2_placement::Axis2Placement;
|
||||
use solidcore::geom::extent1d::Extent1d;
|
||||
use solidcore::mass::integrator::properties::{brep_volume, PropertyAttributes};
|
||||
use solidcore::nurbs::b_spline_curve::BSplineCurve;
|
||||
use solidcore::surface::surface::SurfaceKind;
|
||||
use solidcore::sweep::api_brep::create_rotational_sweep;
|
||||
use solidcore::topology::vertex::point;
|
||||
use solidcore::{Brep, Kernel, Point3, Vec3};
|
||||
use std::f64::consts::PI;
|
||||
|
||||
fn volume(k: &Kernel, b: &mut Brep) -> f64 {
|
||||
let store = AttributeStore::default();
|
||||
let attrs = PropertyAttributes {
|
||||
store: &store,
|
||||
key_of: &|_| solidcore::geom::attribute::ObjectKey(0),
|
||||
};
|
||||
brep_volume(k, b, 1e-6, &attrs).unwrap()
|
||||
}
|
||||
|
||||
/// The circle of radius 3 about (-30, 0, 0) in the plane y = 0, from (-27, 0, 0) (or
|
||||
/// wound the other way), turned a whole turn about `axis`.
|
||||
fn round_bar(k: &mut Kernel, axis: Vec3, other_way: bool) -> Brep {
|
||||
let y = if other_way { -Vec3::Z } else { Vec3::Z };
|
||||
let frame = Axis2Placement::new(Point3::new(-30.0, 0.0, 0.0), Vec3::X, y);
|
||||
let c = Curve::from(
|
||||
Ellipse::create_canonical_circle(
|
||||
k,
|
||||
&frame,
|
||||
3.0,
|
||||
Some(&Extent1d::from_min_max(0.0, 360.0)),
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
create_rotational_sweep(k, &[Some(&c)], Point3::ZERO, axis, 360.0, true).unwrap()
|
||||
}
|
||||
|
||||
/// The torus face's frame X and Z, and the B-rep's vertices.
|
||||
fn seam(b: &Brep) -> (Vec3, Vec3, Vec<Point3>) {
|
||||
let faces = b.face_list();
|
||||
assert_eq!(faces.len(), 1);
|
||||
let SurfaceKind::Torus(t) = b.face(faces[0]).unwrap().surface.as_ref().unwrap().kind() else {
|
||||
panic!("a torus face");
|
||||
};
|
||||
let frame = t.base().position();
|
||||
let vertices = b
|
||||
.vertex_list()
|
||||
.iter()
|
||||
.map(|&v| point(b, v).unwrap())
|
||||
.collect();
|
||||
(frame.x_axis(), frame.z_axis(), vertices)
|
||||
}
|
||||
|
||||
fn near(a: Vec3, b: Vec3) -> bool {
|
||||
(a - b).length() < 1e-9
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_1491_whole_circle_revolve_has_its_seam_at_the_profile() {
|
||||
let want = 2.0 * PI * 30.0 * PI * 9.0;
|
||||
for (axis, other_way) in [(Vec3::Z, false), (Vec3::Z, true), (-Vec3::Z, false)] {
|
||||
let case = format!("axis {axis:?}, other way {other_way}");
|
||||
// the fix: X from the axis into the profile's half-plane, Z the turn axis, the
|
||||
// seam vertex the circle's start
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.revolve_analytic_profile_faces = true;
|
||||
let mut b = round_bar(&mut k, axis, other_way);
|
||||
let (x, z, vertices) = seam(&b);
|
||||
assert!(near(x, -Vec3::X), "{case}: X {x:?}");
|
||||
assert!(near(z, axis), "{case}: Z {z:?}");
|
||||
assert!(!vertices.is_empty(), "{case}");
|
||||
for v in &vertices {
|
||||
assert!(
|
||||
near(*v, Point3::new(-27.0, 0.0, 0.0)),
|
||||
"{case}: vertex {v:?}"
|
||||
);
|
||||
}
|
||||
let got = volume(&k, &mut b);
|
||||
assert!((got - want).abs() <= 1e-6 * want, "{case}: {got} {want}");
|
||||
|
||||
// upstream (the special case): X the circle's plane normal, a quarter turn off
|
||||
let mut k = Kernel::default();
|
||||
let b = round_bar(&mut k, axis, other_way);
|
||||
let (x, _, vertices) = seam(&b);
|
||||
assert!(
|
||||
x.dot(Vec3::Y).abs() > 1.0 - 1e-9,
|
||||
"{case}: upstream X {x:?}"
|
||||
);
|
||||
assert!(
|
||||
vertices.iter().all(|v| v.x.abs() < 1e-9),
|
||||
"{case}: upstream vertices {vertices:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_1491_seam_at_the_profile_unions_with_a_square_ring() {
|
||||
// the rig's sweeps/3006: the round bar joined to the 5 x 5 square swept round the
|
||||
// same circle (the square's seam in the plane x = 0)
|
||||
let mut k = Kernel::default();
|
||||
k.fixes.revolve_analytic_profile_faces = true;
|
||||
let round = round_bar(&mut k, Vec3::Z, false);
|
||||
let p = |y: f64, z: f64| Vec3::new(0.0, y, z);
|
||||
let corners = [p(-27.5, -2.5), p(-32.5, -2.5), p(-32.5, 2.5), p(-27.5, 2.5)];
|
||||
let sides: Vec<Curve> = (0..4)
|
||||
.map(|i| {
|
||||
Curve::from(
|
||||
BSplineCurve::create_line_segment(3, corners[i], corners[(i + 1) % 4]).unwrap(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let refs: Vec<Option<&Curve>> = sides.iter().map(Some).collect();
|
||||
let square =
|
||||
create_rotational_sweep(&mut k, &refs, Point3::ZERO, Vec3::Z, 360.0, true).unwrap();
|
||||
let mut joined = boolean_union(&mut k, round, square).unwrap();
|
||||
// the section: the disc of radius 3 and the square of half side 2.5; their
|
||||
// overlap is 4 quarters, each c h + r^2 (asin(h / r) - asin(c / r)) / 2 with
|
||||
// c = sqrt(r^2 - h^2) (23.772778)
|
||||
let (h, r) = (2.5_f64, 3.0_f64);
|
||||
let c = (r * r - h * h).sqrt();
|
||||
let overlap = 4.0 * (c * h + 0.5 * r * r * ((h / r).asin() - (c / r).asin()));
|
||||
let want = 2.0 * PI * 30.0 * (25.0 + 9.0 * PI - overlap);
|
||||
let got = volume(&k, &mut joined);
|
||||
assert!(
|
||||
(got - want).abs() <= 1e-6 * want,
|
||||
"{got} {want} (overlap {overlap})"
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,724 @@
|
||||
{
|
||||
"cases": [
|
||||
{
|
||||
"name": "issue1491_circle_py_s1_360",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "circle",
|
||||
"center": [
|
||||
-30.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 3.0
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 360.0,
|
||||
"cap": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_circle_py_s1_270",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "circle",
|
||||
"center": [
|
||||
-30.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 3.0
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 270.0,
|
||||
"cap": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_circle_py_s1em3_360",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "circle",
|
||||
"center": [
|
||||
-0.03,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 0.003
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 360.0,
|
||||
"cap": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_circle_py_s1em3_270",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "circle",
|
||||
"center": [
|
||||
-0.03,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 0.003
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 270.0,
|
||||
"cap": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_circle_py_s1e3_360",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "circle",
|
||||
"center": [
|
||||
-30000.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 3000.0
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 360.0,
|
||||
"cap": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_circle_py_s1e3_270",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "circle",
|
||||
"center": [
|
||||
-30000.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 3000.0
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 270.0,
|
||||
"cap": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_circle_my_s1_360",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "circle",
|
||||
"center": [
|
||||
-30.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 3.0
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
-1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 360.0,
|
||||
"cap": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_circle_my_s1_270",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "circle",
|
||||
"center": [
|
||||
-30.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 3.0
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
-1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 270.0,
|
||||
"cap": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_circle_my_s1em3_360",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "circle",
|
||||
"center": [
|
||||
-0.03,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 0.003
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
-1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 360.0,
|
||||
"cap": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_circle_my_s1em3_270",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "circle",
|
||||
"center": [
|
||||
-0.03,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 0.003
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
-1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 270.0,
|
||||
"cap": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_circle_my_s1e3_360",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "circle",
|
||||
"center": [
|
||||
-30000.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 3000.0
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
-1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 360.0,
|
||||
"cap": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_circle_my_s1e3_270",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "circle",
|
||||
"center": [
|
||||
-30000.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 3000.0
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
-1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 270.0,
|
||||
"cap": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_analytic_circle_py_s1_360",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "analytic_circle",
|
||||
"center": [
|
||||
-30.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 3.0
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 360.0,
|
||||
"cap": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_analytic_circle_py_s1_270",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "analytic_circle",
|
||||
"center": [
|
||||
-30.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 3.0
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 270.0,
|
||||
"cap": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_analytic_circle_py_s1em3_360",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "analytic_circle",
|
||||
"center": [
|
||||
-0.03,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 0.003
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 360.0,
|
||||
"cap": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_analytic_circle_py_s1em3_270",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "analytic_circle",
|
||||
"center": [
|
||||
-0.03,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 0.003
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 270.0,
|
||||
"cap": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_analytic_circle_py_s1e3_360",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "analytic_circle",
|
||||
"center": [
|
||||
-30000.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 3000.0
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 360.0,
|
||||
"cap": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_analytic_circle_py_s1e3_270",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "analytic_circle",
|
||||
"center": [
|
||||
-30000.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 3000.0
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 270.0,
|
||||
"cap": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_analytic_circle_my_s1_360",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "analytic_circle",
|
||||
"center": [
|
||||
-30.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 3.0
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
-1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 360.0,
|
||||
"cap": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_analytic_circle_my_s1_270",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "analytic_circle",
|
||||
"center": [
|
||||
-30.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 3.0
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
-1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 270.0,
|
||||
"cap": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_analytic_circle_my_s1em3_360",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "analytic_circle",
|
||||
"center": [
|
||||
-0.03,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 0.003
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
-1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 360.0,
|
||||
"cap": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_analytic_circle_my_s1em3_270",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "analytic_circle",
|
||||
"center": [
|
||||
-0.03,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 0.003
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
-1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 270.0,
|
||||
"cap": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_analytic_circle_my_s1e3_360",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "analytic_circle",
|
||||
"center": [
|
||||
-30000.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 3000.0
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
-1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 360.0,
|
||||
"cap": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "issue1491_analytic_circle_my_s1e3_270",
|
||||
"upstream_ref": "https://github.com/tangency-labs/solidcore/issues/1491; SmPrimitiveCreation::CreateRotationalSweep one-curve case (sm_CreateAnalyticRotationalSweep)",
|
||||
"inputs": {
|
||||
"op": "rotational",
|
||||
"curves": [
|
||||
{
|
||||
"kind": "analytic_circle",
|
||||
"center": [
|
||||
-30000.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"radius": 3000.0
|
||||
}
|
||||
],
|
||||
"base": [
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"axis": [
|
||||
0,
|
||||
-1,
|
||||
0
|
||||
],
|
||||
"angle_deg": 270.0,
|
||||
"cap": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
Updates: WR-448
|
||||
|
||||
Follow-up to the 2026-10-29 addendum (tangency-labs/solidcore#1490, #1491).
|
||||
|
||||
- **Spheres and tori are never inside out either.** With `fixes.revolve_analytic_profile_faces`, `sweep::analytic_revolution` no longer builds a sphere or torus inside out. The fork's `AnalyticSurface` (tangency-labs/smlib 9853e54) builds the sphere over its latitude range and the torus with `CreateCanonical(.., FALSE, FALSE, ..)`. When an arc falls round its circle, its surface is laid out as the B-spline of the reversed arc. The earlier layout gave an inside-out torus whose surface flag the `SmTorus` constructor drops (WR-500), so it failed Test #11.
|
||||
- **A one-curve profile no longer takes the special case.** 9853e54 deletes `sm_CreateAnalyticRotationalSweep`. With the fix, a whole turn of a one-curve profile now skips `analytic_rotational_sweep` and goes through the same per-curve rule as a longer profile.
|
||||
- A whole circle's torus then has X radial into the profile's half-plane, so its seam lies at the profile.
|
||||
- Upstream's special case takes X from the circle's plane normal, which puts the seam a quarter turn away.
|
||||
@@ -0,0 +1,17 @@
|
||||
#### WR-500. The `SmTorus` constructor and copy constructor drop `m_bInsideOut`
|
||||
- **Severity:** Wrong result (the surface and its generating circle disagree on inside-out: `SmSurfOfRevolution` Test #11 "InsideOut GenCurve: Bad GenCurve and Surface bInsideOut bit values don't match").
|
||||
- **Area:** Surfaces (tori)
|
||||
- **Location:**
|
||||
- `source/SMLib/src/SmTorus.cpp`, `SmTorus::SmTorus(..., bInsideOut, pOptCircleNurb)` (`:31-104`): passes `bInsideOut` only to the generating circle (`SmCircle::CreateCanonical`) and leaves `m_bInsideOut` at its default `FALSE` (`SmTorus.h:111`).
|
||||
- The copy constructor (`:109-117`) does not copy the member either.
|
||||
- Only `RebuildSTEPFromNURBParameters` and `ReadFromDB` set it.
|
||||
- **What happens:** a torus made inside out fails Test #11 as soon as it is made. A torus whose rebuild found it inside out fails again after its first copy. `SmSphere`'s constructor and copy keep the flag.
|
||||
- **Trigger:** tangency-labs/solidcore#1490: `SmTorus(origin 0, X, Y, [0,270] x [-90,90], R 20, r 4, inside out)` gives surface FALSE and generator TRUE. Five SolidCraft rig cases (sweeps/3897, 4007, 5195, 5325, crashes/4542) fail Test #11 on a copied inside-out torus.
|
||||
- **How found:** SolidCraft rig, issue #1490.
|
||||
- **Confirmed:** by reading the pinned source. The Rust port matches it; difftest `surface.torus` covers it with cases `issue1490_*` (construct, copy and `IsNurbSurfaceTorus` of inside-out tori, fix off). The fork (tangency-labs/smlib 5afca53) has the same code.
|
||||
- **Fix:** the constructor sets `m_bInsideOut = bInsideOut`, and the copy constructor copies it.
|
||||
- **Rust port:** opt-in `Kernel::fixes.torus_keeps_inside_out` (default `false`, upstream's behaviour) in `crates/solidcore/src/surface/torus.rs`:
|
||||
- `Torus::new` sets the flag.
|
||||
- A torus made, rebuilt or read with the fix keeps it in `Clone`.
|
||||
- Test `crates/solidcore/tests/issue_1490_revolve_torus_inside_out.rs`.
|
||||
- Fixes tangency-labs/solidcore#1490.
|
||||
Reference in New Issue
Block a user