SketchConstrainer (applies the constraint one or two entities nearly satisfy) and sketch.toggle_driven (driven/driving dimensions, with over-constraint check)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-08 00:12:56 +00:00
1 parent 57d01f8405
commit a88b8eb6b3
3 files changed
+165 -4

No files matched your search

+137
View File
@@ -36,6 +36,14 @@ pub static COMMANDS: &[CommandSpec] = &[
.icon("finish")
.enabled(in_sketch)
.params("like AutoConstrain, then Finish Sketch"),
CommandSpec::new("SketchConstrainer", "SketchConstrainer", smart_constrain)
.at("SKETCH", "CREATE")
.icon("constrainer")
.enabled(in_sketch)
.params("entities: [one or two sketch entity ids]: applies the constraint the geometry nearly has (horizontal/vertical, parallel, perpendicular, collinear, coincident, concentric, equal, tangent)"),
CommandSpec::new("sketch.toggle_driven", "Toggle Driven Dimension", toggle_driven)
.enabled(in_sketch)
.params("constraint: dimension id, driven?: bool (default toggles); a driving dimension gets a parameter with its current value"),
CommandSpec::new("sketch.glyphs", "Constraint Glyphs", glyphs)
.enabled(in_sketch)
.noundo()
@@ -262,6 +270,135 @@ fn auto_constrain_finish(s: &mut Session, p: &Value) -> Result<Value> {
Ok(r)
}
/// The constraint two entities (or one line) are closest to satisfying.
fn guess(sk: &Sketch, ents: &[String], cmd: &str) -> Result<ConstraintKind> {
use ConstraintKind::*;
let ent = |r: &str| -> Result<(Option<usize>, Option<usize>)> {
if let Some(c) = sk.curve_index(r) {
return Ok((None, Some(c)));
}
sk.resolve_point(r).map(|p| (Some(p), None)).ok_or_else(|| bad(cmd, format!("unknown sketch entity `{r}`")))
};
let line_dir = |c: usize| match sk.shape(c) {
Some(solvecraft_sketch::Shape::Line { a, b }) => (b - a).normalized(),
_ => None,
};
let round = |c: usize| matches!(sk.curves.get(c).map(|c| &c.kind), Some(CurveKind::Circle { .. } | CurveKind::Arc { .. }));
match ents {
[a] => {
let (_, Some(c)) = ent(a)? else { return Err(bad(cmd, "pick a line, or two entities")) };
let d = line_dir(c).ok_or_else(|| bad(cmd, "a single entity must be a line"))?;
Ok(if d.x.abs() >= d.y.abs() { Horizontal { l: c } } else { Vertical { l: c } })
}
[a, b] => match (ent(a)?, ent(b)?) {
((Some(p), _), (Some(q), _)) => Ok(Coincident { p, q }),
((Some(p), _), (_, Some(c))) | ((_, Some(c)), (Some(p), _)) => {
// A point at a curve's centre is concentric-like: coincident with the centre.
if let Some(cp) = sk.curves.get(c).and_then(|cu| match cu.kind {
CurveKind::Circle { c, .. } | CurveKind::Arc { c, .. } => Some(c),
_ => None,
}) && sk.point(cp).zip(sk.point(p)).is_some_and(|(x, y)| x.dist(y) < sk.radius(c).unwrap_or(1.0) * 0.25)
{
return Ok(Coincident { p, q: cp });
}
Ok(PointOnCurve { p, c })
}
((_, Some(x)), (_, Some(y))) => {
if let (Some(u), Some(v)) = (line_dir(x), line_dir(y)) {
let cross = u.cross(v).abs();
if cross < 0.2 {
// Parallel; on one line → collinear.
let off = match (sk.shape(x), sk.shape(y)) {
(Some(solvecraft_sketch::Shape::Line { a, .. }), Some(solvecraft_sketch::Shape::Line { a: b0, .. })) => {
u.cross(b0 - a).abs()
}
_ => f64::INFINITY,
};
let len = sk.polyline(x).windows(2).map(|w| w[0].dist(w[1])).sum::<f64>();
return Ok(if off < len * 0.05 { Collinear { a: x, b: y } } else { Parallel { a: x, b: y } });
}
if u.dot(v).abs() < 0.2 {
return Ok(Perpendicular { a: x, b: y });
}
return Ok(Equal { a: x, b: y });
}
if round(x) && round(y) {
let (cx, cy) = (sk.center(x).unwrap_or_default(), sk.center(y).unwrap_or_default());
let (rx, ry) = (sk.radius(x).unwrap_or(1.0), sk.radius(y).unwrap_or(1.0));
if cx.dist(cy) < 0.25 * rx.min(ry) {
return Ok(Concentric { a: x, b: y });
}
let d = cx.dist(cy);
if (d - (rx + ry)).abs() < 0.15 * rx.min(ry) || (d - (rx - ry).abs()).abs() < 0.15 * rx.min(ry) {
return Ok(Tangent { a: x, b: y });
}
return Ok(Equal { a: x, b: y });
}
Ok(Tangent { a: x, b: y })
}
_ => Err(bad(cmd, "cannot constrain that combination")),
},
_ => Err(bad(cmd, "`entities` must list one or two entities")),
}
}
fn smart_constrain(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "SketchConstrainer";
let ents = string_list(p, "entities");
let mut before = None;
let mut name = "";
let (id, info) = edit(s, p, cmd, true, |sk, _| {
let k = guess(sk, &ents, cmd)?;
name = k.name();
before = Some(dof_now(sk));
add_c(sk, k)
})?;
reject_redundant(before, &info, cmd)?;
Ok(json!({"constraint": id, "type": name, "sketch": info}))
}
fn toggle_driven(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "sketch.toggle_driven";
let cid = str_(p, "constraint").ok_or_else(|| bad(cmd, "`constraint` must be a dimension id"))?.to_string();
let want = crate::params::bool_(p, "driven");
let mut before = None;
let (driven, info) = edit(s, p, cmd, true, |sk, doc| {
let c = sk.constraints.iter().find(|c| c.id == cid).cloned().ok_or_else(|| bad(cmd, format!("no constraint `{cid}`")))?;
let v = c.kind.value().ok_or_else(|| bad(cmd, format!("`{cid}` is not a dimension")))?;
let driven = want.unwrap_or(!c.driven);
if driven == c.driven {
return Ok(driven);
}
let slot = sk.constraints.iter_mut().find(|c| c.id == cid).ok_or_else(|| bad(cmd, "constraint"))?;
if driven {
slot.driven = true;
slot.param = None;
} else {
let (unit, e) = if c.kind.is_angle() {
("deg", format!("{} deg", (v.to_degrees() * 1e6).round() / 1e6))
} else {
("mm", format!("{} mm", (v * 1e6).round() / 1e6))
};
slot.driven = false;
slot.param = Some(doc.new_model_param(&e, unit));
before = None;
}
Ok(driven)
})?;
if !driven {
// Making it driving must not over-constrain: compare with the sketch without it.
let id = s.active_sketch.ok_or_else(|| bad(cmd, "no sketch"))?;
let mut sk = s.doc.sketch(id)?.clone();
if let Some(c) = sk.constraints.iter_mut().find(|c| c.id == cid) {
c.driven = true;
}
before = Some(dof_now(&sk));
reject_redundant(before, &info, cmd)?;
}
s.doc_mut().prune_model_params();
Ok(json!({"constraint": cid, "driven": driven, "sketch": info}))
}
/// Where a constraint's glyph goes (sketch coordinates): beside the middle of its first curve,
/// or at its point.
fn anchor(sk: &Sketch, k: &ConstraintKind) -> Option<Vec2> {
@@ -146,3 +146,27 @@ fn drawing_infers_tangent_and_perpendicular() {
let names: Vec<&str> = si["constraints"].as_array().unwrap().iter().map(|c| c["name"].as_str().unwrap()).collect();
assert!(names.contains(&"Perpendicular") && names.contains(&"Tangent"), "{names:?} {r}");
}
#[test]
fn smart_constrain_and_driven_toggle() {
let mut s = new_sketch();
let a = ids(&run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [10, 0.5]]}))["curves"])[0].clone();
let b = ids(&run(&mut s, "DrawPolyline", json!({"points": [[0, 5], [10, 6]]}))["curves"])[0].clone();
let r = run(&mut s, "SketchConstrainer", json!({"entities": [a]}));
assert_eq!(r["type"], "Horizontal");
let r = run(&mut s, "SketchConstrainer", json!({"entities": [a, b]}));
assert_eq!(r["type"], "Parallel");
let c1 = ids(&run(&mut s, "CircleCenterRadius", json!({"center": [50, 0], "radius": 5}))["curves"])[0].clone();
let c2 = ids(&run(&mut s, "CircleCenterRadius", json!({"center": [50.3, 0.2], "radius": 3}))["curves"])[0].clone();
let r = run(&mut s, "SketchConstrainer", json!({"entities": [c1, c2]}));
assert_eq!(r["type"], "Concentric");
// Driven toggle.
let d = run(&mut s, "SketchDimension", json!({"entities": [c1], "value": 10}));
let g = run(&mut s, "sketch.glyphs", json!({}));
let did = g["glyphs"].as_array().unwrap().iter().find(|x| x["param"] == d["param"]).unwrap()["id"].as_str().unwrap().to_string();
let r = run(&mut s, "sketch.toggle_driven", json!({"constraint": did}));
assert_eq!(r["driven"], true);
assert!(s.doc.param(d["param"].as_str().unwrap()).is_none(), "the parameter goes");
let r = run(&mut s, "sketch.toggle_driven", json!({"constraint": did}));
assert_eq!(r["driven"], false);
}
+4 -4
View File
@@ -2,9 +2,9 @@
Generated by `cargo xtask parity` from `xtask/data/fusion-catalog.tsv` (the toolbar commands of Fusion's Design workspace, ids and names only) and the live command registry (`solvecraft-cli commands`). A command counts as live when SolveCraft registers the same command id. This measures breadth, not depth; see ROADMAP.md and docs/oracle.md for depth.
**In scope (SOLID, SKETCH, ASSEMBLY, SHEET METAL, PLASTIC enclosure subset): 153 / 541 commands live (28%).** SOLID + SKETCH: 114 / 291 (39%). Registered commands: 160.
**In scope (SOLID, SKETCH, ASSEMBLY, SHEET METAL, PLASTIC enclosure subset): 154 / 541 commands live (28%).** SOLID + SKETCH: 115 / 291 (40%). Registered commands: 162.
Deferred until further notice (not in the in-scope figure): MESH, FORM, SURFACE beyond what solids need, PCB, render, animation, simulation, manufacture, drawings, MANAGE, UTILITIES and the rest of PLASTIC. All Design tabs together: 247 / 1175 (21%).
Deferred until further notice (not in the in-scope figure): MESH, FORM, SURFACE beyond what solids need, PCB, render, animation, simulation, manufacture, drawings, MANAGE, UTILITIES and the rest of PLASTIC. All Design tabs together: 248 / 1175 (21%).
| Tab | Panel | Live | Total | % |
|---|---|---:|---:|---:|
@@ -17,7 +17,7 @@ Deferred until further notice (not in the in-scope figure): MESH, FORM, SURFACE
| SOLID | INSERT | 3 | 11 | 27% |
| SOLID | SELECT | 0 | 15 | 0% |
| SOLID | POSITION | 2 | 2 | 100% |
| SKETCH | CREATE | 40 | 41 | 98% |
| SKETCH | CREATE | 41 | 41 | 100% |
| SKETCH | MODIFY | 12 | 12 | 100% |
| SKETCH | CONSTRAINTS | 15 | 15 | 100% |
| SKETCH | CONFIGURE | 0 | 3 | 0% |
@@ -152,6 +152,7 @@ Deferred until further notice (not in the in-scope figure): MESH, FORM, SURFACE
- SOLID › POSITION › Revert Position (`AsBuiltPositionsCmd`)
- SKETCH › CREATE › Line (`DrawPolyline`)
- SKETCH › CREATE › Midpoint Line (`SketchMidpointLine`)
- SKETCH › CREATE › SketchConstrainer (`SketchConstrainer`)
- SKETCH › CREATE › 2-Point Rectangle (`ShapeRectangleTwoPoint`)
- SKETCH › CREATE › 3-Point Rectangle (`ShapeRectangleThreePoint`)
- SKETCH › CREATE › Center Rectangle (`ShapeRectangleCenter`)
@@ -351,7 +352,6 @@ Deferred until further notice (not in the in-scope figure): MESH, FORM, SURFACE
- SOLID › SELECT › Select Edge Priority (`SelectEdgePriorityCommand`)
- SOLID › SELECT › Select Component Priority (`SelectComponentPriorityCommand`)
- SOLID › SELECT › Selection Filters (`SelectionFilterCommand`)
- SKETCH › CREATE › SketchConstrainer (`SketchConstrainer`)
- SKETCH › CONFIGURE › Configure (`FusionStartDesignConfigModeCmd`)
- SKETCH › CONFIGURE › Display Configuration Table (`FusionShowDesignConfigPanelCmd`)
- SKETCH › CONFIGURE › Create Configuration Rules (`FusionConfigurationRulesCmd`)