From 8ff7b6b25cc75965ab892cede507fbd631016117 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 21 Jul 2026 14:58:56 +0300 Subject: [PATCH] feat(extend): arcs extend to their boundaries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EXTEND's pick dispatcher covered lines, ellipses, polylines and splines but never arcs, though TRIM already handled them (#409). extend_arc gathers every boundary crossing around the arc's full circle (reusing arc_seg_ts against all boundary kinds) and walks the clicked end to the nearest one — CCW for the end angle, CW for the start. The hover preview draws the extended result like the other types, and the replace-snapshot patches the new Arc's handle. Co-Authored-By: Claude Fable 5 --- src/modules/draw/modify/trim.rs | 74 +++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/modules/draw/modify/trim.rs b/src/modules/draw/modify/trim.rs index 9c4dc5dfa..d00e1f36c 100644 --- a/src/modules/draw/modify/trim.rs +++ b/src/modules/draw/modify/trim.rs @@ -1221,6 +1221,62 @@ fn trim_line(orig: &LineEnt, ts: &[f64], t_click: f64) -> Vec { .collect() } +/// Extend a clicked Arc end along its circle to the nearest boundary +/// crossing (#409). `t_click` ∈ [0,1] within the current span picks the end: +/// ≥ 0.5 extends the end angle CCW, < 0.5 extends the start angle CW. +fn extend_arc(orig: &ArcEnt, t_click: f64, geos: &[Geo]) -> Option { + let cx = orig.center.x; + let cy = orig.center.y; + let r = orig.radius; + if r < 1e-9 { + return None; + } + let a0 = orig.start_angle; + let a1 = orig.end_angle; + let span = { + let s = norm(a1) - norm(a0); + if s <= 0.0 { + s + TAU + } else { + s + } + }; + if span >= TAU - 1e-9 { + return None; // already a full circle + } + // Boundary crossings around the FULL circle, then re-expressed relative + // to the arc's own span: t ∈ [0,1] lies on the arc, t > 1 walks CCW past + // the end and wraps around toward the start. + let ts: Vec = arc_seg_ts(cx, cy, r, a0, a0 + TAU, orig.common.handle, geos) + .into_iter() + .map(|t| t * TAU / span) + .collect(); + let extend_end = t_click >= 0.5; + let best = if extend_end { + // First crossing CCW past the end. + ts.iter() + .copied() + .filter(|&t| t > 1.0 + 1e-6) + .min_by(|x, y| x.partial_cmp(y).unwrap()) + } else { + // First crossing CW before the start — in wrapped span-relative + // coordinates that is the LARGEST t outside the arc. + ts.iter() + .copied() + .filter(|&t| t > 1.0 + 1e-6) + .max_by(|x, y| x.partial_cmp(y).unwrap()) + }?; + let ang = norm(a0) + span * best; + let mut a = orig.clone(); + a.common.handle = Handle::NULL; + if extend_end { + a.end_angle = ang; + } else { + a.start_angle = ang; + } + Some(EntityType::Arc(a)) +} + /// Trim an Arc entity. Returns the surviving arc segments. fn trim_arc(orig: &ArcEnt, ts: &[f64], t_click: f64) -> Vec { let a0 = orig.start_angle; @@ -2579,6 +2635,11 @@ impl CadCommand for ExtendCommand { }; extend_line(l, t_click, &self.geos) } + Some(EntityType::Arc(a)) => { + let ang = (pt.y as f64 - a.center.y).atan2(pt.x as f64 - a.center.x); + let t_click = arc_t(ang, a.start_angle, a.end_angle); + extend_arc(a, t_click, &self.geos) + } Some(EntityType::Ellipse(e)) => { let t0 = e.start_parameter; let mut t1 = e.end_parameter; @@ -2633,6 +2694,7 @@ impl CadCommand for ExtendCommand { // Update the snapshot entry: replace geometry + assign real handle. match &mut new_entity { EntityType::Line(l) => l.common.handle = new_handle, + EntityType::Arc(a) => a.common.handle = new_handle, EntityType::Ellipse(e) => e.common.handle = new_handle, EntityType::Spline(s) => s.common.handle = new_handle, EntityType::LwPolyline(p) => p.common.handle = new_handle, @@ -2682,6 +2744,18 @@ impl CadCommand for ExtendCommand { )]; } } + Some(EntityType::Arc(a)) => { + let ang = (pt.y as f64 - a.center.y).atan2(pt.x as f64 - a.center.x); + let t_click = arc_t(ang, a.start_angle, a.end_angle); + if let Some(ext) = extend_arc(a, t_click, &self.geos) { + return vec![WireModel::solid( + "extend_prev".into(), + entity_pts(&ext), + WireModel::CYAN, + false, + )]; + } + } Some(EntityType::Ellipse(e)) => { let a = (e.major_axis.x.powi(2) + e.major_axis.y.powi(2)).sqrt(); if a >= 1e-9 {