Files
cadcraft/crates/engine/tests/it/explode_stretched.rs
T
2026-11-04 19:48:04 -05:00

141 lines
7.1 KiB
Rust

//! EXPLODE of a block reference whose X and Y scales differ: its circles, arcs, ellipses and
//! polyline arcs come out as the ellipses and elliptical arcs the reference draws, not as circles
//! scaled by the mean of the two scales.
use cadcraft_engine::Session;
use cadcraft_engine::doc::{Entity, EntityKind};
use serde_json::json;
const AT: [f64; 2] = [100.0, 50.0];
const ROTATION: f64 = 30.0;
/// Block S (base 0,0): a circle (r 5 at 0,0), an arc (r 4 at 20,0, 0°..90°), an elliptical arc
/// (centre 40,0, major 6 along X, ratio 0.5, 0°..180°), a slot polyline (two lines, two
/// semicircles of r 2 at 60,0 and 70,0) and a line. One reference to it at `AT`, rotated by
/// `ROTATION`, scaled by `scale`, exploded.
fn exploded(scale: [f64; 2]) -> Vec<Entity> {
let mut s = Session::new();
let mut hs = Vec::new();
for (id, p) in [
("circle", json!({"center": [0, 0], "radius": 5})),
("arc", json!({"center": [20, 0], "radius": 4, "start": 0, "end": 90})),
("ellipse", json!({"center": [40, 0], "major": [6, 0], "ratio": 0.5, "start": 0, "end": 180})),
("pline", json!({"vertices": [[60, -2], {"p": [70, -2], "bulge": 1}, [70, 2], {"p": [60, 2], "bulge": 1}], "closed": true})),
("line", json!({"points": [[0, 10], [10, 10]]})),
] {
s.execute(id, &p).unwrap();
hs.push(s.doc().unwrap().model.iter().last().unwrap().handle.hex());
}
s.execute("block", &json!({"name": "S", "base": [0, 0], "keep": "delete", "handles": hs})).unwrap();
let r = s.execute("insert", &json!({"name": "S", "at": AT, "scale": scale, "rotation": ROTATION})).unwrap();
s.execute("explode", &json!({"handles": [r["handle"]]})).unwrap();
s.doc().unwrap().model.iter().map(|e| (**e).clone()).collect()
}
/// A drawing point back in block coordinates.
fn to_block(p: [f64; 2], scale: [f64; 2]) -> [f64; 2] {
let (s, c) = (-ROTATION.to_radians()).sin_cos();
let (dx, dy) = (p[0] - AT[0], p[1] - AT[1]);
[(dx * c - dy * s) / scale[0], (dx * s + dy * c) / scale[1]]
}
/// Points along an ELLIPSE from its start to its end parameter.
fn samples(e: &cadcraft_engine::doc::Ellipse) -> Vec<[f64; 2]> {
let mut sweep = e.end - e.start;
if sweep <= 0.0 {
sweep += std::f64::consts::TAU;
}
let (mx, my) = (e.major.x, e.major.y);
let (nx, ny) = (-my * e.ratio, mx * e.ratio);
(0..=16)
.map(|i| {
let t = e.start + sweep * f64::from(i) / 16.0;
[e.center.x + mx * t.cos() + nx * t.sin(), e.center.y + my * t.cos() + ny * t.sin()]
})
.collect()
}
fn ellipses(v: &[Entity]) -> Vec<cadcraft_engine::doc::Ellipse> {
v.iter()
.filter_map(|e| match &e.kind {
EntityKind::Ellipse(el) => Some(el.clone()),
_ => None,
})
.collect()
}
fn near(a: [f64; 2], b: [f64; 2]) -> bool {
(a[0] - b[0]).hypot(a[1] - b[1]) < 1e-9
}
/// Every point of `el`, taken back into the block, lies on the circle of radius `r` about `c`
/// in block coordinates, and its ends are `ends` (the whole circle when `None`).
fn on_circle(el: &cadcraft_engine::doc::Ellipse, scale: [f64; 2], c: [f64; 2], r: f64, ends: Option<([f64; 2], [f64; 2])>) {
let pts: Vec<[f64; 2]> = samples(el).into_iter().map(|p| to_block(p, scale)).collect();
for q in &pts {
let d = (q[0] - c[0]).hypot(q[1] - c[1]);
assert!((d - r).abs() < 1e-9, "{q:?} is {d} from {c:?}, not {r}: {el:?}");
}
if let Some((a, b)) = ends {
let (first, last) = (pts[0], pts[pts.len() - 1]);
assert!(near(first, a) && near(last, b) || near(first, b) && near(last, a), "ends {first:?} {last:?}, expected {a:?} {b:?}");
}
}
#[test]
fn explode_of_a_stretched_reference_draws_circles_and_arcs_as_ellipses() {
for scale in [[2.0, 0.5], [-2.0, 0.5], [1.0, 3.0]] {
let v = exploded(scale);
assert!(!v.iter().any(|e| matches!(e.kind, EntityKind::Circle(_) | EntityKind::Arc(_) | EntityKind::LwPolyline(_))), "{scale:?}: {v:?}");
let els = ellipses(&v);
// Circle, arc, elliptical arc and the slot's two semicircles.
assert_eq!(els.len(), 5, "{scale:?}: {els:?}");
let at = |c: [f64; 2]| els.iter().filter(move |e| near(to_block([e.center.x, e.center.y], scale), c));
let circle = at([0.0, 0.0]).next().unwrap();
on_circle(circle, scale, [0.0, 0.0], 5.0, None);
// Its axes are the scaled radii: 10 and 2.5 for 2 x 0.5.
let major = circle.major.x.hypot(circle.major.y);
let (big, small) = (5.0 * scale[0].abs().max(scale[1].abs()), 5.0 * scale[0].abs().min(scale[1].abs()));
assert!((major - big).abs() < 1e-9 && (major * circle.ratio - small).abs() < 1e-9, "{circle:?}");
on_circle(at([20.0, 0.0]).next().unwrap(), scale, [20.0, 0.0], 4.0, Some(([24.0, 0.0], [20.0, 4.0])));
on_circle(at([60.0, 0.0]).next().unwrap(), scale, [60.0, 0.0], 2.0, Some(([60.0, 2.0], [60.0, -2.0])));
on_circle(at([70.0, 0.0]).next().unwrap(), scale, [70.0, 0.0], 2.0, Some(([70.0, -2.0], [70.0, 2.0])));
// The elliptical arc: its points, back in the block, are on the block's ellipse
// ((x-40)/6)^2 + (y/3)^2 = 1, from (46,0) over the top to (34,0).
let ell = at([40.0, 0.0]).next().unwrap();
let pts: Vec<[f64; 2]> = samples(ell).into_iter().map(|p| to_block(p, scale)).collect();
for q in &pts {
let f = ((q[0] - 40.0) / 6.0).powi(2) + (q[1] / 3.0).powi(2);
assert!((f - 1.0).abs() < 1e-9 && q[1] > -1e-9, "{q:?}: {ell:?}");
}
let (first, last) = (pts[0], pts[pts.len() - 1]);
assert!(near(first, [46.0, 0.0]) && near(last, [34.0, 0.0]) || near(first, [34.0, 0.0]) && near(last, [46.0, 0.0]), "{pts:?}");
// The slot's straight sides and the line are lines, where the reference draws them.
let lines: Vec<[[f64; 2]; 2]> = v
.iter()
.filter_map(|e| match &e.kind {
EntityKind::Line(l) => Some([to_block([l.a.x, l.a.y], scale), to_block([l.b.x, l.b.y], scale)]),
_ => None,
})
.collect();
assert_eq!(lines.len(), 3);
for [a, b] in [[[60.0, -2.0], [70.0, -2.0]], [[70.0, 2.0], [60.0, 2.0]], [[0.0, 10.0], [10.0, 10.0]]] {
assert!(lines.iter().any(|l| near(l[0], a) && near(l[1], b)), "{a:?}-{b:?} not in {lines:?}");
}
}
}
#[test]
fn explode_of_an_evenly_scaled_or_mirrored_reference_keeps_circles_arcs_and_polylines() {
for scale in [[2.0, 2.0], [-1.0, 1.0]] {
let v = exploded(scale);
let count = |f: fn(&EntityKind) -> bool| v.iter().filter(|e| f(&e.kind)).count();
assert_eq!(count(|k| matches!(k, EntityKind::Circle(_))), 1, "{scale:?}");
assert_eq!(count(|k| matches!(k, EntityKind::Arc(_))), 1, "{scale:?}");
assert_eq!(count(|k| matches!(k, EntityKind::LwPolyline(_))), 1, "{scale:?}");
assert_eq!(count(|k| matches!(k, EntityKind::Ellipse(_))), 1, "{scale:?}");
let r = v.iter().find_map(|e| if let EntityKind::Circle(c) = &e.kind { Some(c.radius) } else { None }).unwrap();
assert!((r - 5.0 * scale[0].abs()).abs() < 1e-9);
}
}