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 <[email protected]>
This commit is contained in:
ramox81andClaude Opus 5 committed 2026-09-23 09:47:56 +03:00
1 parent 133bfddba9
commit 48b8689fc3
4 files changed
+183 -15

No files matched your search

+59 -12
View File
@@ -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;
+3
View File
@@ -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],
},
+11 -3
View File
@@ -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],
}
+110
View File
@@ -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:?}");
}
}