Support parallel offsets for arc polyline segments

This commit is contained in:
Marcel committed 2026-09-13 19:33:38 +02:00
1 parent 0ea220f120
commit 775a891309
1 file changed
+134 -9
+134 -9
View File
@@ -1175,6 +1175,7 @@ fn move_segment_parallel(pline: &mut LwPolyline, seg: usize, offset: f64) {
let i0 = seg;
let i1 = (seg + 1) % n;
if pline.vertices[i0].bulge.abs() >= 1.0e-9 {
move_arc_segment_parallel(pline, seg, offset);
return;
}
let p0 = Vec2::new(pline.vertices[i0].location.x, pline.vertices[i0].location.y);
@@ -1229,6 +1230,89 @@ fn move_segment_parallel(pline: &mut LwPolyline, seg: usize, offset: f64) {
pline.vertices[i1].location = acadrust::types::Vector2::new(new1.x, new1.y);
}
fn move_arc_segment_parallel(pline: &mut LwPolyline, seg: usize, offset: f64) {
let n = pline.vertices.len();
let i0 = seg;
let i1 = (seg + 1) % n;
let p0 = Vec2::new(pline.vertices[i0].location.x, pline.vertices[i0].location.y);
let p1 = Vec2::new(pline.vertices[i1].location.x, pline.vertices[i1].location.y);
let bulge = pline.vertices[i0].bulge;
let Some(arc) = crate::entities::common::BulgeArc::from_bulge(p0.into(), p1.into(), bulge)
else {
return;
};
let center = Vec2::from(arc.center);
let radius = arc.radius + offset;
if radius <= Tolerance::default().linear() {
return;
}
let line_circle_near = |a: Vec2, direction: Vec2, near: Vec2| -> Option<Vec2> {
let aa = direction.dot(direction);
if aa <= Tolerance::default().linear().powi(2) {
return None;
}
let relative = a - center;
let bb = 2.0 * relative.dot(direction);
let cc = relative.dot(relative) - radius * radius;
let discriminant = bb * bb - 4.0 * aa * cc;
if discriminant < -Tolerance::default().linear() {
return None;
}
let root = discriminant.max(0.0).sqrt();
let first = a + direction * ((-bb - root) / (2.0 * aa));
let second = a + direction * ((-bb + root) / (2.0 * aa));
Some(if first.distance(near) <= second.distance(near) {
first
} else {
second
})
};
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
} else {
center + (p0 - center) * (radius / arc.radius)
};
let count = if pline.is_closed { n } else { n - 1 };
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
} else {
center + (p1 - center) * (radius / arc.radius)
};
let start_angle = (new0.y - center.y).atan2(new0.x - center.x);
let end_angle = (new1.y - center.y).atan2(new1.x - center.x);
let sweep = if arc.sweep >= 0.0 {
(end_angle - start_angle).rem_euclid(TAU)
} else {
-(start_angle - end_angle).rem_euclid(TAU)
};
let new_bulge = (sweep * 0.25).tan();
if !new_bulge.is_finite() {
return;
}
pline.vertices[i0].location = acadrust::types::Vector2::new(new0.x, new0.y);
pline.vertices[i1].location = acadrust::types::Vector2::new(new1.x, new1.y);
pline.vertices[i0].bulge = new_bulge.clamp(-1.0e6, 1.0e6);
}
fn apply_transform(pline: &mut LwPolyline, t: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(pline, t, |entity, p1, p2| {
for v in &mut entity.vertices {
@@ -1307,12 +1391,10 @@ impl crate::entities::traits::Grippable for LwPolyline {
}
};
let mut items = Vec::new();
if !is_arc {
items.push(GripMenuItem {
label: "Move Parallel",
action: GripMenuAction::MoveParallel,
});
}
items.push(GripMenuItem {
label: "Move Parallel",
action: GripMenuAction::MoveParallel,
});
if is_rectangle(self) && seg < 4 {
// Moving an edge changes the dimension perpendicular to it.
items.push(GripMenuItem {
@@ -1352,8 +1434,7 @@ impl crate::entities::traits::Grippable for LwPolyline {
} else {
n.saturating_sub(1)
};
return (seg < count && self.vertices[seg].bulge.abs() < 1.0e-9)
.then_some("Parallel offset");
return (seg < count).then_some("Parallel offset");
}
(is_rectangle(self)
&& n == 4
@@ -1380,7 +1461,7 @@ impl crate::entities::traits::Grippable for LwPolyline {
} else {
n.saturating_sub(1)
};
if seg >= count || self.vertices[seg].bulge.abs() >= 1.0e-9 {
if seg >= count {
return None;
}
let plane = crate::entities::curve::lwpolyline_curve(self)?.plane;
@@ -1390,6 +1471,17 @@ impl crate::entities::traits::Grippable for LwPolyline {
self.vertices[(seg + 1) % n].location.x,
self.vertices[(seg + 1) % n].location.y,
);
let bulge = self.vertices[seg].bulge;
if bulge.abs() >= 1.0e-9 {
let arc = crate::entities::common::BulgeArc::from_bulge(
p0.into(),
p1.into(),
bulge,
)?;
let midpoint = Vec2::from(arc.sample(0.5));
let radial = (midpoint - Vec2::from(arc.center)).normalize()?;
return Some((point - midpoint).dot(radial));
}
let direction = p1 - p0;
let length = direction.length();
if length <= Tolerance::default().linear() {
@@ -1688,6 +1780,15 @@ mod tests {
assert_eq!(right[2].action, GripMenuAction::Stretch);
}
#[test]
fn arc_midpoint_offers_move_parallel_first() {
let mut pl = make_test_lwpolyline(2, 0.0);
pl.vertices[0].bulge = 0.5;
let menu = pl.grip_menu(2);
assert_eq!(menu[0].action, GripMenuAction::MoveParallel);
assert_eq!(menu[1].action, GripMenuAction::Stretch);
}
#[test]
fn rectangle_corner_offers_resize_before_stretch() {
let pl = make_test_rectangle();
@@ -1754,6 +1855,30 @@ mod tests {
assert_eq!(pl.vertices[3].location, Vector2::new(10.0, 0.0));
}
#[test]
fn move_parallel_offsets_arc_concentrically() {
let mut pl = polyline(&[(0.0, 0.0), (10.0, 0.0)], false);
pl.vertices[0].bulge = 1.0;
let original = crate::entities::common::BulgeArc::from_bulge(
[0.0, 0.0],
[10.0, 0.0],
pl.vertices[0].bulge,
)
.unwrap();
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!(Vec2::from(moved.center).distance(original.center.into()) < 1.0e-9);
assert!((moved.radius - original.radius - 2.0).abs() < 1.0e-9);
assert_eq!(moved.sweep.signum(), original.sweep.signum());
}
#[test]
fn move_parallel_translates_free_endpoint_of_open_polyline() {
let mut pl = polyline(&[(0.0, 0.0), (6.0, 0.0), (8.0, 2.0)], false);