diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index d8a2f787a..65385f215 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -3486,6 +3486,53 @@ impl OpenCADStudio { } } } + // The solver cannot start from an axis lying exactly across + // the datum (its equations are singular there), so turn the + // object onto the axis about its anchor first — the + // reference turns it about that anchor too — and let the + // constraint hold what already fits. + if let Some((handle, anchor, end, vertex)) = + crate::scene::parametric_constraints::axis_alignment_target( + &self.tabs[i].scene.document, + &refs, + ) + { + let axis = (end.x - anchor.x, end.y - anchor.y); + let length = axis.0.hypot(axis.1); + let target = if axis.0 * direction.x + axis.1 * direction.y >= 0.0 { + (direction.x, direction.y) + } else { + (-direction.x, -direction.y) + }; + let angle = (axis.0 * target.1 - axis.1 * target.0) + .atan2(axis.0 * target.0 + axis.1 * target.1); + if length > 1.0e-9 && angle.abs() > 1.0e-9 { + match vertex { + Some(index) => { + let moved = + self.tabs[i].scene.document.get_entity(handle).cloned(); + if let Some(mut entity) = moved { + if crate::scene::parametric_constraints::set_polyline_vertex( + &mut entity, + index, + anchor.x + length * target.0, + anchor.y + length * target.1, + ) { + self.tabs[i].scene.update_entity(entity); + } + } + } + None => self.tabs[i].scene.transform_entities( + &[handle], + &EntityTransform::Rotate { + center: glam::DVec3::new(anchor.x, anchor.y, anchor.z), + axis: glam::DVec3::Z, + angle_rad: angle, + }, + ), + } + } + } let id = self.tabs[i] .scene .parametric_constraint_set_mut(scope) diff --git a/src/scene/parametric_constraints.rs b/src/scene/parametric_constraints.rs index e599ceb4f..bd0ee0c99 100644 --- a/src/scene/parametric_constraints.rs +++ b/src/scene/parametric_constraints.rs @@ -202,6 +202,69 @@ pub(crate) fn directional_axis_endpoints( } } +/// The axis a lone Horizontal/Vertical reference — or two points on one +/// entity — turns onto its datum: the entity, the anchor that stays, the +/// axis end that moves, and for a polyline the vertex to move instead of +/// turning the whole entity. +pub(crate) fn axis_alignment_target( + document: &acadrust::CadDocument, + refs: &[ParametricRef], +) -> Option<(Handle, Vector3, Vector3, Option)> { + let (handle, start_marker, end_marker) = match refs { + [reference] => { + let entity = document.get_entity(reference.entity)?; + if reference.directional_axis().is_some() { + let [start, end] = directional_axis_endpoints(entity, *reference)?; + return Some((reference.entity, start, end, None)); + } + let index = reference.segment_index().map_or(0, |index| index as i32); + (reference.entity, index, index + 1) + } + [first, second] if first.entity == second.entity => { + (first.entity, first.marker?, second.marker?) + } + _ => return None, + }; + let entity = document.get_entity(handle)?; + let start = resolve_point(entity, start_marker)?; + let end = resolve_point(entity, end_marker)?; + let vertex = matches!( + entity, + acadrust::EntityType::LwPolyline(_) | acadrust::EntityType::Polyline2D(_) + ) + .then(|| usize::try_from(end_marker).ok()) + .flatten(); + Some((handle, start, end, vertex)) +} + +/// Moves one polyline vertex in its plane; `false` for any other entity. +pub(crate) fn set_polyline_vertex( + entity: &mut acadrust::EntityType, + index: usize, + x: f64, + y: f64, +) -> bool { + match entity { + acadrust::EntityType::LwPolyline(polyline) => polyline + .vertices + .get_mut(index) + .map(|vertex| { + vertex.location.x = x; + vertex.location.y = y; + }) + .is_some(), + acadrust::EntityType::Polyline2D(polyline) => polyline + .vertices + .get_mut(index) + .map(|vertex| { + vertex.location.x = x; + vertex.location.y = y; + }) + .is_some(), + _ => false, + } +} + /// 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(