From a457541c211af096d8c17d78d6927cb3146f3b43 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Tue, 15 Sep 2026 13:10:01 +0300 Subject: [PATCH 1/3] Center constraint glyph labels --- src/ui/overlay.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 85dccb70b..4e9b2fab7 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -1789,11 +1789,13 @@ impl canvas::Program for SelectionCanvas { frame.fill_text(canvas::Text { content: label.clone(), position: Point::new( - top_left.x + CONSTRAINT_GLYPH_PAD_X, - top_left.y + CONSTRAINT_GLYPH_PAD_Y, + top_left.x + size.width * 0.5, + top_left.y + size.height * 0.5, ), color: fg, size: iced::Pixels(CONSTRAINT_GLYPH_SIZE), + align_x: iced::alignment::Horizontal::Center.into(), + align_y: iced::alignment::Vertical::Center, shaping: iced::advanced::text::Shaping::Advanced, ..Default::default() }); From c3101919d7db6a6b5b471a6187bb5cde9d19bdc7 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Tue, 15 Sep 2026 13:54:47 +0300 Subject: [PATCH 2/3] Show constraint action points on hover --- src/app/view/mod.rs | 20 +++- src/scene/parametric_constraints.rs | 158 +++++++++++++++++++++++++++- src/ui/overlay.rs | 48 +++++++-- 3 files changed, 210 insertions(+), 16 deletions(-) diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 6730f4509..943119b3d 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -813,7 +813,14 @@ bg={bg_ms:.1}ms n={view_count}" .active_cmd .as_ref() .is_some_and(|cmd| !cmd.needs_entity_pick() && !cmd.is_selection_gathering()); - let constraint_glyphs: Vec<(iced::Point, [f32; 2], String, bool, bool)> = if is_paper { + let constraint_glyphs: Vec<( + iced::Point, + [f32; 2], + String, + bool, + bool, + Vec, + )> = if is_paper { Vec::new() } else { let scope = tab.current_parametric_scope(); @@ -825,9 +832,16 @@ bg={bg_ms:.1}ms n={view_count}" self.constraint_bar_display, ) .into_iter() - .map(|(id, point, direction, label, is_conflicting)| { + .map(|(id, point, direction, label, is_conflicting, hover_points)| { let selected = tab.scene.selected_constraint == Some(id); - (point, direction, label, is_conflicting, selected) + ( + point, + direction, + label, + is_conflicting, + selected, + hover_points, + ) }) .collect() }; diff --git a/src/scene/parametric_constraints.rs b/src/scene/parametric_constraints.rs index c5843fa8b..8ad6cffd6 100644 --- a/src/scene/parametric_constraints.rs +++ b/src/scene/parametric_constraints.rs @@ -606,6 +606,123 @@ pub(crate) fn glyph_placement( } } +/// World-space locations that explain what a hovered constraint acts on. +/// Point constraints expose their referenced point directly; curve relations +/// expose the contact or intersection that makes the relation visible. +fn constraint_segment_endpoints( + document: &acadrust::CadDocument, + reference: ParametricRef, +) -> Option<[Vector3; 2]> { + let segment = reference.segment_index()?; + let entity = document.get_entity(reference.entity)?; + let points = super::dimension_assoc::source_points(entity); + let closed = match entity { + acadrust::EntityType::LwPolyline(polyline) => polyline.is_closed, + acadrust::EntityType::Polyline2D(polyline) => polyline.is_closed(), + _ => return None, + }; + let first = *points.get(segment)?; + let second = if segment + 1 < points.len() { + points[segment + 1] + } else if closed { + *points.first()? + } else { + return None; + }; + Some([first, second]) +} + +fn constraint_reference_curve_xy( + document: &acadrust::CadDocument, + reference: ParametricRef, +) -> Option { + if let Some([first, second]) = constraint_segment_endpoints(document, reference) { + return Some(cadkernel::geom2d::Curve::Line(cadkernel::geom2d::Line { + start: [first.x, first.y], + end: [second.x, second.y], + })); + } + crate::entities::curve::entity_curve_xy(document.get_entity(reference.entity)?) +} + +pub(crate) fn constraint_hover_points( + document: &acadrust::CadDocument, + constraint: &ParametricConstraint, +) -> Vec { + let mut points = Vec::new(); + let push_unique = |points: &mut Vec, point: Vector3| { + if point.x.is_finite() + && point.y.is_finite() + && point.z.is_finite() + && !points + .iter() + .any(|existing| (*existing - point).length_squared() <= 1.0e-12) + { + points.push(point); + } + }; + + for reference in &constraint.refs { + let Some(marker) = reference.marker else { + continue; + }; + let Some(entity) = document.get_entity(reference.entity) else { + continue; + }; + if let Some(point) = resolve_point(entity, marker) { + push_unique(&mut points, point); + } + } + + if matches!( + constraint.kind, + ConstraintKind::Horizontal | ConstraintKind::Vertical + ) { + for reference in &constraint.refs { + let Some(entity) = document.get_entity(reference.entity) else { + continue; + }; + if let Some(endpoints) = constraint_segment_endpoints(document, *reference) { + for point in endpoints { + push_unique(&mut points, point); + } + } else if matches!(entity, acadrust::EntityType::Line(_)) { + for point in super::dimension_assoc::source_points(entity) { + push_unique(&mut points, point); + } + } + } + } + + if matches!( + constraint.kind, + ConstraintKind::Perpendicular | ConstraintKind::Tangent + ) { + if let [first, second, ..] = constraint.refs.as_slice() { + if let (Some(first_curve), Some(second_curve)) = ( + constraint_reference_curve_xy(document, *first), + constraint_reference_curve_xy(document, *second), + ) { + let elevation = glyph_placement(document, constraint) + .map(|(anchor, _)| anchor.z) + .unwrap_or(0.0); + for crossing in cadkernel::geom2d::intersect( + &first_curve, + &second_curve, + cadkernel::geom2d::Tolerance::default(), + ) { + push_unique( + &mut points, + Vector3::new(crossing.point[0], crossing.point[1], elevation), + ); + } + } + } + } + + points +} + impl super::Scene { pub fn is_parametric_constraint_visible( &self, @@ -964,8 +1081,9 @@ impl super::Scene { } /// Screen-projected parametric-constraint glyph placements for `scope`: - /// `(id, anchor, outward_screen_direction, label, is_conflicting)` for - /// every enabled, visible constraint whose glyph projects on-screen. + /// `(id, anchor, outward_screen_direction, label, is_conflicting, + /// hover_points)` for every enabled, visible constraint whose glyph + /// projects on-screen. /// `vp_size` is the full canvas size (as `SelectionState::vp_size` /// reports it), matching what `viewport_edit_frame`/ /// `active_model_tile_bounds` expect. Mirrors the projection @@ -980,7 +1098,14 @@ impl super::Scene { vp_size: (f32, f32), show_values: bool, display_mode: i16, - ) -> Vec<(ConstraintId, iced::Point, [f32; 2], String, bool)> { + ) -> Vec<( + ConstraintId, + iced::Point, + [f32; 2], + String, + bool, + Vec, + )> { let Some(set) = self.parametric_constraint_set(scope) else { return Vec::new(); }; @@ -1034,7 +1159,30 @@ impl super::Scene { } else { c.kind.glyph_symbol().to_string() }; - point.x.is_finite().then_some((c.id, point, direction.to_array(), label, is_conflicting)) + let hover_points = constraint_hover_points(&self.document, c) + .into_iter() + .filter_map(|hover_point| { + let projected = crate::scene::pick::grip::project_rte( + glam::DVec3::new(hover_point.x, hover_point.y, hover_point.z), + view_rot, + eye, + bounds, + )?; + let point = iced::Point::new( + bounds.x + projected.x, + bounds.y + projected.y, + ); + (point.x.is_finite() && point.y.is_finite()).then_some(point) + }) + .collect(); + point.x.is_finite().then_some(( + c.id, + point, + direction.to_array(), + label, + is_conflicting, + hover_points, + )) }) .collect() } @@ -1060,7 +1208,7 @@ impl super::Scene { ); let glyphs: Vec<(iced::Point, [f32; 2], String, bool)> = placements .iter() - .map(|(_, point, direction, label, is_conflicting)| { + .map(|(_, point, direction, label, is_conflicting, _)| { (*point, *direction, label.clone(), *is_conflicting) }) .collect(); diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 4e9b2fab7..40bde4606 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -27,6 +27,7 @@ const CONSTRAINT_GLYPH_PAD_X: f32 = 7.0; const CONSTRAINT_GLYPH_PAD_Y: f32 = 4.0; const CONSTRAINT_GLYPH_GAP: f32 = 6.0; const CONSTRAINT_GLYPH_ROW_GAP: f32 = 4.0; +const CONSTRAINT_HOVER_MARKER_RADIUS: f32 = 7.0; fn constraint_glyph_size(label: &str) -> Size { let w = label.chars().count() as f32 * CONSTRAINT_GLYPH_SIZE * 0.62 @@ -749,7 +750,7 @@ pub fn selection_overlay<'a>( crosshair_bg: [f32; 4], crosshair: CrosshairOptions, selection_visual: SelectionVisualOptions, - constraint_glyphs: Vec<(Point, [f32; 2], String, bool, bool)>, + constraint_glyphs: Vec<(Point, [f32; 2], String, bool, bool, Vec)>, ) -> Element<'a, Message> { canvas(SelectionCanvas { selection, @@ -833,9 +834,9 @@ struct SelectionCanvas { crosshair: CrosshairOptions, selection_visual: SelectionVisualOptions, /// Constraint glyph anchor, outward screen direction, label, conflict - /// state, and whether the pill itself is the current click-to-select - /// target (`Scene::selected_constraint`) — for the current scope. - constraint_glyphs: Vec<(Point, [f32; 2], String, bool, bool)>, + /// state, whether the pill itself is the current click-to-select target, + /// and the points to mark while the pill is hovered. + constraint_glyphs: Vec<(Point, [f32; 2], String, bool, bool, Vec)>, } fn draw_grip_marker( @@ -1021,7 +1022,7 @@ impl canvas::Program for SelectionCanvas { let glyphs: Vec<_> = self .constraint_glyphs .iter() - .map(|(anchor, outward, label, conflict, _)| { + .map(|(anchor, outward, label, conflict, _, _)| { (*anchor, *outward, label.clone(), *conflict) }) .collect(); @@ -1747,15 +1748,18 @@ impl canvas::Program for SelectionCanvas { if !self.constraint_glyphs.is_empty() { // `constraint_glyph_offsets` only needs the anchor/outward/label/ // conflict quadruple it was written against; project away the - // trailing `is_selected` flag rather than widen its signature. + // trailing display fields rather than widen its signature. let glyphs_for_offsets: Vec<(Point, [f32; 2], String, bool)> = self .constraint_glyphs .iter() - .map(|(anchor, outward, label, is_conflicting, _)| { + .map(|(anchor, outward, label, is_conflicting, _, _)| { (*anchor, *outward, label.clone(), *is_conflicting) }) .collect(); let offsets = constraint_glyph_offsets(&glyphs_for_offsets); + let hovered = cursor + .position_in(bounds) + .and_then(|point| constraint_glyph_hit_test(&glyphs_for_offsets, point)); 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 @@ -1765,7 +1769,7 @@ impl canvas::Program for SelectionCanvas { let conflict_bg = theme.palette().danger.base.color; let conflict_fg = theme.palette().danger.base.text; let selected_ring = theme.palette().primary.strong.color; - for ((anchor, outward, label, is_conflicting, is_selected), tangent_offset) in + for ((anchor, outward, label, is_conflicting, is_selected, _), tangent_offset) in self.constraint_glyphs.iter().zip(offsets) { if !anchor.x.is_finite() || !anchor.y.is_finite() { @@ -1800,6 +1804,34 @@ impl canvas::Program for SelectionCanvas { ..Default::default() }); } + if let Some(index) = hovered { + let red = Color::from_rgb(1.0, 0.0, 0.0); + let stroke = canvas::Stroke::default().with_color(red).with_width(1.5); + for point in &self.constraint_glyphs[index].5 { + let first = canvas::Path::line( + Point::new( + point.x - CONSTRAINT_HOVER_MARKER_RADIUS, + point.y - CONSTRAINT_HOVER_MARKER_RADIUS, + ), + Point::new( + point.x + CONSTRAINT_HOVER_MARKER_RADIUS, + point.y + CONSTRAINT_HOVER_MARKER_RADIUS, + ), + ); + let second = canvas::Path::line( + Point::new( + point.x - CONSTRAINT_HOVER_MARKER_RADIUS, + point.y + CONSTRAINT_HOVER_MARKER_RADIUS, + ), + Point::new( + point.x + CONSTRAINT_HOVER_MARKER_RADIUS, + point.y - CONSTRAINT_HOVER_MARKER_RADIUS, + ), + ); + frame.stroke(&first, stroke.clone()); + frame.stroke(&second, stroke.clone()); + } + } } // Small cross at each acquired tracking point. for ost in &self.ost_points { From 1d9fddbdd85a5c5356804fa81d2061f26ae46dbb Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Tue, 15 Sep 2026 14:14:41 +0300 Subject: [PATCH 3/3] Render tangent constraint glyph --- src/ui/overlay.rs | 47 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 40bde4606..eb9b38cfb 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -36,6 +36,22 @@ fn constraint_glyph_size(label: &str) -> Size { Size::new(w, h) } +fn draw_tangent_constraint_glyph( + frame: &mut canvas::Frame, + center: Point, + color: Color, +) { + let radius = 4.5; + let circle_center = Point::new(center.x - 0.3, center.y + 2.75); + let diagonal = radius * std::f32::consts::FRAC_1_SQRT_2; + let contact = Point::new(circle_center.x - diagonal, circle_center.y - diagonal); + let tangent_end = Point::new(contact.x + 8.0, contact.y - 8.0); + let stroke = canvas::Stroke::default().with_color(color).with_width(1.35); + + frame.stroke(&canvas::Path::circle(circle_center, radius), stroke.clone()); + frame.stroke(&canvas::Path::line(contact, tangent_end), stroke); +} + fn constraint_glyph_box( anchor: Point, outward: [f32; 2], @@ -1790,19 +1806,24 @@ impl canvas::Program for SelectionCanvas { canvas::Stroke::default().with_color(selected_ring).with_width(2.0), ); } - frame.fill_text(canvas::Text { - content: label.clone(), - position: Point::new( - top_left.x + size.width * 0.5, - top_left.y + size.height * 0.5, - ), - color: fg, - size: iced::Pixels(CONSTRAINT_GLYPH_SIZE), - align_x: iced::alignment::Horizontal::Center.into(), - align_y: iced::alignment::Vertical::Center, - shaping: iced::advanced::text::Shaping::Advanced, - ..Default::default() - }); + let glyph_center = Point::new( + top_left.x + size.width * 0.5, + top_left.y + size.height * 0.5, + ); + if label == "T" { + draw_tangent_constraint_glyph(frame, glyph_center, fg); + } else { + frame.fill_text(canvas::Text { + content: label.clone(), + position: glyph_center, + color: fg, + size: iced::Pixels(CONSTRAINT_GLYPH_SIZE), + align_x: iced::alignment::Horizontal::Center.into(), + align_y: iced::alignment::Vertical::Center, + shaping: iced::advanced::text::Shaping::Advanced, + ..Default::default() + }); + } } if let Some(index) = hovered { let red = Color::from_rgb(1.0, 0.0, 0.0);