diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index ebe55b0f0..9ab312912 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -3216,16 +3216,17 @@ impl OpenCADStudio { apply_pedit, convert_to_polyline, PeditOp, }; match &op { - PeditOp::JoinSelection(handles, fuzz) => { + PeditOp::JoinSelection(handles, fuzz, kind) => { 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)) }).collect::>(); + let connector_distance = crate::modules::draw::modify::pedit::selection_connector_distance(&available, *fuzz); let mut changes = Vec::new(); 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::pedit::join_selection_extend(&source, &candidates, *fuzz) { + if let Some((mut result, consumed)) = crate::modules::draw::modify::pedit::join_selection_extend(&source, &candidates, *fuzz, *kind, connector_distance) { *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 a021852c1..6bc655f0d 100644 --- a/src/modules/draw/modify/pedit.rs +++ b/src/modules/draw/modify/pedit.rs @@ -12,6 +12,8 @@ use crate::command::{CadCommand, CmdResult}; use crate::t; const TAU: f64 = std::f64::consts::TAU; +pub use cadkernel::space::endpoint_join::JoinType; +static JOIN_TYPE: std::sync::Mutex = std::sync::Mutex::new(JoinType::Extend); static JOIN_FUZZ: std::sync::Mutex = std::sync::Mutex::new(0.0); /// What PEDIT knows about a pickable entity, captured at dispatch. @@ -33,6 +35,7 @@ enum Mode { MultipleGather, MultipleConvert, MultipleJoin, + JoinType, Options, /// Picked a Line/Arc: asking "Turn it into one? [Yes/No]". ConvertPrompt(Handle), @@ -60,6 +63,7 @@ pub struct PeditCommand { multiple_history: Vec>, pending_multiple: Option>, join_fuzz: f64, + join_type: JoinType, info: HashMap, mode: Mode, undo_count: usize, @@ -94,6 +98,7 @@ impl PeditCommand { multiple_history: Vec::new(), pending_multiple: None, join_fuzz: *JOIN_FUZZ.lock().unwrap_or_else(|error| error.into_inner()), + join_type: *JOIN_TYPE.lock().unwrap_or_else(|error| error.into_inner()), info, mode: Mode::PickTarget, undo_count: 0, @@ -238,7 +243,8 @@ 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 => format!("PEDIT Join type: Extend. Enter fuzz distance <{}>:", self.join_fuzz), + Mode::MultipleJoin => format!("PEDIT Join type: {:?}. Enter fuzz distance or [Jointype] <{}>:", self.join_type, self.join_fuzz), + Mode::JoinType => format!("PEDIT Enter join type [Extend/Add/Both] <{:?}>:", self.join_type), 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] :" @@ -326,6 +332,8 @@ impl CadCommand for PeditCommand { vec![CmdOption::new(t!("Yes").as_ref(), "Y"), CmdOption::new(t!("No").as_ref(), "N")] } Mode::JoinGather(_) => vec![CmdOption::enter(t!("Join").as_ref())], + Mode::MultipleJoin => vec![CmdOption::new("Jointype", "J")], + Mode::JoinType => vec![CmdOption::new("Extend", "E"), CmdOption::new("Add", "A"), CmdOption::new("Both", "B")], Mode::PolyVertex(_) => vec![CmdOption::new("Next", "N"), CmdOption::new("Previous", "P"), CmdOption::new("Break", "B"), CmdOption::new("Straighten", "S"), CmdOption::new("Tangent", "T"), CmdOption::new("Insert", "I"), CmdOption::new("Move", "M"), CmdOption::new("Width", "W"), CmdOption::new("Exit", "X")], Mode::PolyRange { .. } => vec![CmdOption::new("Next", "N"), CmdOption::new("Previous", "P"), CmdOption::new("Go", "G"), CmdOption::new("Exit", "X")], Mode::AwaitLinetype => vec![CmdOption::new("On", "ON"), CmdOption::new("Off", "OFF")], @@ -440,14 +448,21 @@ impl CadCommand for PeditCommand { if matches!(up.as_str(), "M" | "MULTIPLE") { self.mode = Mode::MultipleGather; Some(CmdResult::NeedPoint) } else { None } } Mode::MultipleGather => None, + Mode::JoinType => { + self.join_type = match up.as_str() { "E" | "EXTEND" => JoinType::Extend, "A" | "ADD" => JoinType::Add, "B" | "BOTH" => JoinType::Both, _ => return None }; + *JOIN_TYPE.lock().unwrap_or_else(|error| error.into_inner()) = self.join_type; + self.mode = Mode::MultipleJoin; + Some(CmdResult::NeedPoint) + } Mode::MultipleJoin => { + if matches!(up.as_str(), "J" | "JOINTYPE") { self.mode = Mode::JoinType; return Some(CmdResult::NeedPoint); } let fuzz = text.trim().parse::().ok()?; 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(), fuzz) }); + return Some(CmdResult::PeditOp { handle: self.target?, op: PeditOp::JoinSelection(self.multiple.clone(), fuzz, self.join_type) }); }, Mode::MultipleConvert => { match up.as_str() { @@ -768,6 +783,7 @@ impl CadCommand for PeditCommand { } Mode::MultipleConvert => self.on_text_input("Y").unwrap_or(CmdResult::NeedPoint), Mode::MultipleJoin => self.on_text_input(&self.join_fuzz.to_string()).unwrap_or(CmdResult::NeedPoint), + Mode::JoinType => { self.mode = Mode::MultipleJoin; 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 } @@ -805,7 +821,7 @@ impl CadCommand for PeditCommand { #[derive(Clone)] pub enum PeditOp { Multiple(Vec, Box), - JoinSelection(Vec, f64), + JoinSelection(Vec, f64, JoinType), SetClosed(bool), SetWidth(f64), SetVertexWidth { index: usize, start: f64, end: f64 }, @@ -871,7 +887,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 { @@ -1327,7 +1343,7 @@ inventory::submit!(crate::command::CommandRegistration { names: &["PEDIT"] }); /// 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)> { +pub fn join_selection_extend(source: &EntityType, candidates: &[(Handle, &EntityType)], fuzz: f64, kind: JoinType, connector_distance: Option) -> 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 => { @@ -1369,17 +1385,32 @@ pub fn join_selection_extend(source: &EntityType, candidates: &[(Handle, &Entity 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;}; + if joined.is_none() && fuzz > 0.0 && fuzz.is_finite() && (kind == JoinType::Extend || connector_distance.is_some()) { + let a_ends = [false,true].into_iter().filter_map(|start| endpoint(&result,start).map(|points|(start,points))).collect::>(); + let b_ends = [true,false].into_iter().filter_map(|start| endpoint(candidate,start).map(|points|(start,points))).collect::>(); + let a_points = a_ends.iter().map(|(_,points)|*points).collect::>(); + let b_points = b_ends.iter().map(|(_,points)|*points).collect::>(); + if let Some((a_index,b_index,joint)) = cadkernel::space::endpoint_join::closest_line_end_join(&a_points,&b_points,fuzz,connector_distance.unwrap_or(0.0),kind) { + let (a_start,a_points) = a_ends[a_index]; let (b_start,b_points) = b_ends[b_index]; 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; - } - }} + let joined_pair = if let Some(point) = joint { + if move_endpoint(&mut a,a_start,point).is_none() || move_endpoint(&mut b,b_start,point).is_none() {continue;} + super::join::join_to_source(&a,&[(*handle,&b)]) + } else { + let mut line = acadrust::entities::Line::new(); + line.start = Vector3::new(a_points[1][0],a_points[1][1],a_points[1][2]); + line.end = Vector3::new(b_points[1][0],b_points[1][1],b_points[1][2]); + match &a { + EntityType::LwPolyline(p) => {line.normal=p.normal.clone();line.thickness=p.thickness;}, + EntityType::Polyline2D(p) => {line.normal=p.normal.clone();line.thickness=p.thickness;}, + _ => continue, + } + let connector = EntityType::Line(line); + super::join::join_to_source(&a,&[(Handle::NULL,&connector),(*handle,&b)]) + .filter(|(_,used)|used.contains(handle)) + }; + if let Some((entity,_)) = joined_pair { joined = Some(entity); } + } } if let Some(mut entity) = joined { *entity.common_mut() = source.common().clone(); @@ -1390,3 +1421,22 @@ pub fn join_selection_extend(source: &EntityType, candidates: &[(Handle, &Entity } (!consumed.is_empty()).then_some((result,consumed)) } + + +/// Compute the connector search distance once from the complete selected set. +/// Unsupported, curved or noncoplanar paths disable nonzero Add/Both joining. +pub fn selection_connector_distance(entities: &[(Handle, EntityType)], fuzz: f64) -> Option { + let mut points = Vec::new(); let mut elevation: Option = None; + for (_,entity) in entities { + let (vertices,normal,z) = match entity { + EntityType::LwPolyline(p) if !p.is_closed && p.vertices.len() >= 2 && p.vertices.iter().all(|v| v.bulge == 0.0) => + (p.vertices.iter().map(|v|[v.location.x,v.location.y]).collect::>(),&p.normal,p.elevation), + EntityType::Polyline2D(p) if !p.is_closed() && p.vertices.len() >= 2 && p.vertices.iter().all(|v| v.bulge == 0.0) => + (p.vertices.iter().map(|v|[v.location.x,v.location.y]).collect::>(),&p.normal,p.elevation), + _ => return None, + }; + if !z.is_finite() || normal.x != 0.0 || normal.y != 0.0 || !normal.z.is_finite() || normal.z <= 0.0 || elevation.is_some_and(|value|value != z) {return None;} + elevation = Some(z); points.extend(vertices); + } + cadkernel::space::endpoint_join::planar_connector_distance(&points,fuzz) +}