mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 04:52:10 +00:00
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:
@@ -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:?}");
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user