Preserve connected arc geometry when moving polyline segments

This commit is contained in:
Marcel committed 2026-09-14 11:42:30 +02:00
1 parent fb352c34fb
commit e67872386e
1 file changed
+133 -10
+133 -10
View File
@@ -1,7 +1,7 @@
use acadrust::entities::{LwPolyline, LwVertex};
use cadkernel::geom2d::{
fillet_between_rays, signed_area, Curve, Polyline, PolylineVertex, RectangleFrame, Tolerance,
Vec2,
circle_circle_points, fillet_between_rays, signed_area, Curve, Polyline, PolylineVertex,
RectangleFrame, Tolerance, Vec2,
};
use crate::t;
@@ -1269,31 +1269,97 @@ fn move_arc_segment_parallel(pline: &mut LwPolyline, seg: usize, offset: f64) {
second
})
};
let circle_circle_near = |other_center: Vec2, other_radius: f64, near: Vec2| {
circle_circle_points(center.into(), radius, other_center.into(), other_radius)
.into_iter()
.map(Vec2::from)
.min_by(|a, b| a.distance(near).total_cmp(&b.distance(near)))
};
let bulge_on_circle = |circle_center: Vec2, start: Vec2, end: Vec2, sign: f64| {
let start_angle = (start.y - circle_center.y).atan2(start.x - circle_center.x);
let end_angle = (end.y - circle_center.y).atan2(end.x - circle_center.x);
let sweep = if sign >= 0.0 {
(end_angle - start_angle).rem_euclid(TAU)
} else {
-(start_angle - end_angle).rem_euclid(TAU)
};
let bulge = (sweep * 0.25).tan();
bulge.is_finite().then_some(bulge.clamp(-1.0e6, 1.0e6))
};
let linear_sq = Tolerance::default().linear().powi(2);
let mut sync_previous = None;
let mut previous_arc_bulge = None;
let new0 = if pline.is_closed || seg > 0 {
let previous = (i0 + n - 1) % n;
let a = Vec2::new(
pline.vertices[previous].location.x,
pline.vertices[previous].location.y,
);
let Some(point) = line_circle_near(a, p0 - a, p0) else {
return;
};
point
if pline.vertices[previous].bulge.abs() >= 1.0e-9 {
let Some(neighbor) = crate::entities::common::BulgeArc::from_bulge(
a.into(), p0.into(), pline.vertices[previous].bulge,
) else {
return;
};
let neighbor_center = Vec2::from(neighbor.center);
let Some(point) = circle_circle_near(neighbor_center, neighbor.radius, p0) else {
return;
};
let Some(new_bulge) = bulge_on_circle(
neighbor_center, a, point, neighbor.sweep.signum(),
) else {
return;
};
previous_arc_bulge = Some((previous, new_bulge));
point
} else if (p0 - a).length_squared() <= linear_sq {
sync_previous = Some(previous);
center + (p0 - center) * (radius / arc.radius)
} else {
let Some(point) = line_circle_near(a, p0 - a, p0) else {
return;
};
point
}
} else {
center + (p0 - center) * (radius / arc.radius)
};
let count = if pline.is_closed { n } else { n - 1 };
let mut sync_next = None;
let mut next_arc_bulge = None;
let new1 = if pline.is_closed || seg + 1 < count {
let next = (i1 + 1) % n;
let d = Vec2::new(
pline.vertices[next].location.x,
pline.vertices[next].location.y,
);
let Some(point) = line_circle_near(p1, d - p1, p1) else {
return;
};
point
if pline.vertices[i1].bulge.abs() >= 1.0e-9 {
let Some(neighbor) = crate::entities::common::BulgeArc::from_bulge(
p1.into(), d.into(), pline.vertices[i1].bulge,
) else {
return;
};
let neighbor_center = Vec2::from(neighbor.center);
let Some(point) = circle_circle_near(neighbor_center, neighbor.radius, p1) else {
return;
};
let Some(new_bulge) = bulge_on_circle(
neighbor_center, point, d, neighbor.sweep.signum(),
) else {
return;
};
next_arc_bulge = Some((i1, new_bulge));
point
} else if (d - p1).length_squared() <= linear_sq {
sync_next = Some(next);
center + (p1 - center) * (radius / arc.radius)
} else {
let Some(point) = line_circle_near(p1, d - p1, p1) else {
return;
};
point
}
} else {
center + (p1 - center) * (radius / arc.radius)
};
@@ -1311,6 +1377,18 @@ fn move_arc_segment_parallel(pline: &mut LwPolyline, seg: usize, offset: f64) {
}
pline.vertices[i0].location = acadrust::types::Vector2::new(new0.x, new0.y);
pline.vertices[i1].location = acadrust::types::Vector2::new(new1.x, new1.y);
if let Some(previous) = sync_previous {
pline.vertices[previous].location = acadrust::types::Vector2::new(new0.x, new0.y);
}
if let Some(next) = sync_next {
pline.vertices[next].location = acadrust::types::Vector2::new(new1.x, new1.y);
}
if let Some((previous, bulge)) = previous_arc_bulge {
pline.vertices[previous].bulge = bulge;
}
if let Some((next, bulge)) = next_arc_bulge {
pline.vertices[next].bulge = bulge;
}
pline.vertices[i0].bulge = new_bulge.clamp(-1.0e6, 1.0e6);
}
@@ -1881,6 +1959,7 @@ impl crate::entities::traits::MassPropsCalc for acadrust::entities::LwPolyline {
#[cfg(test)]
mod tests {
use super::*;
use crate::entities::traits::{Grippable, PropertyEditable};
use crate::scene::model::object::GripMenuAction;
use acadrust::entities::{LwPolyline, LwVertex};
@@ -2018,6 +2097,50 @@ mod tests {
assert_eq!(moved.sweep.signum(), original.sweep.signum());
}
#[test]
fn move_parallel_allows_an_open_arc_with_a_duplicate_end_vertex() {
let mut pl = polyline(&[(0.0, 0.0), (10.0, 0.0), (10.0, 0.0)], false);
pl.vertices[0].bulge = 1.0;
move_segment_parallel(&mut pl, 0, 2.0);
let moved = crate::entities::common::BulgeArc::from_bulge(
[pl.vertices[0].location.x, pl.vertices[0].location.y],
[pl.vertices[1].location.x, pl.vertices[1].location.y],
pl.vertices[0].bulge,
).unwrap();
assert!((moved.radius - 7.0).abs() < 1.0e-9);
assert_eq!(pl.vertices[2].location, pl.vertices[1].location);
}
#[test]
fn resizing_an_end_arc_only_changes_the_connected_arcs_length() {
let shared = Vec2::new(3.0, 4.0);
let mut pl = polyline(&[(-5.0, 0.0), (shared.x, shared.y), (11.0, 0.0)], false);
let ccw_bulge = |center: Vec2, start: Vec2, end: Vec2| {
let a0 = (start.y - center.y).atan2(start.x - center.x);
let a1 = (end.y - center.y).atan2(end.x - center.x);
((a1 - a0).rem_euclid(TAU) * 0.25).tan()
};
pl.vertices[0].bulge = ccw_bulge(Vec2::ZERO, Vec2::new(-5.0, 0.0), shared);
pl.vertices[1].bulge = ccw_bulge(Vec2::new(6.0, 0.0), shared, Vec2::new(11.0, 0.0));
let before = crate::entities::common::BulgeArc::from_bulge(
[shared.x, shared.y], [11.0, 0.0], pl.vertices[1].bulge,
).unwrap();
move_segment_parallel(&mut pl, 0, 1.0);
let after = crate::entities::common::BulgeArc::from_bulge(
[pl.vertices[1].location.x, pl.vertices[1].location.y],
[pl.vertices[2].location.x, pl.vertices[2].location.y],
pl.vertices[1].bulge,
).unwrap();
assert!(Vec2::from(after.center).distance(Vec2::from(before.center)) < 1.0e-9);
assert!((after.radius - before.radius).abs() < 1.0e-9);
assert_eq!(pl.vertices[2].location, Vector2::new(11.0, 0.0));
assert!((after.sweep - before.sweep).abs() > 1.0e-6);
}
#[test]
fn radius_menu_resizes_a_corner_as_a_tangent_fillet() {
let mut pl = polyline(