From cbf1e76c69bcfb9a97ca24cf67f80e374084aed5 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Thu, 17 Sep 2026 10:48:44 +0300 Subject: [PATCH] Fix perpendicular constraint behavior --- assets/icons/constrain/perpendicular.svg | 4 +- src/app/command_driver.rs | 56 ++++++++++++++++ src/app/control/mod.rs | 19 ++++++ src/modules/parametric/perpendicular.rs | 26 ++++---- src/scene/mod.rs | 17 +++++ src/scene/modify.rs | 81 +++++++++++++++++++++++- src/scene/parametric_constraints.rs | 27 ++++++++ src/scene/parametric_solve.rs | 2 +- 8 files changed, 216 insertions(+), 16 deletions(-) diff --git a/assets/icons/constrain/perpendicular.svg b/assets/icons/constrain/perpendicular.svg index 520becabb..c4d2ac1a1 100644 --- a/assets/icons/constrain/perpendicular.svg +++ b/assets/icons/constrain/perpendicular.svg @@ -1,4 +1,4 @@ - - + + diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 178b41a0a..a56efb790 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -3225,6 +3225,62 @@ impl OpenCADStudio { self.tabs[i] .scene .record_undo_parametric_constraints_before(scope, constraints_before); + if first.entity != second.entity { + use crate::scene::parametric_constraints::directional_reference_endpoints; + + let fixed_axis = self.tabs[i] + .scene + .document + .get_entity(first.entity) + .and_then(|entity| directional_reference_endpoints(entity, first)); + let moving_axis = self.tabs[i] + .scene + .document + .get_entity(second.entity) + .and_then(|entity| directional_reference_endpoints(entity, second)); + if let (Some([fixed_start, fixed_end]), Some([moving_start, moving_end])) = + (fixed_axis, moving_axis) + { + let fixed_direction = fixed_end - fixed_start; + let moving_direction = moving_end - moving_start; + if fixed_direction.length_squared() > 1.0e-24 + && moving_direction.length_squared() > 1.0e-24 + { + let fixed_angle = fixed_direction.y.atan2(fixed_direction.x); + let moving_angle = moving_direction.y.atan2(moving_direction.x); + let normalize = |angle: f64| { + (angle + std::f64::consts::PI) + .rem_euclid(std::f64::consts::TAU) + - std::f64::consts::PI + }; + let positive = normalize( + fixed_angle + std::f64::consts::FRAC_PI_2 - moving_angle, + ); + let negative = normalize( + fixed_angle - std::f64::consts::FRAC_PI_2 - moving_angle, + ); + let angle_rad = if positive.abs() <= negative.abs() { + positive + } else { + negative + }; + if angle_rad.abs() > 1.0e-12 { + self.tabs[i].scene.transform_entities( + &[second.entity], + &crate::command::EntityTransform::Rotate { + center: glam::DVec3::new( + moving_start.x, + moving_start.y, + moving_start.z, + ), + axis: glam::DVec3::Z, + angle_rad, + }, + ); + } + } + } + } let id = self.tabs[i].scene.parametric_constraint_set_mut(scope).add( ConstraintKind::Perpendicular, refs, diff --git a/src/app/control/mod.rs b/src/app/control/mod.rs index b0da52d1e..e846c849a 100644 --- a/src/app/control/mod.rs +++ b/src/app/control/mod.rs @@ -220,6 +220,25 @@ fn command_category(name: &str) -> &'static str { "view" } else if name.starts_with("DIM") || ["LEADER", "MLEADER", "TOLERANCE"].contains(&name) { "annotate" + } else if [ + "GEOMCONSTRAINT", + "GCCOINCIDENT", + "GCCOLLINEAR", + "GCPERPENDICULAR", + "QCONSTRAINT", + "PCONSTRAINT", + "HCONSTRAINT", + "VCONSTRAINT", + "ECONSTRAINT", + "TCONSTRAINT", + "NCONSTRAINT", + "NRCONSTRAINT", + "LCONSTRAINT", + "DELCONSTRAINT", + ] + .contains(&name) + { + "parametric" } else if [ "BOX", "CYLINDER", diff --git a/src/modules/parametric/perpendicular.rs b/src/modules/parametric/perpendicular.rs index b69b5d7d2..8621e64ce 100644 --- a/src/modules/parametric/perpendicular.rs +++ b/src/modules/parametric/perpendicular.rs @@ -101,12 +101,13 @@ impl PerpendicularConstraintCommand { } else { polyline.vertices.len().saturating_sub(1) }; - (segment_count == 1 && polyline.vertices.first()?.bulge.abs() <= 1.0e-12) - .then_some(PerpendicularPick { - reference: ParametricRef::segment(handle, 0), - fixed_reference: ParametricRef::segment(handle, 0), - start_reference: ParametricRef::point(handle, 0), - }) + let index = (0..segment_count) + .find(|index| polyline.vertices[*index].bulge.abs() <= 1.0e-12)?; + Some(PerpendicularPick { + reference: ParametricRef::segment(handle, index), + fixed_reference: ParametricRef::segment(handle, index), + start_reference: ParametricRef::point(handle, index as i32), + }) } EntityType::Polyline2D(polyline) => { let segment_count = if polyline.is_closed() { @@ -114,12 +115,13 @@ impl PerpendicularConstraintCommand { } else { polyline.vertices.len().saturating_sub(1) }; - (segment_count == 1 && polyline.vertices.first()?.bulge.abs() <= 1.0e-12) - .then_some(PerpendicularPick { - reference: ParametricRef::segment(handle, 0), - fixed_reference: ParametricRef::segment(handle, 0), - start_reference: ParametricRef::point(handle, 0), - }) + let index = (0..segment_count) + .find(|index| polyline.vertices[*index].bulge.abs() <= 1.0e-12)?; + Some(PerpendicularPick { + reference: ParametricRef::segment(handle, index), + fixed_reference: ParametricRef::segment(handle, index), + start_reference: ParametricRef::point(handle, index as i32), + }) } _ => None, } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 57e7e9264..4ed4676bf 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -3245,6 +3245,23 @@ impl Scene { ); } + pub fn bump_entities_with_parametric_transform_policy( + &mut self, + changes: &[(Handle, ChangeKind)], + driven_refs: &[parametric_constraints::ParametricRef], + transformed_refs: &[parametric_constraints::ParametricRef], + retained_originals: &[(Handle, EntityType)], + ) { + self.bump_entities_with_solve_policy( + changes, + driven_refs, + true, + retained_originals, + transformed_refs, + true, + ); + } + fn bump_entities_with_solve_policy( &mut self, changes: &[(Handle, ChangeKind)], diff --git a/src/scene/modify.rs b/src/scene/modify.rs index b13a1f853..e4ae6e69c 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -161,6 +161,80 @@ impl Scene { .filter(|&h| !self.is_layer_locked(h)) .collect(); let handles = &handles[..]; + let retained_originals: Vec<(Handle, EntityType)> = handles + .iter() + .filter_map(|handle| { + self.document + .get_entity(*handle) + .cloned() + .map(|entity| (*handle, entity)) + }) + .collect(); + let transformed_refs: Vec<_> = handles + .iter() + .copied() + .map(parametric_constraints::ParametricRef::whole) + .collect(); + let mut driven_refs = Vec::new(); + if !matches!(t, EntityTransform::Translate(_)) { + for set in &self.parametric_constraints { + for constraint in &set.constraints { + if !constraint.enabled + || !matches!( + constraint.kind, + parametric_constraints::ConstraintKind::Parallel + | parametric_constraints::ConstraintKind::Perpendicular + | parametric_constraints::ConstraintKind::Colinear + ) + { + continue; + } + let selected_refs = constraint + .refs + .iter() + .filter(|reference| handles.contains(&reference.entity)) + .count(); + if selected_refs == 0 || selected_refs == constraint.refs.len() { + continue; + } + for reference in constraint + .refs + .iter() + .copied() + .filter(|reference| !handles.contains(&reference.entity)) + { + let anchor = if let Some(index) = reference.segment_index() { + parametric_constraints::ParametricRef::point( + reference.entity, + index as i32, + ) + } else if reference.directional_axis().is_some() + && matches!( + self.document.get_entity(reference.entity), + Some(EntityType::Ellipse(_)) + ) + { + parametric_constraints::ParametricRef::center(reference.entity) + } else { + parametric_constraints::ParametricRef::point(reference.entity, 0) + }; + let follows_selected_point = set.constraints.iter().any(|link| { + link.enabled + && link.kind + == parametric_constraints::ConstraintKind::Coincident + && link.refs.contains(&anchor) + && link + .refs + .iter() + .any(|item| handles.contains(&item.entity)) + }); + if !follows_selected_point && !driven_refs.contains(&anchor) { + driven_refs.push(anchor); + } + } + } + } + } // Preserve readable text orientation when MIRRTEXT is disabled. let preserve_text_orientation = matches!(t, EntityTransform::Mirror { .. }) && !self.document.header.mirror_text; @@ -290,7 +364,12 @@ impl Scene { // per-handle, keeping the block cache + all other memoized wires. let changes: Vec<(Handle, ChangeKind)> = handles.iter().map(|&h| (h, ChangeKind::Modified)).collect(); - self.bump_entities(&changes); + self.bump_entities_with_parametric_transform_policy( + &changes, + &driven_refs, + &transformed_refs, + &retained_originals, + ); self.refresh_meshes_for_handles(&refresh_solid_handles); } diff --git a/src/scene/parametric_constraints.rs b/src/scene/parametric_constraints.rs index 467f8d779..f6b1363fd 100644 --- a/src/scene/parametric_constraints.rs +++ b/src/scene/parametric_constraints.rs @@ -186,6 +186,33 @@ pub(crate) fn directional_axis_endpoints( } } +/// Finite endpoints used to orient a line-like constraint reference. +pub(crate) fn directional_reference_endpoints( + entity: &acadrust::EntityType, + reference: ParametricRef, +) -> Option<[Vector3; 2]> { + if reference.directional_axis().is_some() { + return directional_axis_endpoints(entity, reference); + } + match entity { + acadrust::EntityType::Line(line) if reference.marker.is_none() => { + Some([line.start, line.end]) + } + acadrust::EntityType::LwPolyline(_) | acadrust::EntityType::Polyline2D(_) => { + let index = reference.segment_index()?; + let points = super::dimension_assoc::source_points(entity); + let start = *points.get(index)?; + let end = if let Some(end) = points.get(index + 1) { + *end + } else { + *points.first()? + }; + Some([start, end]) + } + _ => None, + } +} + /// Grabbed points are exact kernel inputs; the solver anchors the remaining /// endpoint coordinates according to the line's directional constraints. pub(crate) fn grip_solve_anchor_refs( diff --git a/src/scene/parametric_solve.rs b/src/scene/parametric_solve.rs index 30e56a8fc..09620b0b8 100644 --- a/src/scene/parametric_solve.rs +++ b/src/scene/parametric_solve.rs @@ -2123,7 +2123,7 @@ fn solve_scope( for (handle, geom) in &cache { if initial_fixed_refs .iter() - .any(|reference| reference.entity == *handle) + .any(|reference| reference.entity == *handle && reference.marker.is_none()) { continue; }