Files
OneCAD/crates/engine/src/cmd/sketch_rounded_rect_tests.rs
T
bherbruckandClaude Opus 5.5 97ff7578c2 Rounded Rectangle sketch tool (#891) (#948)
* Rounded Rectangle sketch tool (#891)

A new SKETCH > CREATE command, sketch.rectangle.rounded, in the Rectangle
family: two opposite corners and a corner radius make a 2-point rectangle
whose four corners are filleted alike, as one undo step. The radius is one
parameter (one radius dimension, the other arcs equal to it), so changing it
changes all four corners. While drawing, Width, Height and Radius boxes take
typed values; typed width and height dimension the overall size between
opposite sides. Without a typed radius the corners get a fifth of the shorter
side.

Fixes #891

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Rounded Rectangle in the Simplified Chinese catalog

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:52:10 -04:00

120 lines
4.9 KiB
Rust

//! Rounded Rectangle (#891): a 2-point rectangle whose corners are filleted alike, in one step.
use serde_json::{Value, json};
use crate::Session;
fn run(s: &mut Session, id: &str, p: Value) -> Value {
match s.execute(id, &p) {
Ok(v) => v,
Err(e) => panic!("{id} {p}: {e}"),
}
}
fn ids(v: &Value) -> Vec<String> {
v.as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect()).unwrap_or_default()
}
fn sketch() -> Session {
let mut s = Session::default();
run(&mut s, "sketch.create", json!({"plane": "XY"}));
s
}
/// The evaluated (solved) sketch.
fn solved(s: &Session) -> solidcraft_sketch::Sketch {
let id = s.active_sketch.expect("an open sketch");
s.model.state().sketch(id).map(|ss| ss.sketch.clone()).expect("an evaluated sketch")
}
/// The corner arcs' radii, from the evaluated sketch.
fn radii(s: &Session, arcs: &[String]) -> Vec<f64> {
let sk = solved(s);
arcs.iter()
.map(|id| {
let i = sk.curves.iter().position(|c| &c.id == id).expect("arc in sketch");
match &sk.curves[i].kind {
solidcraft_sketch::CurveKind::Arc { c, a, .. } => sk.point(*c).unwrap().dist(sk.point(*a).unwrap()),
k => panic!("{id} is not an arc: {k:?}"),
}
})
.collect()
}
#[test]
fn a_rounded_rectangle_is_four_lines_and_four_equal_arcs_in_one_closed_profile() {
let mut s = sketch();
let r = run(&mut s, "sketch.rectangle.rounded", json!({"p0": [2, 3], "p1": [42, 23], "radius": 5}));
let (curves, arcs) = (ids(&r["curves"]), ids(&r["arcs"]));
assert_eq!(curves.len(), 8, "{r}");
assert_eq!(arcs.len(), 4, "{r}");
assert!(r["sides"]["bottom"].is_string() && r["sides"]["left"].is_string(), "{r}");
assert!(r.get("corners").is_none(), "the sharp corners are gone: {r}");
assert!(r["param"].is_string(), "one radius parameter: {r}");
for radius in radii(&s, &arcs) {
assert!((radius - 5.0).abs() < 1e-6, "{radius}");
}
let si = run(&mut s, "sketch.inspect", json!({}));
let profiles = si["profiles"].as_array().expect("profiles");
assert_eq!(profiles.len(), 1, "{si}");
let area = profiles[0]["area_mm2"].as_f64().unwrap();
let want = 40.0 * 20.0 - (4.0 - std::f64::consts::PI) * 25.0;
assert!((area - want).abs() < 1e-6, "area {area}, want {want}");
assert_ne!(si["status"], "conflict", "{si}");
assert!(si["status"].as_str().is_none_or(|t| !t.contains("over")), "{si}");
// Position and size remain free; the radius is dimensioned and the arcs are equal.
assert_eq!(si["dof"], 4, "{si}");
}
#[test]
fn a_rounded_rectangle_is_one_undo_step() {
let mut s = sketch();
run(&mut s, "sketch.rectangle.rounded", json!({"p0": [0, 0], "p1": [30, 30], "radius": 4}));
run(&mut s, "edit.undo", json!({}));
let sk = solved(&s);
assert!(sk.curves.is_empty(), "undo removes the whole shape: {:?}", sk.curves.iter().map(|c| &c.id).collect::<Vec<_>>());
}
#[test]
fn the_radius_parameter_drives_all_four_corners() {
let mut s = sketch();
let r = run(&mut s, "sketch.rectangle.rounded", json!({"p0": [-10, -5], "p1": [30, 25], "radius": "3 mm"}));
let arcs = ids(&r["arcs"]);
let param = r["param"].as_str().unwrap().to_string();
run(&mut s, "parameters.change", json!({"name": param, "expression": "6 mm"}));
for radius in radii(&s, &arcs) {
assert!((radius - 6.0).abs() < 1e-6, "{radius}");
}
}
#[test]
fn a_radius_too_large_or_not_positive_is_refused_and_draws_nothing() {
for radius in [json!(10), json!(12), json!(0), json!(-2), json!("nope")] {
let mut s = sketch();
let err = s.execute("sketch.rectangle.rounded", &json!({"p0": [0, 0], "p1": [40, 20], "radius": radius}));
assert!(err.is_err(), "radius {radius} accepted");
let sk = solved(&s);
assert!(sk.curves.is_empty(), "radius {radius} left curves behind");
}
let mut s = sketch();
assert!(s.execute("sketch.rectangle.rounded", &json!({"p0": [0, 0], "p1": [40, 20]})).is_err(), "radius is required");
}
/// Typed width and height (the drawing tool's boxes) dimension the overall size, between
/// opposite sides, not the straight part of a side.
#[test]
fn typed_width_and_height_measure_the_whole_rounded_rectangle() {
let mut s = sketch();
let r = run(
&mut s,
"sketch.draw.dimensioned",
json!({"command": "sketch.rectangle.rounded", "params": {"p0": [1, 1], "p1": [31, 21], "radius": 4}, "dimensions": {"width": "50 mm", "height": 25}}),
);
assert!(r["dimensions"]["width"].is_object() && r["dimensions"]["height"].is_object(), "{r}");
let si = run(&mut s, "sketch.inspect", json!({}));
let area = si["profiles"][0]["area_mm2"].as_f64().unwrap();
let want = 50.0 * 25.0 - (4.0 - std::f64::consts::PI) * 16.0;
assert!((area - want).abs() < 1e-6, "area {area}, want {want}: {si}");
assert_eq!(si["dof"], 2, "only the position is free: {si}");
}