From 48b8689fc34d6c7c7484414bbcc2785831ccbd73 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Wed, 23 Sep 2026 09:47:56 +0300 Subject: [PATCH] Aligned 2Lines: place the second line where the reference leaves it The second line kept its midpoint and turned around it, so it settled at a different offset than the reference, which keeps the line's start along the first line and moves it to the distance the picked point had from it. Co-Authored-By: Claude Opus 5 --- src/app/command_driver.rs | 71 ++++++++++++--- src/command.rs | 3 + src/modules/parametric/dim_constraint.rs | 14 ++- src/scene/parametric_constraints.rs | 110 +++++++++++++++++++++++ 4 files changed, 183 insertions(+), 15 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 94426080e..c41380914 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -4738,10 +4738,13 @@ impl OpenCADStudio { CmdResult::MakeParallel { first_line, first_ends, + first_pick, second_line, second_ends, } => { - use crate::scene::parametric_constraints::{resolve_point, ConstraintKind}; + use crate::scene::parametric_constraints::{ + resolve_point, set_resolved_point, two_lines_placement, ConstraintKind, + }; let scope = self.tabs[i].current_parametric_scope(); let refs = [first_line, second_line]; if let Err(message) = @@ -4751,6 +4754,39 @@ impl OpenCADStudio { { return self.apply_cmd_result(CmdResult::ReportError(message.to_string())); } + // The reference's placement of the second line, written + // before the Parallel constraint holds it there. + let placed = { + let document = &self.tabs[i].scene.document; + let world = |reference: crate::scene::parametric_constraints::ParametricRef| { + let entity = document.get_entity(reference.entity)?; + let point = resolve_point(entity, reference.marker?)?; + Some(glam::DVec3::new(point.x, point.y, point.z)) + }; + let ends = |pair: [crate::scene::parametric_constraints::ParametricRef; 2]| { + Some([world(pair[0])?, world(pair[1])?]) + }; + ends(first_ends) + .zip(ends(second_ends)) + .and_then(|(first, second)| two_lines_placement(first, first_pick, second)) + }; + let placed_entity = placed.and_then(|points| { + let mut entity = self.tabs[i] + .scene + .document + .get_entity(second_line.entity) + .cloned()?; + let written = second_ends.iter().zip(points).all(|(reference, point)| { + reference.marker.is_some_and(|marker| { + set_resolved_point( + &mut entity, + marker, + acadrust::types::Vector3::new(point.x, point.y, point.z), + ) + }) + }); + written.then_some(entity) + }); let already = self.tabs[i] .scene .parametric_constraint_set(scope) @@ -4762,7 +4798,7 @@ impl OpenCADStudio { && c.refs.contains(&second_line) }) }); - if !already { + if !already || placed_entity.is_some() { let constraints_before = self.tabs[i] .scene .parametric_constraint_set(scope) @@ -4773,21 +4809,32 @@ impl OpenCADStudio { ) }); let pending = self.begin_undo(i, "Parallel constraint", 1, true); + if let Some(entity) = placed_entity { + let before = self.tabs[i].scene.document.get_entity_arc(second_line.entity); + self.tabs[i] + .scene + .record_undo_before(second_line.entity, before); + self.tabs[i].scene.update_entity(entity); + } self.tabs[i] .scene .record_undo_parametric_constraints_before(scope, constraints_before); - let set = self.tabs[i].scene.parametric_constraint_set_mut(scope); - let id = set.add(ConstraintKind::Parallel, refs.to_vec(), None); - self.tabs[i].scene.note_parametric_constraint_applied( - scope, - id, - self.constraint_bar_display, - ); - // The first line stays; the second turns parallel the way the - // Parallel constraint itself moves it. + if !already { + let set = self.tabs[i].scene.parametric_constraint_set_mut(scope); + let id = set.add(ConstraintKind::Parallel, refs.to_vec(), None); + self.tabs[i].scene.note_parametric_constraint_applied( + scope, + id, + self.constraint_bar_display, + ); + } + // Both lines' ends hold the placement; the solve only + // settles whatever else is tied to them. + let mut pins = first_ends.to_vec(); + pins.extend(second_ends); self.tabs[i].scene.bump_entities_with_parametric_policy( &[(second_line.entity, crate::scene::ChangeKind::Modified)], - &first_ends, + &pins, self.constraint_solve_mode, ); self.tabs[i].dirty = true; diff --git a/src/command.rs b/src/command.rs index 01f095ac9..54c7ac6b0 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1676,6 +1676,9 @@ pub enum CmdResult { MakeParallel { first_line: crate::scene::parametric_constraints::ParametricRef, first_ends: [crate::scene::parametric_constraints::ParametricRef; 2], + /// Where the first line was picked: the second line settles at the + /// distance this point had from it. + first_pick: DVec3, second_line: crate::scene::parametric_constraints::ParametricRef, second_ends: [crate::scene::parametric_constraints::ParametricRef; 2], }, diff --git a/src/modules/parametric/dim_constraint.rs b/src/modules/parametric/dim_constraint.rs index 548c4cc96..ab30525f7 100644 --- a/src/modules/parametric/dim_constraint.rs +++ b/src/modules/parametric/dim_constraint.rs @@ -70,7 +70,11 @@ enum Step { /// Aligned's 2Lines: `Select first line:` TwoLinesFirst, /// `Select second line to make parallel:` - TwoLinesSecond { line: LineTarget }, + TwoLinesSecond { + line: LineTarget, + /// Where the first line was picked. + pick: DVec3, + }, /// The host is making the second line parallel; `accept_parallel_line` /// brings its solved ends. TwoLinesParallel { @@ -818,13 +822,16 @@ impl CadCommand for DimConstraintCommand { }, Step::TwoLinesFirst => match self.pick_line(handle, point) { Ok(line) => { - self.step = Step::TwoLinesSecond { line }; + self.step = Step::TwoLinesSecond { line, pick: point }; CmdResult::NeedPoint } Err("") => CmdResult::NeedPoint, Err(message) => Self::report(message), }, - Step::TwoLinesSecond { line: first_line } => match self.pick_line(handle, point) { + Step::TwoLinesSecond { + line: first_line, + pick: first_pick, + } => match self.pick_line(handle, point) { Ok(second_line) => { if second_line.line == first_line.line { return Self::report(Self::SAME_POINT); @@ -839,6 +846,7 @@ impl CadCommand for DimConstraintCommand { CmdResult::MakeParallel { first_line: first_line.line, first_ends: [first_line.ends[0].0, first_line.ends[1].0], + first_pick, second_line: second_line.line, second_ends: [second_line.ends[0].0, second_line.ends[1].0], } diff --git a/src/scene/parametric_constraints.rs b/src/scene/parametric_constraints.rs index a41567d4f..f9dcd2b1f 100644 --- a/src/scene/parametric_constraints.rs +++ b/src/scene/parametric_constraints.rs @@ -523,6 +523,68 @@ pub(crate) fn measured_expression(value: f64) -> String { } } +/// Writes a constraint point's position: a line's start (0) or end (1), a +/// polyline's vertex by index. Other entities and markers are left alone. +pub(crate) fn set_resolved_point( + entity: &mut acadrust::EntityType, + marker: i32, + point: Vector3, +) -> bool { + match entity { + acadrust::EntityType::Line(line) => match marker { + 0 => line.start = point, + 1 => line.end = point, + _ => return false, + }, + acadrust::EntityType::LwPolyline(polyline) if marker >= 0 => { + let Some(vertex) = polyline.vertices.get_mut(marker as usize) else { + return false; + }; + vertex.location = acadrust::types::Vector2::new(point.x, point.y); + } + acadrust::EntityType::Polyline2D(polyline) if marker >= 0 => { + let Some(vertex) = polyline.vertices.get_mut(marker as usize) else { + return false; + }; + vertex.location = point; + } + _ => return false, + } + true +} + +/// Where the reference puts the second line of an Aligned 2Lines pick: it +/// turns about its start to run with the first line (keeping its heading +/// and length) and settles at the distance the first line's picked point +/// had from its original line, on the side nearer to where the turn left +/// it — the right-hand side of the first line when both are as near. +pub(crate) fn two_lines_placement( + first_ends: [glam::DVec3; 2], + first_pick: glam::DVec3, + second_ends: [glam::DVec3; 2], +) -> Option<[glam::DVec3; 2]> { + let [f0, f1] = first_ends; + let [s0, s1] = second_ends; + let f_dir = (f1 - f0).try_normalize()?; + let normal = glam::DVec3::new(-f_dir.y, f_dir.x, 0.0); + let s_dir = (s1 - s0).try_normalize()?; + let length = (s1 - s0).length(); + let pick = f0 + f_dir * (first_pick - f0).dot(f_dir); + let offset = pick - s0; + let distance = (s_dir.x * offset.y - s_dir.y * offset.x).abs(); + let heading = if s_dir.dot(f_dir) >= 0.0 { f_dir } else { -f_dir }; + let turned = (s0 - f0).dot(normal); + let target = if turned.abs() <= 1.0e-9 { + -distance + } else if (turned - distance).abs() <= (turned + distance).abs() { + distance + } else { + -distance + }; + let shift = normal * (target - turned); + Some([s0 + shift, s0 + heading * length + shift]) +} + /// Moves a dynamic dimension's extension origins to `first`/`second`, /// keeping its dimension line where it was; true when anything moved. fn dynamic_dimension_follow_points( @@ -4173,3 +4235,51 @@ mod tests { assert_eq!(constraints[1].axis_direction, Some(direction.normalize())); } } + +#[cfg(test)] +mod two_lines_placement_tests { + use super::two_lines_placement; + use glam::DVec3; + + fn p(x: f64, y: f64) -> DVec3 { + DVec3::new(x, y, 0.0) + } + + fn close(a: DVec3, b: DVec3) -> bool { + (a - b).length() < 1.0e-2 + } + + // Measured on the reference: first line (200,0)-(300,0) picked at its + // midpoint, the second line as listed, and where it ended up. + #[test] + fn follows_the_reference_measurements() { + let first = [p(200.0, 0.0), p(300.0, 0.0)]; + let pick = p(250.0, 0.0); + let cases = [ + ([p(0.0, 0.0), p(100.0, 50.0)], [p(0.0, -111.803), p(111.803, -111.803)]), + ([p(0.0, 40.0), p(100.0, 90.0)], [p(0.0, 147.58), p(111.803, 147.58)]), + ([p(100.0, 0.0), p(200.0, 50.0)], [p(100.0, -67.08), p(211.803, -67.08)]), + ([p(-100.0, 0.0), p(0.0, 50.0)], [p(-100.0, -156.52), p(11.803, -156.52)]), + ([p(0.0, 0.0), p(200.0, 100.0)], [p(0.0, -111.8), p(223.607, -111.8)]), + ([p(0.0, 0.0), p(79.0569, 79.0569)], [p(0.0, -176.78), p(111.803, -176.78)]), + ([p(0.0, 0.0), p(100.0, -50.0)], [p(0.0, -111.8), p(111.803, -111.8)]), + ([p(100.0, 50.0), p(0.0, 0.0)], [p(100.0, 111.8), p(-11.803, 111.8)]), + ([p(0.0, 20.0), p(100.0, 70.0)], [p(0.0, 129.69), p(111.803, 129.69)]), + ]; + for (second, expected) in cases { + let placed = two_lines_placement(first, pick, second).expect("a placement"); + assert!(close(placed[0], expected[0]) && close(placed[1], expected[1]), "{second:?} -> {placed:?}, expected {expected:?}"); + } + // The first line picked near its end: the distance grows with it. + let placed = two_lines_placement(first, p(290.0, 0.0), [p(0.0, 0.0), p(100.0, 50.0)]).unwrap(); + assert!(close(placed[0], p(0.0, -129.69))); + // A slanted first line: the second line's start keeps its place along it. + let placed = two_lines_placement( + [p(500.0, 300.0), p(600.0, 350.0)], + p(550.0, 325.0), + [p(400.0, 300.0), p(500.0, 300.0)], + ) + .unwrap(); + assert!(close(placed[0], p(408.82, 282.36)) && close(placed[1], p(498.26, 327.08)), "{placed:?}"); + } +}