From b511ac61249fefc92445ff208b6468ac98c766e7 Mon Sep 17 00:00:00 2001 From: bherbruck Date: Wed, 7 Oct 2026 23:14:24 +0000 Subject: [PATCH] Sketch: drag points of the active sketch (the solver keeps the constraints, one undo step per drag); closed profiles shaded Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL --- crates/ui-egui/src/theme.rs | 4 ++++ crates/ui-egui/src/viewport.rs | 40 +++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 3075dfaf..b304c5f3 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -83,6 +83,8 @@ pub struct Tokens { /// Sketch points: fully constrained, free. pub sketch_point: Color32, pub sketch_point_free: Color32, + /// Closed sketch profiles (translucent fill). + pub profile_fill: Color32, /// Constraint glyph boxes. pub glyph_bg: Color32, pub glyph_edge: Color32, @@ -165,6 +167,7 @@ impl Tokens { sketch_fixed: Color32::from_rgb(20, 20, 20), sketch_point: Color32::BLACK, sketch_point_free: Color32::from_rgb(30, 90, 200), + profile_fill: Color32::from_rgba_unmultiplied_const(255, 196, 120, 70), glyph_bg: Color32::WHITE, glyph_edge: Color32::from_rgb(90, 150, 90), glyph_text: Color32::from_rgb(40, 110, 40), @@ -215,6 +218,7 @@ impl Tokens { t.sketch_fixed = Color32::from_rgb(232, 234, 238); t.sketch_point = Color32::from_rgb(232, 234, 238); t.sketch_point_free = Color32::from_rgb(92, 150, 255); + t.profile_fill = Color32::from_rgba_unmultiplied_const(120, 150, 200, 40); t.glyph_bg = Color32::from_rgb(44, 50, 60); t.glyph_edge = Color32::from_rgb(96, 170, 110); t.glyph_text = Color32::from_rgb(150, 220, 160); diff --git a/crates/ui-egui/src/viewport.rs b/crates/ui-egui/src/viewport.rs index 8e2ad77c..07170c8a 100644 --- a/crates/ui-egui/src/viewport.rs +++ b/crates/ui-egui/src/viewport.rs @@ -42,6 +42,9 @@ pub struct ViewportState { pub build_ms: f64, /// The right-click menu, open at this screen position. pub context_menu: Option, + /// A sketch point being dragged (its id, and the undo depth when the drag began: the whole + /// drag is one undo step). + pub point_drag: Option<(String, usize)>, } /// Something under the cursor. @@ -214,6 +217,15 @@ fn build_scene(app: &SolveApp) -> GpuScene { for sid in visible_sketches(app) { let Some(ss) = st.sketch(sid) else { continue }; let active = s.active_sketch == Some(sid); + // Closed profiles are shaded (lifted off a face they may lie on). + let lift = ss.plane.normal() * 0.01; + for p in &ss.profiles { + for tri in p.region.triangulate(0.05) { + for q in tri { + sc.trans_tri((ss.plane.to_world(q) + lift).to_f32(), [0.0; 3], c4(tk.profile_fill)); + } + } + } for (pts, col, cons) in sketch_lines(&ss.sketch, &ss.plane, active, &ss.report.curve_determined) { for w in pts.windows(2) { sc.line(w[0].to_f32(), w[1].to_f32(), sketch_color(&tk, col), if cons { 1.2 } else { 2.0 }, active); @@ -881,8 +893,34 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) { } } } + // Dragging a sketch point of the active sketch moves it; the solver keeps the constraints. + if app.tool.is_none() && app.dialog.is_none() && app.viewport.nav.is_none() && app.session.active_sketch.is_some() { + if resp.drag_started_by(egui::PointerButton::Primary) + && let Some(a) = ui.input(|i| i.pointer.press_origin()) + && let Some(Hit::SketchPoint { id, .. }) = pick(app, &proj, a).into_iter().next() + { + app.viewport.point_drag = Some((id, app.session.undo.len())); + } + if let Some((id, depth)) = app.viewport.point_drag.clone() + && resp.dragged_by(egui::PointerButton::Primary) + && let Some(p) = hover + && let Some(ss) = app.session.active_sketch.and_then(|sid| app.session.model.state().sketch(sid).cloned()) + && let Some(w) = { + let (o, d) = proj.ray(p); + ss.plane.intersect_ray(o, d) + } + { + let q = ss.plane.to_local(w); + if app.session.execute("sketch.move_point", &json!({"point": id, "to": [q.x, q.y]})).is_ok() { + app.session.undo.truncate(depth + 1); + } + } + if resp.drag_stopped() { + app.viewport.point_drag = None; + } + } // Box selection: a primary drag on the model when no navigation mode is on. - if app.tool.is_none() && app.viewport.nav.is_none() { + if app.tool.is_none() && app.viewport.nav.is_none() && app.viewport.point_drag.is_none() { let origin = ui.input(|i| i.pointer.press_origin()); if resp.dragged_by(egui::PointerButton::Primary) && let (Some(a), Some(b)) = (origin, hover)