Show active tracking reference tooltip

This commit is contained in:
gianlucafiore 2026-08-08 23:09:06 -03:00
commit a06e8a7a69
7 changed files with 144 additions and 1 deletions

View file

@ -87,6 +87,7 @@ impl OpenCADStudio {
self.snapper.from_point = None; self.snapper.from_point = None;
self.snapper.clear_tracking(); self.snapper.clear_tracking();
self.otrack_active = None; self.otrack_active = None;
self.otrack_kind = None;
self.axis_lock_dir = None; self.axis_lock_dir = None;
self.dyn_user_reshaped = false; self.dyn_user_reshaped = false;
self.dyn_coord_absolute = false; self.dyn_coord_absolute = false;

View file

@ -430,6 +430,9 @@ pub(super) struct OpenCADStudio {
/// cursor is on a tracking ray. Lets a typed distance place a point along /// 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. /// the ray from the tracking point (issue #69). `None` when not aligned.
otrack_active: Option<(glam::DVec3, glam::DVec3)>, 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<crate::snap::TrackingKind>,
/// Whether Tangent snap was enabled before a tangent-pick command started. /// Whether Tangent snap was enabled before a tangent-pick command started.
pre_cmd_tangent: Option<bool>, pre_cmd_tangent: Option<bool>,
/// Drawing defaults captured by ADDSELECTED before it adopts the template /// Drawing defaults captured by ADDSELECTED before it adopts the template
@ -3052,6 +3055,7 @@ impl OpenCADStudio {
annotation_auto_scale: -4, annotation_auto_scale: -4,
last_saved_config: None, last_saved_config: None,
otrack_active: None, otrack_active: None,
otrack_kind: None,
clean_screen: false, clean_screen: false,
quick_properties: false, quick_properties: false,
quick_properties_anchor: Point::new(12.0, 12.0), quick_properties_anchor: Point::new(12.0, 12.0),

View file

@ -255,6 +255,7 @@ impl OpenCADStudio {
{ {
self.snapper.clear_tracking(); self.snapper.clear_tracking();
self.otrack_active = None; self.otrack_active = None;
self.otrack_kind = None;
} }
if let Some(started) = perf_started { if let Some(started) = perf_started {
let elapsed_ms = started.elapsed().as_secs_f64() * 1000.0; 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) { pub(in crate::app) fn reset_tracking_after_point(&mut self) {
self.snapper.clear_tracking(); self.snapper.clear_tracking();
self.otrack_active = None; self.otrack_active = None;
self.otrack_kind = None;
} }
fn update_inner(&mut self, msg: Message) -> Task<Message> { fn update_inner(&mut self, msg: Message) -> Task<Message> {

View file

@ -1083,6 +1083,7 @@ impl OpenCADStudio {
}; };
self.otrack_active = otrack_hit.map(|hit| (hit.base, hit.dir)); 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 { let mut snapped = if let Some(hit) = otrack_hit {
hit.aligned hit.aligned
@ -1519,6 +1520,7 @@ impl OpenCADStudio {
} }
}; };
self.otrack_active = otrack_hit.map(|h| (h.base, h.dir)); 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 // Parallel snap: with nothing else snapped or tracked, lock
// the point onto the line through last_point parallel to the // the point onto the line through last_point parallel to the

View file

@ -809,6 +809,17 @@ impl OpenCADStudio {
.as_ref() .as_ref()
.map(|c| c.prompt()) .map(|c| c.prompt())
.unwrap_or_default(); .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( Some(crate::ui::overlay::dynamic_input_overlay(
tab.last_cursor_screen, tab.last_cursor_screen,
tab.last_point_screen, tab.last_point_screen,
@ -816,6 +827,7 @@ impl OpenCADStudio {
tab.dyn_guide, tab.dyn_guide,
boxes, boxes,
prompt, prompt,
tracking_hint,
)) ))
} else { } else {
None None

View file

@ -15,6 +15,12 @@ const DEFAULT_OSNAP_RADIUS_PX: f32 = 15.0;
// ── Snap type ───────────────────────────────────────────────────────────── // ── Snap type ─────────────────────────────────────────────────────────────
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TrackingKind {
Generic,
Extension,
Perpendicular,
}
/// Every OSNAP mode — mirrors the OpenCADStudio list. /// Every OSNAP mode — mirrors the OpenCADStudio list.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SnapType { pub enum SnapType {
@ -92,6 +98,8 @@ pub struct OtrackHit {
pub dir: DVec3, pub dir: DVec3,
/// The tracking point the ray emanates from. /// The tracking point the ray emanates from.
pub base: DVec3, pub base: DVec3,
pub kind: TrackingKind,
} }
// ── Snapper ─────────────────────────────────────────────────────────────── // ── Snapper ───────────────────────────────────────────────────────────────
@ -511,6 +519,7 @@ impl Snapper {
origin: DVec3, origin: DVec3,
dir: DVec3, dir: DVec3,
group: usize, group: usize,
kind: TrackingKind,
} }
let mut rays: Vec<Ray> = Vec::new(); let mut rays: Vec<Ray> = Vec::new();
for (gi, &tp) in self.tracking_points.iter().enumerate() { for (gi, &tp) in self.tracking_points.iter().enumerate() {
@ -520,6 +529,7 @@ impl Snapper {
origin: tp, origin: tp,
dir: ucs_x * ar.cos() + ucs_y * ar.sin(), dir: ucs_x * ar.cos() + ucs_y * ar.sin(),
group: gi, group: gi,
kind: TrackingKind::Generic,
}); });
} }
// Extension rays along the corner's own edges (world-space geometry // Extension rays along the corner's own edges (world-space geometry
@ -532,6 +542,7 @@ impl Snapper {
origin: tp, origin: tp,
dir: d, dir: d,
group: gi, group: gi,
kind: TrackingKind::Extension,
}); });
} }
} }
@ -547,6 +558,7 @@ impl Snapper {
origin: tp, origin: tp,
dir: d, dir: d,
group: gi, group: gi,
kind: TrackingKind::Perpendicular,
}); });
} }
} }
@ -566,6 +578,7 @@ impl Snapper {
origin: bp, origin: bp,
dir: DVec3::new(d.x * inv, d.y * inv, 0.0), dir: DVec3::new(d.x * inv, d.y * inv, 0.0),
group: gi, group: gi,
kind: TrackingKind::Generic,
}); });
} }
} }
@ -583,6 +596,7 @@ impl Snapper {
origin: lp, origin: lp,
dir: ucs_x * ar.cos() + ucs_y * ar.sin(), dir: ucs_x * ar.cos() + ucs_y * ar.sin(),
group: POLAR_GROUP, group: POLAR_GROUP,
kind: TrackingKind::Generic,
}); });
} }
} }
@ -596,6 +610,7 @@ impl Snapper {
origin: lp, origin: lp,
dir: ucs_x * ar.cos() + ucs_y * ar.sin(), dir: ucs_x * ar.cos() + ucs_y * ar.sin(),
group: ORTHO_GROUP, group: ORTHO_GROUP,
kind: TrackingKind::Generic,
}); });
} }
} }
@ -634,7 +649,8 @@ impl Snapper {
aligned: x, aligned: x,
dir: dir_out, dir: dir_out,
base: ot.origin, base: ot.origin,
}, kind: ot.kind,
}
)); ));
} }
} }
@ -670,6 +686,7 @@ impl Snapper {
aligned, aligned,
dir: dir_out, dir: dir_out,
base: ray.origin, base: ray.origin,
kind: ray.kind,
}, },
)); ));
} }

View file

@ -2166,6 +2166,7 @@ pub fn dynamic_input_overlay<'a>(
guide: DynGuide, guide: DynGuide,
boxes: Vec<DynBox>, boxes: Vec<DynBox>,
prompt: String, prompt: String,
tracking_hint: Option<String>,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
canvas(DynInputCanvas { canvas(DynInputCanvas {
cursor_screen, cursor_screen,
@ -2174,6 +2175,7 @@ pub fn dynamic_input_overlay<'a>(
guide, guide,
boxes, boxes,
prompt, prompt,
tracking_hint,
}) })
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
@ -2191,6 +2193,8 @@ struct DynInputCanvas {
boxes: Vec<DynBox>, boxes: Vec<DynBox>,
/// The active command's current prompt, drawn just above the boxes. /// The active command's current prompt, drawn just above the boxes.
prompt: String, prompt: String,
/// Informational tracking/reference mode currently engaged.
tracking_hint: Option<String>,
} }
impl DynInputCanvas { 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 /// Guided layout: draw the guide geometry anchored at `base`, then place
/// each box according to its role. /// each box according to its role.
fn draw_guided( fn draw_guided(
@ -2496,6 +2543,32 @@ impl DynInputCanvas {
}; };
Self::draw_box(frame, b, center, bounds, theme); 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). /// Fallback row layout near the cursor (no anchor / `None` guide).
@ -2563,6 +2636,25 @@ impl DynInputCanvas {
}); });
x += w + DYN_GAP; 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<Message> for DynInputCanvas {
py = (self.cursor_screen.y - DYN_BOX_H - 4.0).max(0.0); py = (self.cursor_screen.y - DYN_BOX_H - 4.0).max(0.0);
} }
self.draw_prompt(&mut frame, Point { x: px, y: py }, theme); 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()]; return vec![frame.into_geometry()];
} }