Sketch UI: click a constraint glyph to select it, Delete removes the constraint; glyphs sit half way along their curve; lighter randomized sketch test

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 01:31:16 +00:00
1 parent 1c10d1e56a
commit 1b664bb1b0
4 files changed
+66 -6

No files matched your search

+11 -1
View File
@@ -403,9 +403,19 @@ fn toggle_driven(s: &mut Session, p: &Value) -> Result<Value> {
/// or at its point.
fn anchor(sk: &Sketch, k: &ConstraintKind) -> Option<Vec2> {
use ConstraintKind::*;
// Half way along the curve.
let mid = |c: usize| -> Option<Vec2> {
let poly = sk.polyline(c);
poly.get(poly.len() / 2).copied()
let total: f64 = poly.windows(2).map(|w| w[0].dist(w[1])).sum();
let mut acc = 0.0;
for w in poly.windows(2) {
let l = w[0].dist(w[1]);
if acc + l >= total * 0.5 && l > 0.0 {
return Some(w[0].lerp(w[1], (total * 0.5 - acc) / l));
}
acc += l;
}
poly.first().copied()
};
let pt = |p: usize| sk.point(p);
match *k {
+2 -2
View File
@@ -41,12 +41,12 @@ fn entities(s: &Session) -> (Vec<String>, Vec<String>, Vec<String>) {
#[test]
fn random_sketch_editing_never_breaks() {
for seed in 1..=4u64 {
for seed in 1..=2u64 {
let mut r = Rng(0x9E37_79B9_7F4A_7C15 ^ seed);
let mut s = Session::default();
let _ = s.execute("PrimitiveBox", &json!({"length": 30, "width": 20, "height": 10}));
let _ = s.execute("SketchCreate", &json!({"plane": {"face": [15, 10, 10]}}));
for step in 0..150 {
for step in 0..120 {
let (curves, points, cons) = entities(&s);
let c = |r: &mut Rng| r.pick(&curves).cloned().unwrap_or_default();
let p = |r: &mut Rng| r.pick(&points).cloned().unwrap_or_default();
+47 -2
View File
@@ -18,6 +18,30 @@ thread_local! {
static TEXT: RefCell<Option<(Vec2, String)>> = const { RefCell::new(None) };
/// Shift held this frame.
static SHIFT: Cell<bool> = const { Cell::new(false) };
/// Glyphs drawn this frame (constraint id, screen centre) and the selected one.
static GLYPHS: RefCell<Vec<(String, Pos2)>> = const { RefCell::new(Vec::new()) };
static PICKED: RefCell<Option<String>> = const { RefCell::new(None) };
}
/// 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() {
return false;
}
let hit = GLYPHS.with(|g| g.borrow().iter().find(|(_, c)| c.distance(pos) <= 7.0).map(|(id, _)| id.clone()));
let used = hit.is_some();
PICKED.with(|p| *p.borrow_mut() = hit.clone());
if let Some(id) = hit {
app.set_status(format!("Constraint {id} selected: Delete removes it"), false);
}
used
}
/// Delete the selected constraint glyph (true when one was selected).
pub fn delete_glyph(app: &mut SolveApp) -> bool {
let Some(id) = PICKED.with(|p| p.borrow_mut().take()) else { return false };
let _ = app.run("sketch.delete", json!({"entities": [id]}));
true
}
/// What the extra tools do with clicks.
@@ -513,6 +537,8 @@ fn glyphs(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
let Some(ss) = st.sketch(sid) else { return };
let tk = crate::theme::Tokens::get();
let mut placed: Vec<Pos2> = Vec::new();
let picked = PICKED.with(|p| p.borrow().clone());
let mut drawn: Vec<(String, Pos2)> = Vec::new();
for c in &ss.sketch.constraints {
if c.kind.is_dimension() {
continue;
@@ -526,12 +552,21 @@ fn glyphs(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
}
placed.push(sp);
let failing = ss.report.failing.contains(&c.id);
let col = if failing { tk.error } else { tk.text_dim };
let selected = picked.as_deref() == Some(c.id.as_str());
let col = if failing {
tk.error
} else if selected {
tk.accent
} else {
tk.text_dim
};
drawn.push((c.id.clone(), sp));
let r = egui::Rect::from_center_size(sp, egui::vec2(12.0, 12.0));
painter.rect_filled(r, 2.0, tk.panel.gamma_multiply(0.85));
painter.rect_stroke(r, 2.0, Stroke::new(0.8, col), egui::StrokeKind::Inside);
painter.text(sp, egui::Align2::CENTER_CENTER, glyph_letter(c.kind.name()), egui::FontId::proportional(9.0), col);
}
GLYPHS.with(|g| *g.borrow_mut() = drawn);
}
fn glyph_letter(name: &str) -> &'static str {
@@ -555,9 +590,19 @@ fn glyph_letter(name: &str) -> &'static str {
fn glyph_anchor(sk: &solvecraft_engine::sketch::Sketch, k: &solvecraft_engine::sketch::ConstraintKind) -> Option<Vec2> {
use solvecraft_engine::sketch::ConstraintKind::*;
// Half way along the curve.
let mid = |c: usize| -> Option<Vec2> {
let poly = sk.polyline(c);
poly.get(poly.len() / 2).copied()
let total: f64 = poly.windows(2).map(|w| w[0].dist(w[1])).sum();
let mut acc = 0.0;
for w in poly.windows(2) {
let l = w[0].dist(w[1]);
if acc + l >= total * 0.5 && l > 0.0 {
return Some(w[0].lerp(w[1], (total * 0.5 - acc) / l));
}
acc += l;
}
poly.first().copied()
};
match *k {
Coincident { p, .. } | Fix { p } | Midpoint { p, .. } | PointOnCurve { p, .. } => sk.point(p),
+6 -1
View File
@@ -774,6 +774,9 @@ pub fn sketch_point_at(app: &SolveApp, proj: &Proj, pos: Pos2) -> Option<(Vec2,
}
pub fn delete_selection(app: &mut SolveApp) {
if crate::sketch_tools::delete_glyph(app) {
return;
}
let sel = app.session.selection.clone();
if sel.is_empty() {
return;
@@ -930,7 +933,9 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
app.dialog = Some(d);
}
} else {
select(app, cand.and_then(|c| c.1), add);
if !crate::sketch_tools::click_glyph(app, &proj, p) {
select(app, cand.and_then(|c| c.1), add);
}
}
}
if resp.secondary_clicked() && delta == egui::Vec2::ZERO {