forked from 1b-agent/opencadstudio-upstream
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:
1 parent
133bfddba9
commit
48b8689fc3
4 files changed
+183
-15
No files matched your search
+59
-12
@@ -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;
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
|
||||
@@ -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],
|
||||
}
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user