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:
bherbruckandClaude Opus 5.5 committed 2026-10-08 05:12:13 +00:00
1 parent b457b6b7b6
commit 5b8816aa7d
11 files changed
+252 -35

No files matched your search

+8 -7
View File
@@ -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
+40 -15
View File
@@ -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;
}
+2 -1
View File
@@ -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);
}
+88
View File
@@ -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(&param).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));
}
+4
View File
@@ -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 {
+9 -1
View File
@@ -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)?;
+65 -4
View File
@@ -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)
}
+8 -1
View File
@@ -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)
}
+2 -5
View File
@@ -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);
+16
View File
@@ -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
+10 -1
View File
@@ -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 {