Thanks @DuBearte.
This commit is contained in:
1 parent
25c89a706e
commit
61678654c2
2 files changed
+170
-5
No files matched your search
@@ -372,6 +372,10 @@ pub struct Loop2 {
|
||||
}
|
||||
|
||||
impl Loop2 {
|
||||
/// The radius of the largest circle inside the loop, to about `tol` ([`Region2::inradius`]).
|
||||
pub fn inradius(&self, tol: f64) -> f64 {
|
||||
max_clearance(&[self.polyline(tol)])
|
||||
}
|
||||
/// Full circle as two half arcs (counter-clockwise).
|
||||
pub fn circle(center: Vec2, radius: f64) -> Loop2 {
|
||||
Loop2 {
|
||||
@@ -505,6 +509,79 @@ impl Region2 {
|
||||
pub fn contains(&self, p: Vec2) -> bool {
|
||||
self.outer.contains(p) && !self.holes.iter().any(|h| h.contains(p))
|
||||
}
|
||||
/// The radius of the largest circle inside the region: how far its sides can move inwards
|
||||
/// before they meet (a taper going in closes it there). Found by a grid search refined
|
||||
/// around the best point, to about `tol`.
|
||||
pub fn inradius(&self, tol: f64) -> f64 {
|
||||
let loops: Vec<Vec<Vec2>> = std::iter::once(&self.outer).chain(&self.holes).map(|l| l.polyline(tol)).collect();
|
||||
max_clearance(&loops)
|
||||
}
|
||||
}
|
||||
|
||||
/// The largest distance from a point inside `loops` (even-odd) to their sides, by a grid search
|
||||
/// over their box refined around the best point. Zero for an empty or degenerate outline.
|
||||
fn max_clearance(loops: &[Vec<Vec2>]) -> f64 {
|
||||
let sides: Vec<(Vec2, Vec2)> = loops
|
||||
.iter()
|
||||
.flat_map(|pl| (0..pl.len()).filter_map(move |i| Some((*pl.get(i)?, *pl.get((i + 1) % pl.len())?))))
|
||||
.filter(|(a, b)| a.dist(*b) > 0.0)
|
||||
.collect();
|
||||
let (Some(lo), Some(hi)) = (
|
||||
sides.iter().map(|s| s.0).reduce(|a, b| Vec2::new(a.x.min(b.x), a.y.min(b.y))),
|
||||
sides.iter().map(|s| s.0).reduce(|a, b| Vec2::new(a.x.max(b.x), a.y.max(b.y))),
|
||||
) else {
|
||||
return 0.0;
|
||||
};
|
||||
let inside = |p: Vec2| {
|
||||
let mut odd = false;
|
||||
for (a, b) in &sides {
|
||||
if (a.y > p.y) != (b.y > p.y) && p.x < a.x + (p.y - a.y) / (b.y - a.y) * (b.x - a.x) {
|
||||
odd = !odd;
|
||||
}
|
||||
}
|
||||
odd
|
||||
};
|
||||
let clearance = |p: Vec2| -> f64 {
|
||||
if !inside(p) {
|
||||
return 0.0;
|
||||
}
|
||||
sides
|
||||
.iter()
|
||||
.map(|(a, b)| {
|
||||
let ab = *b - *a;
|
||||
let t = ((p - *a).dot(ab) / ab.len2()).clamp(0.0, 1.0);
|
||||
p.dist(*a + ab * t)
|
||||
})
|
||||
.fold(f64::INFINITY, f64::min)
|
||||
};
|
||||
const N: usize = 32;
|
||||
let size = hi - lo;
|
||||
let mut best = (0.0, (lo + hi) * 0.5);
|
||||
for i in 0..=N {
|
||||
for j in 0..=N {
|
||||
let p = Vec2::new(lo.x + size.x * i as f64 / N as f64, lo.y + size.y * j as f64 / N as f64);
|
||||
let c = clearance(p);
|
||||
if c > best.0 {
|
||||
best = (c, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Refine: try the eight neighbours, halving the step whenever none is better.
|
||||
let mut step = size.x.max(size.y) / N as f64;
|
||||
for _ in 0..60 {
|
||||
if step < size.x.max(size.y) * 1e-9 {
|
||||
break;
|
||||
}
|
||||
let around = [(1.0, 0.0), (-1.0, 0.0), (0.0, 1.0), (0.0, -1.0), (1.0, 1.0), (1.0, -1.0), (-1.0, 1.0), (-1.0, -1.0)];
|
||||
match around.iter().map(|(dx, dy)| best.1 + Vec2::new(dx * step, dy * step)).map(|p| (clearance(p), p)).max_by(|a, b| a.0.total_cmp(&b.0)) {
|
||||
Some(n) if n.0 > best.0 => best = n,
|
||||
_ => step *= 0.5,
|
||||
}
|
||||
}
|
||||
best.0
|
||||
}
|
||||
|
||||
impl Region2 {
|
||||
/// Triangles covering the region (for filled display), by slabs between the vertex
|
||||
/// heights: inside each slab the boundary edges don't cross, so consecutive crossings pair
|
||||
/// up into trapezoids.
|
||||
@@ -672,6 +749,22 @@ mod tests {
|
||||
Region2 { outer: l, holes: Vec::new() }
|
||||
}
|
||||
|
||||
/// #1602: the largest circle inside a region, which bounds how far a taper can go.
|
||||
#[test]
|
||||
fn inradius_of_rectangles_circles_and_rings() {
|
||||
let close = |a: f64, b: f64| (a - b).abs() < 1e-3 * b.max(1.0);
|
||||
let rect = Loop2::polygon(&[Vec2::new(0.0, 0.0), Vec2::new(10.0, 0.0), Vec2::new(10.0, 4.0), Vec2::new(0.0, 4.0)]);
|
||||
assert!(close(solid(rect.clone()).inradius(1e-3), 2.0), "{}", solid(rect.clone()).inradius(1e-3));
|
||||
assert!(close(rect.inradius(1e-3), 2.0));
|
||||
let disc = Loop2::circle(Vec2::new(3.0, -2.0), 5.0);
|
||||
assert!(close(solid(disc.clone()).inradius(1e-4), 5.0), "{}", solid(disc).inradius(1e-4));
|
||||
// A ring 10 to 6: the material is 4 wide (radius 2); the hole alone has radius 6.
|
||||
let ring = Region2 { outer: Loop2::circle(Vec2::ZERO, 10.0), holes: vec![Loop2::circle(Vec2::ZERO, 6.0).reversed()] };
|
||||
assert!(close(ring.inradius(1e-4), 2.0), "{}", ring.inradius(1e-4));
|
||||
assert!(close(ring.holes[0].inradius(1e-4), 6.0));
|
||||
assert_eq!(Region2::default().inradius(1e-3), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regions_are_apart_only_when_nothing_meets_or_holds_another() {
|
||||
let gap = 1e-3;
|
||||
|
||||
@@ -425,7 +425,20 @@ fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj,
|
||||
let px = 2.0 * half_height / f64::from(proj.rect.height().max(1.0));
|
||||
let ang = app.session.doc.eval(taper, ValueKind::Angle).ok().filter(|v| v.is_finite()).unwrap_or(0.0);
|
||||
let before = taper.clone();
|
||||
taper_handle(ui, painter, proj, base - e * (40.0 * px), n * l, -e, ang, taper);
|
||||
let st = app.session.world_state();
|
||||
let regions: Vec<&solidcraft_engine::geom::Region2> = d
|
||||
.inputs
|
||||
.first()
|
||||
.map(|i| &i.items[..])
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.filter_map(|x| match x {
|
||||
Sel::Profile { sketch, index } => st.sketch(*sketch).and_then(|ss| ss.profiles.get(*index)).map(|pr| &pr.region),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let limits = taper_limits(®ions, l.abs());
|
||||
taper_handle(ui, painter, proj, base - e * (40.0 * px), n * l, -e, ang, limits, taper);
|
||||
if *taper != before {
|
||||
*taper_present = true;
|
||||
}
|
||||
@@ -857,16 +870,42 @@ fn measure_components(app: &SolveApp, painter: &egui::Painter, proj: &Proj, a: V
|
||||
}
|
||||
}
|
||||
|
||||
/// The tapers (degrees, least and most) a drag of the taper handle may reach for `regions`
|
||||
/// extruded `height` (#1602): going in, the sides close the material where they have moved its
|
||||
/// inradius; going out, they close the smallest hole. Each stops just short, so the drag ends at
|
||||
/// the largest taper that builds instead of one that fails. A typed angle is not limited.
|
||||
pub(crate) fn taper_limits(regions: &[&solidcraft_engine::geom::Region2], height: f64) -> (f64, f64) {
|
||||
const MAX: f64 = 89.0;
|
||||
if !(height > 1e-9) || regions.is_empty() {
|
||||
return (-MAX, MAX);
|
||||
}
|
||||
let at = |r: f64| (0.99 * r / height).atan().to_degrees().min(MAX);
|
||||
let inward = regions.iter().map(|r| r.inradius(1e-3)).fold(f64::INFINITY, f64::min);
|
||||
let outward = regions.iter().flat_map(|r| &r.holes).map(|h| h.inradius(1e-3)).fold(f64::INFINITY, f64::min);
|
||||
(if inward.is_finite() { -at(inward) } else { -MAX }, if outward.is_finite() { at(outward) } else { MAX })
|
||||
}
|
||||
|
||||
/// The taper handle: a line from `foot` along the extrude (`span`) leaning by `angle` towards
|
||||
/// `e`, with a ring at its end; dragging the ring sideways sets the angle (1° steps, Alt/Ctrl:
|
||||
/// free).
|
||||
/// free), within `limits` ([`taper_limits`]).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn taper_handle(ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj, foot: Vec3, span: Vec3, e: Vec3, angle: f64, value: &mut String) {
|
||||
fn taper_handle(
|
||||
ui: &mut egui::Ui,
|
||||
painter: &egui::Painter,
|
||||
proj: &Proj,
|
||||
foot: Vec3,
|
||||
span: Vec3,
|
||||
e: Vec3,
|
||||
angle: f64,
|
||||
limits: (f64, f64),
|
||||
value: &mut String,
|
||||
) {
|
||||
let t = Tokens::get();
|
||||
let l = span.len();
|
||||
let end = foot + span + e * (l * angle.tan().clamp(-10.0, 10.0));
|
||||
let (Some(fs), Some(es)) = (proj.to_screen(foot), proj.to_screen(end)) else { return };
|
||||
let r = ui.interact(egui::Rect::from_center_size(es, vec2(16.0, 16.0)), egui::Id::new("sc_taper"), egui::Sense::drag());
|
||||
crate::scenario::publish_handle("taper", es);
|
||||
let hot = r.hovered() || r.dragged();
|
||||
let col = if hot { t.accent } else { t.manipulator };
|
||||
painter.add(egui::Shape::dashed_line(&[fs, es], Stroke::new(1.2, col), 5.0, 3.0));
|
||||
@@ -879,8 +918,12 @@ fn taper_handle(ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj, foot: V
|
||||
&& let Some(s) = along(proj, p, foot + span, e)
|
||||
{
|
||||
let mut deg = s.atan2(l).to_degrees();
|
||||
if !ui.input(|i| i.modifiers.alt || i.modifiers.ctrl || i.modifiers.command) {
|
||||
deg = deg.round();
|
||||
let (lo, hi) = limits;
|
||||
if ui.input(|i| i.modifiers.alt || i.modifiers.ctrl || i.modifiers.command) {
|
||||
deg = deg.clamp(lo, hi);
|
||||
} else {
|
||||
// Whole degrees, rounded towards zero at a limit so they stay inside it.
|
||||
deg = deg.round().clamp(lo.ceil().min(0.0), hi.floor().max(0.0));
|
||||
}
|
||||
let txt = format!("{deg:.1}");
|
||||
*value = format!("{} deg", txt.trim_end_matches('0').trim_end_matches('.'));
|
||||
@@ -1262,3 +1305,32 @@ mod tests {
|
||||
assert_eq!(format_len(-0.0001), "0 mm");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod taper_limit_tests {
|
||||
use super::taper_limits;
|
||||
use solidcraft_engine::geom::{Loop2, Region2, Vec2};
|
||||
|
||||
/// #1602: a drag stops just short of the taper that closes the profile, either way.
|
||||
#[test]
|
||||
fn a_taper_drag_stops_before_the_profile_closes() {
|
||||
let close = |a: f64, b: f64| (a - b).abs() < 0.05;
|
||||
let deg = |r: f64, h: f64| (0.99 * r / h).atan().to_degrees();
|
||||
// A 10 × 4 rectangle 26 high: going in closes at its half width; going out never does.
|
||||
let rect = Region2 {
|
||||
outer: Loop2::polygon(&[Vec2::new(0.0, 0.0), Vec2::new(10.0, 0.0), Vec2::new(10.0, 4.0), Vec2::new(0.0, 4.0)]),
|
||||
holes: Vec::new(),
|
||||
};
|
||||
let (lo, hi) = taper_limits(&[&rect], 26.0);
|
||||
assert!(close(lo, -deg(2.0, 26.0)) && hi == 89.0, "{lo} {hi}");
|
||||
// A ring 10 to 6, 10 high: in, its 4 mm wall; out, its hole of radius 6.
|
||||
let ring = Region2 { outer: Loop2::circle(Vec2::ZERO, 10.0), holes: vec![Loop2::circle(Vec2::ZERO, 6.0).reversed()] };
|
||||
let (lo, hi) = taper_limits(&[&ring], 10.0);
|
||||
assert!(close(lo, -deg(2.0, 10.0)) && close(hi, deg(6.0, 10.0)), "{lo} {hi}");
|
||||
// The tightest of several profiles; no profile or no height, no limit.
|
||||
let (lo, _) = taper_limits(&[&ring, &rect], 10.0);
|
||||
assert!(close(lo, -deg(2.0, 10.0)));
|
||||
assert_eq!(taper_limits(&[], 10.0), (-89.0, 89.0));
|
||||
assert_eq!(taper_limits(&[&rect], 0.0), (-89.0, 89.0));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user