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 <[email protected]> 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));
|
||||
}
|
||||
@@ -20,6 +20,8 @@ pub mod colors {
|
||||
pub const SKETCH_DONE: Rgb = Rgb(20, 20, 20);
|
||||
pub const SKETCH_CONSTRUCTION: Rgb = Rgb(230, 130, 40);
|
||||
pub const SKETCH_FIXED: Rgb = Rgb(20, 20, 20);
|
||||
/// Fixed (locked) geometry.
|
||||
pub const SKETCH_LOCKED: Rgb = Rgb(0, 140, 60);
|
||||
pub const SKETCH_CENTERLINE: Rgb = Rgb(200, 110, 60);
|
||||
/// Linked (projected) reference geometry.
|
||||
pub const SKETCH_PROJECTED: Rgb = Rgb(150, 60, 190);
|
||||
@@ -72,6 +74,8 @@ pub fn sketch_lines(sk: &Sketch, plane: &Plane, active: bool, determined: &[bool
|
||||
for (i, c) in sk.curves.iter().enumerate() {
|
||||
let col = if c.link.is_some() && !sk.is_text_curve(i) {
|
||||
if sk.is_lost_curve(i) { colors::SKETCH_LOST } else { colors::SKETCH_PROJECTED }
|
||||
} else if c.fixed {
|
||||
colors::SKETCH_LOCKED
|
||||
} else if c.centerline {
|
||||
colors::SKETCH_CENTERLINE
|
||||
} else if c.construction {
|
||||
|
||||
@@ -407,7 +407,15 @@ impl Sketch {
|
||||
}
|
||||
};
|
||||
let cid = self.fresh(prefix);
|
||||
self.curves.push(Curve { id: cid, kind, construction: false, reversed: false, link: Some(id.to_string()), centerline: false });
|
||||
self.curves.push(Curve {
|
||||
id: cid,
|
||||
kind,
|
||||
construction: false,
|
||||
reversed: false,
|
||||
link: Some(id.to_string()),
|
||||
centerline: false,
|
||||
fixed: false,
|
||||
});
|
||||
}
|
||||
for li in &lay.lone {
|
||||
point(self, *li)?;
|
||||
|
||||
@@ -140,6 +140,9 @@ pub struct Curve {
|
||||
/// an axis (revolve, diameter dimensions).
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub centerline: bool,
|
||||
/// Fixed (locked): its points and radius are constants for the solver.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub fixed: bool,
|
||||
}
|
||||
|
||||
/// Geometric constraints and dimensions. Indices refer to `Sketch::points` (`p`, `q`) or
|
||||
@@ -536,7 +539,15 @@ impl Sketch {
|
||||
return Err(SketchError::Invalid("line needs two distinct points".into()));
|
||||
}
|
||||
let id = self.curve_id(id, "l")?;
|
||||
self.curves.push(Curve { id, kind: CurveKind::Line { a, b }, construction: false, reversed: false, link: None, centerline: false });
|
||||
self.curves.push(Curve {
|
||||
id,
|
||||
kind: CurveKind::Line { a, b },
|
||||
construction: false,
|
||||
reversed: false,
|
||||
link: None,
|
||||
centerline: false,
|
||||
fixed: false,
|
||||
});
|
||||
Ok(self.curves.len() - 1)
|
||||
}
|
||||
|
||||
@@ -565,7 +576,15 @@ impl Sketch {
|
||||
Some(i) if i < self.points.len() => i,
|
||||
_ => self.own_point(&id, "center", center)?,
|
||||
};
|
||||
self.curves.push(Curve { id, kind: CurveKind::Circle { c, r }, construction: false, reversed: false, link: None, centerline: false });
|
||||
self.curves.push(Curve {
|
||||
id,
|
||||
kind: CurveKind::Circle { c, r },
|
||||
construction: false,
|
||||
reversed: false,
|
||||
link: None,
|
||||
centerline: false,
|
||||
fixed: false,
|
||||
});
|
||||
Ok(self.curves.len() - 1)
|
||||
}
|
||||
|
||||
@@ -591,7 +610,15 @@ impl Sketch {
|
||||
Some(i) if i < n && i != a => i,
|
||||
_ => self.own_point(&id, "end", p1)?,
|
||||
};
|
||||
self.curves.push(Curve { id, kind: CurveKind::Arc { c, a, b }, construction: false, reversed: false, link: None, centerline: false });
|
||||
self.curves.push(Curve {
|
||||
id,
|
||||
kind: CurveKind::Arc { c, a, b },
|
||||
construction: false,
|
||||
reversed: false,
|
||||
link: None,
|
||||
centerline: false,
|
||||
fixed: false,
|
||||
});
|
||||
Ok(self.curves.len() - 1)
|
||||
}
|
||||
|
||||
@@ -680,6 +707,40 @@ impl Sketch {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Is point `i` held in place: fixed, projected (linked), fixed by a Fix constraint or a
|
||||
/// point of a fixed curve.
|
||||
pub fn point_locked(&self, i: usize) -> bool {
|
||||
self.points.get(i).is_some_and(|p| p.fixed || p.link.is_some())
|
||||
|| self.constraints.iter().any(|c| c.kind == ConstraintKind::Fix { p: i })
|
||||
|| self.curves.iter().any(|c| c.fixed && c.kind.uses(i))
|
||||
}
|
||||
|
||||
/// [`Sketch::point_locked`] for every point at once.
|
||||
pub fn locked_points(&self) -> Vec<bool> {
|
||||
let mut out: Vec<bool> = self.points.iter().map(|p| p.fixed || p.link.is_some()).collect();
|
||||
let mut set = |i: usize| {
|
||||
if let Some(x) = out.get_mut(i) {
|
||||
*x = true;
|
||||
}
|
||||
};
|
||||
for c in &self.constraints {
|
||||
if let ConstraintKind::Fix { p } = c.kind {
|
||||
set(p);
|
||||
}
|
||||
}
|
||||
for c in self.curves.iter().filter(|c| c.fixed) {
|
||||
for p in c.kind.point_ids() {
|
||||
set(p);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Is curve `c` held in place: fixed or projected (linked).
|
||||
pub fn curve_locked(&self, c: usize) -> bool {
|
||||
self.curves.get(c).is_some_and(|c| c.fixed || c.link.is_some())
|
||||
}
|
||||
|
||||
/// Radius of a circle or arc.
|
||||
pub fn radius(&self, ci: usize) -> Option<f64> {
|
||||
match self.curves.get(ci)?.kind {
|
||||
@@ -834,7 +895,7 @@ impl Sketch {
|
||||
}
|
||||
};
|
||||
let id = self.curve_id(id, prefix)?;
|
||||
self.curves.push(Curve { id, kind, construction: false, reversed: false, link: None, centerline: false });
|
||||
self.curves.push(Curve { id, kind, construction: false, reversed: false, link: None, centerline: false, fixed: false });
|
||||
Ok(self.curves.len() - 1)
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,13 @@ fn layout(sk: &Sketch) -> Layout {
|
||||
*f = true;
|
||||
}
|
||||
}
|
||||
for c in sk.curves.iter().filter(|c| c.fixed) {
|
||||
for p in c.kind.point_ids() {
|
||||
if let Some(f) = fixed.get_mut(p) {
|
||||
*f = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut n = 0;
|
||||
let pvar = fixed
|
||||
.iter()
|
||||
@@ -86,7 +93,7 @@ fn layout(sk: &Sketch) -> Layout {
|
||||
.curves
|
||||
.iter()
|
||||
.map(|c| match c.kind {
|
||||
CurveKind::Circle { .. } | CurveKind::Ellipse { .. } if c.link.is_none() => {
|
||||
CurveKind::Circle { .. } | CurveKind::Ellipse { .. } if c.link.is_none() && !c.fixed => {
|
||||
n += 1;
|
||||
Some(n - 1)
|
||||
}
|
||||
|
||||
@@ -586,11 +586,8 @@ pub fn run_item(app: &mut SolveApp, item: &Item, at: Pos2) {
|
||||
"ui.unsection" => drop(app.run("FusionHalfSectionViewCommand", json!({"clear": true}))),
|
||||
"ui.fit" => app.animate_view("fit"),
|
||||
"ui.home" => app.animate_view("home"),
|
||||
"ui.fix" => {
|
||||
for e in strs(&p, "entities") {
|
||||
let _ = app.run("ConstraintFix", json!({ "entity": e }));
|
||||
}
|
||||
}
|
||||
// One command for the whole selection: all fixed unless all already are.
|
||||
"ui.fix" => drop(app.run("ConstraintFix", json!({ "entities": strs(&p, "entities") }))),
|
||||
"ui.editSketch" => {
|
||||
if let Some(id) = id_of(&p, "sketch") {
|
||||
app.edit_sketch(id);
|
||||
|
||||
@@ -295,6 +295,22 @@ impl SolveApp {
|
||||
let edges = self.session.selection.iter().any(|s| matches!(s, solvecraft_engine::Sel::Edge { .. }));
|
||||
return self.start(if edges { "FusionFilletEdgesCommand" } else { "Extrude" });
|
||||
}
|
||||
// Fix/Unfix: the selected sketch points and curves change at once, otherwise pick them.
|
||||
if id == "ConstraintFix" {
|
||||
let ents: Vec<String> = self
|
||||
.session
|
||||
.selection
|
||||
.iter()
|
||||
.filter_map(|s| match s {
|
||||
solvecraft_engine::Sel::SketchCurve { id } | solvecraft_engine::Sel::SketchPoint { id } => Some(id.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
if !ents.is_empty() {
|
||||
let _ = self.run(id, json!({ "entities": ents }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Construction toggle: selected sketch curves switch at once, otherwise pick them.
|
||||
if id == "sketch.construction" {
|
||||
let curves: Vec<String> = self
|
||||
|
||||
@@ -178,6 +178,8 @@ fn sketch_color(t: &Tokens, c: Rgb) -> [u8; 4] {
|
||||
c4(t.sketch)
|
||||
} else if c == colors::SKETCH_DONE || c == colors::SKETCH_FIXED {
|
||||
c4(t.sketch_fixed)
|
||||
} else if c == colors::SKETCH_LOCKED {
|
||||
c4(t.sketch_locked)
|
||||
} else {
|
||||
rgba(c)
|
||||
}
|
||||
@@ -1265,13 +1267,20 @@ fn overlays(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
|
||||
let Some(sid) = app.session.active_sketch else { return };
|
||||
let Some(ss) = st.sketch(sid) else { return };
|
||||
let sk = &ss.sketch;
|
||||
let locked = sk.locked_points();
|
||||
for (i, p) in sk.points.iter().enumerate() {
|
||||
if !solvecraft_engine::view::sketch_point_visible(sk, i) {
|
||||
continue;
|
||||
}
|
||||
let Some(sp) = proj.to_screen(ss.plane.to_world(p.pos)) else { continue };
|
||||
let det = ss.report.point_determined.get(i).copied().unwrap_or(false);
|
||||
let c = if det { t.sketch_point } else { t.sketch_point_free };
|
||||
let c = if p.link.is_none() && locked.get(i).copied().unwrap_or(false) {
|
||||
t.sketch_locked
|
||||
} else if det {
|
||||
t.sketch_point
|
||||
} else {
|
||||
t.sketch_point_free
|
||||
};
|
||||
if i == 0 {
|
||||
painter.circle(sp, 3.5, Color32::from_rgb(240, 200, 60), Stroke::new(1.0, t.body_edge));
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user