From a06e8a7a69b9730795afb488d2d6d0fe5e5d7c6b Mon Sep 17 00:00:00 2001 From: gianlucafiore Date: Sat, 8 Aug 2026 23:09:06 -0300 Subject: [PATCH] Show active tracking reference tooltip --- src/app/command_driver.rs | 1 + src/app/mod.rs | 4 ++ src/app/update/mod.rs | 2 + src/app/update/viewport.rs | 2 + src/app/view/mod.rs | 12 +++++ src/snap.rs | 19 ++++++- src/ui/overlay.rs | 105 +++++++++++++++++++++++++++++++++++++ 7 files changed, 144 insertions(+), 1 deletion(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 206ff8f0..57714318 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -87,6 +87,7 @@ impl OpenCADStudio { self.snapper.from_point = None; self.snapper.clear_tracking(); self.otrack_active = None; + self.otrack_kind = None; self.axis_lock_dir = None; self.dyn_user_reshaped = false; self.dyn_coord_absolute = false; diff --git a/src/app/mod.rs b/src/app/mod.rs index e582987e..d3d6f204 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -430,6 +430,9 @@ pub(super) struct OpenCADStudio { /// cursor is on a tracking ray. Lets a typed distance place a point along /// the ray from the tracking point (issue #69). `None` when not aligned. otrack_active: Option<(glam::DVec3, glam::DVec3)>, + /// Kind of the currently active tracking reference, kept separately from + /// `otrack_active` so typed-distance behavior remains unchanged. + otrack_kind: Option, /// Whether Tangent snap was enabled before a tangent-pick command started. pre_cmd_tangent: Option, /// Drawing defaults captured by ADDSELECTED before it adopts the template @@ -3052,6 +3055,7 @@ impl OpenCADStudio { annotation_auto_scale: -4, last_saved_config: None, otrack_active: None, + otrack_kind: None, clean_screen: false, quick_properties: false, quick_properties_anchor: Point::new(12.0, 12.0), diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 62e382f2..4cf4d814 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -255,6 +255,7 @@ impl OpenCADStudio { { self.snapper.clear_tracking(); self.otrack_active = None; + self.otrack_kind = None; } if let Some(started) = perf_started { let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0; @@ -280,6 +281,7 @@ impl OpenCADStudio { pub(in crate::app) fn reset_tracking_after_point(&mut self) { self.snapper.clear_tracking(); self.otrack_active = None; + self.otrack_kind = None; } fn update_inner(&mut self, msg: Message) -> Task { diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 37219dd6..dc9d44df 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -1083,6 +1083,7 @@ impl OpenCADStudio { }; self.otrack_active = otrack_hit.map(|hit| (hit.base, hit.dir)); + self.otrack_kind = otrack_hit.map(|hit| hit.kind); let mut snapped = if let Some(hit) = otrack_hit { hit.aligned @@ -1519,6 +1520,7 @@ impl OpenCADStudio { } }; self.otrack_active = otrack_hit.map(|h| (h.base, h.dir)); + self.otrack_kind = otrack_hit.map(|h| h.kind); // Parallel snap: with nothing else snapped or tracked, lock // the point onto the line through last_point parallel to the diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index d104c002..082e6bad 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -809,6 +809,17 @@ impl OpenCADStudio { .as_ref() .map(|c| c.prompt()) .unwrap_or_default(); + + let tracking_hint = match self.otrack_kind { + Some(crate::snap::TrackingKind::Perpendicular) => { + Some("Perpendicular".to_string()) + } + Some(crate::snap::TrackingKind::Extension) => { + Some("Extension".to_string()) + } + _ => None, + }; + Some(crate::ui::overlay::dynamic_input_overlay( tab.last_cursor_screen, tab.last_point_screen, @@ -816,6 +827,7 @@ impl OpenCADStudio { tab.dyn_guide, boxes, prompt, + tracking_hint, )) } else { None diff --git a/src/snap.rs b/src/snap.rs index ffdee88b..82d2e54e 100644 --- a/src/snap.rs +++ b/src/snap.rs @@ -15,6 +15,12 @@ const DEFAULT_OSNAP_RADIUS_PX: f32 = 15.0; // ── Snap type ───────────────────────────────────────────────────────────── +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum TrackingKind { + Generic, + Extension, + Perpendicular, +} /// Every OSNAP mode — mirrors the OpenCADStudio list. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SnapType { @@ -92,6 +98,8 @@ pub struct OtrackHit { pub dir: DVec3, /// The tracking point the ray emanates from. pub base: DVec3, + + pub kind: TrackingKind, } // ── Snapper ─────────────────────────────────────────────────────────────── @@ -511,6 +519,7 @@ impl Snapper { origin: DVec3, dir: DVec3, group: usize, + kind: TrackingKind, } let mut rays: Vec = Vec::new(); for (gi, &tp) in self.tracking_points.iter().enumerate() { @@ -520,6 +529,7 @@ impl Snapper { origin: tp, dir: ucs_x * ar.cos() + ucs_y * ar.sin(), group: gi, + kind: TrackingKind::Generic, }); } // Extension rays along the corner's own edges (world-space geometry @@ -532,6 +542,7 @@ impl Snapper { origin: tp, dir: d, group: gi, + kind: TrackingKind::Extension, }); } } @@ -547,6 +558,7 @@ impl Snapper { origin: tp, dir: d, group: gi, + kind: TrackingKind::Perpendicular, }); } } @@ -566,6 +578,7 @@ impl Snapper { origin: bp, dir: DVec3::new(d.x * inv, d.y * inv, 0.0), group: gi, + kind: TrackingKind::Generic, }); } } @@ -583,6 +596,7 @@ impl Snapper { origin: lp, dir: ucs_x * ar.cos() + ucs_y * ar.sin(), group: POLAR_GROUP, + kind: TrackingKind::Generic, }); } } @@ -596,6 +610,7 @@ impl Snapper { origin: lp, dir: ucs_x * ar.cos() + ucs_y * ar.sin(), group: ORTHO_GROUP, + kind: TrackingKind::Generic, }); } } @@ -634,7 +649,8 @@ impl Snapper { aligned: x, dir: dir_out, base: ot.origin, - }, + kind: ot.kind, + } )); } } @@ -670,6 +686,7 @@ impl Snapper { aligned, dir: dir_out, base: ray.origin, + kind: ray.kind, }, )); } diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index c0eef63c..416f0652 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -2166,6 +2166,7 @@ pub fn dynamic_input_overlay<'a>( guide: DynGuide, boxes: Vec, prompt: String, + tracking_hint: Option, ) -> Element<'a, Message> { canvas(DynInputCanvas { cursor_screen, @@ -2174,6 +2175,7 @@ pub fn dynamic_input_overlay<'a>( guide, boxes, prompt, + tracking_hint, }) .width(Length::Fill) .height(Length::Fill) @@ -2191,6 +2193,8 @@ struct DynInputCanvas { boxes: Vec, /// The active command's current prompt, drawn just above the boxes. prompt: String, + /// Informational tracking/reference mode currently engaged. + tracking_hint: Option, } impl DynInputCanvas { @@ -2296,6 +2300,49 @@ impl DynInputCanvas { }); } + fn draw_tracking_hint( + &self, + frame: &mut canvas::Frame, + pos: Point, + theme: &Theme, + ) { + let Some(text) = self.tracking_hint.as_deref() else { + return; + }; + + let palette = theme.palette(); + let pw = (text.len() as f32 * DYN_CHAR_W) + DYN_PAD * 2.0; + + let rect = canvas::Path::rectangle( + pos, + Size { + width: pw, + height: DYN_BOX_H, + }, + ); + + frame.fill(&rect, palette.background.strong.color); + + frame.stroke( + &rect, + canvas::Stroke::default() + .with_color(palette.primary.base.color.scale_alpha(0.9)) + .with_width(1.0), + ); + + frame.fill_text(canvas::Text { + content: text.to_string(), + position: Point { + x: pos.x + DYN_PAD, + y: pos.y + DYN_PAD, + }, + color: palette.background.strong.text, + size: iced::Pixels(DYN_FONT), + shaping: iced::advanced::text::Shaping::Advanced, + ..Default::default() + }); + } + /// Guided layout: draw the guide geometry anchored at `base`, then place /// each box according to its role. fn draw_guided( @@ -2496,6 +2543,32 @@ impl DynInputCanvas { }; Self::draw_box(frame, b, center, bounds, theme); } + // Guided dynamic-input layouts place their editable boxes on the + // construction geometry, so keep the tracking-reference pill near + // the crosshair instead. + if let Some(text) = self.tracking_hint.as_deref() { + let hw = (text.len() as f32 * DYN_CHAR_W) + DYN_PAD * 2.0; + + let mut hx = self.cursor_screen.x + DYN_OFFSET_X; + let mut hy = self.cursor_screen.y + DYN_OFFSET_X; + + if hx + hw > bounds.width { + hx = (self.cursor_screen.x - hw - 4.0).max(0.0); + } + + if hy + DYN_BOX_H > bounds.height { + hy = (self.cursor_screen.y - DYN_BOX_H - 4.0).max(0.0); + } + + self.draw_tracking_hint( + frame, + Point { + x: hx, + y: hy, + }, + theme, + ); + } } /// Fallback row layout near the cursor (no anchor / `None` guide). @@ -2563,6 +2636,25 @@ impl DynInputCanvas { }); x += w + DYN_GAP; } + // Informational tracking reference, using the same pill style as + // the command prompt. Keep it below the editable value row. + if self.tracking_hint.is_some() { + let mut hy = by + DYN_BOX_H + 3.0; + + // If there is no room below, place it above the prompt/value block. + if hy + DYN_BOX_H > bounds.height { + hy = (py - DYN_BOX_H - 3.0).max(0.0); + } + + self.draw_tracking_hint( + frame, + Point { + x: bx, + y: hy, + }, + theme, + ); + } } } @@ -2601,6 +2693,19 @@ impl canvas::Program for DynInputCanvas { py = (self.cursor_screen.y - DYN_BOX_H - 4.0).max(0.0); } self.draw_prompt(&mut frame, Point { x: px, y: py }, theme); + + if self.tracking_hint.is_some() { + let hint_y = py + DYN_BOX_H + 3.0; + + self.draw_tracking_hint( + &mut frame, + Point { + x: px, + y: hint_y, + }, + theme, + ); + } } return vec![frame.into_geometry()]; }