mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-11 03:01:58 +00:00
fix(blend): a round at a plate's arc end builds where offsets continue as themselves (#3045) (#3105)
Fixes #3045 solidcore: 78d1eb12d222652219313c12787b65389c4d9db8
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
//! #3045: a round on the edge where a plate's straight side meets its arc end. The round is the
|
||||
//! cylinder of radius R tangent to the side and inside the arc's cylinder; it removes 8 times
|
||||
//! (the thickness) the area between the corner and that circle.
|
||||
//!
|
||||
//! The failing configuration needs three of the fixes together: the arc face a cylinder
|
||||
//! (`linear_sweep_arc_is_cylinder`) and its offsets continued as themselves
|
||||
//! (`offset_extension_from_base`, `offset_extension_as_itself`). The continued cylinder is a
|
||||
//! whole circle, so the drop of the rail's end onto it found the circle's far side, where the
|
||||
//! solve has a second root outside the face. Measured on this plate (arc 16.25, bisected to
|
||||
//! 0.01): without the fix the round failed with status 1001 for R from 6.26 to 11.8 and built
|
||||
//! below and above; the radii below bracket both ends.
|
||||
|
||||
#![allow(clippy::disallowed_methods)] // closed-form oracle, not ops geometry
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] // test
|
||||
|
||||
mod common;
|
||||
|
||||
use std::f64::consts::{FRAC_PI_2, FRAC_PI_4};
|
||||
|
||||
use common::{edge_at, kernel_copy};
|
||||
use solidcore::surface::surface::SurfaceKind;
|
||||
use solidcore::Point2;
|
||||
use solidcore_ops::blend::{blend, Blend, Corners};
|
||||
use solidcore_ops::sweep::{extrude, Curve, DraftCorner};
|
||||
use solidcore_ops::{make_box, session, Body, CheckLevel};
|
||||
|
||||
/// The plate, scaled by `s`: 100 x 30 with the +x end an arc of radius 16.25 about (93.75, 15),
|
||||
/// 8 thick.
|
||||
struct Plate {
|
||||
s: f64,
|
||||
}
|
||||
|
||||
impl Plate {
|
||||
fn cx(&self) -> f64 {
|
||||
93.75 * self.s
|
||||
}
|
||||
fn cy(&self) -> f64 {
|
||||
15.0 * self.s
|
||||
}
|
||||
fn rc(&self) -> f64 {
|
||||
16.25 * self.s
|
||||
}
|
||||
fn thickness(&self) -> f64 {
|
||||
8.0 * self.s
|
||||
}
|
||||
|
||||
fn body(&self) -> Body {
|
||||
let s = self.s;
|
||||
let half = (self.cy() / self.rc()).asin();
|
||||
let line = |start: [f64; 3], end: [f64; 3]| Curve::Line { start, end };
|
||||
let profile = vec![
|
||||
line([0.0, 0.0, 0.0], [100.0 * s, 0.0, 0.0]),
|
||||
Curve::Arc {
|
||||
center: [self.cx(), self.cy(), 0.0],
|
||||
axis: [0.0, 0.0, 1.0],
|
||||
reference: [1.0, 0.0, 0.0],
|
||||
radius: self.rc(),
|
||||
start: -half,
|
||||
end: half,
|
||||
},
|
||||
line([100.0 * s, 30.0 * s, 0.0], [0.0, 30.0 * s, 0.0]),
|
||||
line([0.0, 30.0 * s, 0.0], [0.0, 0.0, 0.0]),
|
||||
];
|
||||
extrude(
|
||||
&profile,
|
||||
[0.0, 0.0, 1.0],
|
||||
self.thickness(),
|
||||
0.0,
|
||||
DraftCorner::default(),
|
||||
true,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The plate's volume: the 100 x 30 rectangle and the arc's segment past x = 100.
|
||||
fn volume(&self) -> f64 {
|
||||
let turn = 2.0 * (self.cy() / self.rc()).asin();
|
||||
let (rc, s) = (self.rc(), self.s);
|
||||
self.thickness() * (3000.0 * s * s + rc * rc / 2.0 * (turn - turn.sin()))
|
||||
}
|
||||
|
||||
/// The round's centre abscissa, `cx + dx`: the circle of radius `r` tangent to the side
|
||||
/// (y = 0) and inside the arc's circle, `|centre - (cx, cy)| = rc - r`.
|
||||
fn round_x(&self, r: f64) -> f64 {
|
||||
self.cx() + ((self.rc() - r).powi(2) - (self.cy() - r).powi(2)).sqrt()
|
||||
}
|
||||
|
||||
/// The area the round of radius `r` removes: between the corner (100, 0), the side and the
|
||||
/// arc, and the round's circle. Green's theorem round the loop corner, foot on the side,
|
||||
/// contact on the arc: each circular arc contributes `1/2 (r^2 (b - a) + cx r (sin b -
|
||||
/// sin a) + cy r (cos a - cos b))`, the side nothing.
|
||||
fn removed_area(&self, r: f64) -> f64 {
|
||||
let fx = self.round_x(r);
|
||||
let contact = (r - self.cy()).atan2(fx - self.cx());
|
||||
let corner = (-self.cy()).atan2(100.0 * self.s - self.cx());
|
||||
let arc = |(cx, cy): (f64, f64), rad: f64, a: f64, b: f64| {
|
||||
0.5 * (rad * rad * (b - a)
|
||||
+ cx * rad * (b.sin() - a.sin())
|
||||
+ cy * rad * (a.cos() - b.cos()))
|
||||
};
|
||||
(arc((fx, r), r, -FRAC_PI_2, contact)
|
||||
+ arc((self.cx(), self.cy()), self.rc(), contact, corner))
|
||||
.abs()
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the process-wide fixes when the test ends, a failed assertion included.
|
||||
struct ResetFixes;
|
||||
|
||||
impl Drop for ResetFixes {
|
||||
fn drop(&mut self) {
|
||||
session::set_fixes([]).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// `b` is valid, has volume `expected` and a round face that is the cylinder of radius `r`
|
||||
/// about the axis through (`axis_x`, `r`): an exact surface, not a B-spline of that shape.
|
||||
fn check_round(b: &Body, r: f64, axis_x: f64, expected: f64, what: &str) {
|
||||
let report = b.check(CheckLevel::All).unwrap();
|
||||
assert!(report.is_valid(), "{what}: {report}");
|
||||
let volume = b.volume(1e-9).unwrap();
|
||||
assert!(
|
||||
(volume - expected).abs() < 1e-6,
|
||||
"{what}: volume {volume}, closed form {expected}"
|
||||
);
|
||||
let (k, brep) = kernel_copy(b);
|
||||
let rounds: Vec<_> = brep
|
||||
.face_list()
|
||||
.iter()
|
||||
.filter_map(|&f| {
|
||||
let surface = brep.face(f).unwrap().surface.as_ref()?;
|
||||
match surface.kind() {
|
||||
SurfaceKind::Cone(c) if c.is_cylinder() && (c.bot_radius() - r).abs() < 1e-9 => {
|
||||
let uv = surface.natural_uv_domain_with_nurb(&k).unwrap();
|
||||
let mid = Point2::new(
|
||||
0.5 * (uv.min().x + uv.max().x),
|
||||
0.5 * (uv.min().y + uv.max().y),
|
||||
);
|
||||
surface.evaluate_point(&k, mid).ok()
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(rounds.len(), 1, "{what}: one cylinder of radius {r}");
|
||||
let p = rounds.first().copied().unwrap();
|
||||
assert!(
|
||||
((p.x - axis_x).hypot(p.y - r) - r).abs() < 1e-9,
|
||||
"{what}: the round's cylinder is about ({axis_x}, {r})"
|
||||
);
|
||||
}
|
||||
|
||||
fn check_plate_round(s: f64, r: f64, flags: &str) {
|
||||
let plate = Plate { s };
|
||||
let b = plate.body();
|
||||
let e = edge_at(&b, [100.0 * s, 0.0, plate.thickness() / 2.0]);
|
||||
let what = format!("{flags}, scale {s}, R {r}");
|
||||
blend(&b, Blend::Round, &[e], &[r], &[], Corners::Filled)
|
||||
.unwrap_or_else(|err| panic!("{what}: {err:?}"));
|
||||
let expected = plate.volume() - plate.thickness() * plate.removed_area(r);
|
||||
check_round(&b, r, plate.round_x(r), expected, &what);
|
||||
}
|
||||
|
||||
/// The control the issue names: the same plate with a straight end, a round of R 10 on the
|
||||
/// corner edge. It removes `(1 - pi/4) r^2` of area, 8 thick.
|
||||
fn check_straight_end(flags: &str) {
|
||||
let b = make_box([0.0; 3], [100.0, 30.0, 8.0]).unwrap();
|
||||
let e = edge_at(&b, [100.0, 0.0, 4.0]);
|
||||
let r = 10.0;
|
||||
blend(&b, Blend::Round, &[e], &[r], &[], Corners::Filled)
|
||||
.unwrap_or_else(|err| panic!("{flags}, straight end: {err:?}"));
|
||||
let expected = 8.0 * (3000.0 - (1.0 - FRAC_PI_4) * r * r);
|
||||
check_round(
|
||||
&b,
|
||||
r,
|
||||
100.0 - r,
|
||||
expected,
|
||||
&format!("{flags}, straight end"),
|
||||
);
|
||||
}
|
||||
|
||||
const SCALE_1: [f64; 10] = [3.0, 6.0, 6.25, 6.3, 6.5, 7.0, 10.0, 11.8, 11.9, 12.0];
|
||||
const SCALE_2: [f64; 4] = [6.0, 12.5, 13.0, 20.0];
|
||||
|
||||
/// Without the guard the three fixes fail with status 1001 for R from 6.26 to 11.8 on the scale-1
|
||||
/// plate (and the same fractions of the arc's radius on the scale-2 plate); with none of them
|
||||
/// it always built. The radii bracket both ends of the band (6.25 and 6.3, 11.8 and 11.9) and
|
||||
/// sit inside it (6.5, 7, 10). One test: the fixes are one process-wide setting.
|
||||
#[test]
|
||||
fn the_round_builds_for_every_radius() {
|
||||
let _reset = ResetFixes;
|
||||
for (flags, names) in [
|
||||
("default flags", &[][..]),
|
||||
(
|
||||
"the three fixes",
|
||||
&[
|
||||
"linear_sweep_arc_is_cylinder",
|
||||
"offset_extension_from_base",
|
||||
"offset_extension_as_itself",
|
||||
][..],
|
||||
),
|
||||
] {
|
||||
session::set_fixes(names.iter().copied()).unwrap();
|
||||
check_straight_end(flags);
|
||||
for r in SCALE_1 {
|
||||
check_plate_round(1.0, r, flags);
|
||||
}
|
||||
for r in SCALE_2 {
|
||||
check_plate_round(2.0, r, flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,6 +105,10 @@
|
||||
//! the other base is the edge use's own UV point. Upstream reads an existing curve only and
|
||||
//! otherwise solves globally on the base, which finds a point on a closed surface's seam
|
||||
//! once at each end of the period and refuses the two answers.
|
||||
//! - **The drop onto the other offset** (solidcore's): when that offset continues as itself
|
||||
//! (`offset_continues_as_itself`) `LoadInitialValues` drops the rail end onto the face's own
|
||||
//! domain, not the extended one, which holds a second root of the rail-end solve on the far
|
||||
//! side of the whole circle (the solve's interval is the extended one, as upstream's).
|
||||
|
||||
use crate::blend::fillet_custom_solver::{
|
||||
face_owner, offset_compute_inward_vector, offset_evaluate_1st, offset_evaluate_point,
|
||||
@@ -147,7 +151,7 @@ use crate::nurbs::b_spline_curve::BSplineCurve;
|
||||
use crate::nurbs::b_spline_surface::BSplineSurface;
|
||||
use crate::nurbs::b_spline_surface_n_lib::p2::{EvaluateFlags, OrientType};
|
||||
use crate::nurbs::knots::Side;
|
||||
use crate::surface::offset_surface::SS_UNKNOWN;
|
||||
use crate::surface::offset_surface::{offset_continues_as_itself, SS_UNKNOWN};
|
||||
use crate::surface::surface::p3::{out_of_bounds_enabled, set_out_of_bounds_enabled};
|
||||
use crate::surface::surface::Surface;
|
||||
use crate::topology::edgeuse::p1 as edgeuse;
|
||||
@@ -706,7 +710,13 @@ fn cr_initial_body<S: ConstantRadiusOps + ?Sized>(
|
||||
// to get a better guess
|
||||
let uv_guess = Point2::new(srf2_guess_uv.x, srf2_guess_uv.y);
|
||||
let mut domain = base2.natural_uv_domain_with_nurb(k)?;
|
||||
solver.setup_offset_extension(k, base2, &mut domain)?; // expands sDomain
|
||||
// Not upstream (#3045): an offset continued as itself is the whole circle, not upstream's
|
||||
// tangent extension that stays near the face. The drop on the extended domain finds the
|
||||
// nearest point of that circle, whose far side holds a second root of the rail-end solve
|
||||
// outside the face; the rail end lies on the face's edge, so the drop stays on the face.
|
||||
if !offset_continues_as_itself(k, base2) {
|
||||
solver.setup_offset_extension(k, base2, &mut domain)?; // expands sDomain
|
||||
}
|
||||
|
||||
if let Some((sol_uv, dist)) = offset_local_point_solve(k, surface2, &domain, pnt, uv_guess)?
|
||||
{
|
||||
@@ -2947,12 +2957,10 @@ fn rolled_on<'a>(
|
||||
let base = offset.base_surface();
|
||||
match offset.extended_base_surface() {
|
||||
Some(extended)
|
||||
if k.fixes.offset_extension_as_itself
|
||||
&& matches!(
|
||||
base.kind(),
|
||||
crate::surface::surface::SurfaceKind::SurfOfRevolution(_)
|
||||
)
|
||||
&& crate::surface::offset_surface::extends_as_itself(k, base) =>
|
||||
if matches!(
|
||||
base.kind(),
|
||||
crate::surface::surface::SurfaceKind::SurfOfRevolution(_)
|
||||
) && offset_continues_as_itself(k, base) =>
|
||||
{
|
||||
extended
|
||||
}
|
||||
|
||||
@@ -2371,6 +2371,16 @@ pub(crate) fn extends_as_itself(k: &Kernel, surface: &Surface) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the offset of `surface` continues as itself: `fixes.offset_extension_from_base` and
|
||||
/// `fixes.offset_extension_as_itself` on, and [`extends_as_itself`]. The one place that decides
|
||||
/// it: the extension ([`extend_from_base`]), the face a round rolls on and the drop that seeds a
|
||||
/// rail end all ask this.
|
||||
pub(crate) fn offset_continues_as_itself(k: &Kernel, surface: &Surface) -> bool {
|
||||
k.fixes.offset_extension_from_base
|
||||
&& k.fixes.offset_extension_as_itself
|
||||
&& extends_as_itself(k, surface)
|
||||
}
|
||||
|
||||
/// The fork's `sm_GeneratorIsCircle` (tangency-labs/smlib@43fd6b6): whether a surface of
|
||||
/// revolution's generator is an arc of one circle, and if so that circle's centre (in the
|
||||
/// first column's meridian plane) and the arc's angle. A circle's NURB spans are rational
|
||||
@@ -2952,7 +2962,7 @@ fn extend_from_base(
|
||||
// So does (the fork's tangency-labs/smlib@43fd6b6) a surface of revolution whose
|
||||
// generator is a circle (a round's cross section revolved, as SMLib's concave round of an
|
||||
// arc is): its generator carried on as its own circle.
|
||||
let as_itself = k.fixes.offset_extension_as_itself && extends_as_itself(k, base);
|
||||
let as_itself = offset_continues_as_itself(k, base);
|
||||
// WR-650 (the fork's tangency-labs/smlib@b298cc2): round the axis such a surface is a
|
||||
// circle, and its extension reaches no further than closing the turn (half the gap past
|
||||
// either end, `sm_RotTurn`); from there on that direction is periodic, its parameters
|
||||
|
||||
Reference in New Issue
Block a user