EXTEND: a polyline's arc end segment runs on round its circle (#2656)

EXTEND on an open polyline ran a ray along the chord of an arc end segment
and kept the old bulge, so the arc changed radius and centre, or the
boundary was reported missed when only the circle reached it. The arc end
segment now runs on round its own circle to the nearest crossing ahead,
turning the same way, and its bulge becomes that of the total turn, as
EXTEND does with an ARC. The polyline also keeps its constant width,
elevation and linetype generation, which the rebuilt polyline dropped.

Co-authored-by: Tyler Chiriboga <18270301+Tyler-Chiriboga@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Tyler Chiriboga
2026-11-02 22:52:39 -05:00
committed by GitHub
co-authored by Tyler Chiriboga Claude Opus 5.5
parent 1b7c705fac
commit 455b2a87cb
3 changed files with 187 additions and 11 deletions
+49 -11
View File
@@ -283,6 +283,28 @@ pub(super) fn param_on(s: &Segment, p: Vec2) -> f64 {
}
}
/// A polyline arc segment on `ga`'s circle that ends at `end` after turning through `sweep`
/// (signed, positive counterclockwise), run on round the circle to the nearest boundary edge: the
/// new end point and the segment's new bulge. `None` when no edge crosses the rest of the circle.
fn extend_arc_segment(ga: &Arc, end: Vec2, sweep: f64, cut: &[Segment]) -> Option<(Vec2, f64)> {
let full = Segment::Arc { arc: Arc { start: 0.0, end: TAU - 1e-12, ..*ga }, ccw: true };
let a_end = ga.center.angle_to(end);
let ccw = sweep > 0.0;
// How far past `end` each crossing is, turning the way the segment turns; crossings on the
// segment itself (the last |sweep| of a turn) are not ahead of it.
let (x, more) = cut
.iter()
.flat_map(|c| intersect_ext(&full, c, false))
.map(|x| {
let a = ga.center.angle_to(x);
(x, norm_angle(if ccw { a - a_end } else { a_end - a }))
})
.filter(|(_, more)| *more > 1e-9 && *more + sweep.abs() < TAU - 1e-9)
.min_by(|a, b| a.1.total_cmp(&b.1))?;
let total = sweep.abs() + more;
Some((x, cadcraft_geom::arc_to_bulge(if ccw { total } else { -total })))
}
/// Extend the end of `h` nearest `pick` to the nearest boundary edge.
pub(crate) fn extend(s: &mut Session, h: Handle, pick: Vec2, edges: Edges) -> Result<()> {
if crate::cmd::curves::is_locked(s, h) {
@@ -326,18 +348,34 @@ pub(crate) fn extend(s: &mut Session, h: Handle, pick: Vec2, edges: Edges) -> Re
let (i_end, i_prev) = if from_end { (n - 1, n - 2) } else { (0, 1) };
let end = vs.get(i_end).map(|v| v.p).unwrap_or_default();
let prev = vs.get(i_prev).map(|v| v.p).unwrap_or_default();
let dir = (end - prev).normalized();
let ray = Segment::Line(Line::new(end, end + dir * 1e8));
let np = cut
.iter()
.flat_map(|c| intersect_ext(&ray, c, false))
.filter(|x| (*x - end).dot(dir) > 1e-9)
.min_by(|x, y| x.dist(end).total_cmp(&y.dist(end)))
.ok_or_else(|| EngineError::Other("Object does not intersect an edge.".into()))?;
if let Some(v) = vs.get_mut(i_end) {
v.p = np;
// The end segment's bulge, signed for travel from `prev` out to `end`.
let i_seg = if from_end { i_prev } else { i_end };
let bulge = vs.get(i_seg).map_or(0.0, |v| if from_end { v.bulge } else { -v.bulge });
let none = || EngineError::Other("Object does not intersect an edge.".into());
if let Some((ga, _)) = cadcraft_geom::bulge_to_arc(prev, end, bulge) {
// An arc end segment runs on round its circle, as an arc does.
let (np, b) = extend_arc_segment(&ga, end, 4.0 * bulge.atan(), &cut).ok_or_else(none)?;
if let Some(v) = vs.get_mut(i_end) {
v.p = np;
}
if let Some(v) = vs.get_mut(i_seg) {
v.bulge = if from_end { b } else { -b };
}
} else {
let dir = (end - prev).normalized();
let ray = Segment::Line(Line::new(end, end + dir * 1e8));
let np = cut
.iter()
.flat_map(|c| intersect_ext(&ray, c, false))
.filter(|x| (*x - end).dot(dir) > 1e-9)
.min_by(|x, y| x.dist(end).total_cmp(&y.dist(end)))
.ok_or_else(none)?;
if let Some(v) = vs.get_mut(i_end) {
v.p = np;
}
}
lwpoly(vs, false)
// Width, elevation and linetype generation stay as they were.
EntityKind::LwPolyline(cadcraft_doc::LwPolyline { vertices: vs, ..p.clone() })
}
k => crate::cmd::modify2::extend_other(k, &cut, pick)?,
};