Fix/Unfix on every sketch entity: points (Fix constraint) and lines, arcs, circles, ellipses, splines and conics (fixed curves hold their points and radius); ConstraintFix takes entities (fixes them all unless all are fixed); the toolbar button and the context menu act on the whole selection; fixed points never move when dragged or solved; projected geometry counts as fixed; fixed geometry is drawn green; sketch.inspect reports fixed
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
b457b6b7b6
commit
5b8816aa7d
11 files changed
+252
-35
No files matched your search
@@ -100,12 +100,12 @@ fn sketch_json(s: &Session, id: u64) -> Option<Value> {
|
||||
let det = ss.report.curve_determined.get(i).copied().unwrap_or(false);
|
||||
let pid = |k: usize| sk.points.get(k).map(|p| p.id.clone()).unwrap_or_default();
|
||||
let mut v = match c.kind {
|
||||
CurveKind::Line { a, b } => json!({"id": c.id, "type": "line", "start": pid(a), "end": pid(b), "start_at": sk.point(a), "end_at": sk.point(b), "construction": c.construction, "fully_constrained": det}),
|
||||
CurveKind::Circle { c: cc, r } => json!({"id": c.id, "type": "circle", "center": pid(cc), "center_at": sk.point(cc), "radius": r, "construction": c.construction, "fully_constrained": det}),
|
||||
CurveKind::Arc { c: cc, a, b } => json!({"id": c.id, "type": "arc", "center": pid(cc), "start": pid(a), "end": pid(b), "center_at": sk.point(cc), "start_at": sk.point(a), "end_at": sk.point(b), "radius": sk.radius(i), "construction": c.construction, "fully_constrained": det}),
|
||||
CurveKind::Ellipse { c: cc, m, r } => json!({"id": c.id, "type": "ellipse", "center": pid(cc), "major": pid(m), "center_at": sk.point(cc), "major_at": sk.point(m), "minor_radius": r, "construction": c.construction, "fully_constrained": det}),
|
||||
CurveKind::Spline { ref pts, control, degree } => json!({"id": c.id, "type": "spline", "points": pts.iter().map(|q| pid(*q)).collect::<Vec<_>>(), "points_at": pts.iter().map(|q| sk.point(*q)).collect::<Vec<_>>(), "control": control, "degree": degree, "construction": c.construction, "fully_constrained": det}),
|
||||
CurveKind::Conic { a, b, apex, rho } => json!({"id": c.id, "type": "conic", "start": pid(a), "end": pid(b), "apex": pid(apex), "start_at": sk.point(a), "end_at": sk.point(b), "apex_at": sk.point(apex), "rho": rho, "construction": c.construction, "fully_constrained": det}),
|
||||
CurveKind::Line { a, b } => json!({"id": c.id, "type": "line", "start": pid(a), "end": pid(b), "start_at": sk.point(a), "end_at": sk.point(b), "construction": c.construction, "fixed": sk.curve_locked(i), "fully_constrained": det}),
|
||||
CurveKind::Circle { c: cc, r } => json!({"id": c.id, "type": "circle", "center": pid(cc), "center_at": sk.point(cc), "radius": r, "construction": c.construction, "fixed": sk.curve_locked(i), "fully_constrained": det}),
|
||||
CurveKind::Arc { c: cc, a, b } => json!({"id": c.id, "type": "arc", "center": pid(cc), "start": pid(a), "end": pid(b), "center_at": sk.point(cc), "start_at": sk.point(a), "end_at": sk.point(b), "radius": sk.radius(i), "construction": c.construction, "fixed": sk.curve_locked(i), "fully_constrained": det}),
|
||||
CurveKind::Ellipse { c: cc, m, r } => json!({"id": c.id, "type": "ellipse", "center": pid(cc), "major": pid(m), "center_at": sk.point(cc), "major_at": sk.point(m), "minor_radius": r, "construction": c.construction, "fixed": sk.curve_locked(i), "fully_constrained": det}),
|
||||
CurveKind::Spline { ref pts, control, degree } => json!({"id": c.id, "type": "spline", "points": pts.iter().map(|q| pid(*q)).collect::<Vec<_>>(), "points_at": pts.iter().map(|q| sk.point(*q)).collect::<Vec<_>>(), "control": control, "degree": degree, "construction": c.construction, "fixed": sk.curve_locked(i), "fully_constrained": det}),
|
||||
CurveKind::Conic { a, b, apex, rho } => json!({"id": c.id, "type": "conic", "start": pid(a), "end": pid(b), "apex": pid(apex), "start_at": sk.point(a), "end_at": sk.point(b), "apex_at": sk.point(apex), "rho": rho, "construction": c.construction, "fixed": sk.curve_locked(i), "fully_constrained": det}),
|
||||
};
|
||||
if let (Some(l), Some(o)) = (&c.link, v.as_object_mut()) {
|
||||
o.insert("link".into(), json!(l));
|
||||
@@ -113,11 +113,12 @@ fn sketch_json(s: &Session, id: u64) -> Option<Value> {
|
||||
v
|
||||
})
|
||||
.collect();
|
||||
let locked = sk.locked_points();
|
||||
let points: Vec<Value> = sk
|
||||
.points
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, p)| json!({"id": p.id, "at": p.pos, "world": ss.plane.to_world(p.pos), "fixed": p.fixed, "link": p.link, "fully_constrained": ss.report.point_determined.get(i).copied().unwrap_or(false)}))
|
||||
.map(|(i, p)| json!({"id": p.id, "at": p.pos, "world": ss.plane.to_world(p.pos), "fixed": locked.get(i).copied().unwrap_or(false), "link": p.link, "fully_constrained": ss.report.point_determined.get(i).copied().unwrap_or(false)}))
|
||||
.collect();
|
||||
let constraints: Vec<Value> = sk
|
||||
.constraints
|
||||
|
||||
@@ -123,7 +123,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.at("SKETCH", "CONSTRAINTS")
|
||||
.icon("c_fix")
|
||||
.enabled(in_sketch)
|
||||
.params("entity: point or curve; fixed?: bool (default toggles)"),
|
||||
.params("entity: point or curve | entities: [points and curves]; fixed?: bool (default: fix them all unless all are fixed, then unfix)"),
|
||||
CommandSpec::new("ConstraintMidPoint", "MidPoint", c_midpoint)
|
||||
.at("SKETCH", "CONSTRAINTS")
|
||||
.icon("c_midpoint")
|
||||
@@ -972,31 +972,55 @@ fn round_center(sk: &Sketch, c: usize) -> Option<usize> {
|
||||
fn c_fix(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "ConstraintFix";
|
||||
let want = bool_(p, "fixed");
|
||||
let mut refs = string_list(p, "entities");
|
||||
if let Some(e) = str_(p, "entity") {
|
||||
refs.insert(0, e.to_string());
|
||||
}
|
||||
if refs.is_empty() {
|
||||
return Err(bad(cmd, "`entity` or `entities` must name sketch points or curves"));
|
||||
}
|
||||
let (n, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let r = p.get("entity").and_then(Value::as_str).ok_or_else(|| bad(cmd, "`entity` must be a point or curve id"))?;
|
||||
let pts: Vec<usize> = if let Some(i) = sk.resolve_point(r) {
|
||||
vec![i]
|
||||
} else {
|
||||
let c = sk.curve_index(r).ok_or_else(|| bad(cmd, format!("unknown entity `{r}`")))?;
|
||||
sk.curves.get(c).map(|c| c.kind.point_ids()).unwrap_or_default()
|
||||
};
|
||||
let is_fixed = |sk: &Sketch, q: usize| sk.constraints.iter().any(|c| c.kind == ConstraintKind::Fix { p: q });
|
||||
let all_fixed = pts.iter().all(|q| is_fixed(sk, *q));
|
||||
// Points and curves; a point is fixed by a Fix constraint, a curve by its flag (which
|
||||
// also holds its radius). Projected geometry is always fixed.
|
||||
let mut pts = Vec::new();
|
||||
let mut curves = Vec::new();
|
||||
for r in &refs {
|
||||
if let Some(i) = sk.resolve_point(r) {
|
||||
pts.push(i);
|
||||
} else {
|
||||
curves.push(sk.curve_index(r).ok_or_else(|| bad(cmd, format!("unknown entity `{r}`")))?);
|
||||
}
|
||||
}
|
||||
let all_fixed = pts.iter().all(|q| *q == 0 || sk.point_locked(*q)) && curves.iter().all(|c| sk.curve_locked(*c));
|
||||
let fix = want.unwrap_or(!all_fixed);
|
||||
let mut n = 0;
|
||||
for q in pts {
|
||||
if q == 0 {
|
||||
let own = sk.points.get(q).is_some_and(|x| x.fixed || x.link.is_some());
|
||||
if q == 0 || own {
|
||||
continue;
|
||||
}
|
||||
if fix && !is_fixed(sk, q) {
|
||||
let has = sk.constraints.iter().any(|c| c.kind == ConstraintKind::Fix { p: q });
|
||||
if fix && !has {
|
||||
add_c(sk, ConstraintKind::Fix { p: q })?;
|
||||
n += 1;
|
||||
} else if !fix {
|
||||
} else if !fix && has {
|
||||
sk.constraints.retain(|c| c.kind != ConstraintKind::Fix { p: q });
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
Ok(json!({"fixed": fix, "points": n}))
|
||||
for c in curves {
|
||||
let Some(cu) = sk.curves.get_mut(c) else { continue };
|
||||
if cu.link.is_some() {
|
||||
continue;
|
||||
}
|
||||
cu.fixed = fix;
|
||||
n += 1;
|
||||
if !fix {
|
||||
let ids = cu.kind.point_ids();
|
||||
sk.constraints.retain(|k| !matches!(k.kind, ConstraintKind::Fix { p } if ids.contains(&p)));
|
||||
}
|
||||
}
|
||||
Ok(json!({"fixed": fix, "changed": n}))
|
||||
})?;
|
||||
Ok(json!({"result": n, "sketch": info}))
|
||||
}
|
||||
@@ -1319,8 +1343,9 @@ fn move_point(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let to = req_vec2(cmd, p, "to")?;
|
||||
let (_, info) = edit(s, p, cmd, false, |sk, _| {
|
||||
let q = point_ref(sk, p.get("point"), cmd, "point")?;
|
||||
let locked = sk.point_locked(q);
|
||||
if let Some(pt) = sk.points.get_mut(q)
|
||||
&& !pt.fixed
|
||||
&& !locked
|
||||
{
|
||||
pt.pos = to;
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ fn split_at(sk: &mut Sketch, ci: usize, ts: &[f64]) -> Result<(Vec<usize>, Vec<u
|
||||
let mut pieces = vec![ci];
|
||||
let mut push = |sk: &mut Sketch, kind: CurveKind, prefix: &str| -> Result<()> {
|
||||
let id = sk.fresh(prefix);
|
||||
sk.curves.push(Curve { id, kind, construction, reversed: false, link: None, centerline: false });
|
||||
sk.curves.push(Curve { id, kind, construction, reversed: false, link: None, centerline: false, fixed: false });
|
||||
pieces.push(sk.curves.len() - 1);
|
||||
Ok(())
|
||||
};
|
||||
@@ -980,6 +980,7 @@ fn copy_through(
|
||||
reversed: if flip { !cu.reversed } else { cu.reversed },
|
||||
link: None,
|
||||
centerline: false,
|
||||
fixed: false,
|
||||
});
|
||||
made.push(sk.curves.len() - 1);
|
||||
}
|
||||
|
||||
@@ -110,3 +110,91 @@ fn deleting_a_dimension_frees_its_parameter() {
|
||||
assert!(sketch(&s).constraints.iter().all(|c| !c.kind.is_dimension()));
|
||||
assert!(s.doc.param(¶m).is_none());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Fix / Unfix
|
||||
|
||||
fn pt(s: &Session, id: &str) -> solvecraft_geom::Vec2 {
|
||||
let sk = sketch(s);
|
||||
sk.resolve_point(id).and_then(|i| sk.point(i)).unwrap_or_else(|| panic!("no point {id}"))
|
||||
}
|
||||
|
||||
fn dof(s: &mut Session) -> i64 {
|
||||
run(s, "sketch.inspect", json!({}))["dof"].as_i64().unwrap_or(-1)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_points_survive_drags_and_solves_and_unfix_frees_them() {
|
||||
let mut s = new_sketch();
|
||||
run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [30, 0], [30, 20]]}));
|
||||
let free = dof(&mut s);
|
||||
let r = run(&mut s, "ConstraintFix", json!({"entity": "l1.end"}));
|
||||
assert_eq!(r["result"]["fixed"], true);
|
||||
assert_eq!(dof(&mut s), free - 2);
|
||||
let corner = pt(&s, "l1.end");
|
||||
// Dragging the fixed point does nothing; dragging its neighbours never moves it.
|
||||
run(&mut s, "sketch.move_point", json!({"point": "l1.end", "to": [50, 50]}));
|
||||
assert_eq!(pt(&s, "l1.end"), corner);
|
||||
run(&mut s, "ConstraintHorizontalVertical", json!({"line": "l2", "mode": "vertical"}));
|
||||
run(&mut s, "sketch.move_point", json!({"point": "l2.end", "to": [40, 25]}));
|
||||
run(&mut s, "SketchDimension", json!({"entities": ["l1"], "value": 12}));
|
||||
assert!(pt(&s, "l1.end").dist(corner) < 1e-12, "{:?}", pt(&s, "l1.end"));
|
||||
assert!((pt(&s, "l1.start").dist(corner) - 12.0).abs() < 1e-6);
|
||||
// Unfix: the point moves again.
|
||||
let r = run(&mut s, "ConstraintFix", json!({"entity": "l1.end"}));
|
||||
assert_eq!(r["result"]["fixed"], false);
|
||||
run(&mut s, "sketch.move_point", json!({"point": "l1.end", "to": [30, -5]}));
|
||||
assert!(pt(&s, "l1.end").dist(corner) > 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixing_curves_holds_their_points_and_radius() {
|
||||
let mut s = new_sketch();
|
||||
run(&mut s, "CircleCenterRadius", json!({"center": [10, 10], "radius": 5}));
|
||||
run(&mut s, "DrawPolyline", json!({"points": [[30, 0], [50, 0]]}));
|
||||
run(&mut s, "ArcCenterTwoPoint", json!({"center": [0, 40], "start": [10, 40], "end": [0, 50]}));
|
||||
run(&mut s, "CircleElipse", json!({"center": [60, 40], "major": [70, 40], "minor_radius": 4}));
|
||||
let free = dof(&mut s);
|
||||
// Multi-select fixes them all (circle 3, line 4, arc 5, ellipse 5 degrees of freedom).
|
||||
let r = run(&mut s, "ConstraintFix", json!({"entities": ["c1", "l1", "a1", "e1"]}));
|
||||
assert_eq!(r["result"]["changed"], 4);
|
||||
assert_eq!(dof(&mut s), free - 17);
|
||||
let si = run(&mut s, "sketch.inspect", json!({}));
|
||||
assert!(si["curves"].as_array().is_some_and(|c| c.iter().all(|c| c["fixed"] == true)), "{si}");
|
||||
// A fixed circle keeps centre and radius; a dimension on it conflicts.
|
||||
run(&mut s, "sketch.move_point", json!({"point": "c1.center", "to": [0, 0]}));
|
||||
assert_eq!(pt(&s, "c1.center"), solvecraft_geom::Vec2::new(10.0, 10.0));
|
||||
assert!(s.execute("SketchDimension", &json!({"entities": ["c1"], "value": 20})).is_err());
|
||||
let r0 = sketch(&s).radius(0);
|
||||
assert_eq!(r0, Some(5.0));
|
||||
// Toggle without `fixed`: all fixed, so all are unfixed.
|
||||
let r = run(&mut s, "ConstraintFix", json!({"entities": ["c1", "l1", "a1", "e1"]}));
|
||||
assert_eq!(r["result"]["fixed"], false);
|
||||
assert_eq!(dof(&mut s), free);
|
||||
run(&mut s, "SketchDimension", json!({"entities": ["c1"], "type": "radius", "value": 7}));
|
||||
assert!((sketch(&s).radius(0).unwrap_or(0.0) - 7.0).abs() < 1e-9);
|
||||
// Mixed selection: one fixed, one free → both end up fixed.
|
||||
run(&mut s, "ConstraintFix", json!({"entity": "l1"}));
|
||||
let r = run(&mut s, "ConstraintFix", json!({"entities": ["l1", "l1.start", "a1"]}));
|
||||
assert_eq!(r["result"]["fixed"], true);
|
||||
assert!(sketch(&s).curves.iter().filter(|c| c.id == "l1" || c.id == "a1").all(|c| c.fixed));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn projected_geometry_counts_as_fixed() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "SketchCreate", json!({"plane": "XY"}));
|
||||
run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [20, 10]}));
|
||||
run(&mut s, "SketchStop", json!({}));
|
||||
run(&mut s, "Extrude", json!({"distance": 5}));
|
||||
run(&mut s, "SketchCreate", json!({"plane": "XY"}));
|
||||
let r = run(&mut s, "ProjectNewCmd", json!({"refs": [{"edge": [10, 0, 5]}]}));
|
||||
let sk = sketch(&s);
|
||||
let ci = sk.curves.iter().position(|c| c.link.is_some()).unwrap_or_else(|| panic!("projected: {r}"));
|
||||
let id = sk.curves[ci].id.clone();
|
||||
assert!(sk.curve_locked(ci));
|
||||
let r = run(&mut s, "ConstraintFix", json!({"entity": id}));
|
||||
// Already fixed: the toggle unfixes, which projected geometry ignores.
|
||||
assert_eq!(r["result"]["changed"], 0);
|
||||
assert!(sketch(&s).curve_locked(ci));
|
||||
}
|
||||
Reference in new issue
Block a user