Integrate upstream constraint editing fixes with viewport dimensions

This commit is contained in:
Christopher Overbeck committed 2026-09-15 14:56:00 -05:00
commit 71aae72219
9 files changed
+485 -93

No files matched your search

+4
View File
@@ -642,6 +642,9 @@ pub(super) struct OpenCADStudio {
/// `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.
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
/// mutates the document live, so Escape restores this group atomically.
grip_originals: Vec<(acadrust::Handle, acadrust::EntityType)>,
@@ -3711,6 +3714,7 @@ impl OpenCADStudio {
grip_add_provisional: None,
grip_preview_handles: Vec::new(),
hover_dwell: None,
constraint_glyph_tooltip: None,
grip_originals: Vec::new(),
grip_history_originals: Vec::new(),
grip_dirty_before: None,
+121 -11
View File
@@ -933,9 +933,10 @@ impl OpenCADStudio {
return Task::none();
}
let i = self.active_tab;
self.constraint_glyph_tooltip = None;
let constraint_hover = self
.constraint_glyph_under(i, p)
.map(|(_, handles)| handles)
.map(|(_, references)| references)
.unwrap_or_default();
self.tabs[i]
.scene
@@ -1752,7 +1753,7 @@ impl OpenCADStudio {
.scene
.document
.get_entity(target.handle)?;
crate::scene::parametric_constraints::driven_ref_for_grip(
crate::scene::parametric_constraints::grip_solve_anchor_ref(
entity,
target.handle,
target.grip_id,
@@ -1766,7 +1767,7 @@ impl OpenCADStudio {
.solve_parametric_constraints_preview(
&edited_handles,
&driven_refs,
self.constraint_solve_mode && !driven_refs.is_empty(),
false,
&self.grip_originals,
);
for (handle, _) in &solved_by_constraints {
@@ -3012,7 +3013,7 @@ impl OpenCADStudio {
cursor: iced::Point,
) -> Option<(
crate::scene::parametric_constraints::ConstraintKind,
Vec<acadrust::Handle>,
Vec<crate::scene::parametric_constraints::ParametricRef>,
)> {
if self.tabs[i].scene.current_layout != "Model" {
return None;
@@ -3028,10 +3029,7 @@ impl OpenCADStudio {
)?;
let set = self.tabs[i].scene.parametric_constraint_set(scope)?;
let constraint = set.get(id)?;
let mut handles: Vec<_> = constraint.refs.iter().map(|r| r.entity).collect();
handles.sort_unstable_by_key(|handle| handle.value());
handles.dedup();
Some((constraint.kind, handles))
Some((constraint.kind, constraint.refs.clone()))
}
pub(super) fn on_viewport_left_press(&mut self) -> Task<Message> {
@@ -3090,7 +3088,10 @@ impl OpenCADStudio {
let (vw, vh) = vp_size;
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.select_entities(&handles);
self.refresh_selected_grips();
@@ -5253,6 +5254,7 @@ properties={:.1}ms picked={}",
}
self.tabs[i].scene.set_hover_highlight(None);
self.hover_dwell = None;
self.constraint_glyph_tooltip = None;
self.grip_hover = None;
self.ucs_icon_hover = false;
}
@@ -5574,6 +5576,10 @@ properties={:.1}ms picked={}",
self.clear_navigation_hover(i);
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 {
x: 0.0,
y: 0.0,
@@ -6205,9 +6211,13 @@ mod selection_preview_tests {
.normalize()
};
assert!(direction(current[0]).dot(direction(current[1])).abs() < 1e-8);
assert_ne!(
assert_eq!(
current[1], before[1],
"constrained neighbor must follow each grip frame"
"geometry outside the grip edit must stay fixed"
);
assert_eq!(
current[0][0], before[0][0],
"the endpoint opposite the dragged grip must stay fixed"
);
}
let _ = app.on_viewport_left_release();
@@ -6219,6 +6229,106 @@ mod selection_preview_tests {
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")]
#[test]
fn hyperlink_click_respects_existing_pointer_owners() {
+2
View File
@@ -888,6 +888,8 @@ bg={bg_ms:.1}ms n={view_count}"
grip_hover: self.model_space.grip_hover,
},
constraint_glyphs,
self.constraint_glyph_tooltip
.map(|kind| crate::t!(kind.label()).into_owned()),
)
};
+47 -5
View File
@@ -1685,6 +1685,10 @@ pub struct Scene {
pub hover_highlight: Option<Handle>,
/// All entities related to the constraint indicator currently under the cursor.
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).
pub selection_color: [f32; 4],
/// Whether selection visual effect (glow/highlight) is enabled (SELECTIONEFFECT).
@@ -2215,6 +2219,8 @@ impl Scene {
refedit_keep: None,
hover_highlight: None,
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_effect: true,
transparency_display: true,
@@ -5126,12 +5132,48 @@ impl Scene {
handles
}
pub fn set_constraint_hover_highlights(&mut self, handles: &[Handle]) {
let desired: HashSet<_> = handles.iter().copied().collect();
if desired != self.constraint_hover_highlights {
self.constraint_hover_highlights = desired;
self.bump_selection();
pub fn set_constraint_hover_highlights(
&mut self,
references: &[crate::scene::parametric_constraints::ParametricRef],
) {
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
+210 -40
View File
@@ -83,17 +83,17 @@ impl ParametricRef {
}
}
/// Point reference driven by a native entity grip. Segment midpoint and
/// scalar radius grips deliberately return None because they do not identify
/// one stored point parameter.
pub(crate) fn driven_ref_for_grip(
/// Point reference temporarily pinned while solving a native grip edit.
/// A line endpoint anchors its opposite endpoint so the grabbed endpoint is
/// projected onto the constraint solution instead of moving the whole line.
pub(crate) fn grip_solve_anchor_ref(
entity: &acadrust::EntityType,
handle: Handle,
grip_id: usize,
) -> Option<ParametricRef> {
match entity {
acadrust::EntityType::Line(_) if grip_id <= 1 => {
Some(ParametricRef::point(handle, grip_id as i32))
Some(ParametricRef::point(handle, 1 - grip_id as i32))
}
acadrust::EntityType::LwPolyline(polyline) if grip_id < polyline.vertices.len() => {
Some(ParametricRef::point(handle, grip_id as i32))
@@ -575,6 +575,37 @@ pub(crate) fn parametric_curve_ref_for_pick(
}
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
/// ribbon icons (`crate::modules::parametric::{tools,value}`) for
/// 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,
constraint: &ParametricConstraint,
) -> 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 line_midpoint = |line: &acadrust::entities::Line| {
Vector3::new(
@@ -661,7 +698,7 @@ pub(crate) fn glyph_placement(
entity,
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)));
}
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.
/// Point constraints expose their referenced point directly; curve relations
/// expose the contact or intersection that makes the relation visible.
@@ -1334,33 +1417,14 @@ impl super::Scene {
.any(|reference| self.selected.contains(&reference.entity));
self.should_display_parametric_constraint(scope, c.id, selected, display_mode)
})
.filter_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);
.flat_map(|c| {
let is_conflicting = set.conflicts.iter().any(|(id, _)| *id == c.id);
let label = if show_values {
glyph_label(c)
} else {
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()
.filter_map(|hover_point| {
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)
})
.collect();
point.x.is_finite().then_some((
c.id,
point,
direction.to_array(),
label,
is_conflicting,
hover_points,
))
glyph_placements(&self.document, c)
.into_iter()
.filter_map(|(anchor, outward)| {
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);
point.x.is_finite().then(|| (
c.id,
point,
direction.to_array(),
label.clone(),
is_conflicting,
hover_points.clone(),
))
})
.collect::<Vec<_>>()
})
.collect()
}
@@ -1580,6 +1668,88 @@ mod tests {
glyph_placement(&document, &constraint(ParametricRef::segment(h(3), 0))).unwrap();
assert_eq!(anchor, Vector3::new(5.0, 0.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]
@@ -1718,18 +1888,18 @@ mod tests {
));
assert_eq!(
driven_ref_for_grip(&arc, handle, 0),
grip_solve_anchor_ref(&arc, handle, 0),
Some(ParametricRef::center(handle))
);
assert_eq!(
driven_ref_for_grip(&arc, handle, 1),
grip_solve_anchor_ref(&arc, handle, 1),
Some(ParametricRef::point(handle, 0))
);
assert_eq!(
driven_ref_for_grip(&arc, handle, 2),
grip_solve_anchor_ref(&arc, handle, 2),
Some(ParametricRef::point(handle, 1))
);
assert_eq!(driven_ref_for_grip(&arc, handle, 3), None);
assert_eq!(grip_solve_anchor_ref(&arc, handle, 3), None);
}
#[test]
+44 -34
View File
@@ -1942,10 +1942,10 @@ fn solve_scope(
sys.add_constraint(Rc::new(Difference::new(el.center.y, el.focus1.y, dy)));
}
// The first ordered Coincident selection is the anchor for this solve.
// Pin either its addressed point or every defining parameter of its whole
// curve temporarily; the relation remains persistent, while these pins do
// not enter the drawing's constraint graph.
// Temporarily make each requested anchor a fixed kernel input. Marking the
// geometry parameters as driven keeps them exact and out of the solver's
// free-variable list; an approximate equality equation would still allow
// visible drift after repeated grip frames.
for reference in driven_refs {
if !set
.constraints
@@ -1996,9 +1996,7 @@ fn solve_scope(
}
};
for parameter in params_to_pin {
let value = sys.store().get(parameter);
let target = sys.add_param(value, true);
sys.add_constraint(Rc::new(Equal::new(parameter, target, 1.0)));
sys.store_mut().set_driven(parameter, true);
}
}
@@ -2638,11 +2636,24 @@ impl Scene {
if !is_touched {
continue;
}
let mut anchors = driven_refs.to_vec();
for reference in set
.constraints
.iter()
.filter(|constraint| constraint.enabled)
.flat_map(|constraint| &constraint.refs)
{
if !touched.contains(&reference.entity)
&& !anchors.iter().any(|anchor| anchor.entity == reference.entity)
{
anchors.push(ParametricRef::whole(reference.entity));
}
}
let Some((solved, _dof, _conflicts)) = solve_scope(
&self.document,
&self.named_parameters,
set,
driven_refs,
&anchors,
retain_size,
&retained_before,
)
@@ -2914,11 +2925,11 @@ mod tests {
);
}
/// `solve_parametric_constraints_preview` reports the constrained
/// neighbor's solved position without writing anything into the
/// document itself — the grip-drag caller owns applying it.
/// A grip preview treats geometry outside the edit as a fixed reference,
/// so a parallel constraint projects the dragged endpoint without moving
/// the other line or changing their separation.
#[test]
fn preview_solve_reports_the_neighbors_new_state_without_mutating_the_document() {
fn preview_solve_keeps_untouched_parallel_geometry_fixed() {
let mut scene = Scene::new();
let a = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
Vector3::new(0.0, 0.0, 0.0),
@@ -2943,40 +2954,39 @@ mod tests {
}
let b_before = scene.document.get_entity(b).cloned();
let solved = scene.solve_parametric_constraints_preview(&[a], &[], false, &[]);
let solved = scene.solve_parametric_constraints_preview(
&[a],
&[ParametricRef::point(a, 0)],
false,
&[],
);
assert_eq!(
scene.document.get_entity(b),
b_before.as_ref(),
"preview must not mutate the document"
);
// The whole scope solves together (every registered entity's params
// are free, not just `b`'s — see `register_entity`'s doc comment),
// so `a` itself may also have shifted slightly to reach the nearest
// mutually-parallel configuration; read whichever position `a` ends
// up at from `solved` too, rather than assuming it stayed exactly
// where the simulated drag put it.
let line_dir = |entity: &EntityType| {
let EntityType::Line(l) = entity else {
panic!("expected a Line")
};
(l.end.x - l.start.x, l.end.y - l.start.y)
};
let dir_a = solved
let moved_a = solved
.iter()
.find(|(h, _)| *h == a)
.map(|(_, e)| line_dir(e))
.unwrap_or((10.0, 6.0));
let (_, moved_entity) = solved
.iter()
.find(|(h, _)| *h == b)
.expect("b should be reported as moved");
let dir_b = line_dir(moved_entity);
let cross = dir_a.0 * dir_b.1 - dir_a.1 * dir_b.0;
.map(|(_, entity)| entity)
.expect("the dragged line should be projected");
assert!(solved.iter().all(|(handle, _)| *handle != b));
let EntityType::Line(moved_a_line) = moved_a else {
panic!("expected a Line")
};
assert_eq!(moved_a_line.start, Vector3::new(0.0, 0.0, 0.0));
let EntityType::Line(fixed_b) = b_before.unwrap() else {
panic!("expected a Line")
};
let dir_a = moved_a_line.end - moved_a_line.start;
let dir_b = fixed_b.end - fixed_b.start;
let cross = dir_a.x * dir_b.y - dir_a.y * dir_b.x;
assert!(
cross.abs() < 1e-6,
"the previewed positions should be mutually parallel: dir_a={dir_a:?} dir_b={dir_b:?}"
);
assert_eq!(fixed_b.start.y - moved_a_line.start.y, 5.0);
}
#[test]
+10
View File
@@ -1008,6 +1008,16 @@ impl Scene {
hover_changed = true;
}
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.images.remove(&h);
self.meshes.remove(&h);
+7 -2
View File
@@ -4254,15 +4254,20 @@ impl Scene {
Arc::new(Vec::new())
};
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())
} 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 {
v.push(iw.clone());
}
v.extend(self.preview_wires.iter().cloned());
v.extend(self.constraint_hover_wires.iter().cloned());
Arc::new(v)
};
let preview_hatches = if show_live_overlay {
+40 -1
View File
@@ -767,6 +767,7 @@ pub fn selection_overlay<'a>(
crosshair: CrosshairOptions,
selection_visual: SelectionVisualOptions,
constraint_glyphs: Vec<(Point, [f32; 2], String, bool, bool, Vec<Point>)>,
constraint_glyph_tooltip: Option<String>,
) -> Element<'a, Message> {
canvas(SelectionCanvas {
selection,
@@ -791,6 +792,7 @@ pub fn selection_overlay<'a>(
crosshair,
selection_visual,
constraint_glyphs,
constraint_glyph_tooltip,
})
.width(Length::Fill)
.height(Length::Fill)
@@ -853,6 +855,8 @@ struct SelectionCanvas {
/// 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<Point>)>,
/// Localized kind name made visible after the app-level hover dwell.
constraint_glyph_tooltip: Option<String>,
}
fn draw_grip_marker(
@@ -1786,7 +1790,7 @@ impl canvas::Program<Message> for SelectionCanvas {
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
self.constraint_glyphs.iter().zip(offsets)
self.constraint_glyphs.iter().zip(offsets.iter().copied())
{
if !anchor.x.is_finite() || !anchor.y.is_finite() {
continue;
@@ -1852,6 +1856,41 @@ impl canvas::Program<Message> for SelectionCanvas {
frame.stroke(&first, 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.