mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 04:52:10 +00:00
Merge pull request #3097 from tangency-labs/fix/2845-crossing-rails
fillet: rounds whose rails only run on past their edges' ends do not cross (#2845)
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,84 @@
|
||||
//! #2845: rounds on a stepped wall. The union of a 4 mm wall on a 7 mm base, so the base stands
|
||||
//! 3 mm proud of the wall as a ledge. The wall's edge and the ledge's both end on the back face
|
||||
//! y = 20, an L, and share no vertex. Their rails run on past the edges' ends until they are
|
||||
//! trimmed, and the crossing check read that run-on as a rail inside the other round's strip.
|
||||
//! Its own test binary: the fix set is the process kernel's.
|
||||
|
||||
#![allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::indexing_slicing,
|
||||
clippy::disallowed_methods
|
||||
)] // test: the closed form uses pi
|
||||
|
||||
mod common;
|
||||
|
||||
use common::{all_fixes, built, edge_at, kernel_copy, volume};
|
||||
use solidcore::blend::fillet_executive::api::fillet_edges;
|
||||
use solidcore::kernel::Kernel;
|
||||
use solidcore_ops::boolean::boolean;
|
||||
use solidcore_ops::{blend, make_box, Blend, Body, BooleanOp, CheckLevel, Corners};
|
||||
|
||||
const WALL: [f64; 3] = [20.0, 20.0, -19.0];
|
||||
const LEDGE: [f64; 3] = [23.0, 20.0, -45.0];
|
||||
|
||||
fn stepped_wall() -> Body {
|
||||
let wall = make_box([16.0, -20.0, -40.0], [4.0, 40.0, 42.0]).unwrap();
|
||||
let base = make_box([16.0, -20.0, -50.0], [7.0, 40.0, 10.0]).unwrap();
|
||||
boolean(wall, base, BooleanOp::Union).unwrap().unwrap()
|
||||
}
|
||||
|
||||
/// The two rounds are apart, each between faces square to it: each takes (1 - pi/4) r^2 per
|
||||
/// length, over 42 + 10 mm.
|
||||
fn closed_form(r: f64) -> f64 {
|
||||
9520.0 - (1.0 - std::f64::consts::FRAC_PI_4) * r * r * 52.0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn both_rounds_that_fit_build_their_closed_form() {
|
||||
all_fixes();
|
||||
for r in [2.5, 3.0] {
|
||||
let b = stepped_wall();
|
||||
let edges = [edge_at(&b, WALL), edge_at(&b, LEDGE)];
|
||||
blend(&b, Blend::Round, &edges, &[r], &[], Corners::Filled).unwrap();
|
||||
assert!(b.check(CheckLevel::All).unwrap().is_valid(), "r {r}");
|
||||
let v = volume(&b);
|
||||
assert!((v - closed_form(r)).abs() <= 1e-9 * v, "r {r}: {v}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Past the wall's 4 mm the ball rolls on the face x = 20 and on the wall's far edge (x = 16,
|
||||
/// y = 20): the arc runs from that edge to its rail, and the section it takes off the wall is
|
||||
/// the strip less the disc's part under the rail. With every fix on: SolidCraft's set fails here
|
||||
/// with a bare 1001 (the case `12342-round-wall-edge-r5-past-the-wall-refused`).
|
||||
fn past_the_wall(r: f64) -> f64 {
|
||||
let near = r - 4.0;
|
||||
let height = (r * r - near * near).sqrt();
|
||||
let under = |u: f64| u / 2.0 * (r * r - u * u).max(0.0).sqrt() + r * r / 2.0 * (u / r).asin();
|
||||
9520.0 - 42.0 * (4.0 * height - (under(r) - under(near)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_wall_alone_rides_its_far_edge_past_its_width() {
|
||||
all_fixes();
|
||||
for r in [4.5, 5.0, 6.0] {
|
||||
let b = stepped_wall();
|
||||
let e = edge_at(&b, WALL);
|
||||
blend(&b, Blend::Round, &[e], &[r], &[], Corners::Filled).unwrap();
|
||||
assert!(b.check(CheckLevel::All).unwrap().is_valid(), "r {r}");
|
||||
let v = volume(&b);
|
||||
assert!((v - past_the_wall(r)).abs() <= 1e-9 * v, "r {r}: {v}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_same_on_the_default_kernel() {
|
||||
for r in [2.5, 3.0] {
|
||||
let body = stepped_wall();
|
||||
let edges = [edge_at(&body, WALL), edge_at(&body, LEDGE)];
|
||||
let (_, mut b) = kernel_copy(&body);
|
||||
let ids = edges.map(|i| b.edge_list()[i]);
|
||||
fillet_edges(&mut Kernel::default(), &mut b, &ids, r, 1, 1, 1.0).unwrap();
|
||||
let v = volume(&built(|_| Ok(b)));
|
||||
assert!((v - closed_form(r)).abs() <= 1e-9 * v, "r {r}: {v}");
|
||||
}
|
||||
}
|
||||
@@ -27,12 +27,9 @@ fn slab_less_loft() -> f64 {
|
||||
900.0 * 480.0 * 160.0 - 4.0 / 6.0 * (top + 4.0 * middle + bottom)
|
||||
}
|
||||
|
||||
/// SolidCraft's set: every fix but two.
|
||||
/// SolidCraft's set: every fix but one.
|
||||
fn fixes() {
|
||||
let off = [
|
||||
"fillet_refuse_rounds_breaking_through",
|
||||
"fillet_round_ends_on_outline",
|
||||
];
|
||||
let off = ["fillet_round_ends_on_outline"];
|
||||
let on = solidcore::kernel::UpstreamFixes::NAMES
|
||||
.iter()
|
||||
.copied()
|
||||
@@ -57,6 +54,15 @@ fn blended(kind: Blend, p: [f64; 3]) -> f64 {
|
||||
volume(&b)
|
||||
}
|
||||
|
||||
/// SolidCraft's `loft_cut_beside_round_hole`: the same cut from a pocket's floor, a hole beside it.
|
||||
fn beside_a_hole() -> Body {
|
||||
Body::from_smb(
|
||||
include_bytes!("data/issue_2906_loft_cut_beside_hole.smb").to_vec(),
|
||||
false,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// The operand's volume to 1e-8, so the closed forms are held to 2e-10 of the body (0.014 mm3).
|
||||
const TOL: f64 = 2e-10;
|
||||
|
||||
@@ -100,3 +106,51 @@ fn the_rest_of_the_rim_builds() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The same floor edges from a pocket's floor: the same closed forms on the operand's volume.
|
||||
#[test]
|
||||
fn the_cut_from_a_pocket_floor_takes_the_same_closed_forms() {
|
||||
fixes();
|
||||
let v = volume(&beside_a_hole());
|
||||
let phi = (4.0_f64 / 9.5).atan();
|
||||
let round = 80.0 * 64.0 * ((phi / 2.0).tan() - phi / 2.0);
|
||||
let chamfer = 80.0 * 32.0 * phi.sin();
|
||||
for y in [-61.16, -141.16] {
|
||||
for (kind, added) in [(Blend::Round, round), (Blend::Chamfer, chamfer)] {
|
||||
let b = beside_a_hole();
|
||||
let e = edge_at(&b, [450.0, y, 726.0]);
|
||||
blend(&b, kind, &[e], &[8.0], &[], Corners::Filled).unwrap();
|
||||
assert!(b.check(CheckLevel::All).unwrap().is_valid(), "{y}");
|
||||
let made = volume(&b);
|
||||
assert!((made - (v + added)).abs() <= TOL * v, "{made}");
|
||||
}
|
||||
}
|
||||
for p in [[290.83, -56.41, 728.0], [609.17, -145.91, 728.0]] {
|
||||
let b = beside_a_hole();
|
||||
let e = edge_at(&b, p);
|
||||
blend(&b, Blend::Round, &[e], &[8.0], &[], Corners::Filled).unwrap();
|
||||
assert!(b.check(CheckLevel::All).unwrap().is_valid(), "{p:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes the operand and each blended body for the PR's pictures, when asked.
|
||||
#[test]
|
||||
#[ignore = "writes the bodies for the PR's pictures"]
|
||||
fn bodies_for_the_pictures() {
|
||||
fixes();
|
||||
let dir = std::env::var("ISSUE_2906_OUT").unwrap();
|
||||
let write = |name: &str, b: &Body| {
|
||||
std::fs::write(format!("{dir}/{name}.smb"), b.to_smb(false).unwrap()).unwrap();
|
||||
};
|
||||
write("before", &operand());
|
||||
for (name, kind, p) in [
|
||||
("round", Blend::Round, [450.0, 418.84, 156.0]),
|
||||
("chamfer", Blend::Chamfer, [450.0, 418.84, 156.0]),
|
||||
("side", Blend::Round, [290.83, 423.59, 158.0]),
|
||||
] {
|
||||
let b = operand();
|
||||
let e = edge_at(&b, p);
|
||||
blend(&b, kind, &[e], &[8.0], &[], Corners::Filled).unwrap();
|
||||
write(name, &b);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
//! face with no room between their rails. `SmFilletExecutive::DoFilleting` builds
|
||||
//! the open body anyway. After `CreateFilletBrep` and before the target is edited,
|
||||
//! each rail of a round is compared with the strip the other round takes from the
|
||||
//! same face: a rail lying inside that strip means the rounds do not fit.
|
||||
//! same face: a rail lying inside that strip means the rounds do not fit. Only what is on
|
||||
//! the face and across the strip counts: the rails are not trimmed yet and run on past their
|
||||
//! edges' ends, so a sample off the face, or beside the other edge's end, is no crossing
|
||||
//! (#2845: a wall's edge and a ledge's on a stepped face, whose rounds never meet).
|
||||
//!
|
||||
//! Deviations: not upstream. Rail positions are sampled, and the distance to the other
|
||||
//! round's edge is a 3D distance (exact on planar faces, a lower bound on curved ones
|
||||
@@ -13,18 +16,22 @@
|
||||
//! solution (`SmEdge::ClosestPoint` finding none, #2050) measures nothing: a rail point
|
||||
//! without one is not inside the other round's strip, and gives its own round no width;
|
||||
//! a rail with no width at all takes no strip it can be tested against.
|
||||
// ponytail: sampled 3D distances, no face-interior test; a C-shaped face whose two
|
||||
// edges face each other across its notch would be refused though they fit. Upgrade to
|
||||
// the surface distance within the face if that shows up.
|
||||
// ponytail: sampled 3D distances; a C-shaped face whose two edges face each other across
|
||||
// its notch would be refused though they fit. Upgrade to the surface distance within the
|
||||
// face if that shows up.
|
||||
|
||||
use super::{FilletBody, FilletObject, FilletSession};
|
||||
use crate::error::{EntityRef, Result, SolidError};
|
||||
use crate::geom::tol::zone_tol3d;
|
||||
use crate::intersect::point_class::{PointClassType, PointClassification};
|
||||
use crate::kernel::Kernel;
|
||||
use crate::math::Point3;
|
||||
use crate::query::edge::closest_point;
|
||||
use crate::query::face::p1::{point_3d_classify, Point3dClassifyOptions};
|
||||
use crate::topology::edge;
|
||||
use crate::topology::edgeuse::p1 as eu1;
|
||||
use crate::topology::{EdgeId, FaceId};
|
||||
use crate::topology::face::p2 as fp2;
|
||||
use crate::topology::{Brep, EdgeId, FaceId};
|
||||
|
||||
const OP: &str = "blend::fillet_executive::session::crossing_rails";
|
||||
/// Rail points compared per rail.
|
||||
@@ -47,18 +54,26 @@ impl FilletSession<'_> {
|
||||
pub(super) fn refuse_crossing_rails(&mut self, k: &Kernel) -> Result<()> {
|
||||
let rails = self.collect_rails(k)?;
|
||||
let tolerance = self.objects.target.tolerance();
|
||||
let target = &*self.objects.target;
|
||||
for a in &rails {
|
||||
for b in &rails {
|
||||
if a.face != b.face || a.edge == b.edge || shares_vertex(target, a.edge, b.edge)? {
|
||||
if a.face != b.face
|
||||
|| a.edge == b.edge
|
||||
|| shares_vertex(self.objects.target, a.edge, b.edge)?
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let along = edge::interval(self.objects.target, b.edge)?;
|
||||
for &p in &a.points {
|
||||
// No closest point on the other round's edge: no crossing found here.
|
||||
let Some(closest) = closest_point(k, target, b.edge, p)? else {
|
||||
let Some(closest) = closest_point(k, self.objects.target, b.edge, p)? else {
|
||||
continue;
|
||||
};
|
||||
if closest.distance + tolerance < b.width {
|
||||
// At an end of the edge, the point is beside the strip, not across it.
|
||||
let across = closest.parameter > along.min() && closest.parameter < along.max();
|
||||
if across
|
||||
&& closest.distance + tolerance < b.width
|
||||
&& on_face(k, self.objects.target, a.face, p)?
|
||||
{
|
||||
self.executive.error.code = 4; // SM_FILERR_EDGE_PROBLEM
|
||||
self.executive.error.status = 2; // SM_FIL_FAILURE
|
||||
self.executive.error.edge = Some(FilletObject::Topology(
|
||||
@@ -139,7 +154,24 @@ impl FilletSession<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn shares_vertex(target: &crate::topology::Brep, a: EdgeId, b: EdgeId) -> Result<bool> {
|
||||
/// Whether `p` lies on `face`, within the zone of the face's tolerance.
|
||||
fn on_face(k: &Kernel, target: &mut Brep, face: FaceId, p: Point3) -> Result<bool> {
|
||||
let zone = zone_tol3d(k, fp2::tolerance(target, face)?);
|
||||
let mut on = PointClassification::<EntityRef>::new(zone);
|
||||
point_3d_classify(
|
||||
k,
|
||||
target,
|
||||
face,
|
||||
p,
|
||||
zone,
|
||||
true,
|
||||
&mut on,
|
||||
Point3dClassifyOptions::default(),
|
||||
)?;
|
||||
Ok(on.point_class() != PointClassType::Unknown)
|
||||
}
|
||||
|
||||
fn shares_vertex(target: &Brep, a: EdgeId, b: EdgeId) -> Result<bool> {
|
||||
let (a0, a1) = edge::start_end_vertices(target, a)?;
|
||||
let (b0, b1) = edge::start_end_vertices(target, b)?;
|
||||
Ok([a0, a1]
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 10 KiB |
+11
-5
@@ -8,11 +8,13 @@
|
||||
},
|
||||
"calls": [
|
||||
{
|
||||
"error": "solidcore failed (status 1001)",
|
||||
"inputs": [
|
||||
"b1"
|
||||
],
|
||||
"op": "blend_dropping",
|
||||
"outputs": [
|
||||
"b2"
|
||||
],
|
||||
"params": {
|
||||
"edges": {
|
||||
"kind": "edge",
|
||||
@@ -84,18 +86,22 @@
|
||||
"rel": 1e-09
|
||||
},
|
||||
"volume_mm3": 9450.25440310417,
|
||||
"what": "an L body: a 4 mm wall (x 16..20, y -20..20, z -40..2) on a 7 mm base (x 16..23, z -50..-40), so the base stands 3 mm proud of the wall as a ledge at z = -40; the wall's outer vertical edge at x = 20, y = 20 and the ledge's at x = 23, y = 20, both convex edges between the back face y = 20 and the faces x = 20 and x = 23. Both edges rounded R2.5, a size that fits each: one closed solid; it fails with status 1001 (R3 the same), while each edge alone builds"
|
||||
"what": "an L body: a 4 mm wall (x 16..20, y -20..20, z -40..2) on a 7 mm base (x 16..23, z -50..-40), so the base stands 3 mm proud of the wall as a ledge at z = -40; the wall's outer vertical edge at x = 20, y = 20 and the ledge's at x = 23, y = 20, both convex edges between the back face y = 20 and the faces x = 20 and x = 23. Both edges rounded R2.5, a size that fits each: one closed solid (R3 the same), as each edge alone builds"
|
||||
},
|
||||
"expressible": true,
|
||||
"gallery_note": "the back face y = 20 and the ledge top highlighted: R2.5 fits each edge alone, but the two together fail with status 1001",
|
||||
"gallery_note": "R2.5 fits each edge alone and the two together: the rounds end apart on the stepped back face",
|
||||
"operation_expressible": true,
|
||||
"records": "12342-round-wall-and-ledge-edges-r2-5.bodies.json.gz",
|
||||
"result": [],
|
||||
"result": [
|
||||
{
|
||||
"made": "b2"
|
||||
}
|
||||
],
|
||||
"sources": [
|
||||
"tangency-labs/solidcraft#12342",
|
||||
"tangency-labs/solidcore#2845"
|
||||
],
|
||||
"ungraded": [],
|
||||
"what": "an L body: a 4 mm wall (x 16..20, y -20..20, z -40..2) on a 7 mm base (x 16..23, z -50..-40), so the base stands 3 mm proud of the wall as a ledge at z = -40; the wall's outer vertical edge at x = 20, y = 20 and the ledge's at x = 23, y = 20, both convex edges between the back face y = 20 and the faces x = 20 and x = 23. Both edges rounded R2.5, a size that fits each: one closed solid; it fails with status 1001 (R3 the same), while each edge alone builds",
|
||||
"what": "an L body: a 4 mm wall (x 16..20, y -20..20, z -40..2) on a 7 mm base (x 16..23, z -50..-40), so the base stands 3 mm proud of the wall as a ledge at z = -40; the wall's outer vertical edge at x = 20, y = 20 and the ledge's at x = 23, y = 20, both convex edges between the back face y = 20 and the faces x = 20 and x = 23. Both edges rounded R2.5, a size that fits each: one closed solid (R3 the same), as each edge alone builds",
|
||||
"why_not": []
|
||||
}
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
"issue": "solidcore#2844",
|
||||
"reason": "the round of the loft's circular rim is not a closed solid"
|
||||
},
|
||||
{
|
||||
"case": "fillets/12342-round-wall-and-ledge-edges-r2-5",
|
||||
"issue": "solidcore#2845",
|
||||
"reason": "status 1001 instead of a closed solid"
|
||||
},
|
||||
{
|
||||
"case": "fillets/12342-round-wall-and-ledge-edges-r5-refused",
|
||||
"issue": "solidcore#2845",
|
||||
|
||||
Reference in New Issue
Block a user