mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 18:45:55 +00:00
Tests: the fit and taper tests use geometry that still doesn't fit
Under the blend-overflow decision (237f0ae74) a convex round running over a face's far
edge (a cliff) is built and cut from the part, so the truck-era fixtures of the reduce
and taper fits (a 2 mm fin, a plate narrowing to 2 mm, a wedge, a 10 mm cube's edge at
R20) no longer refuse their radius and the fits have nothing to react to. Each test now
uses a case SMLib still refuses, and checks the cliff case builds:
- engine fillet_fit_reduce_keeps_the_radius_where_it_fits: a 14 x 6 x 10 post on a
40 x 30 x 20 block, its four verticals at 3.5 (two rounds would cross on its 6 mm ends)
reduced to 3 on 4 of 5 edges, the block's corner kept at 3.5: 24840 - (4 - pi) 90 -
(1 - pi/4) 3.5^2 20. New fillet_fit_reduce_keeps_a_round_over_a_cliff: the fin's R4
builds, nothing reduced, at the kernel's fillet_max_each answer.
- engine fillet_fit_taper_follows_the_room_along_the_edge, spline_fillet_takes_over_a_taper,
kernel taper_follows_the_room_along_the_edge, ui-egui fillet_spline_handle_tests: the
narrowing plate stands against a wall behind its slanted side (z -15 to 30), so its top
and bottom end in concave edges: R3 is refused and tapers 3 -> 1.8 as before. Without
the wall, R3 is cut over the 2 mm top's far edge and kept constant: at x the section
loses (1 - pi/4) 9, or 3 w - (F(3) - F(3 - w)) where the top is w < 3 wide, 6522.7745566.
- The wedge closing to a point: on its own R16 is cut over its cliffs (the section
integral, 4195.578821, SMLib within 4e-8) and kept; its room's taper is checked as a
spline fillet. Against a wall (new fillet_fit_taper_closes_to_a_point) the taper must
close to 0, which SMLib fails today (status 1001).
- kernel taper_mesh_tests: the collapsed taper is built from taper() as a spline fillet.
- ui-egui the_feature_menu_copies_a_warning_message: the 10 mm cube's four verticals at
6 are reduced to 5 (a full round), which the warning says.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+99
-34
@@ -4007,42 +4007,85 @@ fn fillet_fit_reduce_uses_the_largest_radius_that_fits() {
|
||||
assert!(s.execute("solid.fillet", &json!({"edges": mids, "radius": 1, "fit": "taper", "type": "chord"})).is_err());
|
||||
}
|
||||
|
||||
/// A plate 15 high whose top narrows from 20 to `tip` mm along 40 mm (a wedge at 0), and, with
|
||||
/// `wall`, a wall standing behind its slanted side from z = -15 to 30, which the plate's top and
|
||||
/// bottom end on in concave edges: a blend of its front edges can't run past them.
|
||||
fn tapering_plate(tip: f64, wall: bool) -> Session {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
||||
let pts = if tip > 0.0 { json!([[0, 0], [40, 0], [40, tip], [0, 20]]) } else { json!([[0, 0], [40, 0], [0, 20]]) };
|
||||
run(&mut s, "sketch.line", json!({"points": pts, "closed": true}));
|
||||
run(&mut s, "solid.extrude", json!({"distance": 15}));
|
||||
if wall {
|
||||
run(&mut s, "sketch.create", json!({"plane": {"origin": [0, 0, -15], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0]}}));
|
||||
run(&mut s, "sketch.line", json!({"points": [[40, tip], [0, 20], [0, 30], [40, 30]], "closed": true}));
|
||||
run(&mut s, "solid.extrude", json!({"distance": 45, "operation": "join"}));
|
||||
assert_eq!(s.model.state().bodies.len(), 1);
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
/// [`tapering_plate`]'s volume: the plate, and the wall's 20 (40 − tip) section along 45.
|
||||
fn tapering_plate_volume(tip: f64, wall: bool) -> f64 {
|
||||
(40.0 * (20.0 + tip) / 2.0) * 15.0 + if wall { 20.0 * (40.0 - tip) * 45.0 } else { 0.0 }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fillet_fit_taper_follows_the_room_along_the_edge() {
|
||||
// #293: the top front edge of a plate whose top narrows from 20 to `tip` mm along 40 mm.
|
||||
// At a 2 mm tip it can't take 3 mm; taper keeps 3 where it fits and comes down to 0.9 of the
|
||||
// 2 mm there. With an 8 mm tip 3 fits and stays constant; a plate closing to a point
|
||||
// tapers to 0. The warning says what was done.
|
||||
// #293: the top front edge of a plate whose top narrows from 20 to `tip` mm along 40 mm,
|
||||
// against a wall. At a 2 mm tip it can't take 3 mm; taper keeps 3 where it fits and comes
|
||||
// down to 0.9 of the 2 mm there. With an 8 mm tip 3 fits and stays constant. The warning
|
||||
// says what was done.
|
||||
let (k, edge) = (1.0 - PI / 4.0, json!([[20, 0, 15]]));
|
||||
let plate = |tip: f64| {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
||||
let pts = if tip > 0.0 { json!([[0, 0], [40, 0], [40, tip], [0, 20]]) } else { json!([[0, 0], [40, 0], [0, 20]]) };
|
||||
run(&mut s, "sketch.line", json!({"points": pts, "closed": true}));
|
||||
run(&mut s, "solid.extrude", json!({"distance": 15}));
|
||||
s
|
||||
};
|
||||
let warning = |s: &Session, out: &Value| out["feature"].as_u64().and_then(|id| s.model.result(id)).and_then(|r| r.warning.clone());
|
||||
|
||||
let mut s = plate(2.0);
|
||||
let mut s = tapering_plate(2.0, true);
|
||||
assert!(s.execute("solid.fillet", &json!({"edges": edge, "radius": 3})).is_err());
|
||||
let out = run(&mut s, "solid.fillet", json!({"edges": edge, "radius": 3, "fit": "taper"}));
|
||||
let w = warning(&s, &out).unwrap_or_default();
|
||||
assert!(w.contains("tapers to the room on 1 of 1 edges, down to 1.8 mm"), "{w}");
|
||||
// Between the material a constant 1.8 and a constant 3 would remove.
|
||||
let (v, full) = (volume(&mut s), 40.0 * 22.0 / 2.0 * 15.0);
|
||||
let (v, full) = (volume(&mut s), tapering_plate_volume(2.0, true));
|
||||
assert!(v < full - k * 1.8 * 1.8 * 40.0 && v > full - k * 9.0 * 40.0, "{v}");
|
||||
let json = serde_json::to_string(&s.doc.features).unwrap();
|
||||
assert_eq!(json.matches("\"fit\":\"taper\"").count(), 1, "{json}");
|
||||
|
||||
let mut s = plate(8.0);
|
||||
let mut s = tapering_plate(8.0, true);
|
||||
let out = run(&mut s, "solid.fillet", json!({"edges": edge, "radius": 3, "fit": "taper"}));
|
||||
assert_eq!(warning(&s, &out), None);
|
||||
assert!(rel(volume(&mut s), 40.0 * 28.0 / 2.0 * 15.0 - k * 9.0 * 40.0) < 2e-4);
|
||||
assert!(rel(volume(&mut s), tapering_plate_volume(8.0, true) - k * 9.0 * 40.0) < 2e-4);
|
||||
|
||||
let mut s = plate(0.0);
|
||||
let out = run(&mut s, "solid.fillet", json!({"edges": edge, "radius": 16, "fit": "taper"}));
|
||||
let w = warning(&s, &out).unwrap_or_default();
|
||||
// Without the wall the 2 mm top's far edge is a cliff: R3 runs over it and is cut from the
|
||||
// plate as if the top went on (the blend-overflow decision, 237f0ae74), so it fits and the
|
||||
// taper leaves it constant. At x the plate's section is [0, w] × [0, 15], w = 20 − 0.45 x:
|
||||
// the round takes its whole (1 − π/4)·9 where w >= 3, and where w < 3 the part of the
|
||||
// 3 × 3 corner outside the circle about (3, 3) with s <= w, 3 w − (F(3) − F(3 − w)),
|
||||
// F(v) = (v √(9 − v²) + 9 asin(v/3)) / 2.
|
||||
let mut s = tapering_plate(2.0, false);
|
||||
let out = run(&mut s, "solid.fillet", json!({"edges": edge, "radius": 3, "fit": "taper"}));
|
||||
assert_eq!(warning(&s, &out), None);
|
||||
let f = |v: f64| 0.5 * (v * (9.0 - v * v).max(0.0).sqrt() + 9.0 * (v / 3.0).asin());
|
||||
let n = 20000;
|
||||
let taken: f64 = (0..n)
|
||||
.map(|i| {
|
||||
let w = 20.0 - 0.45 * 40.0 * (i as f64 + 0.5) / n as f64;
|
||||
if w >= 3.0 { 9.0 * k } else { 3.0 * w - (f(3.0) - f(3.0 - w)) }
|
||||
})
|
||||
.sum::<f64>()
|
||||
* 40.0
|
||||
/ n as f64;
|
||||
let want = tapering_plate_volume(2.0, false) - taken;
|
||||
assert!(rel(volume(&mut s), want) < 1e-6, "{} vs {want}", volume(&mut s));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fillet_fit_taper_closes_to_a_point() {
|
||||
// #293: a wedge against a wall, its top closing to a point at x = 40: no constant radius
|
||||
// fits there, and taper comes down to 0 at the point.
|
||||
let mut s = tapering_plate(0.0, true);
|
||||
assert!(s.execute("solid.fillet", &json!({"edges": [[20, 0, 15]], "radius": 16})).is_err());
|
||||
let out = run(&mut s, "solid.fillet", json!({"edges": [[20, 0, 15]], "radius": 16, "fit": "taper"}));
|
||||
let w = out["feature"].as_u64().and_then(|id| s.model.result(id)).and_then(|r| r.warning.clone()).unwrap_or_default();
|
||||
assert!(w.contains("on 1 of 1 edges, down to 0 mm"), "{w}");
|
||||
}
|
||||
|
||||
@@ -4050,10 +4093,7 @@ fn fillet_fit_taper_follows_the_room_along_the_edge() {
|
||||
fn spline_fillet_takes_over_a_taper() {
|
||||
// A tapered fillet keeps each edge's radius spline; given back as a `spline` fillet it makes
|
||||
// the same part, and its control radii are then the user's to change.
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY"}));
|
||||
run(&mut s, "sketch.line", json!({"points": [[0, 0], [40, 0], [40, 2], [0, 20]], "closed": true}));
|
||||
run(&mut s, "solid.extrude", json!({"distance": 15}));
|
||||
let mut s = tapering_plate(2.0, true);
|
||||
let edge = json!([[20, 0, 15]]);
|
||||
let out = run(&mut s, "solid.fillet", json!({"edges": edge, "radius": 3, "fit": "taper"}));
|
||||
let id = out["feature"].as_u64().unwrap();
|
||||
@@ -4079,29 +4119,54 @@ fn spline_fillet_takes_over_a_taper() {
|
||||
|
||||
#[test]
|
||||
fn fillet_fit_reduce_keeps_the_radius_where_it_fits() {
|
||||
// A 2 mm fin on a block: its top edge can't take 4, a corner of the block far from it can.
|
||||
// Only the fin's edge is reduced; the reply gives each edge's radius.
|
||||
// A 14 x 6 x 10 post on a block: its four vertical edges can't take 3.5 (two rounds on its
|
||||
// 6 mm ends would cross), a corner of the block far from it can. Only the post's edges are
|
||||
// reduced, to 3 (a full round); the reply gives each edge's radius.
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20}));
|
||||
run(&mut s, "solid.box", json!({"length": 2, "width": 30, "height": 10, "corner": [19, 0, 20], "operation": "join"}));
|
||||
run(&mut s, "solid.box", json!({"length": 14, "width": 6, "height": 10, "corner": [2, 2, 20], "operation": "join"}));
|
||||
assert_eq!(s.model.state().bodies.len(), 1);
|
||||
let edges = json!([[21, 15, 30], [40, 0, 10]]);
|
||||
let out = run(&mut s, "solid.fillet", json!({"edges": edges, "radius": 4, "fit": "reduce"}));
|
||||
let fin = out["radius_applied"].as_f64().unwrap();
|
||||
assert!(fin < 4.0, "{out}");
|
||||
let post = [[2.0, 2.0, 25.0], [16.0, 2.0, 25.0], [2.0, 8.0, 25.0], [16.0, 8.0, 25.0]];
|
||||
let edges = json!([post[0], post[1], post[2], post[3], [40, 0, 10]]);
|
||||
let out = run(&mut s, "solid.fillet", json!({"edges": edges, "radius": 3.5, "fit": "reduce"}));
|
||||
assert_eq!(out["radius_applied"].as_f64(), Some(3.0), "{out}");
|
||||
let each: Vec<(Vec<f64>, f64)> = out["radii_applied"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|x| (x["edge"].as_array().unwrap().iter().filter_map(Value::as_f64).collect(), x["radius"].as_f64().unwrap()))
|
||||
.collect();
|
||||
assert_eq!(each, [(vec![21.0, 15.0, 30.0], fin), (vec![40.0, 0.0, 10.0], 4.0)], "{out}");
|
||||
let removed = (1.0 - PI / 4.0) * (fin * fin * 30.0 + 16.0 * 20.0);
|
||||
let want = 40.0 * 30.0 * 20.0 + 2.0 * 30.0 * 10.0 - removed;
|
||||
let mut want_each: Vec<(Vec<f64>, f64)> = post.iter().map(|p| (p.to_vec(), 3.0)).collect();
|
||||
want_each.push((vec![40.0, 0.0, 10.0], 3.5));
|
||||
assert_eq!(each, want_each, "{out}");
|
||||
// The post's full round takes (4 − π)·9 of its section along its 10 mm height, the block's
|
||||
// corner (1 − π/4)·3.5² along 20.
|
||||
let removed = (4.0 - PI) * 9.0 * 10.0 + (1.0 - PI / 4.0) * 3.5 * 3.5 * 20.0;
|
||||
let want = 40.0 * 30.0 * 20.0 + 14.0 * 6.0 * 10.0 - removed;
|
||||
assert!(rel(volume(&mut s), want) < 2e-4, "{} vs {want}", volume(&mut s));
|
||||
let id = out["feature"].as_u64().unwrap();
|
||||
let warning = s.model.result(id).and_then(|r| r.warning.clone()).unwrap_or_default();
|
||||
assert!(warning.contains("on 1 of 2 edges"), "{warning}");
|
||||
assert!(warning.contains("on 4 of 5 edges"), "{warning}");
|
||||
}
|
||||
|
||||
/// A 2 mm fin's top edge at R4 runs over the fin's top, past its far edge (a cliff): it is cut
|
||||
/// as if the top went on (the blend-overflow decision), so with `fit: reduce` nothing is reduced.
|
||||
/// The fin's section loses the 4 x 4 square at its edge less the quarter disc about (17, 26)
|
||||
/// inside the fin (x >= 19), as `fillet_max_each_reduces_only_where_the_radius_does_not_fit`
|
||||
/// in the kernel finds it.
|
||||
#[test]
|
||||
fn fillet_fit_reduce_keeps_a_round_over_a_cliff() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "solid.box", json!({"length": 40, "width": 30, "height": 20}));
|
||||
run(&mut s, "solid.box", json!({"length": 2, "width": 30, "height": 10, "corner": [19, 0, 20], "operation": "join"}));
|
||||
let out = run(&mut s, "solid.fillet", json!({"edges": [[21, 15, 30], [40, 0, 10]], "radius": 4, "fit": "reduce"}));
|
||||
assert_eq!(out["radius_applied"].as_f64(), None, "{out}");
|
||||
let id = out["feature"].as_u64().unwrap();
|
||||
assert_eq!(s.model.result(id).and_then(|r| r.warning.clone()), None);
|
||||
let disc = |a: f64| 0.5 * (a * (16.0 - a * a).max(0.0).sqrt() + 16.0 * (a / 4.0).asin());
|
||||
let removed = (2.0 * 4.0 - (disc(4.0) - disc(2.0))) * 30.0 + (1.0 - PI / 4.0) * 16.0 * 20.0;
|
||||
let want = 40.0 * 30.0 * 20.0 + 2.0 * 30.0 * 10.0 - removed;
|
||||
assert!(rel(volume(&mut s), want) < 1e-6, "{} vs {want}", volume(&mut s));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -642,11 +642,26 @@ mod tests {
|
||||
assert!(fillet_variable(&b, at, start, 4.0, 1e-9).is_err());
|
||||
}
|
||||
|
||||
/// A prism of the polygon `pts` (in xy) from z = 0 to `h`.
|
||||
fn prism(pts: &[(f64, f64)], h: f64) -> Body {
|
||||
/// A prism of the polygon `pts` (in xy) from z = `z0` to `z1`.
|
||||
fn prism_between(pts: &[(f64, f64)], z0: f64, z1: f64) -> Body {
|
||||
use solidcraft_geom::{Loop2, Plane, Region2, Vec2};
|
||||
let outer = Loop2::polygon(&pts.iter().map(|(x, y)| Vec2::new(*x, *y)).collect::<Vec<_>>());
|
||||
crate::extrude(&Plane::XY, &[Region2 { outer, holes: vec![] }], 0.0, h).unwrap().pop().unwrap()
|
||||
crate::extrude(&Plane::XY, &[Region2 { outer, holes: vec![] }], z0, z1).unwrap().pop().unwrap()
|
||||
}
|
||||
|
||||
/// A prism of the polygon `pts` (in xy) from z = 0 to `h`.
|
||||
fn prism(pts: &[(f64, f64)], h: f64) -> Body {
|
||||
prism_between(pts, 0.0, h)
|
||||
}
|
||||
|
||||
/// A plate 15 high whose top narrows from 20 to 2 mm along 40 mm (the trapezoid (0, 0),
|
||||
/// (40, 0), (40, 2), (0, 20)), against a wall standing behind its slanted side from z = -15
|
||||
/// to 30: the plate's top and bottom end on the wall in concave edges, so a blend of its
|
||||
/// front edges can't run past them (no cliff to cut over). 6600 + 20 · 38 · 45.
|
||||
fn ledge() -> Body {
|
||||
let plate = prism(&[(0.0, 0.0), (40.0, 0.0), (40.0, 2.0), (0.0, 20.0)], 15.0);
|
||||
let wall = prism_between(&[(40.0, 2.0), (0.0, 20.0), (0.0, 30.0), (40.0, 30.0)], -15.0, 30.0);
|
||||
crate::boolean(&plate, &wall, crate::BoolOp::Union).unwrap().unwrap()
|
||||
}
|
||||
|
||||
/// The material a right-angled blend with the radius `law` removes along an edge of length L:
|
||||
@@ -663,17 +678,48 @@ mod tests {
|
||||
}
|
||||
|
||||
/// A taper on the top front edge of a wedge, whose top face narrows to a point, and of a
|
||||
/// trapezoid, whose top face narrows to 2 mm: the radius asked for where it fits, below
|
||||
/// 0.9 of the room everywhere, down to zero at the wedge's tip and to 1.8 mm at the
|
||||
/// trapezoid's narrow end; the volume integrates the spline.
|
||||
/// plate against a wall, whose top face narrows to 2 mm: the radius asked for where it fits,
|
||||
/// below 0.9 of the room everywhere, down to zero at the wedge's tip and to 1.8 mm at the
|
||||
/// plate's narrow end; the volume integrates the spline. On the free-standing wedge R16 runs
|
||||
/// over the top's far edge and the front's bottom edge (cliffs), so it is built as if those
|
||||
/// faces went on and cut from the wedge (the blend-overflow decision, 237f0ae74), and a
|
||||
/// tapered fillet keeps it; against the wall the plate's R3 has no room at the narrow end
|
||||
/// and tapers.
|
||||
#[test]
|
||||
fn taper_follows_the_room_along_the_edge() {
|
||||
let (at, start) = (Vec3::new(20.0, 0.0, 15.0), Vec3::new(0.0, 0.0, 15.0));
|
||||
let plate = ledge();
|
||||
let full = 6600.0 + 20.0 * 38.0 * 45.0;
|
||||
assert!(matches!(crate::fillet(&plate, &[at], 3.0), Err(KernelError::TooLarge(_))));
|
||||
let (f, tapers) = fillet_tapered(&plate, &[at], 3.0).unwrap();
|
||||
let t = tapers.into_iter().flatten().next().unwrap();
|
||||
let law = if t.start.dist(start) < 1e-9 { t.spline } else { t.spline.reversed() };
|
||||
assert!((law.eval(0.0) - 3.0).abs() < 1e-9 && (law.eval(1.0) - 1.8).abs() < 1e-9);
|
||||
let v = fine_volume(&f);
|
||||
assert!(rel(v, full - removed(&law, 40.0)) < 1e-4, "{v} vs {}", full - removed(&law, 40.0));
|
||||
// A radius that fits stays constant.
|
||||
let (g, tapers) = fillet_tapered(&plate, &[at], 1.0).unwrap();
|
||||
assert_eq!(tapers, vec![None]);
|
||||
assert!(rel(measure(&g).unwrap().volume, measure(&crate::fillet(&plate, &[at], 1.0).unwrap()).unwrap().volume) < 1e-12);
|
||||
|
||||
let wedge = prism(&[(0.0, 0.0), (40.0, 0.0), (0.0, 20.0)], 15.0);
|
||||
assert!(matches!(crate::fillet(&wedge, &[at], 16.0), Err(KernelError::TooLarge(_))));
|
||||
// At x the section is the rectangle [0, w] × [0, 15], w = 20 (1 − x/40), less the part
|
||||
// of the 16 × 16 square at the edge outside the circle about (16, 16): integrated along x.
|
||||
let cut = |w: f64| {
|
||||
let (r, s, t) = (16.0f64, 16.0f64.min(w), 15.0);
|
||||
let f = |v: f64| 0.5 * (v * (r * r - v * v).max(0.0).sqrt() + r * r * (v / r).clamp(-1.0, 1.0).asin());
|
||||
let s1 = (r - (r * r - (r - t) * (r - t)).sqrt()).min(s);
|
||||
t * s1 + r * (s - s1) - (f(r - s1) - f(r - s))
|
||||
};
|
||||
let n = 20000;
|
||||
let removed16: f64 = (0..n).map(|i| cut(20.0 * (1.0 - (i as f64 + 0.5) / n as f64))).sum::<f64>() * 40.0 / n as f64;
|
||||
let (f, tapers) = fillet_tapered(&wedge, &[at], 16.0).unwrap();
|
||||
assert_eq!(tapers, vec![None]);
|
||||
let v = measure(&f).unwrap().volume;
|
||||
assert!(rel(v, 6000.0 - removed16) < 1e-6, "{v} vs {}", 6000.0 - removed16);
|
||||
// The taper the room gives: the top face is 20 (1 − u) wide, the front 15 high.
|
||||
let t = taper(&wedge, at, 16.0).unwrap();
|
||||
let law = if t.start.dist(start) < 1e-9 { t.spline.clone() } else { t.spline.reversed() };
|
||||
// The room: the top face is 20 (1 − u) wide, the front 15 high.
|
||||
for i in 0..=100 {
|
||||
let u = i as f64 / 100.0;
|
||||
let r = law.eval(u);
|
||||
@@ -681,25 +727,11 @@ mod tests {
|
||||
}
|
||||
assert_eq!(law.eval(1.0), 0.0);
|
||||
assert!((law.eval(0.0) - 13.5).abs() < 1e-9);
|
||||
let (f, tapers) = fillet_tapered(&wedge, &[at], 16.0).unwrap();
|
||||
assert_eq!(tapers, vec![Some(t)]);
|
||||
let f = fillet_variable_spline(&wedge, at, t.start, &t.spline).unwrap();
|
||||
let v = fine_volume(&f);
|
||||
assert!(rel(v, 6000.0 - removed(&law, 40.0)) < 1e-4, "{v} vs {}", 6000.0 - removed(&law, 40.0));
|
||||
assert_eq!(f.unmeshed_faces(), 0);
|
||||
assert!(faces_and_vertices(&f).0.contains(&3));
|
||||
|
||||
let trap = prism(&[(0.0, 0.0), (40.0, 0.0), (40.0, 2.0), (0.0, 20.0)], 15.0);
|
||||
assert!(matches!(crate::fillet(&trap, &[at], 3.0), Err(KernelError::TooLarge(_))));
|
||||
let (f, tapers) = fillet_tapered(&trap, &[at], 3.0).unwrap();
|
||||
let t = tapers.into_iter().flatten().next().unwrap();
|
||||
let law = if t.start.dist(start) < 1e-9 { t.spline } else { t.spline.reversed() };
|
||||
assert!((law.eval(0.0) - 3.0).abs() < 1e-9 && (law.eval(1.0) - 1.8).abs() < 1e-9);
|
||||
let v = fine_volume(&f);
|
||||
assert!(rel(v, 6600.0 - removed(&law, 40.0)) < 1e-4, "{v} vs {}", 6600.0 - removed(&law, 40.0));
|
||||
// A radius that fits stays constant.
|
||||
let (g, tapers) = fillet_tapered(&trap, &[at], 1.0).unwrap();
|
||||
assert_eq!(tapers, vec![None]);
|
||||
assert!(rel(measure(&g).unwrap().volume, measure(&crate::fillet(&trap, &[at], 1.0).unwrap()).unwrap().volume) < 1e-12);
|
||||
}
|
||||
|
||||
/// A radius spline given directly: a cubic with its control radii pulling the radius up in
|
||||
|
||||
@@ -5,9 +5,11 @@ fn wedge() -> (Body, RadiusSpline) {
|
||||
let outer = Loop2::polygon(&[Vec2::new(0., 0.), Vec2::new(40., 0.), Vec2::new(0., 20.)]);
|
||||
let body = crate::extrude(&Plane::XY, &[Region2 { outer, holes: vec![] }], 0., 15.).unwrap().pop().unwrap();
|
||||
let at = Vec3::new(20., 0., 15.);
|
||||
let (filleted, tapers) = fillet_tapered(&body, &[at], 16.).unwrap();
|
||||
let taper = tapers.into_iter().flatten().next().unwrap();
|
||||
(filleted, taper.spline)
|
||||
// The room's taper, closing to zero at the wedge's tip, given as a spline fillet: R16 itself
|
||||
// is built over the wedge's cliffs (the blend-overflow decision), so a tapered fillet no
|
||||
// longer tapers it.
|
||||
let t = taper(&body, at, 16.).unwrap();
|
||||
(fillet_variable_spline(&body, at, t.start, &t.spline).unwrap(), t.spline)
|
||||
}
|
||||
|
||||
fn expected(law: &RadiusSpline) -> f64 {
|
||||
|
||||
@@ -2,13 +2,22 @@ use crate::scenario::Harness;
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_engine::doc::{FeatureKind, FilletStyle, kernel};
|
||||
|
||||
/// A plate whose top narrows from 20 to 2 mm along 40 mm, its top front edge rounded at 3 mm
|
||||
/// with Taper to Fit.
|
||||
fn fixture() -> Harness {
|
||||
let mut h = Harness::new();
|
||||
/// A plate whose top narrows from 20 to 2 mm along 40 mm, against a wall behind its slanted
|
||||
/// side from z = -15 to 30 (so a blend of its front edges has no cliff to run over and is cut
|
||||
/// short by the wall: the blend-overflow decision, 237f0ae74).
|
||||
fn plate(h: &mut Harness) {
|
||||
h.app.run("sketch.create", json!({"plane": "XY"})).unwrap();
|
||||
h.app.run("sketch.line", json!({"points": [[0, 0], [40, 0], [40, 2], [0, 20]], "closed": true})).unwrap();
|
||||
h.app.run("solid.extrude", json!({"distance": 15, "body_name": "Plate"})).unwrap();
|
||||
h.app.run("sketch.create", json!({"plane": {"origin": [0, 0, -15], "x_dir": [1, 0, 0], "y_dir": [0, 1, 0]}})).unwrap();
|
||||
h.app.run("sketch.line", json!({"points": [[40, 2], [0, 20], [0, 30], [40, 30]], "closed": true})).unwrap();
|
||||
h.app.run("solid.extrude", json!({"distance": 45, "operation": "join"})).unwrap();
|
||||
}
|
||||
|
||||
/// [`plate`], its top front edge rounded at 3 mm with Taper to Fit.
|
||||
fn fixture() -> Harness {
|
||||
let mut h = Harness::new();
|
||||
plate(&mut h);
|
||||
h.app.run("solid.fillet", json!({"edges": [[20, 0, 15]], "radius": 3, "fit": "taper", "name": "Round"})).unwrap();
|
||||
h.frames(4);
|
||||
h
|
||||
@@ -174,9 +183,7 @@ fn double_clicks_add_and_remove_control_radii() {
|
||||
#[test]
|
||||
fn a_taper_over_several_edges_keeps_the_radius_arrow() {
|
||||
let mut h = Harness::new();
|
||||
h.app.run("sketch.create", json!({"plane": "XY"})).unwrap();
|
||||
h.app.run("sketch.line", json!({"points": [[0, 0], [40, 0], [40, 2], [0, 20]], "closed": true})).unwrap();
|
||||
h.app.run("solid.extrude", json!({"distance": 15, "body_name": "Plate"})).unwrap();
|
||||
plate(&mut h);
|
||||
h.app.run("solid.fillet", json!({"edges": [[20, 0, 15], [20, 0, 0]], "radius": 3, "fit": "taper", "name": "Round"})).unwrap();
|
||||
h.frames(4);
|
||||
open(&mut h);
|
||||
|
||||
@@ -1154,8 +1154,11 @@ mod tests {
|
||||
fn the_feature_menu_copies_a_warning_message() {
|
||||
for left in [false, true] {
|
||||
let mut h = two_boxes(left);
|
||||
// Too large for the box: reduced to what fits, which the feature's warning says.
|
||||
h.app.run("solid.fillet", json!({"edges": [[5, 0, 10]], "radius": 20, "fit": "reduce", "body": "One"})).unwrap();
|
||||
// Too large for the box: R6 on its four vertical edges would cross on the 10 mm faces,
|
||||
// so it is reduced to what fits (a full round, 5), which the feature's warning says.
|
||||
h.app
|
||||
.run("solid.fillet", json!({"edges": [[0, 0, 5], [10, 0, 5], [10, 10, 5], [0, 10, 5]], "radius": 6, "fit": "reduce", "body": "One"}))
|
||||
.unwrap();
|
||||
h.frames(4);
|
||||
let id = h.app.session.doc.features.last().map(|f| f.id).unwrap();
|
||||
let name = h.app.session.doc.features.last().map(|f| f.name.clone()).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user