From d2068e07a391c0f2845ab0dd84061d2e29486b57 Mon Sep 17 00:00:00 2001 From: bherbruck Date: Thu, 8 Oct 2026 08:16:31 +0000 Subject: [PATCH] Selection inside sketches: double-click a curve selects the chain joined to it end to end (sketch.chain; shared or coincident ends), dimensions and constraint glyphs are selectable like other entities (Sel::SketchConstraint: Ctrl/Shift toggle, window and crossing boxes take them, Delete removes them with the rest), boxes leave out the origin, and the selection filter has sketch curves, points, dimensions and constraints Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL --- crates/engine/src/cmd/delete.rs | 2 +- crates/engine/src/cmd/sketch.rs | 12 ++++++ crates/engine/src/cmd/sketch_ux_tests.rs | 38 +++++++++++++++++ crates/engine/src/lib.rs | 4 ++ crates/sketch/src/model.rs | 54 ++++++++++++++++++++++++ crates/ui-egui/src/control.rs | 2 +- crates/ui-egui/src/delete.rs | 2 +- crates/ui-egui/src/dialogs.rs | 2 +- crates/ui-egui/src/dim_view.rs | 53 +++++++---------------- crates/ui-egui/src/sketch_tools.rs | 13 +++++- crates/ui-egui/src/toolbar.rs | 12 +++++- crates/ui-egui/src/viewport.rs | 53 ++++++++++++++++++++--- 12 files changed, 195 insertions(+), 52 deletions(-) diff --git a/crates/engine/src/cmd/delete.rs b/crates/engine/src/cmd/delete.rs index d1a53cba..c42959f5 100644 --- a/crates/engine/src/cmd/delete.rs +++ b/crates/engine/src/cmd/delete.rs @@ -65,7 +65,7 @@ fn apply(s: &mut Session, p: &Value) -> Result { let mut bodies: Vec = Vec::new(); for x in &items { match x { - Sel::SketchCurve { id } | Sel::SketchPoint { id } => entities.push(id.clone()), + Sel::SketchCurve { id } | Sel::SketchPoint { id } | Sel::SketchConstraint { id } => entities.push(id.clone()), Sel::Feature { id } => { if s.doc.feature(*id).is_none() { return Err(bad(cmd, format!("no feature {id}"))); diff --git a/crates/engine/src/cmd/sketch.rs b/crates/engine/src/cmd/sketch.rs index 1d68ade2..02ca94ed 100644 --- a/crates/engine/src/cmd/sketch.rs +++ b/crates/engine/src/cmd/sketch.rs @@ -163,6 +163,10 @@ pub static COMMANDS: &[CommandSpec] = &[ CommandSpec::new("sketch.options", "Sketch Palette Options", sketch_options).icon("sketch").params( "sketch?: id|name (default active); any of show_profile, show_points, show_dimensions, show_constraints, show_projected, grid, snap, slice, sketch_3d: bool. Returns them all", ), + CommandSpec::new("sketch.chain", "Chain", chain_cmd) + .enabled(in_sketch) + .noundo() + .params("curve: id: the curves joined to it end to end (what a double-click selects)"), CommandSpec::new("sketch.solve", "Solve Sketch", solve_cmd) .enabled(in_sketch) .noundo() @@ -1660,6 +1664,14 @@ fn sketch_options(s: &mut Session, p: &Value) -> Result { Ok(json!({"options": v, "sketch": info})) } +fn chain_cmd(s: &mut Session, p: &Value) -> Result { + let cmd = "sketch.chain"; + let id = target_sketch(s, p, cmd)?; + let sk = s.doc.sketch(id)?; + let c = curve_ref(sk, p.get("curve"), cmd, "curve")?; + Ok(json!({"curves": ids_of(sk, &sk.chain(c))})) +} + fn solve_cmd(s: &mut Session, p: &Value) -> Result { let id = target_sketch(s, p, "sketch.solve")?; let st = s.model.state(); diff --git a/crates/engine/src/cmd/sketch_ux_tests.rs b/crates/engine/src/cmd/sketch_ux_tests.rs index b354d4bf..d837d866 100644 --- a/crates/engine/src/cmd/sketch_ux_tests.rs +++ b/crates/engine/src/cmd/sketch_ux_tests.rs @@ -526,3 +526,41 @@ fn fully_constrained_feedback_is_per_entity() { assert!(lines.iter().filter(|l| l.1 == crate::view::colors::SKETCH_FIXED).count() >= 4); assert!(lines.iter().any(|l| l.1 == crate::view::colors::SKETCH)); } + +// --------------------------------------------------------------------------------------------- +// Selection inside sketches + +#[test] +fn chain_follows_shared_and_coincident_ends() { + let mut s = new_sketch(); + run(&mut s, "ShapeRectangleTwoPoint", json!({"p0": [0, 0], "p1": [20, 10]})); + run(&mut s, "DrawPolyline", json!({"points": [[40, 0], [50, 0]]})); + run(&mut s, "DrawPolyline", json!({"points": [[50.5, 0], [60, 5]]})); + run(&mut s, "CircleCenterRadius", json!({"center": [80, 0], "radius": 3})); + let chain = |s: &mut Session, c: &str| -> Vec { + let v = run(s, "sketch.chain", json!({"curve": c})); + v["curves"].as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect()).unwrap_or_default() + }; + assert_eq!(chain(&mut s, "l3"), ["l3", "l1", "l2", "l4"]); + assert_eq!(chain(&mut s, "l5"), ["l5"]); + assert_eq!(chain(&mut s, "c1"), ["c1"]); + // Joined by a coincident constraint, the two lines are one chain. + run(&mut s, "ConstraintCoincident", json!({"a": "l5.end", "b": "l6.start"})); + assert_eq!(chain(&mut s, "l6"), ["l6", "l5"]); + assert!(s.execute("sketch.chain", &json!({"curve": "nope"})).is_err()); +} + +#[test] +fn constraints_and_dimensions_can_be_selected_and_deleted_together() { + let mut s = new_sketch(); + run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [30, 0]]})); + run(&mut s, "ConstraintHorizontalVertical", json!({"line": "l1"})); + run(&mut s, "SketchDimension", json!({"entities": ["l1"], "value": 30})); + let ids: Vec = sketch(&s).constraints.iter().map(|c| c.id.clone()).collect(); + assert_eq!(ids.len(), 2); + let items: Vec = ids.iter().map(|id| json!({"type": "sketch_constraint", "id": id})).collect(); + run(&mut s, "select.set", json!({ "items": items })); + assert_eq!(s.selection.len(), 2); + run(&mut s, "selection.delete", json!({"items": items})); + assert!(sketch(&s).constraints.is_empty()); +} diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 130ba8c4..35ebf0ac 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -94,6 +94,10 @@ pub enum Sel { SketchPoint { id: String, }, + /// A constraint or dimension (by id) of the active sketch. + SketchConstraint { + id: String, + }, /// A closed profile of a sketch (by index). Profile { sketch: u64, diff --git a/crates/sketch/src/model.rs b/crates/sketch/src/model.rs index f20d8301..602610bc 100644 --- a/crates/sketch/src/model.rs +++ b/crates/sketch/src/model.rs @@ -772,6 +772,60 @@ impl Sketch { out } + /// The curves joined end to end with curve `ci` (through shared or coincident end points), + /// `ci` first, then the others in sketch order: what a double-click selects. + pub fn chain(&self, ci: usize) -> Vec { + if ci >= self.curves.len() { + return Vec::new(); + } + // Points made one by coincident constraints. + let mut root: Vec = (0..self.points.len()).collect(); + fn find(r: &mut [usize], mut i: usize) -> usize { + while let Some(&p) = r.get(i) { + if p == i { + break; + } + i = p; + } + i + } + for c in &self.constraints { + if let ConstraintKind::Coincident { p, q } = c.kind { + let (a, b) = (find(&mut root, p), find(&mut root, q)); + if let Some(x) = root.get_mut(a) { + *x = b; + } + } + } + let ends: Vec> = + self.curves.iter().map(|c| c.kind.ends().map(|(a, b)| vec![find(&mut root, a), find(&mut root, b)]).unwrap_or_default()).collect(); + let mut seen = vec![false; self.curves.len()]; + let mut stack = vec![ci]; + let mut out = Vec::new(); + while let Some(i) = stack.pop() { + if seen.get(i).copied().unwrap_or(true) { + continue; + } + if let Some(s) = seen.get_mut(i) { + *s = true; + } + out.push(i); + let mine = ends.get(i).cloned().unwrap_or_default(); + for (j, e) in ends.iter().enumerate() { + if !seen.get(j).copied().unwrap_or(true) && e.iter().any(|p| mine.contains(p)) { + stack.push(j); + } + } + } + let first = out.first().copied(); + out.sort_unstable(); + if let Some(f) = first { + out.retain(|x| *x != f); + out.insert(0, f); + } + 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()) diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 79e64e4e..94e534c7 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -204,7 +204,7 @@ pub fn handle(app: &mut SolveApp, ctx: &egui::Context, req: &ControlRequest) -> "dialog": app.dialog.as_ref().map(|d| d.inputs.iter().map(|i| json!({"label": i.label, "items": i.items})).collect::>()), "hover": app.viewport.hover.as_ref().map(|h| format!("{h:?}")), "dimensions": crate::dim_view::drawn().into_iter().map(|(id, p)| json!({"id": id, "x": p.x, "y": p.y})).collect::>(), - "dimension_selected": crate::dim_view::selected(), + "dimension_selected": crate::dim_view::selected(app), "dimension_editing": crate::dim_view::editing(), })), "ui.sketchToScreen" => { diff --git a/crates/ui-egui/src/delete.rs b/crates/ui-egui/src/delete.rs index 818ca92c..b22b7125 100644 --- a/crates/ui-egui/src/delete.rs +++ b/crates/ui-egui/src/delete.rs @@ -31,7 +31,7 @@ fn is_empty(p: &Value) -> bool { /// Delete what is selected (the Delete key). A selected sketch dimension or constraint glyph /// goes first, as before. pub fn delete_selection(app: &mut SolveApp) { - if crate::dim_view::delete_selected(app) || crate::sketch_tools::delete_glyph(app) { + if crate::sketch_tools::delete_glyph(app) { return; } request(app, selection_params(app)); diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index b5479418..f7838e7f 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -255,7 +255,7 @@ fn fits(a: Accept, s: &Sel) -> bool { Sel::Axis { .. } => a & AXES != 0, Sel::SketchCurve { .. } => a & (AXES | CURVES) != 0, Sel::Vertex { .. } => a & selection::VERTICES != 0, - Sel::Feature { .. } | Sel::SketchPoint { .. } => false, + Sel::Feature { .. } | Sel::SketchPoint { .. } | Sel::SketchConstraint { .. } => false, } } diff --git a/crates/ui-egui/src/dim_view.rs b/crates/ui-egui/src/dim_view.rs index ef83cc5f..3761d3fa 100644 --- a/crates/ui-egui/src/dim_view.rs +++ b/crates/ui-egui/src/dim_view.rs @@ -13,6 +13,7 @@ use std::cell::RefCell; use egui::{FontId, Pos2, Rect, Shape, Stroke, vec2}; use serde_json::json; +use solvecraft_engine::Sel; use solvecraft_engine::geom::Vec2; use solvecraft_engine::sketch::{ConstraintKind, DimFrame, chain_centres, default_text, dim_frame, dim_layout}; @@ -44,7 +45,6 @@ struct Edit { thread_local! { static DRAWN: RefCell> = const { RefCell::new(Vec::new()) }; - static SELECTED: RefCell> = const { RefCell::new(None) }; /// A text being dragged: dimension id, undo depth at the start, grab offset (sketch). static DRAG: RefCell> = const { RefCell::new(None) }; static EDIT: RefCell> = const { RefCell::new(None) }; @@ -74,36 +74,20 @@ pub fn hit(pos: Pos2) -> Option { }) } -/// The selected dimension (constraint id). -pub fn selected() -> Option { - SELECTED.with(|s| s.borrow().clone()) +/// The selected dimensions (constraint ids; they are in the session's selection). +pub fn selected(app: &SolveApp) -> Vec { + let dims: Vec = DRAWN.with(|d| d.borrow().iter().map(|x| x.id.clone()).collect()); + app.session + .selection + .iter() + .filter_map(|s| if let Sel::SketchConstraint { id } = s { Some(id.clone()) } else { None }) + .filter(|id| dims.contains(id)) + .collect() } -/// A click without a tool: select the dimension under it (true: the click was used). A click -/// elsewhere drops the dimension selection. -pub fn click(app: &mut SolveApp, pos: Pos2) -> bool { - if app.session.active_sketch.is_none() { - return false; - } - let h = hit(pos); - SELECTED.with(|s| *s.borrow_mut() = h.clone()); - if let Some(id) = &h { - let _ = app.run("select.clear", json!({})); - app.set_status(format!("Dimension {id} selected: drag to move, double-click to edit, Delete removes it"), false); - } - h.is_some() -} - -/// Drop the dimension selection (true when there was one). -pub fn deselect() -> bool { - SELECTED.with(|s| s.borrow_mut().take()).is_some() -} - -/// Delete the selected dimension (true when one was selected). -pub fn delete_selected(app: &mut SolveApp) -> bool { - let Some(id) = SELECTED.with(|s| s.borrow_mut().take()) else { return false }; - let _ = app.run("sketch.delete", json!({"entities": [id]})); - true +/// Dimension text boxes drawn last frame (for box selection). +pub fn text_rects() -> Vec<(String, Rect)> { + DRAWN.with(|d| d.borrow().iter().map(|x| (x.id.clone(), x.text)).collect()) } /// A double-click on a dimension opens its value for editing (true: it was on one). @@ -114,7 +98,6 @@ pub fn double_click(app: &mut SolveApp, pos: Pos2) -> bool { }) else { return false; }; - SELECTED.with(|s| *s.borrow_mut() = Some(d.id.clone())); let Some(param) = d.param else { app.set_status("A driven dimension measures the sketch; it has no value to edit", false); return true; @@ -132,7 +115,6 @@ pub fn drag_start(app: &SolveApp, press: Pos2, press_sketch: Vec2) -> bool { }) else { return false; }; - SELECTED.with(|s| *s.borrow_mut() = Some(d.id.clone())); DRAG.with(|g| *g.borrow_mut() = Some((d.id, app.session.undo.len(), d.at - press_sketch))); true } @@ -181,7 +163,6 @@ pub fn show(app: &mut SolveApp, ui: &egui::Ui, painter: &egui::Painter, proj: &P let st = app.session.model.state(); let Some(ss) = app.session.active_sketch.and_then(|s| st.sketch(s)) else { DRAWN.with(|d| d.borrow_mut().clear()); - SELECTED.with(|s| *s.borrow_mut() = None); EDIT.with(|e| *e.borrow_mut() = None); return; }; @@ -191,11 +172,7 @@ pub fn show(app: &mut SolveApp, ui: &egui::Ui, painter: &egui::Painter, proj: &P return; } let tk = Tokens::get(); - let sel = selected(); - // A selected dimension that was deleted (undo, another command) is forgotten. - if sel.as_ref().is_some_and(|id| !sk.constraints.iter().any(|c| &c.id == id)) { - SELECTED.with(|s| *s.borrow_mut() = None); - } + let is_sel = |id: &str| app.session.selection.iter().any(|s| matches!(s, Sel::SketchConstraint { id: x } if x == id)); let hover = ui.input(|i| i.pointer.hover_pos()).filter(|p| proj.rect.contains(*p)).and_then(hit); let to = |p: Vec2| proj.to_screen(ss.plane.to_world(p)); let font = FontId::proportional(12.0); @@ -217,7 +194,7 @@ pub fn show(app: &mut SolveApp, ui: &egui::Ui, painter: &egui::Painter, proj: &P let failing = ss.report.failing.contains(&c.id); let col = if failing { tk.error - } else if sel.as_deref() == Some(c.id.as_str()) || hover.as_deref() == Some(c.id.as_str()) { + } else if is_sel(&c.id) || hover.as_deref() == Some(c.id.as_str()) { tk.accent } else if c.driven { tk.dimension_driven diff --git a/crates/ui-egui/src/sketch_tools.rs b/crates/ui-egui/src/sketch_tools.rs index b9fb0d47..7dbd02c3 100644 --- a/crates/ui-egui/src/sketch_tools.rs +++ b/crates/ui-egui/src/sketch_tools.rs @@ -147,6 +147,16 @@ pub fn refine_snap( }) } +/// The constraint glyph under a screen position (its constraint id). +pub fn glyph_at(pos: Pos2) -> Option { + GLYPHS.with(|g| g.borrow().iter().find(|(_, c)| c.distance(pos) <= 7.0).map(|(id, _)| id.clone())) +} + +/// Constraint glyphs drawn last frame: (constraint id, screen centre). +pub fn glyph_positions() -> Vec<(String, Pos2)> { + GLYPHS.with(|g| g.borrow().clone()) +} + /// A click on a constraint glyph selects it (true: the click was used). pub fn click_glyph(app: &mut SolveApp, _proj: &Proj, pos: Pos2) -> bool { if app.session.active_sketch.is_none() { @@ -731,7 +741,8 @@ fn glyphs(app: &SolveApp, painter: &egui::Painter, proj: &Proj) { } placed.push(sp); let failing = ss.report.failing.contains(&c.id); - let selected = picked.as_deref() == Some(c.id.as_str()); + let selected = picked.as_deref() == Some(c.id.as_str()) + || app.session.selection.iter().any(|s| matches!(s, solvecraft_engine::Sel::SketchConstraint { id } if *id == c.id)); let col = if failing { tk.error } else if selected { diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 4fbbff99..824e8056 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -364,7 +364,16 @@ fn selection_filter(app: &mut SolveApp, ui: &mut egui::Ui) { let t = Tokens::get(); ui.label(egui::RichText::new("Selection filter").color(t.text_dim).size(11.0)); ui.checkbox(&mut app.ui.pick_bodies, "Select bodies (a face picks its body)"); - for (k, label) in [("faces", "Faces"), ("edges", "Edges"), ("vertices", "Vertices"), ("sketch", "Sketch geometry and profiles")] { + for (k, label) in [ + ("faces", "Faces"), + ("edges", "Edges"), + ("vertices", "Vertices"), + ("sketch", "Sketch geometry and profiles"), + ("sketch_curves", "Sketch curves"), + ("sketch_points", "Sketch points"), + ("dimensions", "Sketch dimensions"), + ("constraints", "Sketch constraints"), + ] { let mut on = !app.ui.pick_off.iter().any(|x| x == k); if ui.checkbox(&mut on, label).changed() { app.ui.pick_off.retain(|x| x != k); @@ -417,7 +426,6 @@ pub fn shortcuts(app: &mut SolveApp, ctx: &egui::Context) { crate::tools::finish(app); } else if app.dialog.is_some() { crate::dialogs::cancel(app); - } else if crate::dim_view::deselect() { } else if !app.session.selection.is_empty() || app.session.active_sketch.is_none() { let _ = app.run("select.clear", json!({})); } else { diff --git a/crates/ui-egui/src/viewport.rs b/crates/ui-egui/src/viewport.rs index 86903057..98297df5 100644 --- a/crates/ui-egui/src/viewport.rs +++ b/crates/ui-egui/src/viewport.rs @@ -672,8 +672,9 @@ pub fn candidate(app: &SolveApp, hits: &[Hit]) -> Option<(Hit, Sel)> { let off = |k: &str| app.ui.pick_off.iter().any(|x| x == k); hits.iter() .filter(|h| match h { - Hit::SketchCurve { sketch, .. } => app.session.active_sketch == Some(*sketch) && !off("sketch"), - Hit::SketchPoint { .. } | Hit::Profile { .. } => !off("sketch"), + Hit::SketchCurve { sketch, .. } => app.session.active_sketch == Some(*sketch) && !off("sketch") && !off("sketch_curves"), + Hit::SketchPoint { .. } => !off("sketch") && !off("sketch_points"), + Hit::Profile { .. } => !off("sketch"), Hit::Vertex { .. } => !off("vertices") && !app.ui.pick_bodies, Hit::Edge { .. } => !off("edges") && !app.ui.pick_bodies, Hit::Face { .. } => !off("faces") || app.ui.pick_bodies, @@ -1132,13 +1133,32 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) { app.dialog = Some(d); } } else { - if !crate::dim_view::click(app, p) && !crate::sketch_tools::click_glyph(app, &proj, p) { - select(app, cand.and_then(|c| c.1), add); + // Dimensions and constraint glyphs sit on top of the geometry. + let off = |k: &str| app.ui.pick_off.iter().any(|x| x == k); + let sketching = app.session.active_sketch.is_some(); + let on_dim = crate::dim_view::hit(p).filter(|_| sketching && !off("dimensions")); + let on_glyph = crate::sketch_tools::glyph_at(p).filter(|_| sketching && !off("constraints")); + match on_dim.or(on_glyph) { + Some(id) => select(app, Some(Sel::SketchConstraint { id }), add), + None => select(app, cand.and_then(|c| c.1), add), } } } // Double-click a dimension: edit its value in place. let dim_edit = resp.double_clicked() && app.tool.is_none() && app.dialog.is_none() && crate::dim_view::double_click(app, p); + // Double-click a sketch curve: select the chain joined to it end to end. + if resp.double_clicked() + && !dim_edit + && app.tool.is_none() + && app.dialog.is_none() + && let Some(Hit::SketchCurve { sketch, id, .. }) = app.viewport.hover.clone() + && app.session.active_sketch == Some(sketch) + && let Ok(v) = app.session.execute("sketch.chain", &json!({ "curve": id })) + { + let items: Vec = + v["curves"].as_array().into_iter().flatten().filter_map(|c| c.as_str()).map(|id| Sel::SketchCurve { id: id.into() }).collect(); + let _ = app.run("select.set", json!({ "items": items, "add": add })); + } // Double-click an edge: select the edges that continue it smoothly (the loop). if resp.double_clicked() && !dim_edit @@ -1302,17 +1322,36 @@ fn box_select(app: &mut SolveApp, proj: &Proj, bx: BoxSel, add: bool) { if app.dialog.is_none() && let Some(ss) = app.session.active_sketch.and_then(|sid| st.sketch(sid)) { - for (i, c) in ss.sketch.curves.iter().enumerate() { + // What the selection filter lets through (window: wholly inside; crossing: touching). + let off = |k: &str| app.ui.pick_off.iter().any(|x| x == k); + for (i, c) in ss.sketch.curves.iter().enumerate().filter(|_| !off("sketch") && !off("sketch_curves")) { + if c.link.is_some() && ss.sketch.view.hide_projected { + continue; + } let pts: Vec = ss.sketch.segs(i).iter().flat_map(|sg| sg.polyline(0.05)).map(|q| ss.plane.to_world(q)).collect(); if bx.takes(&to2(&pts)) { out.push(Sel::SketchCurve { id: c.id.clone() }); } } - for p in &ss.sketch.points { - if bx.takes(&to2(&[ss.plane.to_world(p.pos)])) { + for (i, p) in ss.sketch.points.iter().enumerate().filter(|_| !off("sketch") && !off("sketch_points")) { + if i != 0 && solvecraft_engine::view::sketch_point_visible(&ss.sketch, i) && bx.takes(&to2(&[ss.plane.to_world(p.pos)])) { out.push(Sel::SketchPoint { id: p.id.clone() }); } } + if !off("dimensions") { + for (id, r) in crate::dim_view::text_rects() { + if bx.takes(&[r.left_top(), r.right_top(), r.right_bottom(), r.left_bottom()]) { + out.push(Sel::SketchConstraint { id }); + } + } + } + if !off("constraints") { + for (id, c) in crate::sketch_tools::glyph_positions() { + if bx.takes(&[c]) { + out.push(Sel::SketchConstraint { id }); + } + } + } } else { let accept = app.dialog.as_ref().and_then(|d| d.active_input()).map(|i| i.accept).unwrap_or(BODIES); for b in &st.bodies {