mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
fix: Sketch rectangles and centre circles take expressions (#10251)
- sketch.rectangle.center and two_point: a corner given with expressions is a fixed point that follows its parameters, as with sketch.line - sketch.circle.center: a radius or diameter naming a parameter is a driving dimension; a string naming none is just the number; bare numbers read in the design unit - A radius that is not a number is refused with its own message and no longer passed over for diameter - sketch.circle.two_tangent: a string radius reads bare numbers in the design unit Co-authored-by: P'tite Tête <asso@ptitetete.com>
This commit is contained in:
@@ -59,7 +59,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.icon("rect")
|
||||
.key("R")
|
||||
.enabled(in_sketch)
|
||||
.params(concat!("p0, p1: opposite corners [x,y]; construction?. Returns sides {bottom, right, top, left} (line ids) and corners {bottom_left, bottom_right, top_right, top_left} (point ids)", connect_doc!(), created_doc!())),
|
||||
.params(concat!("p0, p1: opposite corners [x,y] (a coordinate may be an expression over the design's parameters: the corner follows them); construction?. Returns sides {bottom, right, top, left} (line ids) and corners {bottom_left, bottom_right, top_right, top_left} (point ids)", connect_doc!(), created_doc!())),
|
||||
CommandSpec::new("sketch.rectangle.three_point", "3-Point Rectangle", rect_three_point)
|
||||
.at("SKETCH", "CREATE")
|
||||
.icon("rect3")
|
||||
@@ -74,14 +74,14 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.at("SKETCH", "CREATE")
|
||||
.icon("rect_center")
|
||||
.enabled(in_sketch)
|
||||
.params(concat!("center, corner; construction?: bool. Returns sides {bottom, right, top, left} (line ids) and corners {bottom_left, bottom_right, top_right, top_left} (point ids)", connect_doc!(), created_doc!())),
|
||||
.params(concat!("center, corner: [x,y] (a coordinate may be an expression over the design's parameters: the point follows them); construction?: bool. Returns sides {bottom, right, top, left} (line ids) and corners {bottom_left, bottom_right, top_right, top_left} (point ids)", connect_doc!(), created_doc!())),
|
||||
CommandSpec::new("sketch.circle.center", "Center Diameter Circle", circle_center)
|
||||
.at("SKETCH", "CREATE")
|
||||
.icon("circle")
|
||||
.key("C")
|
||||
.enabled(in_sketch)
|
||||
.params(concat!(
|
||||
"center: [x,y] or point ref, radius | diameter: number; through?: point ref (a point the circle passes through, kept on it: where the tool's second click snapped); construction?: bool",
|
||||
"center: [x,y] or point ref (a coordinate may be an expression over the design's parameters: the centre follows them), radius | diameter: number or expression (an expression naming a parameter is a dimension driven by it; with `through` a point ref, that and the point both fix the radius); through?: point ref (a point the circle passes through, kept on it: where the tool's second click snapped); construction?: bool",
|
||||
connect_doc!(),
|
||||
created_doc!()
|
||||
)),
|
||||
@@ -578,10 +578,7 @@ pub(super) fn expr_point(doc: &solidcraft_doc::Document, v: &Value, cmd: &str) -
|
||||
/// again whenever the parameters change.
|
||||
pub(super) fn add_placed_point(sk: &mut Sketch, at: Vec2, exprs: PointExprs, id: Option<&str>) -> Result<usize> {
|
||||
let i = sk.add_point(at, id)?;
|
||||
if let Some(q) = sk.points.get_mut(i) {
|
||||
q.fixed = true;
|
||||
q.exprs = Some(Box::new(exprs));
|
||||
}
|
||||
place_point(sk, Some(&i), exprs);
|
||||
Ok(i)
|
||||
}
|
||||
|
||||
@@ -1216,16 +1213,42 @@ fn rect_result(sk_out: (Vec<String>, Vec<String>), names: Value, info: Value) ->
|
||||
out
|
||||
}
|
||||
|
||||
/// A rectangle corner: `[x, y]` of numbers, or with a coordinate typed as an expression (as a
|
||||
/// `sketch.line` point is): where it is now and the expressions that place it.
|
||||
fn corner_arg(doc: &solidcraft_doc::Document, p: &Value, k: &str, cmd: &str) -> Result<(Vec2, PointExprs)> {
|
||||
let v = p.get(k).ok_or_else(|| bad(cmd, format!("`{k}` must be a point [x, y]")))?;
|
||||
match expr_point(doc, v, cmd)? {
|
||||
Some((at, exprs)) => Ok((at, exprs.unwrap_or_default())),
|
||||
None => Ok((req_vec2(cmd, p, k)?, PointExprs::default())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds sketch point `i` where `exprs` place it, when it has any (see [`add_placed_point`]).
|
||||
fn place_point(sk: &mut Sketch, i: Option<&usize>, exprs: PointExprs) {
|
||||
if exprs.x.is_none() && exprs.y.is_none() {
|
||||
return;
|
||||
}
|
||||
if let Some(q) = i.and_then(|i| sk.points.get_mut(*i)) {
|
||||
q.fixed = true;
|
||||
q.exprs = Some(Box::new(exprs));
|
||||
}
|
||||
}
|
||||
|
||||
fn rect_two_point(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.rectangle.two_point";
|
||||
let (a, b) = (req_vec2(cmd, p, "p0")?, req_vec2(cmd, p, "p1")?);
|
||||
let ((a, ea), (b, eb)) = (corner_arg(&s.doc, p, "p0", cmd)?, corner_arg(&s.doc, p, "p1", cmd)?);
|
||||
if (a.x - b.x).abs() < 1e-9 || (a.y - b.y).abs() < 1e-9 {
|
||||
return Err(bad(cmd, "the corners must differ in x and y"));
|
||||
}
|
||||
let (lo, hi) = (Vec2::new(a.x.min(b.x), a.y.min(b.y)), Vec2::new(a.x.max(b.x), a.y.max(b.y)));
|
||||
// Each side of the rectangle takes its coordinate from the corner it comes from.
|
||||
let (lo_x, hi_x) = if a.x <= b.x { (&ea.x, &eb.x) } else { (&eb.x, &ea.x) };
|
||||
let (lo_y, hi_y) = if a.y <= b.y { (&ea.y, &eb.y) } else { (&eb.y, &ea.y) };
|
||||
let ((out, names), info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let first = sk.points.len();
|
||||
let (l, pts) = polygon_lines(sk, &[lo, Vec2::new(hi.x, lo.y), hi, Vec2::new(lo.x, hi.y)])?;
|
||||
place_point(sk, pts.first(), PointExprs { x: lo_x.clone(), y: lo_y.clone() });
|
||||
place_point(sk, pts.get(2), PointExprs { x: hi_x.clone(), y: hi_y.clone() });
|
||||
let mut cons = rect_constraints(sk, &l)?;
|
||||
mark_construction(sk, &l, p);
|
||||
cons.extend(connect_points(sk, first, p)?);
|
||||
@@ -1301,7 +1324,7 @@ fn rect_rounded(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn rect_center(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.rectangle.center";
|
||||
let (c, k) = (req_vec2(cmd, p, "center")?, req_vec2(cmd, p, "corner")?);
|
||||
let ((c, ec), (k, ek)) = (corner_arg(&s.doc, p, "center", cmd)?, corner_arg(&s.doc, p, "corner", cmd)?);
|
||||
let h = Vec2::new((k.x - c.x).abs(), (k.y - c.y).abs());
|
||||
if h.x < 1e-9 || h.y < 1e-9 {
|
||||
return Err(bad(cmd, "the corner must differ from the centre in x and y"));
|
||||
@@ -1309,6 +1332,14 @@ fn rect_center(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ((out, names), info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let first = sk.points.len();
|
||||
let (l, pts) = polygon_lines(sk, &[c - h, Vec2::new(c.x + h.x, c.y - h.y), c + h, Vec2::new(c.x - h.x, c.y + h.y)])?;
|
||||
// The corner given sits in the quadrant it was drawn in.
|
||||
let at = match (k.x >= c.x, k.y >= c.y) {
|
||||
(true, true) => 2,
|
||||
(false, true) => 3,
|
||||
(true, false) => 1,
|
||||
(false, false) => 0,
|
||||
};
|
||||
place_point(sk, pts.get(at), ek);
|
||||
let mut cons = rect_constraints(sk, &l)?;
|
||||
// Both diagonals (construction), the centre at their crossing. Only the first carries
|
||||
// the midpoint constraint: diagonals of a parallelogram bisect each other, so a second
|
||||
@@ -1322,6 +1353,7 @@ fn rect_center(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
let cp = sk.add_point(c, None)?;
|
||||
place_point(sk, Some(&cp), ec);
|
||||
cons.push(add_c(sk, ConstraintKind::Midpoint { p: cp, l: diag })?);
|
||||
}
|
||||
mark_construction(sk, &l, p);
|
||||
@@ -1358,16 +1390,43 @@ fn rect_three_point(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
pub(super) fn radius_arg(p: &Value, cmd: &str) -> Result<f64> {
|
||||
let r = match (num(p, "radius"), num(p, "diameter")) {
|
||||
(Some(r), _) => r,
|
||||
// `radius` wins over `diameter` whatever its form, so a bad one is not passed over.
|
||||
(None, _) if p.get("radius").is_some_and(|v| !v.is_null()) => return Err(bad(cmd, "`radius` must be a number")),
|
||||
(None, Some(d)) => d / 2.0,
|
||||
_ => return Err(bad(cmd, "needs `radius` or `diameter`")),
|
||||
// A value that is there but is no number is not a missing one.
|
||||
_ => {
|
||||
let given = ["radius", "diameter"].into_iter().find(|k| p.get(k).is_some_and(|v| !v.is_null()));
|
||||
return Err(bad(cmd, given.map_or_else(|| "needs `radius` or `diameter`".to_string(), |k| format!("`{k}` must be a number"))));
|
||||
}
|
||||
};
|
||||
if r > 1e-9 { Ok(r) } else { Err(bad(cmd, "radius must be positive")) }
|
||||
}
|
||||
|
||||
/// The size of a circle: [`radius_arg`], or a `radius` / `diameter` typed as an expression over
|
||||
/// the design's parameters, which is then a driving dimension on the circle (the parameter
|
||||
/// `dN` holds the expression) so that the circle follows it. A bare number reads in the design's
|
||||
/// unit, and an expression naming no parameter is just the number it comes to. Returns the
|
||||
/// radius, and the expression with whether it is a diameter. `radius` wins over `diameter`.
|
||||
fn circle_size(s: &Session, p: &Value, cmd: &str) -> Result<(f64, Option<(String, bool)>)> {
|
||||
let radius_given = p.get("radius").is_some_and(|v| !v.is_null());
|
||||
let key = if radius_given { "radius" } else { "diameter" };
|
||||
if !p.get(key).is_some_and(Value::is_string) {
|
||||
return Ok((radius_arg(p, cmd)?, None));
|
||||
}
|
||||
let e = s.doc.with_design_unit(req_expr(cmd, p, key)?.as_str(), Kind::Length);
|
||||
let v = s.doc.eval(&e, Kind::Length).map_err(|e| bad(cmd, format!("{key}: {e}")))?;
|
||||
let r = if key == "diameter" { v / 2.0 } else { v };
|
||||
if !(r > 1e-9 && r < 1e8) {
|
||||
return Err(bad(cmd, "radius must be positive"));
|
||||
}
|
||||
let driven = !solidcraft_doc::expr::references(&e).is_empty();
|
||||
Ok((r, driven.then_some((e, key == "diameter"))))
|
||||
}
|
||||
|
||||
fn circle_center(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.circle.center";
|
||||
let p = &super::sketch_project::snap_points(s, p, cmd)?;
|
||||
let r = radius_arg(p, cmd)?;
|
||||
let (r, size) = circle_size(s, p, cmd)?;
|
||||
let id = named_id(cmd, str_(p, "id"))?;
|
||||
let (out, info) = draw(s, p, cmd, |sk, doc| {
|
||||
let c = req_parg(sk, doc, p, "center", cmd)?;
|
||||
@@ -1386,6 +1445,13 @@ fn circle_center(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
{
|
||||
cons.push(add_c(sk, ConstraintKind::PointOnCurve { p: q, c: ci })?);
|
||||
}
|
||||
// A size typed as an expression is a dimension driven by the parameters it names.
|
||||
if let Some((e, diameter)) = &size {
|
||||
let unit = solidcraft_doc::expr::length_unit_or_mm(&doc.units).to_string();
|
||||
let pname = doc.new_model_param(e, &unit);
|
||||
let kind = if *diameter { ConstraintKind::Diameter { c: ci, value: 2.0 * r } } else { ConstraintKind::Radius { c: ci, value: r } };
|
||||
cons.push(sk.add_constraint(kind, Some(pname))?);
|
||||
}
|
||||
Ok((ids_of(sk, &[ci]), cons))
|
||||
})?;
|
||||
Ok(result(out, info))
|
||||
|
||||
@@ -994,6 +994,8 @@ fn tangent_circle(s: &mut Session, p: &Value, cmd: &str, n: usize, radius: Optio
|
||||
fn circle_tan_tan(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.circle.two_tangent";
|
||||
let r = match (crate::params::expr(p, "radius"), num(p, "diameter")) {
|
||||
// A bare number typed as text reads in the design's unit, as in `sketch.dimension`.
|
||||
(Some(r), _) if p.get("radius").is_some_and(Value::is_string) => s.doc.with_design_unit(r.as_str(), solidcraft_doc::expr::Kind::Length),
|
||||
(Some(r), _) => r.into_string(),
|
||||
(None, Some(d)) => format!("{}", d / 2.0),
|
||||
_ => return Err(bad(cmd, "needs `radius` or `diameter`")),
|
||||
|
||||
@@ -187,3 +187,216 @@ fn a_tangent_from_a_placed_point_keeps_the_side_it_was_drawn_on() {
|
||||
}
|
||||
assert!(flipped.is_empty(), "the tangent went over to the other side, without a word, at {flipped:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rectangle_corner_typed_as_an_expression_follows_its_parameters() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "parameters.add", json!({"name": "W", "expression": "40 mm"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "S"}));
|
||||
let c = run(&mut s, "sketch.rectangle.center", json!({"center": [0, 0], "corner": ["W / 2", "W / 4"]}));
|
||||
// A second one, from two corners, one of them mixing a number and an expression.
|
||||
let t = run(&mut s, "sketch.rectangle.two_point", json!({"p0": [100, 100], "p1": ["100 + W", 100 + 10]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "S", "distance": 10}));
|
||||
no_trouble(&s);
|
||||
let corner = |r: &Value, k: &str| r["corners"][k].as_str().unwrap_or_default().to_string();
|
||||
assert_eq!(solved(&s, &corner(&c, "top_right")), Vec2::new(20.0, 10.0));
|
||||
assert_eq!(solved(&s, &corner(&t, "top_right")), Vec2::new(140.0, 110.0));
|
||||
run(&mut s, "parameters.change", json!({"name": "W", "expression": "80 mm"}));
|
||||
no_trouble(&s);
|
||||
assert_eq!(solved(&s, &corner(&c, "bottom_left")), Vec2::new(-40.0, -20.0));
|
||||
assert_eq!(solved(&s, &corner(&c, "top_right")), Vec2::new(40.0, 20.0));
|
||||
// The other corners are solved, not placed: equal within the solver's tolerance.
|
||||
let br = solved(&s, &corner(&t, "bottom_right"));
|
||||
assert!((br.x - 180.0).abs() < 1e-6 && (br.y - 100.0).abs() < 1e-6, "{br:?}");
|
||||
assert!((volume(&mut s) - (80.0 * 40.0 + 80.0 * 10.0) * 10.0).abs() < 1e-6);
|
||||
// The design file keeps them.
|
||||
let mut r = reopened(&s);
|
||||
no_trouble(&r);
|
||||
assert!((volume(&mut r) - (80.0 * 40.0 + 80.0 * 10.0) * 10.0).abs() < 1e-6);
|
||||
// An expression that cannot be worked out is refused, and draws nothing.
|
||||
run(&mut s, "sketch.edit", json!({"sketch": "S"}));
|
||||
let points = s.doc.sketch(s.doc.find_feature("S").unwrap().id).unwrap().points.len();
|
||||
assert!(s.execute("sketch.rectangle.center", &json!({"center": [0, 0], "corner": ["nope", 5]})).is_err());
|
||||
assert!(s.execute("sketch.rectangle.two_point", &json!({"p0": [0, 0], "p1": [5, "nope"]})).is_err());
|
||||
assert_eq!(s.doc.sketch(s.doc.find_feature("S").unwrap().id).unwrap().points.len(), points);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_circle_size_typed_as_an_expression_is_a_dimension_that_follows_its_parameters() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "parameters.add", json!({"name": "X", "expression": "10 mm"}));
|
||||
run(&mut s, "parameters.add", json!({"name": "R", "expression": "4 mm"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "S"}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": ["X", 0], "radius": "R", "id": "a"}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [50, 0], "diameter": "R * 4", "id": "b"}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "solid.extrude", json!({"sketch": "S", "distance": 10}));
|
||||
no_trouble(&s);
|
||||
let radius = |s: &Session, id: &str| {
|
||||
let f = s.doc.find_feature("S").unwrap().id;
|
||||
let st = s.model.state();
|
||||
let sk = &st.sketch(f).unwrap().sketch;
|
||||
let c = sk.curves.iter().find(|c| c.id == id).unwrap();
|
||||
match c.kind {
|
||||
solidcraft_sketch::CurveKind::Circle { r, .. } => r,
|
||||
_ => f64::NAN,
|
||||
}
|
||||
};
|
||||
assert!((radius(&s, "a") - 4.0).abs() < 1e-9 && (radius(&s, "b") - 8.0).abs() < 1e-9);
|
||||
let before = volume(&mut s);
|
||||
run(&mut s, "parameters.change", json!({"name": "R", "expression": "9 mm"}));
|
||||
no_trouble(&s);
|
||||
assert!((radius(&s, "a") - 9.0).abs() < 1e-9 && (radius(&s, "b") - 18.0).abs() < 1e-9);
|
||||
assert!(volume(&mut s) > before);
|
||||
// A value that is no size is refused with its own message, not a missing one.
|
||||
run(&mut s, "sketch.edit", json!({"sketch": "S"}));
|
||||
for bad in [json!({"center": [0, 0], "radius": "nope"}), json!({"center": [0, 0], "radius": "0 mm"}), json!({"center": [0, 0], "radius": true})] {
|
||||
let e = s.execute("sketch.circle.center", &bad).err().map(|e| e.to_string()).unwrap_or_default();
|
||||
assert!(!e.is_empty() && !e.contains("needs `radius` or `diameter`"), "{bad}: {e}");
|
||||
}
|
||||
// `radius` wins over `diameter` whatever its form: a bad one is refused, not passed over.
|
||||
for bad in [json!({"center": [0, 0], "radius": true, "diameter": 10}), json!({"center": [0, 0], "radius": [1], "diameter": "R"})] {
|
||||
let e = s.execute("sketch.circle.center", &bad).err().map(|e| e.to_string()).unwrap_or_default();
|
||||
assert!(e.contains("`radius` must be a number"), "{bad}: {e}");
|
||||
}
|
||||
let e = s.execute("sketch.circle.center", &json!({"center": [0, 0]})).err().map(|e| e.to_string()).unwrap_or_default();
|
||||
assert!(e.contains("needs `radius` or `diameter`"), "{e}");
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [100, 0], "radius": "R", "diameter": 30, "id": "c"}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [150, 0], "radius": 5, "diameter": "R * 2", "id": "d"}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
assert!((circle_radius(&s, "c") - 9.0).abs() < 1e-9 && (circle_radius(&s, "d") - 5.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
fn circle_radius(s: &Session, id: &str) -> f64 {
|
||||
let f = s.doc.find_feature("S").unwrap().id;
|
||||
let st = s.model.state();
|
||||
let sk = &st.sketch(f).unwrap().sketch;
|
||||
match sk.curves.iter().find(|c| c.id == id).map(|c| &c.kind) {
|
||||
Some(solidcraft_sketch::CurveKind::Circle { r, .. }) => *r,
|
||||
_ => f64::NAN,
|
||||
}
|
||||
}
|
||||
|
||||
fn corner_at(s: &Session, r: &Value, k: &str) -> Vec2 {
|
||||
solved(s, r["corners"][k].as_str().unwrap_or_default())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_centre_rectangle_follows_its_parameters_from_any_quadrant_and_an_expression_centre() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "parameters.add", json!({"name": "W", "expression": "40 mm"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "S"}));
|
||||
let rects: Vec<Value> = [(1.0, 1.0), (-1.0, 1.0), (1.0, -1.0), (-1.0, -1.0)]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (sx, sy))| {
|
||||
let at = 200.0 * i as f64;
|
||||
let corner = json!([format!("{at} + {sx} * W / 2"), format!("{sy} * W / 4")]);
|
||||
run(
|
||||
&mut s,
|
||||
"sketch.rectangle.center", // The centre is held by an expression (a plain-number centre stays free: it moves with the solver).
|
||||
json!({"center": [format!("{at} + 0 * W"), "0 * W"], "corner": corner}),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
// An expression centre too, with a corner expressed from it.
|
||||
let c = run(&mut s, "sketch.rectangle.center", json!({"center": [1000, "W"], "corner": [1010, "W + 5"]}));
|
||||
let e = run(&mut s, "sketch.rectangle.center", json!({"center": ["W * 20", "-W"], "corner": ["W * 20 + 10", "-W - 5"]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
no_trouble(&s);
|
||||
run(&mut s, "parameters.change", json!({"name": "W", "expression": "80 mm"}));
|
||||
no_trouble(&s);
|
||||
for (i, r) in rects.iter().enumerate() {
|
||||
let at = 200.0 * i as f64;
|
||||
let (bl, tr) = (corner_at(&s, r, "bottom_left"), corner_at(&s, r, "top_right"));
|
||||
assert!((bl.x - (at - 40.0)).abs() < 1e-6 && (bl.y + 20.0).abs() < 1e-6, "{i}: {bl:?}");
|
||||
assert!((tr.x - (at + 40.0)).abs() < 1e-6 && (tr.y - 20.0).abs() < 1e-6, "{i}: {tr:?}");
|
||||
}
|
||||
let (bl, tr) = (corner_at(&s, &c, "bottom_left"), corner_at(&s, &c, "top_right"));
|
||||
assert!((bl.x - 990.0).abs() < 1e-6 && (bl.y - 75.0).abs() < 1e-6 && (tr.x - 1010.0).abs() < 1e-6 && (tr.y - 85.0).abs() < 1e-6, "{bl:?} {tr:?}");
|
||||
let (bl, tr) = (corner_at(&s, &e, "bottom_left"), corner_at(&s, &e, "top_right"));
|
||||
assert!(
|
||||
(bl.x - 1590.0).abs() < 1e-6 && (bl.y + 85.0).abs() < 1e-6 && (tr.x - 1610.0).abs() < 1e-6 && (tr.y + 75.0).abs() < 1e-6,
|
||||
"{bl:?} {tr:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_two_point_rectangle_drawn_from_swapped_corners_follows_its_parameters() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "parameters.add", json!({"name": "W", "expression": "40 mm"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "S"}));
|
||||
// p0 is the top right, p1 the bottom left: each coordinate is kept with its corner.
|
||||
let r = run(&mut s, "sketch.rectangle.two_point", json!({"p0": ["W", "W / 2"], "p1": [0, 0]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
run(&mut s, "parameters.change", json!({"name": "W", "expression": "60 mm"}));
|
||||
no_trouble(&s);
|
||||
assert_eq!(corner_at(&s, &r, "top_right"), Vec2::new(60.0, 30.0));
|
||||
let bl = corner_at(&s, &r, "bottom_left");
|
||||
assert!(bl.x.abs() < 1e-9 && bl.y.abs() < 1e-9, "{bl:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_size_expression_reads_in_the_design_unit_and_one_naming_no_parameter_is_a_number() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "document.units", json!({"units": "in"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "S"}));
|
||||
let params = s.doc.params.len();
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [0, 0], "radius": "1", "id": "a"}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [50, 0], "radius": "2 in", "id": "b"}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [100, 0], "diameter": "1", "id": "c"}));
|
||||
// An expression naming no parameter is the number it comes to: no dimension behind it.
|
||||
assert_eq!(s.doc.params.len(), params);
|
||||
assert!(
|
||||
(circle_radius(&s, "a") - 25.4).abs() < 1e-9 && (circle_radius(&s, "b") - 50.8).abs() < 1e-9 && (circle_radius(&s, "c") - 12.7).abs() < 1e-9
|
||||
);
|
||||
run(&mut s, "parameters.add", json!({"name": "R", "expression": "1 in"}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [150, 0], "radius": "R / 2", "id": "d"}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
no_trouble(&s);
|
||||
assert!((circle_radius(&s, "d") - 12.7).abs() < 1e-9);
|
||||
run(&mut s, "parameters.change", json!({"name": "R", "expression": "2 in"}));
|
||||
no_trouble(&s);
|
||||
assert!((circle_radius(&s, "d") - 25.4).abs() < 1e-9);
|
||||
// The 2-tangent circle reads a bare number typed as text the same way.
|
||||
run(&mut s, "sketch.edit", json!({"sketch": "S"}));
|
||||
let x = run(&mut s, "sketch.line", json!({"points": [[200, 0], [300, 0]]}));
|
||||
let y = run(&mut s, "sketch.line", json!({"points": [[200, 0], [200, 100]], "connect": false}));
|
||||
let t = run(&mut s, "sketch.circle.two_tangent", json!({"curves": [x["curves"][0], y["curves"][0]], "radius": "1", "near": [225, 25]}));
|
||||
let id = t["curves"][0].as_str().unwrap_or_default().to_string();
|
||||
assert!((circle_radius_live(&s, &id) - 25.4).abs() < 1e-6);
|
||||
}
|
||||
|
||||
/// The radius of circle `id` in the sketch being edited.
|
||||
fn circle_radius_live(s: &Session, id: &str) -> f64 {
|
||||
let f = s.doc.find_feature("S").unwrap().id;
|
||||
match s.doc.sketch(f).ok().and_then(|sk| sk.curves.iter().find(|c| c.id == id).map(|c| c.kind.clone())) {
|
||||
Some(solidcraft_sketch::CurveKind::Circle { r, .. }) => r,
|
||||
_ => f64::NAN,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rejected_size_or_corner_expression_leaves_nothing_behind() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "parameters.add", json!({"name": "W", "expression": "40 mm"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "S"}));
|
||||
let f = s.doc.find_feature("S").unwrap().id;
|
||||
let count = |s: &Session| {
|
||||
let sk = s.doc.sketch(f).unwrap();
|
||||
(sk.points.len(), sk.curves.len(), sk.constraints.len(), s.doc.params.len())
|
||||
};
|
||||
let before = count(&s);
|
||||
let tries = [
|
||||
("sketch.circle.center", json!({"center": ["W", 0], "radius": "W - 40"})),
|
||||
("sketch.circle.center", json!({"center": ["W", 0], "radius": "nope"})),
|
||||
("sketch.circle.center", json!({"center": ["W", 0], "radius": "W", "id": "bad|id"})),
|
||||
("sketch.rectangle.center", json!({"center": ["W", 0], "corner": ["W", 5]})),
|
||||
("sketch.rectangle.two_point", json!({"p0": ["W", 0], "p1": ["W", 5]})),
|
||||
];
|
||||
for (cmd, p) in tries {
|
||||
assert!(s.execute(cmd, &p).is_err(), "{cmd} {p}");
|
||||
assert_eq!(count(&s), before, "{cmd} {p}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user