An angle dimension between two lines takes the reading nearest its value and reports degrees: without text_at the angle was measured between the lines' directions, so a riser pointing up and an outlet pointing down-left (meeting at 30°, directions 150° apart) asked for 30° swung the outlet round by 120°, which the fillets between them could not follow, and the dimension was refused as over-constraining with a degree of freedom left. Two lines make two angles, θ and 180° − θ; the dimension now takes the one nearer the value (the acute one without a value), and text_at still picks a sector. sketch.dimension reports measured in the dimension's unit (deg for angles, mm for lengths) with a unit field, instead of radians for angles
This commit is contained in:
1 parent
81be4e74b8
commit
90ce5261b3
2 files changed
+90
-9
No files matched your search
@@ -1,7 +1,7 @@
|
||||
//! Sketch commands: create/edit/finish sketches, draw curves, constraints and dimensions.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solvecraft_doc::{FeatureKind, PlaneRef};
|
||||
use solvecraft_doc::{FeatureKind, PlaneRef, expr::Kind};
|
||||
use solvecraft_geom::{Plane, Vec2, Vec3};
|
||||
use solvecraft_sketch::{ConstraintKind, CurveKind, Hold, Sketch, SolveReport, solve, solve_holding};
|
||||
|
||||
@@ -94,7 +94,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.params(concat!("p0, p1: overall ends, width", connect_doc!())),
|
||||
CommandSpec::new("sketch.point", "Point", draw_point).at("SKETCH", "CREATE").icon("point").enabled(in_sketch).params(concat!("point: [x,y], id?", connect_doc!())),
|
||||
CommandSpec::new("sketch.dimension", "Sketch Dimension", dimension).at("SKETCH", "CREATE").icon("dimension").key("D").enabled(in_sketch).params(
|
||||
"entities: [refs], type?: auto|distance|horizontal|vertical|length|radius|diameter|angle, value?: number or expression (default: current)",
|
||||
"entities: [refs], type?: auto|distance|horizontal|vertical|length|radius|diameter|angle, value?: number or expression (default: current); text_at?: [x, y]. Two lines: the angle between them (acute, or 180° − it when the value is nearer that) unless text_at picks a sector. Returns measured in unit (mm or deg)",
|
||||
),
|
||||
CommandSpec::new("sketch.constraint.horizontal_vertical", "Horizontal/Vertical", c_horizontal_vertical)
|
||||
.at("SKETCH", "CONSTRAINTS")
|
||||
@@ -1332,7 +1332,9 @@ fn dimension(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let is_line = |c: usize| matches!(sk.curves.get(c).map(|c| &c.kind), Some(CurveKind::Line { .. }));
|
||||
let is_circle = |c: usize| matches!(sk.curves.get(c).map(|c| &c.kind), Some(CurveKind::Circle { .. }));
|
||||
let (k, cur): (ConstraintKind, f64) = match (e0, e1) {
|
||||
// The other reading of an angle between two lines (see below).
|
||||
let mut alt: Option<(ConstraintKind, f64)> = None;
|
||||
let (mut k, mut cur): (ConstraintKind, f64) = match (e0, e1) {
|
||||
((None, Some(c)), None) if is_line(c) => {
|
||||
let (a, b) = line_pts(sk, c).unwrap_or_default();
|
||||
match ty.as_str() {
|
||||
@@ -1409,7 +1411,14 @@ fn dimension(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
ang = -ang;
|
||||
std::mem::swap(&mut ka, &mut kb);
|
||||
}
|
||||
(ConstraintKind::Angle { a: ka, b: kb, value: ang, flip: false }, ang)
|
||||
// Two lines make two angles, θ and 180° − θ, whichever way they were
|
||||
// drawn: the acute one unless the value asks for the other.
|
||||
let direct = (ConstraintKind::Angle { a: ka, b: kb, value: ang, flip: false }, ang);
|
||||
let supplement =
|
||||
(ConstraintKind::Angle { a: kb, b: ka, value: std::f64::consts::PI - ang, flip: true }, std::f64::consts::PI - ang);
|
||||
let (acute, obtuse) = if ang <= std::f64::consts::FRAC_PI_2 { (direct, supplement) } else { (supplement, direct) };
|
||||
alt = Some(obtuse);
|
||||
acute
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1426,7 +1435,7 @@ fn dimension(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
c.driven = true;
|
||||
}
|
||||
before = None;
|
||||
return Ok((String::new(), name, cur));
|
||||
return Ok((String::new(), name, measured(is_angle, cur)));
|
||||
}
|
||||
let (unit, default_expr) =
|
||||
if is_angle { ("deg", format!("{} deg", round6(cur.to_degrees()))) } else { ("mm", format!("{} mm", round6(cur))) };
|
||||
@@ -1437,20 +1446,30 @@ fn dimension(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(err) = errs.get(&pname) {
|
||||
return Err(bad(cmd, format!("value: {err}")));
|
||||
}
|
||||
let _ = vals;
|
||||
// An angle between two lines dimensions whichever of its two readings is nearer the value.
|
||||
if let (Some((ka, ca)), Some(want)) = (alt, vals.get(&pname).and_then(|v| v.to_kind(Kind::Angle).ok()))
|
||||
&& (want - ca).abs() < (want - cur).abs()
|
||||
{
|
||||
(k, cur) = (ka, ca);
|
||||
}
|
||||
let name = k.name();
|
||||
let id = sk.add_constraint(k, Some(pname.clone()))?;
|
||||
place_text(sk, &id, text_at);
|
||||
Ok((pname, name, cur))
|
||||
Ok((pname, name, measured(is_angle, cur)))
|
||||
})?;
|
||||
if driven {
|
||||
return Ok(json!({"param": Value::Null, "driven": true, "type": kind_name, "measured": current, "sketch": info}));
|
||||
return Ok(json!({"param": Value::Null, "driven": true, "type": kind_name, "measured": current.0, "unit": current.1, "sketch": info}));
|
||||
}
|
||||
if reject_redundant(before, &info, cmd).is_err() {
|
||||
return Err(bad(cmd, "that dimension would over-constrain the sketch; add it as a driven (reference) dimension with driven: true"));
|
||||
}
|
||||
let v = s.doc.param(¶m).map(|p| p.expr.clone()).unwrap_or_default();
|
||||
Ok(json!({"param": param, "type": kind_name, "expression": v, "measured": current, "sketch": info}))
|
||||
Ok(json!({"param": param, "type": kind_name, "expression": v, "measured": current.0, "unit": current.1, "sketch": info}))
|
||||
}
|
||||
|
||||
/// A dimension's measured value as reported, in the unit its expression is written in.
|
||||
fn measured(is_angle: bool, v: f64) -> (f64, &'static str) {
|
||||
if is_angle { (round6(v.to_degrees()), "deg") } else { (round6(v), "mm") }
|
||||
}
|
||||
|
||||
/// Keep a dimension's text where it was placed (`at`, sketch coordinates), relative to the
|
||||
|
||||
@@ -201,3 +201,65 @@ fn drawing_onto_a_curve_keeps_the_point_on_it() {
|
||||
let cc = sk.center(sk.curve_index(&c).unwrap()).unwrap();
|
||||
assert!((st.dist(cc) - 8.0).abs() < 1e-7, "{st:?} {cc:?}");
|
||||
}
|
||||
|
||||
/// A faucet spout path: a vertical riser, a horizontal top and an outlet sloping down, joined
|
||||
/// by fillets. The riser points up and the outlet down-left, so their directions are 150° apart
|
||||
/// while the lines meet at 30°.
|
||||
fn spout() -> Session {
|
||||
let mut s = new_sketch();
|
||||
run(&mut s, "sketch.line", json!({"points": [[795, 955], [795, 1068], [689, 1068], [661, 1020]], "ids": ["l1", "l2", "l3"]}));
|
||||
run(&mut s, "sketch.fillet", json!({"point": "l1.end", "radius": 50}));
|
||||
run(&mut s, "sketch.fillet", json!({"point": "l2.end", "radius": 35}));
|
||||
run(&mut s, "sketch.constraint.horizontal_vertical", json!({"line": "l1", "mode": "vertical"}));
|
||||
run(&mut s, "sketch.constraint.horizontal_vertical", json!({"line": "l2", "mode": "horizontal"}));
|
||||
run(&mut s, "sketch.dimension", json!({"entities": ["origin", "l1.start"], "type": "horizontal", "value": 795}));
|
||||
run(&mut s, "sketch.dimension", json!({"entities": ["origin", "l1.start"], "type": "vertical", "value": 955}));
|
||||
run(&mut s, "sketch.dimension", json!({"entities": ["origin", "l2.start"], "type": "vertical", "value": 1068}));
|
||||
run(&mut s, "sketch.dimension", json!({"entities": ["l3.end", "l1.start"], "type": "horizontal", "value": 134}));
|
||||
let d = run(&mut s, "sketch.dimension", json!({"entities": ["origin", "l3.end"], "type": "vertical", "value": 1020}));
|
||||
assert_eq!(d["sketch"]["dof"], 1, "{d}");
|
||||
s
|
||||
}
|
||||
|
||||
/// The angle (degrees, 0..=90) between two lines of the active sketch.
|
||||
fn line_angle(s: &Session, a: &str, b: &str) -> f64 {
|
||||
let sk = sketch(s);
|
||||
let dir = |l: &str| {
|
||||
let c = sk.curve_index(l).unwrap();
|
||||
let (p, q) = crate::cmd::sketch::line_pts(&sk, c).unwrap();
|
||||
q - p
|
||||
};
|
||||
let (u, v) = (dir(a), dir(b));
|
||||
let t = u.cross(v).abs().atan2(u.dot(v)).to_degrees();
|
||||
t.min(180.0 - t)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_angle_between_lines_takes_the_reading_nearest_its_value_and_reports_degrees() {
|
||||
// The riser and the outlet are not adjacent and meet at 30°; their directions are 150°
|
||||
// apart. Asking for 30° used to swing the outlet round and was refused as over-constraining.
|
||||
for ents in [["l1", "l3"], ["l3", "l1"]] {
|
||||
let mut s = spout();
|
||||
let d = run(&mut s, "sketch.dimension", json!({"entities": ents, "type": "angle", "value": "30 deg"}));
|
||||
assert_eq!(d["sketch"]["dof"], 0, "{d}");
|
||||
assert_eq!(d["unit"], "deg", "{d}");
|
||||
assert!((d["measured"].as_f64().unwrap() - 30.3).abs() < 0.1, "{d}");
|
||||
assert!((line_angle(&s, "l1", "l3") - 30.0).abs() < 1e-6);
|
||||
}
|
||||
// The other reading still works, and the adjacent top line measures 60° the same way.
|
||||
let mut s = spout();
|
||||
let d = run(&mut s, "sketch.dimension", json!({"entities": ["l1", "l3"], "value": "150 deg"}));
|
||||
assert_eq!(d["sketch"]["dof"], 0, "{d}");
|
||||
assert!((line_angle(&s, "l1", "l3") - 30.0).abs() < 1e-6);
|
||||
let mut s = spout();
|
||||
let d = run(&mut s, "sketch.dimension", json!({"entities": ["l2", "l3"], "value": "60 deg"}));
|
||||
assert!((d["measured"].as_f64().unwrap() - 59.74).abs() < 0.01, "{d}");
|
||||
// Without a value the acute angle is kept as it is.
|
||||
let mut s = spout();
|
||||
let d = run(&mut s, "sketch.dimension", json!({"entities": ["l1", "l3"]}));
|
||||
let e = d["expression"].as_str().unwrap();
|
||||
assert!(e.ends_with(" deg") && (e.trim_end_matches(" deg").parse::<f64>().unwrap() - 30.3).abs() < 0.1, "{d}");
|
||||
// Lengths report millimetres.
|
||||
let d = run(&mut s, "sketch.dimension", json!({"entities": ["l2"], "driven": true}));
|
||||
assert_eq!(d["unit"], "mm", "{d}");
|
||||
}
|
||||
Reference in new issue
Block a user