Sketch dimensions drawn as dimensions: extension lines, dimension lines and arcs with arrowheads (flipped outside when tight), the value centred on the line (fx: for expressions), R/Ø leaders, angle arcs, arc-length mark, driven ones in parentheses in a muted colour; click selects, Delete removes, drag moves the text (sketch.dimension_text, kept relative to the dimension), double-click edits the value in place with parameter completion
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
4adfd69d6a
commit
187ffd0a03
12 files changed
+946
-57
No files matched your search
@@ -154,6 +154,9 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.enabled(in_sketch)
|
||||
.params("entities: [curve, point or constraint ids]"),
|
||||
CommandSpec::new("sketch.move_point", "Drag Sketch Point", move_point).enabled(in_sketch).params("point: ref, to: [x,y]"),
|
||||
CommandSpec::new("sketch.dimension_text", "Move Dimension Text", dimension_text)
|
||||
.enabled(in_sketch)
|
||||
.params("dimension: constraint id or parameter name; at: [x,y] text centre (sketch coordinates) | reset: true (default place)"),
|
||||
CommandSpec::new("sketch.solve", "Solve Sketch", solve_cmd)
|
||||
.enabled(in_sketch)
|
||||
.noundo()
|
||||
@@ -1132,6 +1135,7 @@ fn dimension(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// A reference dimension: measures, no parameter.
|
||||
let name = k.name();
|
||||
let id = sk.add_constraint(k, None)?;
|
||||
place_text(sk, &id, text_at);
|
||||
if let Some(c) = sk.constraints.iter_mut().find(|c| c.id == id) {
|
||||
c.driven = true;
|
||||
}
|
||||
@@ -1149,7 +1153,8 @@ fn dimension(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let _ = vals;
|
||||
let name = k.name();
|
||||
sk.add_constraint(k, Some(pname.clone()))?;
|
||||
let id = sk.add_constraint(k, Some(pname.clone()))?;
|
||||
place_text(sk, &id, text_at);
|
||||
Ok((pname, name, cur))
|
||||
})?;
|
||||
if driven {
|
||||
@@ -1162,6 +1167,36 @@ fn dimension(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"param": param, "type": kind_name, "expression": v, "measured": current, "sketch": info}))
|
||||
}
|
||||
|
||||
/// Keep a dimension's text where it was placed (`at`, sketch coordinates), relative to the
|
||||
/// dimension so it follows the geometry.
|
||||
fn place_text(sk: &mut Sketch, id: &str, at: Option<Vec2>) {
|
||||
let Some(i) = sk.constraints.iter().position(|c| c.id == id) else { return };
|
||||
let text = match (at, sk.constraints.get(i).and_then(|c| solvecraft_sketch::dim_frame(sk, &c.kind))) {
|
||||
(Some(at), Some(f)) => Some(solvecraft_sketch::encode_text(&f, at)),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(c) = sk.constraints.get_mut(i) {
|
||||
c.text = text.filter(|t| t.is_finite() && t.x.abs() < 1e9 && t.y.abs() < 1e9);
|
||||
}
|
||||
}
|
||||
|
||||
fn dimension_text(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "sketch.dimension_text";
|
||||
let key = str_(p, "dimension").ok_or_else(|| bad(cmd, "`dimension` must be a dimension id or parameter name"))?.to_string();
|
||||
let at = if bool_(p, "reset").unwrap_or(false) { None } else { Some(req_vec2(cmd, p, "at")?) };
|
||||
let (id, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let c = sk
|
||||
.constraints
|
||||
.iter()
|
||||
.find(|c| c.kind.is_dimension() && (c.id == key || c.param.as_deref() == Some(key.as_str())))
|
||||
.ok_or_else(|| bad(cmd, format!("no dimension `{key}` in this sketch")))?;
|
||||
let id = c.id.clone();
|
||||
place_text(sk, &id, at);
|
||||
Ok(id)
|
||||
})?;
|
||||
Ok(json!({"dimension": id, "sketch": info}))
|
||||
}
|
||||
|
||||
/// Dimension a point (or a parallel line) to line `l` as a diameter: when `l` is a centerline
|
||||
/// (unless a plain distance was asked for) or when a diameter was asked for.
|
||||
fn diameter_about(sk: &Sketch, l: usize, ty: &str) -> bool {
|
||||
@@ -1305,3 +1340,7 @@ fn solve_cmd(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
#[cfg(test)]
|
||||
#[path = "sketch_rect_tests.rs"]
|
||||
mod rect_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sketch_ux_tests.rs"]
|
||||
mod ux_tests;
|
||||
@@ -0,0 +1,112 @@
|
||||
//! Sketch interaction tests: dimension placement, Fix/Unfix, the construction toggle, and
|
||||
//! which entity moves when a two-entity constraint is applied.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solvecraft_sketch::{DimFrame, Sketch, dim_frame, dim_layout};
|
||||
|
||||
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 sketch(s: &Session) -> Sketch {
|
||||
let id = s.active_sketch.expect("a sketch is being edited");
|
||||
s.doc.sketch(id).expect("the sketch").clone()
|
||||
}
|
||||
|
||||
fn new_sketch() -> Session {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "SketchCreate", json!({"plane": "XY"}));
|
||||
s
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Dimensions
|
||||
|
||||
#[test]
|
||||
fn every_dimension_kind_has_a_drawable_frame() {
|
||||
let mut s = new_sketch();
|
||||
run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [60, 40]}));
|
||||
run(&mut s, "CircleCenterRadius", json!({"center": [30, 20], "radius": 8}));
|
||||
run(&mut s, "ArcCenterTwoPoint", json!({"center": [100, 0], "start": [110, 0], "end": [100, 10]}));
|
||||
run(&mut s, "DrawPolyline", json!({"points": [[-40, 0], [-10, 0]]}));
|
||||
run(&mut s, "DrawPolyline", json!({"points": [[-40, 0], [-20, 25]]}));
|
||||
for (ents, ty) in [
|
||||
(json!(["l1"]), "auto"),
|
||||
(json!(["l2"]), "vertical"),
|
||||
(json!(["l1.start", "c1.center"]), "horizontal"),
|
||||
(json!(["c1"]), "auto"),
|
||||
(json!(["a1"]), "radius"),
|
||||
(json!(["a1"]), "arc_length"),
|
||||
(json!(["l5", "l6"]), "angle"),
|
||||
] {
|
||||
run(&mut s, "SketchDimension", json!({"entities": ents, "type": ty}));
|
||||
}
|
||||
run(&mut s, "SketchDimension", json!({"entities": ["c1.center", "l2"], "driven": true}));
|
||||
let sk = sketch(&s);
|
||||
let dims: Vec<_> = sk.constraints.iter().filter(|c| c.kind.is_dimension()).collect();
|
||||
assert_eq!(dims.len(), 8);
|
||||
let mut kinds = Vec::new();
|
||||
for c in dims {
|
||||
let f = dim_frame(&sk, &c.kind).unwrap_or_else(|| panic!("no frame for {c:?}"));
|
||||
let l = dim_layout(&f, None, 0.1, solvecraft_geom::Vec2::new(1.0, 0.5));
|
||||
assert!(!l.lines.is_empty() && !l.arrows.is_empty(), "{c:?}");
|
||||
assert!(l.text.is_finite());
|
||||
kinds.push(std::mem::discriminant(&f));
|
||||
}
|
||||
for f in [
|
||||
DimFrame::Linear { p0: Default::default(), p1: Default::default(), dir: Default::default() },
|
||||
DimFrame::Radial { center: Default::default(), radius: 1.0, diameter: true },
|
||||
DimFrame::Angular { vertex: Default::default(), d0: Default::default(), sweep: 1.0 },
|
||||
DimFrame::ArcLength { center: Default::default(), radius: 1.0, start: 0.0, sweep: 1.0 },
|
||||
] {
|
||||
assert!(kinds.contains(&std::mem::discriminant(&f)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dimension_text_is_stored_relative_to_the_dimension_and_follows_it() {
|
||||
let mut s = new_sketch();
|
||||
run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [40, 0]]}));
|
||||
let d = run(&mut s, "SketchDimension", json!({"entities": ["l1"], "text_at": [20, -8]}));
|
||||
let param = d["param"].as_str().unwrap_or_default().to_string();
|
||||
let text = |s: &Session| {
|
||||
let sk = sketch(s);
|
||||
let c = sk.constraints.iter().find(|c| c.kind.is_dimension()).cloned().expect("dimension");
|
||||
let f = dim_frame(&sk, &c.kind).expect("frame");
|
||||
dim_layout(&f, c.text, 0.1, Default::default()).text
|
||||
};
|
||||
assert!(text(&s).dist(solvecraft_geom::Vec2::new(20.0, -8.0)) < 1e-9);
|
||||
// Moved by the parameter name; then the line moves and the text goes with it.
|
||||
run(&mut s, "sketch.dimension_text", json!({"dimension": param, "at": [30, 12]}));
|
||||
assert!(text(&s).dist(solvecraft_geom::Vec2::new(30.0, 12.0)) < 1e-9);
|
||||
run(&mut s, "sketch.move_point", json!({"point": "l1.start", "to": [0, 10]}));
|
||||
run(&mut s, "sketch.move_point", json!({"point": "l1.end", "to": [40, 10]}));
|
||||
// Still 10 along and 12 across from the middle of the (moved) line.
|
||||
let sk = sketch(&s);
|
||||
let Some(DimFrame::Linear { p0, p1, dir }) = sk.constraints.iter().find_map(|c| dim_frame(&sk, &c.kind)) else { panic!("linear frame") };
|
||||
assert!(p0.y > 5.0, "the line moved: {p0:?}");
|
||||
let off = text(&s) - (p0 + p1) * 0.5;
|
||||
assert!((off.dot(dir) - 10.0).abs() < 1e-9 && (off.dot(dir.perp()) - 12.0).abs() < 1e-9, "{off:?}");
|
||||
// Reset puts it back to the default place; undo restores the drag.
|
||||
run(&mut s, "sketch.dimension_text", json!({"dimension": param, "reset": true}));
|
||||
assert!(sketch(&s).constraints.iter().all(|c| c.text.is_none()));
|
||||
assert!(s.execute("sketch.dimension_text", &json!({"dimension": "nope", "at": [0, 0]})).is_err());
|
||||
assert!(s.execute("sketch.dimension_text", &json!({"dimension": param, "at": [f64::NAN, 0]})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleting_a_dimension_frees_its_parameter() {
|
||||
let mut s = new_sketch();
|
||||
run(&mut s, "CircleCenterRadius", json!({"center": [0, 0], "radius": 5}));
|
||||
let d = run(&mut s, "SketchDimension", json!({"entities": ["c1"]}));
|
||||
let param = d["param"].as_str().unwrap_or_default().to_string();
|
||||
let id = sketch(&s).constraints.iter().find(|c| c.param.as_deref() == Some(param.as_str())).map(|c| c.id.clone()).expect("dim");
|
||||
run(&mut s, "sketch.delete", json!({"entities": [id]}));
|
||||
assert!(sketch(&s).constraints.iter().all(|c| !c.kind.is_dimension()));
|
||||
assert!(s.doc.param(¶m).is_none());
|
||||
}
|
||||
Reference in new issue
Block a user