forked from 1b-agent/opencadstudio-upstream
Fix constraint editing and glyph feedback
This commit is contained in:
1 parent
d3c12431af
commit
08bb17b0b8
8 files changed
+431
-47
No files matched your search
@@ -639,6 +639,9 @@ pub(super) struct OpenCADStudio {
|
|||||||
/// `HOVER_DWELL_MS`. Skipping the pick mid-stroke avoids the per-frame
|
/// `HOVER_DWELL_MS`. Skipping the pick mid-stroke avoids the per-frame
|
||||||
/// O(N) wire+hatch+mesh sweep that froze the cursor on large drawings.
|
/// O(N) wire+hatch+mesh sweep that froze the cursor on large drawings.
|
||||||
hover_dwell: Option<HoverDwell>,
|
hover_dwell: Option<HoverDwell>,
|
||||||
|
/// Constraint kind shown after the ordinary rollover dwell while the
|
||||||
|
/// cursor remains over one of its viewport indicators.
|
||||||
|
constraint_glyph_tooltip: Option<crate::scene::parametric_constraints::ConstraintKind>,
|
||||||
/// Snapshots of edited entities taken at the start of a grip drag. The drag
|
/// Snapshots of edited entities taken at the start of a grip drag. The drag
|
||||||
/// mutates the document live, so Escape restores this group atomically.
|
/// mutates the document live, so Escape restores this group atomically.
|
||||||
grip_originals: Vec<(acadrust::Handle, acadrust::EntityType)>,
|
grip_originals: Vec<(acadrust::Handle, acadrust::EntityType)>,
|
||||||
@@ -3689,6 +3692,7 @@ impl OpenCADStudio {
|
|||||||
grip_add_provisional: None,
|
grip_add_provisional: None,
|
||||||
grip_preview_handles: Vec::new(),
|
grip_preview_handles: Vec::new(),
|
||||||
hover_dwell: None,
|
hover_dwell: None,
|
||||||
|
constraint_glyph_tooltip: None,
|
||||||
grip_originals: Vec::new(),
|
grip_originals: Vec::new(),
|
||||||
grip_history_originals: Vec::new(),
|
grip_history_originals: Vec::new(),
|
||||||
grip_dirty_before: None,
|
grip_dirty_before: None,
|
||||||
|
|||||||
+120
-8
@@ -933,9 +933,10 @@ impl OpenCADStudio {
|
|||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
self.constraint_glyph_tooltip = None;
|
||||||
let constraint_hover = self
|
let constraint_hover = self
|
||||||
.constraint_glyph_under(i, p)
|
.constraint_glyph_under(i, p)
|
||||||
.map(|(_, handles)| handles)
|
.map(|(_, references)| references)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
@@ -1766,7 +1767,7 @@ impl OpenCADStudio {
|
|||||||
.solve_parametric_constraints_preview(
|
.solve_parametric_constraints_preview(
|
||||||
&edited_handles,
|
&edited_handles,
|
||||||
&driven_refs,
|
&driven_refs,
|
||||||
self.constraint_solve_mode && !driven_refs.is_empty(),
|
false,
|
||||||
&self.grip_originals,
|
&self.grip_originals,
|
||||||
);
|
);
|
||||||
for (handle, _) in &solved_by_constraints {
|
for (handle, _) in &solved_by_constraints {
|
||||||
@@ -2983,7 +2984,7 @@ impl OpenCADStudio {
|
|||||||
cursor: iced::Point,
|
cursor: iced::Point,
|
||||||
) -> Option<(
|
) -> Option<(
|
||||||
crate::scene::parametric_constraints::ConstraintKind,
|
crate::scene::parametric_constraints::ConstraintKind,
|
||||||
Vec<acadrust::Handle>,
|
Vec<crate::scene::parametric_constraints::ParametricRef>,
|
||||||
)> {
|
)> {
|
||||||
if self.tabs[i].scene.current_layout != "Model" {
|
if self.tabs[i].scene.current_layout != "Model" {
|
||||||
return None;
|
return None;
|
||||||
@@ -2999,10 +3000,7 @@ impl OpenCADStudio {
|
|||||||
)?;
|
)?;
|
||||||
let set = self.tabs[i].scene.parametric_constraint_set(scope)?;
|
let set = self.tabs[i].scene.parametric_constraint_set(scope)?;
|
||||||
let constraint = set.get(id)?;
|
let constraint = set.get(id)?;
|
||||||
let mut handles: Vec<_> = constraint.refs.iter().map(|r| r.entity).collect();
|
Some((constraint.kind, constraint.refs.clone()))
|
||||||
handles.sort_unstable_by_key(|handle| handle.value());
|
|
||||||
handles.dedup();
|
|
||||||
Some((constraint.kind, handles))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn on_viewport_left_press(&mut self) -> Task<Message> {
|
pub(super) fn on_viewport_left_press(&mut self) -> Task<Message> {
|
||||||
@@ -3061,7 +3059,10 @@ impl OpenCADStudio {
|
|||||||
let (vw, vh) = vp_size;
|
let (vw, vh) = vp_size;
|
||||||
|
|
||||||
if self.tabs[i].active_cmd.is_none() {
|
if self.tabs[i].active_cmd.is_none() {
|
||||||
if let Some((kind, handles)) = self.constraint_glyph_under(i, p) {
|
if let Some((kind, references)) = self.constraint_glyph_under(i, p) {
|
||||||
|
let mut handles: Vec<_> = references.iter().map(|reference| reference.entity).collect();
|
||||||
|
handles.sort_unstable_by_key(|handle| handle.value());
|
||||||
|
handles.dedup();
|
||||||
self.tabs[i].scene.deselect_all();
|
self.tabs[i].scene.deselect_all();
|
||||||
self.tabs[i].scene.select_entities(&handles);
|
self.tabs[i].scene.select_entities(&handles);
|
||||||
self.refresh_selected_grips();
|
self.refresh_selected_grips();
|
||||||
@@ -5179,6 +5180,7 @@ properties={:.1}ms picked={}",
|
|||||||
}
|
}
|
||||||
self.tabs[i].scene.set_hover_highlight(None);
|
self.tabs[i].scene.set_hover_highlight(None);
|
||||||
self.hover_dwell = None;
|
self.hover_dwell = None;
|
||||||
|
self.constraint_glyph_tooltip = None;
|
||||||
self.grip_hover = None;
|
self.grip_hover = None;
|
||||||
self.ucs_icon_hover = false;
|
self.ucs_icon_hover = false;
|
||||||
}
|
}
|
||||||
@@ -5500,6 +5502,10 @@ properties={:.1}ms picked={}",
|
|||||||
self.clear_navigation_hover(i);
|
self.clear_navigation_hover(i);
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
let cursor = self.tabs[i].scene.selection.borrow().last_move_pos;
|
||||||
|
self.constraint_glyph_tooltip = cursor.and_then(|point| {
|
||||||
|
self.constraint_glyph_under(i, point).map(|(kind, _)| kind)
|
||||||
|
});
|
||||||
let bounds = iced::Rectangle {
|
let bounds = iced::Rectangle {
|
||||||
x: 0.0,
|
x: 0.0,
|
||||||
y: 0.0,
|
y: 0.0,
|
||||||
@@ -6135,6 +6141,12 @@ mod selection_preview_tests {
|
|||||||
current[1], before[1],
|
current[1], before[1],
|
||||||
"constrained neighbor must follow each grip frame"
|
"constrained neighbor must follow each grip frame"
|
||||||
);
|
);
|
||||||
|
let start_delta = current[0][0] - before[0][0];
|
||||||
|
let end_delta = current[0][1] - before[0][1];
|
||||||
|
assert!(
|
||||||
|
(start_delta + end_delta).length_squared() > 1.0e-8,
|
||||||
|
"the opposite endpoint must not mirror the dragged endpoint around a fixed midpoint"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let _ = app.on_viewport_left_release();
|
let _ = app.on_viewport_left_release();
|
||||||
assert!(app.tabs[i].active_grip.is_none());
|
assert!(app.tabs[i].active_grip.is_none());
|
||||||
@@ -6145,6 +6157,106 @@ mod selection_preview_tests {
|
|||||||
assert_eq!(geometry(&app), after);
|
assert_eq!(geometry(&app), after);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn constrained_polyline_vertex_grip_reshapes_instead_of_translating() {
|
||||||
|
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef, ParametricScope};
|
||||||
|
use acadrust::{entities::LwPolyline, types::Vector2, EntityType};
|
||||||
|
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let i = app.active_tab;
|
||||||
|
app.snapper.snap_enabled = false;
|
||||||
|
app.snapper.grid_snap_on = false;
|
||||||
|
app.snapper.otrack_enabled = false;
|
||||||
|
app.ortho_mode = false;
|
||||||
|
app.polar_mode = false;
|
||||||
|
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
|
||||||
|
let mut rectangle = LwPolyline::from_points(vec![
|
||||||
|
Vector2::new(0.0, 0.0),
|
||||||
|
Vector2::new(4.0, 0.0),
|
||||||
|
Vector2::new(4.0, 2.0),
|
||||||
|
Vector2::new(0.0, 2.0),
|
||||||
|
]);
|
||||||
|
rectangle.is_closed = true;
|
||||||
|
let handle = app.tabs[i].scene.add_entity(EntityType::LwPolyline(rectangle));
|
||||||
|
let set = app.tabs[i].scene.parametric_constraint_set_mut(ParametricScope::ModelSpace);
|
||||||
|
set.add(ConstraintKind::Parallel, vec![ParametricRef::segment(handle, 0), ParametricRef::segment(handle, 2)], None);
|
||||||
|
set.add(ConstraintKind::Parallel, vec![ParametricRef::segment(handle, 1), ParametricRef::segment(handle, 3)], None);
|
||||||
|
set.add(ConstraintKind::Perpendicular, vec![ParametricRef::segment(handle, 3), ParametricRef::segment(handle, 2)], None);
|
||||||
|
set.add(ConstraintKind::Horizontal, vec![ParametricRef::segment(handle, 2)], None);
|
||||||
|
let vertices = |app: &OpenCADStudio| {
|
||||||
|
let EntityType::LwPolyline(polyline) =
|
||||||
|
app.tabs[i].scene.document.get_entity(handle).unwrap()
|
||||||
|
else {
|
||||||
|
panic!("expected polyline")
|
||||||
|
};
|
||||||
|
polyline.vertices.iter().map(|vertex| vertex.location).collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
let before = vertices(&app);
|
||||||
|
app.tabs[i].active_grip = Some(GripEdit::single(handle, 0, false, glam::DVec3::ZERO));
|
||||||
|
|
||||||
|
let _ = app.on_viewport_move(Point::new(460.0, 220.0));
|
||||||
|
|
||||||
|
let after = vertices(&app);
|
||||||
|
let dragged = after[0] - before[0];
|
||||||
|
assert!(dragged.length_squared() > 1.0e-12);
|
||||||
|
assert!(after.iter().zip(&before).skip(1).any(|(after, before)|
|
||||||
|
(*after - *before - dragged).length_squared() > 1.0e-12));
|
||||||
|
let segments = [
|
||||||
|
after[1] - after[0],
|
||||||
|
after[2] - after[1],
|
||||||
|
after[3] - after[2],
|
||||||
|
after[0] - after[3],
|
||||||
|
];
|
||||||
|
let cross = |a: Vector2, b: Vector2| a.x * b.y - a.y * b.x;
|
||||||
|
let dot = |a: Vector2, b: Vector2| a.x * b.x + a.y * b.y;
|
||||||
|
assert!(cross(segments[0], segments[2]).abs() < 1.0e-6);
|
||||||
|
assert!(cross(segments[1], segments[3]).abs() < 1.0e-6);
|
||||||
|
assert!(dot(segments[3], segments[2]).abs() < 1.0e-6);
|
||||||
|
assert!(segments[2].y.abs() < 1.0e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn constraint_glyph_tooltip_appears_after_hover_dwell() {
|
||||||
|
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef, ParametricScope};
|
||||||
|
use acadrust::{entities::Line, types::Vector3, EntityType};
|
||||||
|
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let i = app.active_tab;
|
||||||
|
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
|
||||||
|
let handle = app.tabs[i].scene.add_entity(EntityType::Line(Line::from_points(
|
||||||
|
Vector3::new(-1.0, 0.0, 0.0),
|
||||||
|
Vector3::new(1.0, 0.0, 0.0),
|
||||||
|
)));
|
||||||
|
app.tabs[i].scene.parametric_constraint_set_mut(ParametricScope::ModelSpace).add(
|
||||||
|
ConstraintKind::Horizontal,
|
||||||
|
vec![ParametricRef::whole(handle)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let anchor = app.tabs[i].scene.constraint_glyph_placements_screen(
|
||||||
|
ParametricScope::ModelSpace,
|
||||||
|
(800.0, 600.0),
|
||||||
|
true,
|
||||||
|
3,
|
||||||
|
)[0].1;
|
||||||
|
let point = (-30..=30).find_map(|y| {
|
||||||
|
(-30..=30).find_map(|x| {
|
||||||
|
let point = Point::new(anchor.x + x as f32, anchor.y + y as f32);
|
||||||
|
app.constraint_glyph_under(i, point).is_some().then_some(point)
|
||||||
|
})
|
||||||
|
}).expect("projected constraint glyph should be hit-testable");
|
||||||
|
|
||||||
|
let _ = app.on_viewport_move(point);
|
||||||
|
assert_eq!(app.constraint_glyph_tooltip, None);
|
||||||
|
app.hover_dwell.as_mut().unwrap().last_move_at =
|
||||||
|
Instant::now()
|
||||||
|
- std::time::Duration::from_millis(crate::app::HOVER_DWELL_MS as u64 + 1);
|
||||||
|
let _ = app.on_hover_dwell_tick();
|
||||||
|
|
||||||
|
assert_eq!(app.constraint_glyph_tooltip, Some(ConstraintKind::Horizontal));
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
#[test]
|
#[test]
|
||||||
fn hyperlink_click_respects_existing_pointer_owners() {
|
fn hyperlink_click_respects_existing_pointer_owners() {
|
||||||
|
|||||||
@@ -888,6 +888,8 @@ bg={bg_ms:.1}ms n={view_count}"
|
|||||||
grip_hover: self.model_space.grip_hover,
|
grip_hover: self.model_space.grip_hover,
|
||||||
},
|
},
|
||||||
constraint_glyphs,
|
constraint_glyphs,
|
||||||
|
self.constraint_glyph_tooltip
|
||||||
|
.map(|kind| crate::t!(kind.label()).into_owned()),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+47
-5
@@ -1683,6 +1683,10 @@ pub struct Scene {
|
|||||||
pub hover_highlight: Option<Handle>,
|
pub hover_highlight: Option<Handle>,
|
||||||
/// All entities related to the constraint indicator currently under the cursor.
|
/// All entities related to the constraint indicator currently under the cursor.
|
||||||
constraint_hover_highlights: HashSet<Handle>,
|
constraint_hover_highlights: HashSet<Handle>,
|
||||||
|
/// Sub-entity references related to that indicator. Polyline segments are
|
||||||
|
/// drawn separately so hovering one constraint does not light the whole chain.
|
||||||
|
constraint_hover_refs: Vec<crate::scene::parametric_constraints::ParametricRef>,
|
||||||
|
constraint_hover_wires: Vec<WireModel>,
|
||||||
/// Color used to draw the selection highlight overlay wires (SELECTIONEFFECTCOLOR).
|
/// Color used to draw the selection highlight overlay wires (SELECTIONEFFECTCOLOR).
|
||||||
pub selection_color: [f32; 4],
|
pub selection_color: [f32; 4],
|
||||||
/// Whether selection visual effect (glow/highlight) is enabled (SELECTIONEFFECT).
|
/// Whether selection visual effect (glow/highlight) is enabled (SELECTIONEFFECT).
|
||||||
@@ -2213,6 +2217,8 @@ impl Scene {
|
|||||||
refedit_keep: None,
|
refedit_keep: None,
|
||||||
hover_highlight: None,
|
hover_highlight: None,
|
||||||
constraint_hover_highlights: HashSet::default(),
|
constraint_hover_highlights: HashSet::default(),
|
||||||
|
constraint_hover_refs: Vec::new(),
|
||||||
|
constraint_hover_wires: Vec::new(),
|
||||||
selection_color: crate::scene::model::wire_model::WireModel::SELECTED,
|
selection_color: crate::scene::model::wire_model::WireModel::SELECTED,
|
||||||
selection_effect: true,
|
selection_effect: true,
|
||||||
transparency_display: true,
|
transparency_display: true,
|
||||||
@@ -5124,12 +5130,48 @@ impl Scene {
|
|||||||
handles
|
handles
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_constraint_hover_highlights(&mut self, handles: &[Handle]) {
|
pub fn set_constraint_hover_highlights(
|
||||||
let desired: HashSet<_> = handles.iter().copied().collect();
|
&mut self,
|
||||||
if desired != self.constraint_hover_highlights {
|
references: &[crate::scene::parametric_constraints::ParametricRef],
|
||||||
self.constraint_hover_highlights = desired;
|
) {
|
||||||
self.bump_selection();
|
let mut references = references.to_vec();
|
||||||
|
references.dedup();
|
||||||
|
if references == self.constraint_hover_refs {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut handles = HashSet::default();
|
||||||
|
let mut wires = Vec::new();
|
||||||
|
for reference in &references {
|
||||||
|
let Some(segment) = reference.segment_index() else {
|
||||||
|
handles.insert(reference.entity);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(entity) = self.document.get_entity(reference.entity) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(mut curve) = crate::entities::curve::entity_curve(entity) else {
|
||||||
|
handles.insert(reference.entity);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(segment_curve) = curve.curve.segments().into_iter().nth(segment) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
curve.curve = segment_curve;
|
||||||
|
let mut wire = WireModel::solid_f64(
|
||||||
|
format!("constraint-segment:{}:{segment}", reference.entity.value()),
|
||||||
|
curve.tessellate(8.0),
|
||||||
|
WireModel::HOVER,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
wire.set_fixed_screen_width(2.0);
|
||||||
|
wires.push(wire);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.constraint_hover_refs = references;
|
||||||
|
self.constraint_hover_highlights = handles;
|
||||||
|
self.constraint_hover_wires = wires;
|
||||||
|
self.bump_selection();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Keep the current selection visible and temporarily filter every other
|
/// Keep the current selection visible and temporarily filter every other
|
||||||
|
|||||||
@@ -575,6 +575,37 @@ pub(crate) fn parametric_curve_ref_for_pick(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ConstraintKind {
|
impl ConstraintKind {
|
||||||
|
pub const fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Coincident => "Coincident",
|
||||||
|
Self::Horizontal => "Horizontal",
|
||||||
|
Self::Vertical => "Vertical",
|
||||||
|
Self::Parallel => "Parallel",
|
||||||
|
Self::Perpendicular => "Perpendicular",
|
||||||
|
Self::Equal => "Equal",
|
||||||
|
Self::Distance => "Distance",
|
||||||
|
Self::Angle => "Angle",
|
||||||
|
Self::Angle3Point => "3-point angle",
|
||||||
|
Self::Radius => "Radius",
|
||||||
|
Self::Tangent => "Tangent",
|
||||||
|
Self::Smooth => "Smooth",
|
||||||
|
Self::Concentric => "Concentric",
|
||||||
|
Self::CenterPoint => "Center point",
|
||||||
|
Self::Colinear => "Collinear",
|
||||||
|
Self::Midpoint => "Midpoint",
|
||||||
|
Self::Fixed => "Fixed",
|
||||||
|
Self::PointOnCurve => "Point on curve",
|
||||||
|
Self::EqualDistance => "Equal distance",
|
||||||
|
Self::Symmetric => "Symmetric",
|
||||||
|
Self::Diameter => "Diameter",
|
||||||
|
Self::DistanceX => "Horizontal distance",
|
||||||
|
Self::DistanceY => "Vertical distance",
|
||||||
|
Self::DistanceDirected => "Directed distance",
|
||||||
|
Self::Normal => "Normal",
|
||||||
|
Self::RigidSet => "Rigid set",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The short symbol a constraint glyph shows — matches the existing
|
/// The short symbol a constraint glyph shows — matches the existing
|
||||||
/// ribbon icons (`crate::modules::parametric::{tools,value}`) for
|
/// ribbon icons (`crate::modules::parametric::{tools,value}`) for
|
||||||
/// the kinds that have a one-click button, so the same glyph means the
|
/// the kinds that have a one-click button, so the same glyph means the
|
||||||
@@ -640,7 +671,13 @@ pub(crate) fn glyph_placement(
|
|||||||
document: &acadrust::CadDocument,
|
document: &acadrust::CadDocument,
|
||||||
constraint: &ParametricConstraint,
|
constraint: &ParametricConstraint,
|
||||||
) -> Option<(Vector3, Vector3)> {
|
) -> Option<(Vector3, Vector3)> {
|
||||||
let r = constraint.refs.first()?;
|
glyph_placements(document, constraint).into_iter().next()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn glyph_placement_for_reference(
|
||||||
|
document: &acadrust::CadDocument,
|
||||||
|
r: ParametricRef,
|
||||||
|
) -> Option<(Vector3, Vector3)> {
|
||||||
let entity = document.get_entity(r.entity)?;
|
let entity = document.get_entity(r.entity)?;
|
||||||
let line_midpoint = |line: &acadrust::entities::Line| {
|
let line_midpoint = |line: &acadrust::entities::Line| {
|
||||||
Vector3::new(
|
Vector3::new(
|
||||||
@@ -661,7 +698,7 @@ pub(crate) fn glyph_placement(
|
|||||||
entity,
|
entity,
|
||||||
POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE - segment as i32,
|
POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE - segment as i32,
|
||||||
)?;
|
)?;
|
||||||
let [start, end] = constraint_segment_endpoints(document, *r)?;
|
let [start, end] = constraint_segment_endpoints(document, r)?;
|
||||||
return Some((anchor, segment_normal(start, end)));
|
return Some((anchor, segment_normal(start, end)));
|
||||||
}
|
}
|
||||||
match (entity, r.marker) {
|
match (entity, r.marker) {
|
||||||
@@ -704,6 +741,52 @@ pub(crate) fn glyph_placement(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn glyph_placements(
|
||||||
|
document: &acadrust::CadDocument,
|
||||||
|
constraint: &ParametricConstraint,
|
||||||
|
) -> Vec<(Vector3, Vector3)> {
|
||||||
|
if constraint.kind == ConstraintKind::Parallel {
|
||||||
|
return constraint
|
||||||
|
.refs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|reference| glyph_placement_for_reference(document, *reference))
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(first) = constraint.refs.first().copied() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let fallback = glyph_placement_for_reference(document, first);
|
||||||
|
if matches!(
|
||||||
|
constraint.kind,
|
||||||
|
ConstraintKind::Perpendicular | ConstraintKind::Tangent
|
||||||
|
) {
|
||||||
|
if let [first, second, ..] = constraint.refs.as_slice() {
|
||||||
|
if let (Some(first_curve), Some(second_curve), Some((anchor, outward))) = (
|
||||||
|
constraint_reference_curve_xy(document, *first),
|
||||||
|
constraint_reference_curve_xy(document, *second),
|
||||||
|
fallback,
|
||||||
|
) {
|
||||||
|
if let Some(crossing) = cadkernel::geom2d::intersect(
|
||||||
|
&first_curve,
|
||||||
|
&second_curve,
|
||||||
|
cadkernel::geom2d::Tolerance::default(),
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
{
|
||||||
|
return vec![(
|
||||||
|
Vector3::new(crossing.point[0], crossing.point[1], anchor.z),
|
||||||
|
outward,
|
||||||
|
)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fallback.into_iter().collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// World-space locations that explain what a hovered constraint acts on.
|
/// World-space locations that explain what a hovered constraint acts on.
|
||||||
/// Point constraints expose their referenced point directly; curve relations
|
/// Point constraints expose their referenced point directly; curve relations
|
||||||
/// expose the contact or intersection that makes the relation visible.
|
/// expose the contact or intersection that makes the relation visible.
|
||||||
@@ -1334,33 +1417,14 @@ impl super::Scene {
|
|||||||
.any(|reference| self.selected.contains(&reference.entity));
|
.any(|reference| self.selected.contains(&reference.entity));
|
||||||
self.should_display_parametric_constraint(scope, c.id, selected, display_mode)
|
self.should_display_parametric_constraint(scope, c.id, selected, display_mode)
|
||||||
})
|
})
|
||||||
.filter_map(|c| {
|
.flat_map(|c| {
|
||||||
let (anchor, outward) = glyph_placement(&self.document, c)?;
|
|
||||||
let screen = crate::scene::pick::grip::project_rte(
|
|
||||||
glam::DVec3::new(anchor.x, anchor.y, anchor.z),
|
|
||||||
view_rot,
|
|
||||||
eye,
|
|
||||||
bounds,
|
|
||||||
)?;
|
|
||||||
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 is_conflicting = set.conflicts.iter().any(|(id, _)| *id == c.id);
|
||||||
let label = if show_values {
|
let label = if show_values {
|
||||||
glyph_label(c)
|
glyph_label(c)
|
||||||
} else {
|
} else {
|
||||||
c.kind.glyph_symbol().to_string()
|
c.kind.glyph_symbol().to_string()
|
||||||
};
|
};
|
||||||
let hover_points = constraint_hover_points(&self.document, c)
|
let hover_points: Vec<iced::Point> = constraint_hover_points(&self.document, c)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|hover_point| {
|
.filter_map(|hover_point| {
|
||||||
let projected = crate::scene::pick::grip::project_rte(
|
let projected = crate::scene::pick::grip::project_rte(
|
||||||
@@ -1376,14 +1440,38 @@ impl super::Scene {
|
|||||||
(point.x.is_finite() && point.y.is_finite()).then_some(point)
|
(point.x.is_finite() && point.y.is_finite()).then_some(point)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
point.x.is_finite().then_some((
|
glyph_placements(&self.document, c)
|
||||||
c.id,
|
.into_iter()
|
||||||
point,
|
.filter_map(|(anchor, outward)| {
|
||||||
direction.to_array(),
|
let screen = crate::scene::pick::grip::project_rte(
|
||||||
label,
|
glam::DVec3::new(anchor.x, anchor.y, anchor.z),
|
||||||
is_conflicting,
|
view_rot,
|
||||||
hover_points,
|
eye,
|
||||||
))
|
bounds,
|
||||||
|
)?;
|
||||||
|
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);
|
||||||
|
point.x.is_finite().then(|| (
|
||||||
|
c.id,
|
||||||
|
point,
|
||||||
|
direction.to_array(),
|
||||||
|
label.clone(),
|
||||||
|
is_conflicting,
|
||||||
|
hover_points.clone(),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -1580,6 +1668,88 @@ mod tests {
|
|||||||
glyph_placement(&document, &constraint(ParametricRef::segment(h(3), 0))).unwrap();
|
glyph_placement(&document, &constraint(ParametricRef::segment(h(3), 0))).unwrap();
|
||||||
assert_eq!(anchor, Vector3::new(5.0, 0.0, 0.0));
|
assert_eq!(anchor, Vector3::new(5.0, 0.0, 0.0));
|
||||||
assert_eq!(direction, Vector3::new(0.0, 10.0, 0.0));
|
assert_eq!(direction, Vector3::new(0.0, 10.0, 0.0));
|
||||||
|
|
||||||
|
let mut tangent_line = acadrust::entities::Line::from_points(
|
||||||
|
Vector3::new(-10.0, 5.0, 0.0),
|
||||||
|
Vector3::new(10.0, 5.0, 0.0),
|
||||||
|
);
|
||||||
|
tangent_line.common.handle = h(4);
|
||||||
|
document.add_entity(acadrust::EntityType::Line(tangent_line)).unwrap();
|
||||||
|
let relation = |id, kind, refs| ParametricConstraint {
|
||||||
|
id,
|
||||||
|
kind,
|
||||||
|
refs,
|
||||||
|
driving_param: None,
|
||||||
|
enabled: true,
|
||||||
|
native_origin: None,
|
||||||
|
rigid_points: Vec::new(),
|
||||||
|
distance_direction_type: 0,
|
||||||
|
distance_direction: None,
|
||||||
|
angle_sector: angle_sector::PARALLEL_COUNTERCLOCKWISE,
|
||||||
|
};
|
||||||
|
let tangent = relation(
|
||||||
|
1,
|
||||||
|
ConstraintKind::Tangent,
|
||||||
|
vec![ParametricRef::whole(h(4)), ParametricRef::whole(h(2))],
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
glyph_placement(&document, &tangent).unwrap().0,
|
||||||
|
Vector3::new(0.0, 5.0, 0.0)
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut rectangle = acadrust::entities::LwPolyline::from_points(vec![
|
||||||
|
acadrust::types::Vector2::new(0.0, 0.0),
|
||||||
|
acadrust::types::Vector2::new(4.0, 0.0),
|
||||||
|
acadrust::types::Vector2::new(4.0, 2.0),
|
||||||
|
acadrust::types::Vector2::new(0.0, 2.0),
|
||||||
|
]);
|
||||||
|
rectangle.common.handle = h(5);
|
||||||
|
rectangle.is_closed = true;
|
||||||
|
document.add_entity(acadrust::EntityType::LwPolyline(rectangle)).unwrap();
|
||||||
|
let parallel = glyph_placements(
|
||||||
|
&document,
|
||||||
|
&relation(
|
||||||
|
2,
|
||||||
|
ConstraintKind::Parallel,
|
||||||
|
vec![ParametricRef::segment(h(5), 0), ParametricRef::segment(h(5), 2)],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert_eq!(parallel.len(), 2);
|
||||||
|
assert_eq!(parallel[0].0, Vector3::new(2.0, 0.0, 0.0));
|
||||||
|
assert_eq!(parallel[1].0, Vector3::new(2.0, 2.0, 0.0));
|
||||||
|
let perpendicular = relation(
|
||||||
|
3,
|
||||||
|
ConstraintKind::Perpendicular,
|
||||||
|
vec![ParametricRef::segment(h(5), 3), ParametricRef::segment(h(5), 2)],
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
glyph_placement(&document, &perpendicular).unwrap().0,
|
||||||
|
Vector3::new(0.0, 2.0, 0.0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn polyline_constraint_hover_builds_only_the_referenced_segment() {
|
||||||
|
let mut scene = super::super::Scene::new();
|
||||||
|
let handle = scene.add_entity(acadrust::EntityType::LwPolyline(
|
||||||
|
acadrust::entities::LwPolyline::from_points(vec![
|
||||||
|
acadrust::types::Vector2::new(0.0, 0.0),
|
||||||
|
acadrust::types::Vector2::new(4.0, 0.0),
|
||||||
|
acadrust::types::Vector2::new(4.0, 2.0),
|
||||||
|
]),
|
||||||
|
));
|
||||||
|
|
||||||
|
scene.set_constraint_hover_highlights(&[ParametricRef::segment(handle, 1)]);
|
||||||
|
|
||||||
|
assert!(scene.constraint_hover_highlights.is_empty());
|
||||||
|
assert_eq!(scene.constraint_hover_wires.len(), 1);
|
||||||
|
let wire = &scene.constraint_hover_wires[0];
|
||||||
|
let points: Vec<_> = wire.points.iter().zip(&wire.points_low).map(|(high, low)| [
|
||||||
|
high[0] as f64 + low[0] as f64,
|
||||||
|
high[1] as f64 + low[1] as f64,
|
||||||
|
high[2] as f64 + low[2] as f64,
|
||||||
|
]).collect();
|
||||||
|
assert_eq!(points, vec![[4.0, 0.0, 0.0], [4.0, 2.0, 0.0]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1008,6 +1008,16 @@ impl Scene {
|
|||||||
hover_changed = true;
|
hover_changed = true;
|
||||||
}
|
}
|
||||||
hover_changed |= self.constraint_hover_highlights.remove(&h);
|
hover_changed |= self.constraint_hover_highlights.remove(&h);
|
||||||
|
if self
|
||||||
|
.constraint_hover_refs
|
||||||
|
.iter()
|
||||||
|
.any(|reference| reference.entity == h)
|
||||||
|
{
|
||||||
|
self.constraint_hover_refs.clear();
|
||||||
|
self.constraint_hover_wires.clear();
|
||||||
|
self.constraint_hover_highlights.clear();
|
||||||
|
hover_changed = true;
|
||||||
|
}
|
||||||
self.hatches.remove(&h);
|
self.hatches.remove(&h);
|
||||||
self.images.remove(&h);
|
self.images.remove(&h);
|
||||||
self.meshes.remove(&h);
|
self.meshes.remove(&h);
|
||||||
|
|||||||
@@ -4254,15 +4254,20 @@ impl Scene {
|
|||||||
Arc::new(Vec::new())
|
Arc::new(Vec::new())
|
||||||
};
|
};
|
||||||
let preview_wires = if !show_live_overlay
|
let preview_wires = if !show_live_overlay
|
||||||
|| (self.interim_wire.is_none() && self.preview_wires.is_empty())
|
|| (self.interim_wire.is_none()
|
||||||
|
&& self.preview_wires.is_empty()
|
||||||
|
&& self.constraint_hover_wires.is_empty())
|
||||||
{
|
{
|
||||||
Arc::new(Vec::new())
|
Arc::new(Vec::new())
|
||||||
} else {
|
} else {
|
||||||
let mut v: Vec<WireModel> = Vec::with_capacity(self.preview_wires.len() + 1);
|
let mut v: Vec<WireModel> = Vec::with_capacity(
|
||||||
|
self.preview_wires.len() + self.constraint_hover_wires.len() + 1,
|
||||||
|
);
|
||||||
if let Some(iw) = &self.interim_wire {
|
if let Some(iw) = &self.interim_wire {
|
||||||
v.push(iw.clone());
|
v.push(iw.clone());
|
||||||
}
|
}
|
||||||
v.extend(self.preview_wires.iter().cloned());
|
v.extend(self.preview_wires.iter().cloned());
|
||||||
|
v.extend(self.constraint_hover_wires.iter().cloned());
|
||||||
Arc::new(v)
|
Arc::new(v)
|
||||||
};
|
};
|
||||||
let preview_hatches = if show_live_overlay {
|
let preview_hatches = if show_live_overlay {
|
||||||
|
|||||||
+40
-1
@@ -767,6 +767,7 @@ pub fn selection_overlay<'a>(
|
|||||||
crosshair: CrosshairOptions,
|
crosshair: CrosshairOptions,
|
||||||
selection_visual: SelectionVisualOptions,
|
selection_visual: SelectionVisualOptions,
|
||||||
constraint_glyphs: Vec<(Point, [f32; 2], String, bool, bool, Vec<Point>)>,
|
constraint_glyphs: Vec<(Point, [f32; 2], String, bool, bool, Vec<Point>)>,
|
||||||
|
constraint_glyph_tooltip: Option<String>,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
canvas(SelectionCanvas {
|
canvas(SelectionCanvas {
|
||||||
selection,
|
selection,
|
||||||
@@ -791,6 +792,7 @@ pub fn selection_overlay<'a>(
|
|||||||
crosshair,
|
crosshair,
|
||||||
selection_visual,
|
selection_visual,
|
||||||
constraint_glyphs,
|
constraint_glyphs,
|
||||||
|
constraint_glyph_tooltip,
|
||||||
})
|
})
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
@@ -853,6 +855,8 @@ struct SelectionCanvas {
|
|||||||
/// state, whether the pill itself is the current click-to-select target,
|
/// state, whether the pill itself is the current click-to-select target,
|
||||||
/// and the points to mark while the pill is hovered.
|
/// and the points to mark while the pill is hovered.
|
||||||
constraint_glyphs: Vec<(Point, [f32; 2], String, bool, bool, Vec<Point>)>,
|
constraint_glyphs: Vec<(Point, [f32; 2], String, bool, bool, Vec<Point>)>,
|
||||||
|
/// Localized kind name made visible after the app-level hover dwell.
|
||||||
|
constraint_glyph_tooltip: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_grip_marker(
|
fn draw_grip_marker(
|
||||||
@@ -1786,7 +1790,7 @@ impl canvas::Program<Message> for SelectionCanvas {
|
|||||||
let conflict_fg = theme.palette().danger.base.text;
|
let conflict_fg = theme.palette().danger.base.text;
|
||||||
let selected_ring = theme.palette().primary.strong.color;
|
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)
|
self.constraint_glyphs.iter().zip(offsets.iter().copied())
|
||||||
{
|
{
|
||||||
if !anchor.x.is_finite() || !anchor.y.is_finite() {
|
if !anchor.x.is_finite() || !anchor.y.is_finite() {
|
||||||
continue;
|
continue;
|
||||||
@@ -1852,6 +1856,41 @@ impl canvas::Program<Message> for SelectionCanvas {
|
|||||||
frame.stroke(&first, stroke.clone());
|
frame.stroke(&first, stroke.clone());
|
||||||
frame.stroke(&second, stroke.clone());
|
frame.stroke(&second, stroke.clone());
|
||||||
}
|
}
|
||||||
|
if let Some(label) = &self.constraint_glyph_tooltip {
|
||||||
|
let (glyph_top_left, glyph_size) = constraint_glyph_box(
|
||||||
|
self.constraint_glyphs[index].0,
|
||||||
|
self.constraint_glyphs[index].1,
|
||||||
|
&self.constraint_glyphs[index].2,
|
||||||
|
offsets[index],
|
||||||
|
);
|
||||||
|
let width = (label.chars().count() as f32 * 7.0 + 14.0).max(54.0);
|
||||||
|
let height = 24.0;
|
||||||
|
let left = glyph_top_left.x.clamp(2.0, (bounds.width - width - 2.0).max(2.0));
|
||||||
|
let top = (glyph_top_left.y + glyph_size.height + 4.0)
|
||||||
|
.clamp(2.0, (bounds.height - height - 2.0).max(2.0));
|
||||||
|
let tooltip = canvas::Path::rounded_rectangle(
|
||||||
|
Point::new(left, top),
|
||||||
|
Size::new(width, height),
|
||||||
|
4.0.into(),
|
||||||
|
);
|
||||||
|
frame.fill(&tooltip, theme.palette().background.strong.color);
|
||||||
|
frame.stroke(
|
||||||
|
&tooltip,
|
||||||
|
canvas::Stroke::default()
|
||||||
|
.with_color(theme.palette().background.strong.text)
|
||||||
|
.with_width(1.0),
|
||||||
|
);
|
||||||
|
frame.fill_text(canvas::Text {
|
||||||
|
content: label.clone(),
|
||||||
|
position: Point::new(left + width * 0.5, top + height * 0.5),
|
||||||
|
color: theme.palette().background.strong.text,
|
||||||
|
size: iced::Pixels(12.0),
|
||||||
|
align_x: iced::alignment::Horizontal::Center.into(),
|
||||||
|
align_y: iced::alignment::Vertical::Center,
|
||||||
|
shaping: iced::advanced::text::Shaping::Advanced,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Small cross at each acquired tracking point.
|
// Small cross at each acquired tracking point.
|
||||||
|
|||||||
Reference in new issue
Block a user