mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
fix: Projection d'une silhouette à congés coniques fermée en un profil (#10773)
- snap_tangent_arcs : chaque extrémité d'arc prend la ligne la plus proche - une ligne ne prend qu'une extrémité d'un arc ; l'autre passe à sa ligne suivante - une ligne plus loin sur le contour est ignorée quand l'extrémité touche déjà une autre pièce - tests : 4 unitaires (project_order.rs) et the_silhouette_of_conic_rounds_closes Co-authored-by: P'tite Tête <asso@ptitetete.com>
This commit is contained in:
+33
-10
@@ -1543,8 +1543,9 @@ fn snap_tangent_arcs(out: &mut [LinkGeom], tol: f64) {
|
||||
for i in 0..out.len() {
|
||||
let Some(LinkGeom::Arc { c, a, b }) = out.get(i).cloned() else { continue };
|
||||
let r = (c.dist(a) + c.dist(b)) / 2.0;
|
||||
// (arc end, line index, line end index, tangent point, unit normal towards the centre, centre distance)
|
||||
let mut hits: Vec<(usize, usize, usize, Vec2, Vec2, f64)> = Vec::new();
|
||||
// Per arc end: (gap to the line's end, line index, line end index, tangent point, unit
|
||||
// normal towards the centre, centre distance)
|
||||
let mut hits: [Vec<(f64, usize, usize, Vec2, Vec2, f64)>; 2] = [Vec::new(), Vec::new()];
|
||||
for (end, e) in [a, b].into_iter().enumerate() {
|
||||
for (j, g) in out.iter().enumerate() {
|
||||
let LinkGeom::Line(p, q) = g else { continue };
|
||||
@@ -1562,17 +1563,39 @@ fn snap_tangent_arcs(out: &mut [LinkGeom], tol: f64) {
|
||||
if (foot - far).dot(near_pt - far) <= 0.0 || near_pt.dist(foot) > 0.25 * r {
|
||||
continue;
|
||||
}
|
||||
hits.push((end, j, near, foot, n, d));
|
||||
// When this end of the arc already meets another piece of the outline and the line
|
||||
// doesn't end there, the line lies further along (past that piece, as along a
|
||||
// conic fitted with several arcs): moving the end onto it would leave a gap.
|
||||
let gap = near_pt.dist(e);
|
||||
let meets = |g: &LinkGeom| match *g {
|
||||
LinkGeom::Line(s, t) | LinkGeom::Arc { a: s, b: t, .. } => s.dist(e) <= tol || t.dist(e) <= tol,
|
||||
_ => false,
|
||||
};
|
||||
if gap > tol && out.iter().enumerate().any(|(k, g)| k != i && k != j && meets(g)) {
|
||||
continue;
|
||||
}
|
||||
if let Some(h) = hits.get_mut(end) {
|
||||
h.push((gap, j, near, foot, n, d));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut hits_by_end: [Option<(usize, usize, Vec2, Vec2, f64)>; 2] = [None, None];
|
||||
for (end, j, near, foot, n, d) in hits {
|
||||
if let Some(slot) = hits_by_end.get_mut(end)
|
||||
&& slot.is_none()
|
||||
{
|
||||
*slot = Some((j, near, foot, n, d));
|
||||
}
|
||||
for h in &mut hits {
|
||||
h.sort_by(|x, y| x.0.total_cmp(&y.0));
|
||||
}
|
||||
// Each end takes the line ending nearest it. Both ends on one line would pull the arc down
|
||||
// to a point: the end nearer that line's end keeps it, the other takes its next line.
|
||||
let [ha, hb] = &hits;
|
||||
let (ta, tb) = match (ha.first(), hb.first()) {
|
||||
(Some(x), Some(y)) if x.1 == y.1 => {
|
||||
if x.0 <= y.0 {
|
||||
(Some(x), hb.iter().find(|h| h.1 != x.1))
|
||||
} else {
|
||||
(ha.iter().find(|h| h.1 != y.1), Some(y))
|
||||
}
|
||||
}
|
||||
pair => pair,
|
||||
};
|
||||
let hits_by_end: [Option<(usize, usize, Vec2, Vec2, f64)>; 2] = [ta, tb].map(|h| h.map(|&(_, j, near, foot, n, d)| (j, near, foot, n, d)));
|
||||
let (mut c, mut r) = (c, r);
|
||||
if let [Some((_, _, _, n1, d1)), Some((_, _, _, n2, d2))] = hits_by_end {
|
||||
// One circle tangent to both lines: equal distance to each.
|
||||
|
||||
@@ -235,6 +235,117 @@ mod tests {
|
||||
let (Some(LinkGeom::Arc { a, .. }), Some(LinkGeom::Line(p, _))) = (out.get(1), out.get(2)) else { panic!("{out:?}") };
|
||||
assert!(a.dist(*p) < 1e-9, "{out:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tangent_snapping_leaves_a_line_further_along_alone() {
|
||||
use solidcraft_geom::Vec2;
|
||||
use solidcraft_sketch::LinkGeom;
|
||||
let v = Vec2::new;
|
||||
let y = |x: f64| 2.0 - (4.0 - x * x).sqrt();
|
||||
// The arc's end at x = 0.5 already meets the next piece; the line along y = 0 ends at
|
||||
// x = 0.3, further along the outline the other way. Snapping onto it would pull the end
|
||||
// off the piece it meets.
|
||||
let mut out = vec![
|
||||
LinkGeom::Arc { c: v(0., 2.), a: v(0.5, y(0.5)), b: v(-1.5, y(-1.5)) },
|
||||
LinkGeom::Line(v(0.3, 0.), v(10., 0.)),
|
||||
LinkGeom::Line(v(0.5, y(0.5)), v(3., 3.)),
|
||||
];
|
||||
let before = out.clone();
|
||||
super::super::snap_tangent_arcs(&mut out, 0.05);
|
||||
assert_eq!(out.get(1), before.get(1), "{out:?}");
|
||||
let (Some(LinkGeom::Arc { a, .. }), Some(LinkGeom::Line(p, _))) = (out.first(), out.get(2)) else { panic!("{out:?}") };
|
||||
assert!(a.dist(*p) < 1e-9, "{out:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tangent_snapping_gives_one_line_to_one_end_of_an_arc() {
|
||||
use solidcraft_geom::Vec2;
|
||||
use solidcraft_sketch::LinkGeom;
|
||||
let v = Vec2::new;
|
||||
let y = |x: f64| 2.0 - (4.0 - x * x).sqrt();
|
||||
// A short arc whose two ends both lie near the end of one line: both snapping onto it
|
||||
// would shrink the arc to the tangent point.
|
||||
let mut out = vec![LinkGeom::Arc { c: v(0., 2.), a: v(-0.2, y(-0.2)), b: v(0.2, y(0.2)) }, LinkGeom::Line(v(-10., 0.), v(0., 0.))];
|
||||
super::super::snap_tangent_arcs(&mut out, 0.05);
|
||||
let Some(LinkGeom::Arc { a, b, .. }) = out.first() else { panic!("{out:?}") };
|
||||
assert!(a.dist(*b) > 0.1, "{out:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tangent_snapping_moves_the_other_end_of_an_arc_to_its_next_line() {
|
||||
use solidcraft_geom::Vec2;
|
||||
use solidcraft_sketch::LinkGeom;
|
||||
let v = Vec2::new;
|
||||
let y = |x: f64| 2.0 - (4.0 - x * x).sqrt();
|
||||
let (a, b) = (v(-0.2, y(-0.2)), v(0.2, y(0.2)));
|
||||
// The line along y = 0 ends nearest both ends of the arc but nearer b, which keeps it; a
|
||||
// takes the next line, tangent to the circle at a and ending a little past it.
|
||||
let back = (v(0., 2.) - a).normalized().map(|n| v(-n.y, n.x)).unwrap_or(Vec2::X);
|
||||
let mut out =
|
||||
vec![LinkGeom::Arc { c: v(0., 2.), a, b }, LinkGeom::Line(v(0.1, 0.), v(10., 0.)), LinkGeom::Line(a + back * 10., a + back * 0.35)];
|
||||
super::super::snap_tangent_arcs(&mut out, 0.05);
|
||||
let (Some(LinkGeom::Arc { a, b, .. }), Some(LinkGeom::Line(p, _)), Some(LinkGeom::Line(_, q))) = (out.first(), out.get(1), out.get(2)) else {
|
||||
panic!("{out:?}")
|
||||
};
|
||||
assert!(b.dist(*p) < 1e-9 && a.dist(*q) < 1e-9 && a.dist(*b) > 0.1, "{out:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tangent_snapping_closes_a_conic_corner_in_any_order() {
|
||||
use solidcraft_geom::Vec2;
|
||||
use solidcraft_sketch::LinkGeom;
|
||||
// A square of side 150 with a conic round of 10 mm legs at each corner, fitted with lines
|
||||
// and arcs the way a silhouette is, then snapped with the pieces in each order.
|
||||
for rho in [0.1, 0.15, 0.5, 0.8, 0.9] {
|
||||
let w = rho / (1. - rho);
|
||||
let conic = |s: Vec2, apex: Vec2, e: Vec2, t: f64| {
|
||||
let (b0, b1, b2) = ((1. - t) * (1. - t), 2. * t * (1. - t) * w, t * t);
|
||||
(s * b0 + apex * b1 + e * b2) * (1. / (b0 + b1 + b2))
|
||||
};
|
||||
let v = Vec2::new;
|
||||
let corners = [
|
||||
(v(-75., 65.), v(-75., 75.), v(-65., 75.)),
|
||||
(v(65., 75.), v(75., 75.), v(75., 65.)),
|
||||
(v(75., -65.), v(75., -75.), v(65., -75.)),
|
||||
(v(-65., -75.), v(-75., -75.), v(-75., -65.)),
|
||||
];
|
||||
let mut chain = Vec::new();
|
||||
for (s, apex, e) in corners {
|
||||
chain.extend((0..=40).map(|k| conic(s, apex, e, f64::from(k) / 40.)));
|
||||
}
|
||||
if let Some(&first) = chain.first() {
|
||||
chain.push(first);
|
||||
}
|
||||
let mut fitted = Vec::new();
|
||||
super::super::fit_chain_with_arcs(&chain, Some(212. * 1e-5), &mut fitted, true);
|
||||
assert!(fitted.iter().any(|g| matches!(g, LinkGeom::Arc { .. })), "{fitted:?}");
|
||||
let ends = |g: &LinkGeom| match *g {
|
||||
LinkGeom::Line(s, t) | LinkGeom::Arc { a: s, b: t, .. } => vec![s, t],
|
||||
_ => Vec::new(),
|
||||
};
|
||||
for k in 0..fitted.len() {
|
||||
let mut out = fitted.clone();
|
||||
out.rotate_left(k);
|
||||
super::super::snap_tangent_arcs(&mut out, 212. * 1e-3);
|
||||
// Every end still meets another piece's end. An end left without a line moves a little
|
||||
// when the arc's radius is set from the lines (well under 0.01 mm here); before, ends
|
||||
// pulled onto lines further along left gaps of 0.25 to 0.55 mm.
|
||||
for (i, g) in out.iter().enumerate() {
|
||||
for e in ends(g) {
|
||||
let gap = out
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(j, _)| *j != i)
|
||||
.flat_map(|(_, h)| ends(h))
|
||||
.map(|x| x.dist(e))
|
||||
.fold(f64::INFINITY, f64::min);
|
||||
assert!(gap < 212. * 1e-3 * 0.02, "rho {rho}, rotation {k}: gap {gap} at {e:?}: {out:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boundaries_touching_at_a_vertex_project() {
|
||||
// Two squares of one face family meet only at the corner (1, 1): four boundary edges meet
|
||||
|
||||
@@ -1298,3 +1298,46 @@ fn a_thicker_angle_moves_the_projected_face_of_the_sketch_on_it() {
|
||||
check(&s);
|
||||
check(&Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap()));
|
||||
}
|
||||
|
||||
/// The silhouette of a square with a conic round at each corner (rho 0.19, 0.4, 0.5 and 0.7) is
|
||||
/// one closed outline of the square's true area. Fitted arcs along a conic were snapped onto lines
|
||||
/// that ended further along the outline, which left gaps of up to 3.6 mm: no profile.
|
||||
#[test]
|
||||
fn the_silhouette_of_conic_rounds_closes() {
|
||||
let mut s = Session::default();
|
||||
let corners = [
|
||||
([-75., 65.], [-75., 75.], [-65., 75.], 0.19),
|
||||
([65., 75.], [75., 75.], [75., 65.], 0.4),
|
||||
([75., -65.], [75., -75.], [65., -75.], 0.5),
|
||||
([-65., -75.], [-75., -75.], [-75., -65.], 0.7),
|
||||
];
|
||||
run(&mut s, "sketch.create", json!({"plane": "XZ"}));
|
||||
for (i, (start, apex, end, rho)) in corners.iter().enumerate() {
|
||||
run(&mut s, "sketch.conic", json!({"start": start, "apex": apex, "end": end, "rho": rho}));
|
||||
let next = corners[(i + 1) % 4].0;
|
||||
run(&mut s, "sketch.line", json!({"points": [end, next]}));
|
||||
}
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"distance": 55, "body_name": "Part"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XZ", "offset": -10}));
|
||||
run(&mut s, "sketch.project", json!({"refs": [{"body": "Part"}]}));
|
||||
// The true area: the outline through the four rational quadratic Béziers (apex weight
|
||||
// rho / (1 - rho)), finely sampled, and the straight sides between them.
|
||||
let mut outline: Vec<[f64; 2]> = Vec::new();
|
||||
for (start, apex, end, rho) in corners {
|
||||
let w = rho / (1. - rho);
|
||||
for k in 0..=2000 {
|
||||
let t = f64::from(k) / 2000.;
|
||||
let (b0, b1, b2) = ((1. - t) * (1. - t), 2. * t * (1. - t) * w, t * t);
|
||||
let sum = b0 + b1 + b2;
|
||||
outline.push([(b0 * start[0] + b1 * apex[0] + b2 * end[0]) / sum, (b0 * start[1] + b1 * apex[1] + b2 * end[1]) / sum]);
|
||||
}
|
||||
}
|
||||
let n = outline.len();
|
||||
let exact = (0..n).map(|i| outline[i][0] * outline[(i + 1) % n][1] - outline[(i + 1) % n][0] * outline[i][1]).sum::<f64>().abs() / 2.;
|
||||
let si = inspect(&mut s);
|
||||
let profiles = si["profiles"].as_array().unwrap();
|
||||
assert_eq!(profiles.len(), 1, "{}", si["profiles"]);
|
||||
let area = profiles[0]["area_mm2"].as_f64().unwrap();
|
||||
assert!((area - exact).abs() < exact * 2e-4, "area {area}, exact {exact}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user