From aa11b10c9a60dd7761dbde56d4e675e488c69020 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:05:50 +0300 Subject: [PATCH] Extend selected polyline terminals using bounded kernel intersections --- src/app/command_driver.rs | 4 +- src/modules/draw/modify/pedit.rs | 84 +++++++++++++++++++++++++++++--- 2 files changed, 80 insertions(+), 8 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 0017ad2b2..ebe55b0f0 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -3216,7 +3216,7 @@ impl OpenCADStudio { apply_pedit, convert_to_polyline, PeditOp, }; match &op { - PeditOp::JoinSelection(handles) => { + PeditOp::JoinSelection(handles, fuzz) => { let mut available = handles.iter().filter_map(|handle| { if self.tabs[i].scene.is_layer_locked(*handle) { return None; } self.tabs[i].scene.document.get_entity(*handle).cloned().map(|entity| (*handle, entity)) @@ -3225,7 +3225,7 @@ impl OpenCADStudio { while !available.is_empty() { let (source_handle, source) = available.remove(0); let candidates = available.iter().map(|(handle, entity)| (*handle, entity)).collect::>(); - if let Some((mut result, consumed)) = crate::modules::draw::modify::join::join_to_source(&source, &candidates) { + if let Some((mut result, consumed)) = crate::modules::draw::modify::pedit::join_selection_extend(&source, &candidates, *fuzz) { *result.common_mut() = source.common().clone(); available.retain(|(handle, _)| !consumed.contains(handle)); changes.push((source_handle, result, consumed)); diff --git a/src/modules/draw/modify/pedit.rs b/src/modules/draw/modify/pedit.rs index 963d9d350..a021852c1 100644 --- a/src/modules/draw/modify/pedit.rs +++ b/src/modules/draw/modify/pedit.rs @@ -12,6 +12,7 @@ use crate::command::{CadCommand, CmdResult}; use crate::t; const TAU: f64 = std::f64::consts::TAU; +static JOIN_FUZZ: std::sync::Mutex = std::sync::Mutex::new(0.0); /// What PEDIT knows about a pickable entity, captured at dispatch. #[derive(Clone, Copy)] @@ -58,6 +59,7 @@ pub struct PeditCommand { multiple: Vec, multiple_history: Vec>, pending_multiple: Option>, + join_fuzz: f64, info: HashMap, mode: Mode, undo_count: usize, @@ -91,6 +93,7 @@ impl PeditCommand { multiple: Vec::new(), multiple_history: Vec::new(), pending_multiple: None, + join_fuzz: *JOIN_FUZZ.lock().unwrap_or_else(|error| error.into_inner()), info, mode: Mode::PickTarget, undo_count: 0, @@ -235,7 +238,7 @@ impl CadCommand for PeditCommand { t!("PEDIT Select polyline (or a line/arc to convert) or [Multiple]:").into_owned() } Mode::MultipleGather => format!("PEDIT Select objects ({} selected, Enter when done):", self.multiple.len()), - Mode::MultipleJoin => "PEDIT Enter fuzz distance <0>:".to_string(), + Mode::MultipleJoin => format!("PEDIT Join type: Extend. Enter fuzz distance <{}>:", self.join_fuzz), Mode::MultipleConvert => t!("PEDIT Convert lines and arcs to polylines [Yes/No] :").into_owned(), Mode::ConvertPrompt(_) => t!( "PEDIT Object is not a polyline. Turn it into one? [Yes/No] :" @@ -439,10 +442,12 @@ impl CadCommand for PeditCommand { Mode::MultipleGather => None, Mode::MultipleJoin => { let fuzz = text.trim().parse::().ok()?; - if fuzz != 0.0 { return Some(CmdResult::ReportError("PEDIT: only zero fuzz distance is currently supported.".to_string())); } + if !fuzz.is_finite() || fuzz < 0.0 { return Some(CmdResult::ReportError("PEDIT: fuzz distance must be nonnegative and finite.".to_string())); } + self.join_fuzz = fuzz; + *JOIN_FUZZ.lock().unwrap_or_else(|error| error.into_inner()) = fuzz; self.pending_multiple = Some(self.multiple.clone()); self.mode = Mode::Options; - return Some(CmdResult::PeditOp { handle: self.target?, op: PeditOp::JoinSelection(self.multiple.clone()) }); + return Some(CmdResult::PeditOp { handle: self.target?, op: PeditOp::JoinSelection(self.multiple.clone(), fuzz) }); }, Mode::MultipleConvert => { match up.as_str() { @@ -762,7 +767,7 @@ impl CadCommand for PeditCommand { CmdResult::NeedPoint } Mode::MultipleConvert => self.on_text_input("Y").unwrap_or(CmdResult::NeedPoint), - Mode::MultipleJoin => self.on_text_input("0").unwrap_or(CmdResult::NeedPoint), + Mode::MultipleJoin => self.on_text_input(&self.join_fuzz.to_string()).unwrap_or(CmdResult::NeedPoint), Mode::PolyWidthStart(_, width) | Mode::PolyWidthEnd(_, width) => { let value = width.to_string(); self.on_text_input(&value).unwrap_or(CmdResult::NeedPoint) } Mode::PolyVertex(_) | Mode::PolyRange { .. } => self.on_text_input("N").unwrap_or(CmdResult::NeedPoint), Mode::PolyMove(_) | Mode::PolyInsert(_) => { self.mode = Mode::Options; CmdResult::NeedPoint } @@ -800,7 +805,7 @@ impl CadCommand for PeditCommand { #[derive(Clone)] pub enum PeditOp { Multiple(Vec, Box), - JoinSelection(Vec), + JoinSelection(Vec, f64), SetClosed(bool), SetWidth(f64), SetVertexWidth { index: usize, start: f64, end: f64 }, @@ -866,7 +871,7 @@ pub fn edit_vertex_range(entity: &EntityType, first: usize, last: usize, split: pub fn apply_pedit(entity: &mut EntityType, op: &PeditOp) -> bool { match op { - PeditOp::Multiple(_, _) | PeditOp::JoinSelection(_) | PeditOp::VertexRange { .. } => false, + PeditOp::Multiple(_, _) | PeditOp::JoinSelection(_, _) | PeditOp::VertexRange { .. } => false, PeditOp::SetVertexWidth { index, start, end } => { if !start.is_finite() || !end.is_finite() || *start < 0.0 || *end < 0.0 { return false; } match entity { @@ -1318,3 +1323,70 @@ fn spline_smooth(p: &mut acadrust::LwPolyline) -> bool { // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { names: &["PEDIT"] }); // PeditCommand + + +/// Extend straight terminal spans before reusing the ordinary source join. +/// Curved terminal spans and unsupported representations remain unchanged. +pub fn join_selection_extend(source: &EntityType, candidates: &[(Handle, &EntityType)], fuzz: f64) -> Option<(EntityType, Vec)> { + fn endpoint(entity: &EntityType, start: bool) -> Option<[[f64; 3]; 2]> { + let (points, normal, elevation) = match entity { + EntityType::LwPolyline(p) if !p.is_closed && p.vertices.len() >= 2 => { + let n = p.vertices.len(); let segment = if start {0} else {n-2}; + if p.vertices[segment].bulge != 0.0 { return None; } + let indices = if start {[1,0]} else {[n-2,n-1]}; + (indices.map(|i| [p.vertices[i].location.x,p.vertices[i].location.y]),p.normal.clone(),p.elevation) + } + EntityType::Polyline2D(p) if !p.is_closed() && p.vertices.len() >= 2 => { + let n = p.vertices.len(); let segment = if start {0} else {n-2}; + if p.vertices[segment].bulge != 0.0 { return None; } + let indices = if start {[1,0]} else {[n-2,n-1]}; + (indices.map(|i| [p.vertices[i].location.x,p.vertices[i].location.y]),p.normal.clone(),p.elevation) + } + _ => return None, + }; + let plane = crate::entities::curve::ocs_plane(normal,elevation); + Some(points.map(|point| plane.point_at(point))) + } + fn move_endpoint(entity: &mut EntityType, start: bool, point: [f64;3]) -> Option<()> { + match entity { + EntityType::LwPolyline(p) => { + let local = crate::entities::curve::ocs_plane(p.normal.clone(),p.elevation).project(point)?; + let index = if start {0} else {p.vertices.len().checked_sub(1)?}; + p.vertices[index].location = Vector2::new(local[0],local[1]); + } + EntityType::Polyline2D(p) => { + let local = crate::entities::curve::ocs_plane(p.normal.clone(),p.elevation).project(point)?; + let index = if start {0} else {p.vertices.len().checked_sub(1)?}; + p.vertices[index].location.x = local[0]; p.vertices[index].location.y = local[1]; + } + _ => return None, + } + Some(()) + } + let mut result = source.clone(); let mut consumed = Vec::new(); + loop { + let mut progress = false; + for (handle,candidate) in candidates { + if consumed.contains(handle) {continue;} + let mut joined = super::join::join_to_source(&result,&[(*handle,*candidate)]).map(|(entity,_)|entity); + if joined.is_none() && fuzz > 0.0 && fuzz.is_finite() { + 'ends: for a_start in [false,true] { for b_start in [true,false] { + let Some(a) = endpoint(&result,a_start) else {continue;}; + let Some(b) = endpoint(candidate,b_start) else {continue;}; + let Some(point) = cadkernel::space::endpoint_join::extend_line_ends(a,b,fuzz) else {continue;}; + let mut a = result.clone(); let mut b = (*candidate).clone(); + if move_endpoint(&mut a,a_start,point).is_none() || move_endpoint(&mut b,b_start,point).is_none() {continue;} + if let Some((entity,_)) = super::join::join_to_source(&a,&[(*handle,&b)]) { + joined = Some(entity); break 'ends; + } + }} + } + if let Some(mut entity) = joined { + *entity.common_mut() = source.common().clone(); + result = entity; consumed.push(*handle); progress = true; + } + } + if !progress {break;} + } + (!consumed.is_empty()).then_some((result,consumed)) +}