diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 465eded5c..d79da3bcd 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -818,7 +818,7 @@ bg={bg_ms:.1}ms n={view_count}" !cmd.needs_entity_pick() && !cmd.is_selection_gathering() }); // Constraint glyphs are displayed in model space only. - let constraint_glyphs: Vec<(iced::Point, String, bool)> = if is_paper { + let constraint_glyphs: Vec<(iced::Point, [f32; 2], String, bool)> = if is_paper { Vec::new() } else { let (vw, vh) = sel_ref.vp_size; @@ -840,21 +840,43 @@ bg={bg_ms:.1}ms n={view_count}" .iter() .filter(|c| c.enabled) .filter_map(|c| { - let anchor = crate::scene::sketch_constraints::glyph_anchor(&tab.scene.document, c)?; + let (anchor, outward) = + crate::scene::sketch_constraints::glyph_placement( + &tab.scene.document, + c, + )?; let screen = crate::scene::pick::grip::project_rte( glam::DVec3::new(anchor.x, anchor.y, anchor.z), view_rot, eye, bounds, )?; - let point = iced::Point::new(bounds.x + screen.x, bounds.y + screen.y); - let is_conflicting = set.conflicts.iter().any(|(id, _)| *id == c.id); + let outward_screen = crate::scene::pick::grip::project_rte( + glam::DVec3::new( + anchor.x + outward.x, + anchor.y + outward.y, + anchor.z + outward.z, + ), + view_rot, + eye, + bounds, + )?; + let direction = (outward_screen - screen) + .normalize_or(glam::Vec2::NEG_Y); + let point = iced::Point::new( + bounds.x + screen.x, + bounds.y + screen.y, + ); + let is_conflicting = + set.conflicts.iter().any(|(id, _)| *id == c.id); let label = if self.show_constraint_values { crate::scene::sketch_constraints::glyph_label(c) } else { c.kind.glyph_symbol().to_string() }; - point.x.is_finite().then(|| (point, label, is_conflicting)) + point.x.is_finite().then(|| { + (point, direction.to_array(), label, is_conflicting) + }) }) .collect() } diff --git a/src/scene/sketch_constraints.rs b/src/scene/sketch_constraints.rs index af0a7dba3..202f1f76b 100644 --- a/src/scene/sketch_constraints.rs +++ b/src/scene/sketch_constraints.rs @@ -400,28 +400,68 @@ pub(crate) fn glyph_label(constraint: &SketchConstraint) -> String { } } -/// World-space point to anchor `constraint`'s glyph at, or `None` if it -/// can't be resolved (dangling ref, unsupported entity/marker). A point ref -/// resolves straight through [`resolve_point`]; a whole-entity ref falls -/// back to a representative point on the entity (a line's midpoint, a -/// circle's rightmost point) since there's no single named point to anchor -/// on otherwise. -pub(crate) fn glyph_anchor( +/// World-space anchor and outward direction for a constraint glyph. +pub(crate) fn glyph_placement( document: &acadrust::CadDocument, constraint: &SketchConstraint, -) -> Option { +) -> Option<(Vector3, Vector3)> { let r = constraint.refs.first()?; let entity = document.get_entity(r.entity)?; - if let Some(marker) = r.marker { - return resolve_point(entity, marker); - } - match entity { - acadrust::EntityType::Line(l) => Some(Vector3::new( - (l.start.x + l.end.x) * 0.5, - (l.start.y + l.end.y) * 0.5, - (l.start.z + l.end.z) * 0.5, - )), - acadrust::EntityType::Circle(c) => Some(c.point_at_angle_wcs(0.0)), + let line_midpoint = |line: &acadrust::entities::Line| { + Vector3::new( + (line.start.x + line.end.x) * 0.5, + (line.start.y + line.end.y) * 0.5, + (line.start.z + line.end.z) * 0.5, + ) + }; + let line_normal = |line: &acadrust::entities::Line| { + let direction = Vector3::new( + -(line.end.y - line.start.y), + line.end.x - line.start.x, + 0.0, + ); + (direction.length_squared() > 1e-24) + .then_some(direction) + .unwrap_or(Vector3::UNIT_Y) + }; + match (entity, r.marker) { + (acadrust::EntityType::Line(line), None) => { + Some((line_midpoint(line), line_normal(line))) + } + (acadrust::EntityType::Circle(circle), None | Some(-3)) => { + let center = circle.center_wcs(); + let anchor = circle.point_at_angle_wcs(0.0); + Some((anchor, anchor - center)) + } + (acadrust::EntityType::Arc(arc), None | Some(-3)) => { + let center = arc.center_wcs(); + let anchor = arc.midpoint_wcs(); + Some((anchor, anchor - center)) + } + (acadrust::EntityType::Line(line), Some(marker)) => { + let anchor = resolve_point(entity, marker)?; + let direction = anchor - line_midpoint(line); + Some(( + anchor, + (direction.length_squared() > 1e-24) + .then_some(direction) + .unwrap_or_else(|| line_normal(line)), + )) + } + (acadrust::EntityType::Arc(arc), Some(marker)) => { + let anchor = resolve_point(entity, marker)?; + let direction = anchor - arc.center_wcs(); + Some(( + anchor, + (direction.length_squared() > 1e-24) + .then_some(direction) + .unwrap_or(Vector3::UNIT_Y), + )) + } + (_, Some(marker)) => { + let anchor = resolve_point(entity, marker)?; + Some((anchor, Vector3::UNIT_Y)) + } _ => None, } } @@ -557,6 +597,44 @@ mod tests { Handle::new(v) } + #[test] + fn glyph_placement_points_away_from_its_geometry() { + let mut document = acadrust::CadDocument::new(); + let mut line = acadrust::entities::Line::from_points( + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(10.0, 0.0, 0.0), + ); + line.common.handle = h(1); + document + .add_entity(acadrust::EntityType::Line(line)) + .unwrap(); + let mut circle = acadrust::entities::Circle::from_center_radius(Vector3::ZERO, 5.0); + circle.common.handle = h(2); + document + .add_entity(acadrust::EntityType::Circle(circle)) + .unwrap(); + + let constraint = |reference| SketchConstraint { + id: 0, + kind: ConstraintKind::Fixed, + refs: vec![reference], + driving_param: None, + enabled: true, + }; + let (anchor, direction) = + glyph_placement(&document, &constraint(SketchRef::whole(h(1)))).unwrap(); + assert_eq!(anchor, Vector3::new(5.0, 0.0, 0.0)); + assert_eq!(direction, Vector3::new(0.0, 10.0, 0.0)); + let (anchor, direction) = + glyph_placement(&document, &constraint(SketchRef::point(h(1), 0))).unwrap(); + assert_eq!(anchor, Vector3::ZERO); + assert_eq!(direction, Vector3::new(-5.0, 0.0, 0.0)); + let (anchor, direction) = + glyph_placement(&document, &constraint(SketchRef::center(h(2)))).unwrap(); + assert_eq!(anchor, Vector3::new(5.0, 0.0, 0.0)); + assert_eq!(direction, Vector3::new(5.0, 0.0, 0.0)); + } + #[test] fn add_assigns_increasing_ids_and_get_finds_them() { let mut set = SketchConstraintSet::new(SketchScope::ModelSpace); diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 32d03653d..c93e8ffbf 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -21,6 +21,26 @@ pub const CROSSHAIR_ARM: f32 = 60.0; const DEFAULT_CURSOR_SIZE: i32 = 5; const DEFAULT_PICK_BOX: i32 = 3; const DEFAULT_PICK_APERTURE: f32 = 8.0; +const CONSTRAINT_GLYPH_SIZE: f32 = 14.0; +const CONSTRAINT_GLYPH_PAD_X: f32 = 7.0; +const CONSTRAINT_GLYPH_PAD_Y: f32 = 4.0; +const CONSTRAINT_GLYPH_GAP: f32 = 6.0; + +fn constraint_glyph_box(anchor: Point, outward: [f32; 2], label: &str) -> (Point, Size) { + let w = label.chars().count() as f32 * CONSTRAINT_GLYPH_SIZE * 0.62 + + CONSTRAINT_GLYPH_PAD_X * 2.0; + let h = CONSTRAINT_GLYPH_SIZE + CONSTRAINT_GLYPH_PAD_Y * 2.0; + let distance = outward[0].abs() * w * 0.5 + + outward[1].abs() * h * 0.5 + + CONSTRAINT_GLYPH_GAP; + ( + Point::new( + anchor.x + outward[0] * distance - w * 0.5, + anchor.y + outward[1] * distance - h * 0.5, + ), + Size::new(w, h), + ) +} /// Convert CURSORSIZE to a screen-space arm length while keeping the original /// 60 px cursor at the default value and the full-viewport result at 100. @@ -651,7 +671,7 @@ pub fn selection_overlay<'a>( crosshair_bg: [f32; 4], crosshair: CrosshairOptions, selection_visual: SelectionVisualOptions, - constraint_glyphs: Vec<(Point, String, bool)>, + constraint_glyphs: Vec<(Point, [f32; 2], String, bool)>, ) -> Element<'a, Message> { canvas(SelectionCanvas { selection, @@ -734,8 +754,8 @@ struct SelectionCanvas { crosshair_bg: [f32; 4], crosshair: CrosshairOptions, selection_visual: SelectionVisualOptions, - /// Constraint glyph anchor, label, and conflict state for the current scope. - constraint_glyphs: Vec<(Point, String, bool)>, + /// Constraint glyph anchor, outward screen direction, label, and conflict state. + constraint_glyphs: Vec<(Point, [f32; 2], String, bool)>, } fn draw_grip_marker( @@ -1633,9 +1653,6 @@ impl canvas::Program for SelectionCanvas { } // Constraint glyphs are visual-only and have no hit testing. if !self.constraint_glyphs.is_empty() { - const GLYPH_SIZE: f32 = 11.0; - const GLYPH_PAD_X: f32 = 5.0; - const GLYPH_PAD_Y: f32 = 2.0; let normal_bg = theme.palette().primary.base.color; let normal_fg = theme.palette().primary.base.text; // A redundant or conflicting constraint gets the danger palette @@ -1644,23 +1661,26 @@ impl canvas::Program for SelectionCanvas { // would show, surfaced right on the geometry. let conflict_bg = theme.palette().danger.base.color; let conflict_fg = theme.palette().danger.base.text; - for (anchor, label, is_conflicting) in &self.constraint_glyphs { + for (anchor, outward, label, is_conflicting) in &self.constraint_glyphs { if !anchor.x.is_finite() || !anchor.y.is_finite() { continue; } let (bg, fg) = if *is_conflicting { (conflict_bg, conflict_fg) } else { (normal_bg, normal_fg) }; - // Rough width estimate (monospace-ish glyph set, short labels) - // avoids a text-measurement pass just to size the background pill. - let w = label.chars().count() as f32 * GLYPH_SIZE * 0.62 + GLYPH_PAD_X * 2.0; - let h = GLYPH_SIZE + GLYPH_PAD_Y * 2.0; - let top_left = Point::new(anchor.x - w * 0.5, anchor.y - h * 0.5); - let pill = canvas::Path::rounded_rectangle(top_left, iced::Size::new(w, h), (h * 0.5).into()); + let (top_left, size) = constraint_glyph_box(*anchor, *outward, label); + let pill = canvas::Path::rounded_rectangle( + top_left, + size, + (size.height * 0.5).into(), + ); frame.fill(&pill, bg); frame.fill_text(canvas::Text { content: label.clone(), - position: Point::new(top_left.x + GLYPH_PAD_X, top_left.y + GLYPH_PAD_Y), + position: Point::new( + top_left.x + CONSTRAINT_GLYPH_PAD_X, + top_left.y + CONSTRAINT_GLYPH_PAD_Y, + ), color: fg, - size: iced::Pixels(GLYPH_SIZE), + size: iced::Pixels(CONSTRAINT_GLYPH_SIZE), shaping: iced::advanced::text::Shaping::Advanced, ..Default::default() }); @@ -3313,6 +3333,22 @@ mod clip_tests { } } +#[cfg(test)] +mod constraint_glyph_tests { + use super::*; + + #[test] + fn enlarged_glyph_box_stays_clear_of_its_geometry_anchor() { + let anchor = Point::new(100.0, 80.0); + let (right, size) = constraint_glyph_box(anchor, [1.0, 0.0], "⊥"); + assert_eq!(size.height, 22.0); + assert!((right.x - anchor.x - CONSTRAINT_GLYPH_GAP).abs() < 1e-6); + + let (above, size) = constraint_glyph_box(anchor, [0.0, -1.0], "↔ 25.00"); + assert!((anchor.y - (above.y + size.height) - CONSTRAINT_GLYPH_GAP).abs() < 1e-6); + } +} + #[cfg(test)] mod grid_key_tests { use super::*;