Merge pull request #3068 from tangency-labs/fix/extend-label-drop

extend: the pleat offset: a label its form does not prove is dropped, one weight ratio per turn
This commit is contained in:
bherbruck
2026-11-07 01:04:40 -05:00
committed by GitHub
5 changed files with 212 additions and 11 deletions
+22 -11
View File
@@ -586,6 +586,12 @@ fn certify_iso(
(middle.x, Direction::V)
};
let curve = extract_iso_crv(net, t, dir)?;
// the iso is interpolated between the net's rows, so its points are known to the rounding
// at the size of those rows, not at their own: a short arc's middle iso between rulings
// extended 500 mm each way
let rows = net.cpts().iter().flatten().fold(0.0_f64, |m, p| {
m.max(p.x.abs()).max(p.y.abs()).max(p.z.abs())
});
let points: Vec<(Vec3, f64, f64)> = curve
.cpts()
.iter()
@@ -593,7 +599,7 @@ fn certify_iso(
let w = if p.w == NOW { 1.0 } else { p.w };
let h = Vec3::new(p.x, p.y, p.z);
// X W from the origin, and the size of its terms
let mag = p.x.abs().max(p.y.abs()).max(p.z.abs()) + f.size * w.abs();
let mag = rows + f.size * w.abs();
(h - f.origin * w, w, mag)
})
.collect();
@@ -753,17 +759,24 @@ fn on_definition(
});
}
let (origin, axis) = (f.origin, f.axis);
// every row turns by the same angle (the form is a tensor product): take it from
// the row farthest from the axis, since a row on the axis (a pole) has no angle
let mut far = (0.0_f64, 0.0_f64);
for (&(j, _), row) in joint.iter().zip(&past) {
let &[_, (end, _)] = row.as_slice() else {
// every row turns by the same angle and ends at the same weight ratio (the form is
// a tensor product): take them from the row farthest from the axis, since a row on
// the axis (a pole) has no angle. A ratio per row would differ by the rounding of
// the continuation, and rows far apart along the axis turn that into a height:
// the iso between them leaves its plane
let mut far = (0.0_f64, 0.0_f64, 1.0_f64);
for (&(j, wj), row) in joint.iter().zip(&past) {
let &[_, (end, wn)] = row.as_slice() else {
return Err(SolidError::sm_err(OP));
};
let centre = origin + axis * axis.dot(j - origin);
let (r0, r1) = (j - centre, end - centre);
if r0.length() > far.0 {
far = (r0.length(), axis.dot(r0.cross(r1)).atan2(r0.dot(r1)));
far = (
r0.length(),
axis.dot(r0.cross(r1)).atan2(r0.dot(r1)),
wn / wj,
);
}
}
if !(far.0.is_finite() && far.0 > 0.0) {
@@ -774,10 +787,8 @@ fn on_definition(
if half.cos().is_nan() || half.cos() <= 0.0 {
return Ok(None);
}
for (&(j, wj), row) in joint.iter().zip(&past) {
let &[_, (_, wn)] = row.as_slice() else {
return Err(SolidError::sm_err(OP));
};
for &(j, wj) in &joint {
let wn = wj * far.2;
let centre = origin + axis * axis.dot(j - origin);
let r0 = j - centre;
let turn = |phi: f64| r0 * phi.cos() + axis.cross(r0) * phi.sin();
@@ -41,6 +41,7 @@ use crate::error::{Result, SolidError};
use crate::kernel::Kernel;
use crate::math::INFINITE_PARAMETER;
use crate::nurbs::b_spline_surface::SurfParamType;
use crate::surface::implicit::AnalyticSupport;
use crate::surface::side::{Across, BoxSide};
use crate::surface::surface::{Surface, SurfaceKind};
@@ -145,6 +146,16 @@ impl Surface {
distance: f64,
keeping: bool,
) -> Result<ExtendedSides> {
// a label its form does not prove is dropped (owner 22254, as a face's read label is,
// `face_surface::labelled`): the surface is its exact NURBS, extended as one. A body
// can carry such a label from recognition, rounding in its frame (an axis 1.4e-13 off
// straight rows 94 long). The form has no label, so this goes one level down.
if !AnalyticSupport::label_stands(k, self)? {
if let Some(form) = crate::blend::fillet_section::station::as_b_spline(k, self)? {
return Surface::from(form.into_owned())
.extended_sides_with(k, asked, how, distance, keeping);
}
}
// a ball patch as its sphere (`ball`)
if let SurfaceKind::BallPatch(patch) = self.kind() {
if let Some(e) = super::ball::ball_extended_sides(k, self, patch, asked, distance)? {
@@ -1949,3 +1949,56 @@ fn a_small_torus_patch_far_from_the_origin_extends_on_its_torus() {
assert!((a.enclosure.mid() - 1.5).abs() <= 1e-9 * 1.5, "{a:?}");
}
}
/// A cylinder whose label's axis is tilted off its form's rulings past the proof (recognition
/// can leave one on a body): the label is dropped and the form is extended, not refused.
#[test]
fn a_label_its_form_does_not_prove_extends_as_the_form() {
use crate::geom::axis2_placement::Axis2Placement;
use crate::surface::cone::p1::Cone;
use crate::surface::implicit::AnalyticSupport;
let k = Kernel::default();
let mut s = Surface::from(Cone::new_cylinder_bounded(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 0.0),
Vec3::new(0.0, 1.0, 0.0),
40.0,
0.0,
9.0,
94.0,
false,
false,
false,
));
let SurfaceKind::Cone(c) = s.kind_mut() else {
panic!("a cone");
};
c.make_nurb(&k).unwrap();
c.rev.position = Axis2Placement::from_stored(
Vec3::new(0.0, 0.0, 0.0),
Vec3::new(1.0, 0.0, 1e-9),
Vec3::new(0.0, 1.0, 0.0),
);
assert!(!AnalyticSupport::label_stands(&k, &s).unwrap());
assert!(AnalyticSupport::check(&k, &s).is_err());
let d = s.natural_uv_domain_with_nurb(&k).unwrap();
let grown = s
.extended_sides_keeping_parameters(&k, &BoxSide::ALL, Continuity::Ruled, 5.0)
.unwrap()
.surface;
assert!(!matches!(grown.kind(), SurfaceKind::Cone(_)));
let n = grown.natural_uv_domain_with_nurb(&k).unwrap();
assert!(n.min().x < d.min().x && n.max().x > d.max().x);
let uv = Point2::new(d.u_interval().mid(), d.v_interval().mid());
// the form, not the label: the result is the form's surface
let form = crate::blend::fillet_section::station::as_b_spline(&k, &s)
.unwrap()
.unwrap()
.into_owned();
let (a, b) = (
Surface::from(form).evaluate_point(&k, uv).unwrap(),
grown.evaluate_point(&k, uv).unwrap(),
);
assert!((a - b).length() < 1e-12, "{a:?} {b:?}");
}
+16
View File
@@ -1064,6 +1064,22 @@ impl AnalyticSupport {
Ok(a)
}
/// Whether `s`'s form proves its analytic label ([`Self::check`]'s test): true where it
/// has no label or no form, so nothing to prove.
///
/// # Errors
///
/// Those of making the form and of [`lies_on`].
pub fn label_stands(k: &Kernel, s: &Surface) -> Result<bool> {
let Some(a) = Self::of(s) else {
return Ok(true);
};
match crate::blend::fillet_section::station::as_b_spline(k, s)? {
Some(form) => lies_on(&form, &a.implicit()),
None => Ok(true),
}
}
/// The analytic surface on the NURB `form`: refused unless `form` lies on it
/// ([`lies_on`]) and its placement's axes are orthonormal. A plane keeps its placement
/// as given; a cylinder, cone, sphere or torus is parametrised as the kernel recognises
@@ -0,0 +1,110 @@
//! A cylinder wall of a short arc far from the origin, extended 1500 mm along its rulings and
//! round its axis. The iso that certifies the turn is interpolated between rulings 3 m long.
//! At 500 mm the certificate took its rounding at the iso's own size and refused; at 1500 mm a
//! weight ratio per row, differing by the continuation's rounding, took the iso out of its
//! plane by 1.8e-10. Every point of the result stays on the cylinder, radius 80 about its axis.
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)] // test helpers
use solidcore::boolean::api_brep::boolean_intersection;
use solidcore::curve::curve::Curve;
use solidcore::kernel::UpstreamFixes;
use solidcore::math::{Point2, Point3, Vec3};
use solidcore::primitive::api_curves as curves;
use solidcore::primitive::api_primitives::create_box;
use solidcore::surface::extend::Continuity;
use solidcore::surface::implicit::AnalyticSupport;
use solidcore::surface::side::BoxSide;
use solidcore::sweep::api_brep::create_linear_sweep;
use solidcore::topology::brep::p1 as bp1;
use solidcore::{Brep, Kernel};
const CENTRE: (f64, f64) = (-90.0, -280.0);
const RADIUS: f64 = 80.0;
fn p(x: f64, y: f64) -> Point3 {
Point3::new(x, y, 0.0)
}
fn line(a: Point3, b: Point3) -> Curve {
Curve::from(curves::create_line_segment(a, b).unwrap())
}
/// A ring sector outside the arc from 90 to 99 degrees, swept 94 up, its ends cut off by a
/// box: the cut wall carries its cylinder, with SolidCraft's fix list.
fn wedge(k: &mut Kernel) -> Brep {
let c = p(CENTRE.0, CENTRE.1);
let at = |r: f64, deg: f64| {
let a = f64::to_radians(deg);
p(CENTRE.0 + r * a.cos(), CENTRE.1 + r * a.sin())
};
let profile = [
Curve::from(curves::create_arc(c, RADIUS, 90.0, 99.0).unwrap()),
line(at(RADIUS, 99.0), at(120.0, 99.0)),
line(at(120.0, 99.0), at(120.0, 90.0)),
line(at(120.0, 90.0), at(RADIUS, 90.0)),
];
let refs: Vec<Option<&Curve>> = profile.iter().map(Some).collect();
let prism = create_linear_sweep(k, &refs, Vec3::new(0.0, 0.0, 1.0), 94.0, true).unwrap();
let block = create_box(k, Point3::new(-200.0, -300.0, 1.0), 300.0, 300.0, 92.0).unwrap();
boolean_intersection(k, prism, block).unwrap()
}
#[test]
fn a_short_arc_extends_along_long_rulings_on_its_cylinder() {
let names: Vec<String> = serde_json::from_str(include_str!("kernel_cases/fixes.json")).unwrap();
let mut f = UpstreamFixes::default();
for name in &names {
if let Some(flag) = f.flag_mut(name) {
*flag = true;
}
}
let mut k = Kernel {
fixes: f,
..Kernel::default()
};
let b = wedge(&mut k);
let wall = bp1::faces(&b, None)
.unwrap()
.into_iter()
.map(|f| b.face(f).unwrap().surface.as_ref().unwrap())
.find(|s| {
matches!(
AnalyticSupport::of(s),
Some(AnalyticSupport::Cylinder { radius, .. }) if (radius - RADIUS).abs() < 1e-6
)
})
.expect("the wall is a cylinder");
// the rulings run along u, so they are extended first and the turn's iso is taken between them
let dom = wall.natural_uv_domain_with_nurb(&k).unwrap();
let x0 = wall
.evaluate_point(&k, Point2::new(dom.min().x, dom.min().y))
.unwrap();
let x1 = wall
.evaluate_point(&k, Point2::new(dom.max().x, dom.min().y))
.unwrap();
assert!((x0.z - x1.z).abs() > 90.0, "{x0:?} {x1:?}");
for d in [500.0, 1500.0] {
let grown = wall
.extended_sides_keeping_parameters(&k, &BoxSide::ALL, Continuity::Ruled, d)
.unwrap()
.surface;
let dom = grown.natural_uv_domain_with_nurb(&k).unwrap();
let (u, v) = (dom.u_interval(), dom.v_interval());
for i in 0..=8 {
for j in 0..=8 {
let uv = Point2::new(
u.min() + u.length() * f64::from(i) / 8.0,
v.min() + v.length() * f64::from(j) / 8.0,
);
let x = grown.evaluate_point(&k, uv).unwrap();
let r = (x.x - CENTRE.0).hypot(x.y - CENTRE.1);
assert!((r - RADIUS).abs() < 1e-9, "{d} {i} {j} {r}");
}
}
}
}