From 376faec4db2031622bf4efa6e4c320458a28b767 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 10:25:12 +0300 Subject: [PATCH 1/8] Add polyline vertex and multiple-object editing --- src/app/command_driver.rs | 45 +++++- src/app/commands/inquiry.rs | 1 + src/modules/draw/modify/pedit.rs | 215 +++++++++++++++++++++++++++-- src/modules/draw/modify/reverse.rs | 2 +- 4 files changed, 249 insertions(+), 14 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 87635b15f..7737ae7cc 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -2312,8 +2312,12 @@ impl OpenCADStudio { self.tabs[i].scene.invalidate_dim_block_recorded(nh); } } + let pedit_entities = if self.tabs[i].active_cmd.as_ref().is_some_and(|command| command.name() == "PEDIT") { + new_handles.iter().filter_map(|new| self.tabs[i].scene.document.get_entity(*new).cloned()).collect::>() + } else { Vec::new() }; if let Some(cmd) = &mut self.tabs[i].active_cmd { cmd.on_entity_replaced(handle, &new_handles); + for entity in pedit_entities { cmd.inject_picked_entity(entity); } } self.tabs[i].dirty = true; let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); @@ -3205,13 +3209,44 @@ impl OpenCADStudio { self.restore_pre_cmd_tangent(); } CmdResult::PeditOp { handle, op } => { - if self.reject_locked_edit(i, handle) { + if !matches!(&op, crate::modules::draw::modify::pedit::PeditOp::Multiple(_, _)) && self.reject_locked_edit(i, handle) { return Task::none(); } use crate::modules::draw::modify::pedit::{ apply_pedit, convert_to_polyline, PeditOp, }; match &op { + PeditOp::Multiple(handles, operation) => { + let replacements: Vec<_> = handles.iter().filter(|handle| !self.tabs[i].scene.is_layer_locked(**handle)).filter_map(|handle| { + let original = self.tabs[i].scene.document.get_entity(*handle)?; + if matches!(operation.as_ref(), PeditOp::ConvertToPolyline) { + convert_to_polyline(original).map(|replacement| (*handle, replacement, true)) + } else { + let mut replacement = original.clone(); + apply_pedit(&mut replacement, operation).then_some((*handle, replacement, false)) + } + }).collect(); + if replacements.is_empty() { return Task::none(); } + self.push_undo_snapshot(i, "PEDIT"); + for (handle, replacement, converted) in replacements { + let updated_handle = if converted { + self.tabs[i].scene.erase_entities(&[handle]); + let new_handle = self.tabs[i].scene.add_entity(replacement); + if let Some(command) = self.tabs[i].active_cmd.as_mut() { command.on_entity_replaced(handle, &[new_handle]); } + new_handle + } else { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(handle) { *entity = replacement; } + self.tabs[i].scene.refresh_fill_model(handle); + self.tabs[i].scene.bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]); + handle + }; + let updated = self.tabs[i].scene.document.get_entity(updated_handle).cloned(); + if let (Some(command), Some(entity)) = (self.tabs[i].active_cmd.as_mut(), updated) { command.inject_picked_entity(entity); } + } + if let Some(command) = self.tabs[i].active_cmd.as_mut() { command.on_pedit_applied(); } + self.tabs[i].dirty = true; + self.refresh_properties(); + } // The convert replaces the entity (new handle). PeditOp::ConvertToPolyline => { let converted = self.tabs[i] @@ -3240,7 +3275,9 @@ impl OpenCADStudio { .map(|e| apply_pedit(e, &op)) .unwrap_or(false); if changed { + let updated = self.tabs[i].scene.document.get_entity(handle).cloned(); if let Some(command) = self.tabs[i].active_cmd.as_mut() { + if let Some(entity) = updated { command.inject_picked_entity(entity); } command.on_pedit_applied(); } self.tabs[i].dirty = true; @@ -4749,6 +4786,12 @@ impl OpenCADStudio { let active = self.tabs[i].active_cmd.take(); self.undo_active_tab(); self.tabs[i].active_cmd = active; + if self.tabs[i].active_cmd.as_ref().is_some_and(|command| command.name() == "PEDIT") { + let entities: Vec<_> = self.tabs[i].scene.document.entities().cloned().collect(); + if let Some(command) = self.tabs[i].active_cmd.as_mut() { + for entity in entities { command.inject_picked_entity(entity); } + } + } let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); if let Some(p) = prompt { self.command_line.push_info(&p); diff --git a/src/app/commands/inquiry.rs b/src/app/commands/inquiry.rs index 08f920696..63de892f9 100644 --- a/src/app/commands/inquiry.rs +++ b/src/app/commands/inquiry.rs @@ -294,6 +294,7 @@ impl OpenCADStudio { header.surface_u_density, header.surface_v_density, ) + .with_entities(self.tabs[i].scene.document.entities().cloned()) .with_preselection(&preselected); self.command_line.push_info(&cmd_obj.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); diff --git a/src/modules/draw/modify/pedit.rs b/src/modules/draw/modify/pedit.rs index 54e53e178..8f6c3214a 100644 --- a/src/modules/draw/modify/pedit.rs +++ b/src/modules/draw/modify/pedit.rs @@ -29,10 +29,16 @@ pub struct PeditTarget { enum Mode { PickTarget, + MultipleGather, + MultipleConvert, Options, /// Picked a Line/Arc: asking "Turn it into one? [Yes/No]". ConvertPrompt(Handle), AwaitWidth, + AwaitLinetype, + PolyVertex(usize), + PolyMove(usize), + PolyInsert(usize), /// Join: gathering additional segments; Enter merges. JoinGather(Vec), /// Polygon-mesh vertex navigation uses the mesh's row-major control net. @@ -43,6 +49,10 @@ enum Mode { pub struct PeditCommand { target: Option, + entities: HashMap, + multiple: Vec, + multiple_history: Vec>, + pending_multiple: Option>, info: HashMap, mode: Mode, undo_count: usize, @@ -69,6 +79,10 @@ impl PeditCommand { }; Self { target: None, + entities: HashMap::default(), + multiple: Vec::new(), + multiple_history: Vec::new(), + pending_multiple: None, info, mode: Mode::PickTarget, undo_count: 0, @@ -86,6 +100,17 @@ impl PeditCommand { /// Adopt a pre-selected entity (pickfirst): a selected polyline skips the /// pick step, a selected line/arc goes straight to the convert prompt. pub fn with_preselection(mut self, handles: &[Handle]) -> Self { + let multiple: Vec<_> = handles.iter().copied().filter(|handle| { + self.info.get(&handle.value()).is_some_and(|info| info.mesh_size.is_none()) + }).collect(); + if multiple.len() > 1 { + self.target = multiple.first().copied(); + self.mode = if multiple.iter().any(|handle| self.info.get(&handle.value()).is_some_and(|info| info.convertible)) { + Mode::MultipleConvert + } else { Mode::Options }; + self.multiple = multiple; + return self; + } for &h in handles { let Some(info) = self.info.get(&h.value()).copied() else { continue; @@ -103,6 +128,26 @@ impl PeditCommand { self } + pub fn with_entities(mut self, entities: impl IntoIterator) -> Self { + self.entities = entities.into_iter().map(|entity| (entity.common().handle.value(), entity)).collect(); + self + } + + fn multiple_result(&self, result: Option) -> Option { + match result { + Some(CmdResult::PeditOp { handle, op }) if !self.multiple.is_empty() => + Some(CmdResult::PeditOp { handle, op: PeditOp::Multiple(self.multiple.clone(), Box::new(op)) }), + other => other, + } + } + + fn vertex_count(&self) -> usize { + self.target.and_then(|handle| self.entities.get(&handle.value())).map_or(0, |entity| match entity { + EntityType::LwPolyline(polyline) => polyline.vertices.len(), + EntityType::Polyline2D(polyline) => polyline.vertices.len(), + _ => 0, + }) + } fn mesh_size(&self) -> Option<(usize, usize)> { let handle = self.target?; self.info.get(&handle.value())?.mesh_size @@ -148,13 +193,16 @@ impl CadCommand for PeditCommand { fn prompt(&self) -> String { match &self.mode { Mode::PickTarget => { - t!("PEDIT Select polyline (or a line/arc to convert):").into_owned() + 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::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] :" ) .into_owned(), Mode::AwaitWidth => t!("PEDIT Specify new width:").into_owned(), + Mode::AwaitLinetype => t!("PEDIT Enter polyline linetype generation option [ON/OFF]:").into_owned(), Mode::JoinGather(list) => t!( "PEDIT Join Select objects to join (%{count} picked), Enter to merge:", count = list.len().saturating_sub(1) @@ -170,6 +218,9 @@ impl CadCommand for PeditCommand { vertex = index + 1 ) .into_owned(), + Mode::PolyVertex(index) => format!("PEDIT Vertex {} [Next/Previous/Insert/Move/eXit] :", index + 1), + Mode::PolyMove(index) => format!("PEDIT Specify new location for vertex {}:", index + 1), + Mode::PolyInsert(index) => format!("PEDIT Specify location after vertex {}:", index + 1), Mode::Options => t!("PEDIT Enter option:").into_owned(), } } @@ -177,6 +228,8 @@ impl CadCommand for PeditCommand { fn options(&self) -> Vec { use crate::command::CmdOption; match &self.mode { + Mode::PickTarget => vec![CmdOption::new("Multiple", "M")], + Mode::MultipleConvert => vec![CmdOption::new("Yes", "Y"), CmdOption::new("No", "N")], Mode::Options if self.mesh_size().is_some() => { let (closed_m, closed_n) = self.mesh_closed().unwrap_or((false, false)); vec![ @@ -205,8 +258,12 @@ impl CadCommand for PeditCommand { CmdOption::new(t!("Fit").as_ref(), "F"), CmdOption::new(t!("Spline").as_ref(), "S"), CmdOption::new(t!("Decurve").as_ref(), "D"), + CmdOption::new(t!("Edit vertex").as_ref(), "E"), + CmdOption::new(t!("Ltype gen").as_ref(), "L"), + CmdOption::new(t!("Reverse").as_ref(), "R"), + CmdOption::new(t!("Undo").as_ref(), "U"), CmdOption::new(t!("eXit").as_ref(), "X"), - ], + ].into_iter().filter(|option| self.multiple.is_empty() || option.keyword != "E").collect(), Mode::MeshVertex(_) => vec![ CmdOption::new(t!("Next").as_ref(), "N"), CmdOption::new(t!("Previous").as_ref(), "P"), @@ -222,6 +279,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::PolyVertex(_) => vec![CmdOption::new("Next", "N"), CmdOption::new("Previous", "P"), CmdOption::new("Insert", "I"), CmdOption::new("Move", "M"), CmdOption::new("Exit", "X")], + Mode::AwaitLinetype => vec![CmdOption::new("On", "ON"), CmdOption::new("Off", "OFF")], Mode::MeshVertexMove(_) => vec![], _ => vec![], } @@ -234,10 +293,14 @@ impl CadCommand for PeditCommand { fn is_selection_gathering(&self) -> bool { // Join uses the normal selection system, so single picks AND // window/crossing boxes both gather objects. - matches!(self.mode, Mode::JoinGather(_)) + matches!(self.mode, Mode::JoinGather(_) | Mode::MultipleGather) } fn on_selection_complete(&mut self, handles: Vec) -> CmdResult { + if matches!(self.mode, Mode::MultipleGather) { + self.multiple = handles.into_iter().filter(|handle| self.info.get(&handle.value()).is_some_and(|info| info.mesh_size.is_none())).collect(); + return CmdResult::NeedPoint; + } if let (Some(target), Mode::JoinGather(list)) = (self.target, &mut self.mode) { list.clear(); list.push(target); @@ -275,7 +338,8 @@ impl CadCommand for PeditCommand { // A Yes-conversion (or Break) replaced the entity — adopt the first // piece as the live target and carry its bookkeeping over. if let Some(&nh) = new_handles.first() { - self.info.remove(&old.value()); + if self.multiple.is_empty() { self.info.remove(&old.value()); } + for handle in &mut self.multiple { if *handle == old { *handle = nh; } } self.info.insert( nh.value(), PeditTarget { @@ -290,8 +354,13 @@ impl CadCommand for PeditCommand { } } + fn inject_picked_entity(&mut self, entity: EntityType) { + self.entities.insert(entity.common().handle.value(), entity); + } + fn on_pedit_applied(&mut self) { self.undo_count = self.undo_count.saturating_add(1); + self.multiple_history.push(self.pending_multiple.take().unwrap_or_else(|| self.multiple.clone())); if let Some((m_direction, closed)) = self.pending_mesh_closed.take() { let previous = self.mesh_closed(); self.mesh_closed_history.push(previous); @@ -302,15 +371,35 @@ impl CadCommand for PeditCommand { } fn wants_text_input(&self) -> bool { - !matches!(self.mode, Mode::PickTarget | Mode::MeshVertexMove(_)) + !matches!(self.mode, Mode::PickTarget | Mode::MeshVertexMove(_) | Mode::PolyMove(_) | Mode::PolyInsert(_)) } fn on_text_input(&mut self, text: &str) -> Option { self.pending_mesh_closed = None; let up = text.trim().to_uppercase(); let mesh_size = self.mesh_size(); - match &mut self.mode { - Mode::PickTarget => None, + let vertex_count = self.vertex_count(); + let result = match &mut self.mode { + Mode::PickTarget => { + if matches!(up.as_str(), "M" | "MULTIPLE") { self.mode = Mode::MultipleGather; Some(CmdResult::NeedPoint) } else { None } + } + Mode::MultipleGather => None, + Mode::MultipleConvert => { + match up.as_str() { + "Y" | "YES" | "" => { + self.pending_multiple = Some(self.multiple.clone()); + self.mode = Mode::Options; + Some(CmdResult::PeditOp { handle: *self.multiple.first()?, op: PeditOp::ConvertToPolyline }) + } + "N" | "NO" => { + self.multiple.retain(|handle| self.info.get(&handle.value()).is_some_and(|info| info.is_poly)); + self.target = self.multiple.first().copied(); + self.mode = Mode::Options; + Some(if self.target.is_some() { CmdResult::NeedPoint } else { CmdResult::Cancel }) + } + _ => Some(CmdResult::NeedPoint), + } + } Mode::ConvertPrompt(handle) => { let handle = *handle; match up.as_str() { @@ -331,14 +420,31 @@ impl CadCommand for PeditCommand { .replace(',', ".") .parse() .ok() - .filter(|&v: &f64| v >= 0.0)?; + .filter(|&v: &f64| v.is_finite() && v >= 0.0)?; self.mode = Mode::Options; Some(CmdResult::PeditOp { handle, op: PeditOp::SetWidth(w), }) } + Mode::AwaitLinetype => { + let enabled = match up.as_str() { "ON" => true, "OFF" => false, _ => return Some(CmdResult::NeedPoint) }; + self.mode = Mode::Options; + Some(CmdResult::PeditOp { handle: self.target?, op: PeditOp::SetLinetypeGeneration(enabled) }) + } Mode::JoinGather(_) => None, + Mode::PolyMove(_) | Mode::PolyInsert(_) => None, + Mode::PolyVertex(index) => { + match up.as_str() { + "N" | "NEXT" => { if *index + 1 < vertex_count { *index += 1; } } + "P" | "PREVIOUS" => { *index = index.saturating_sub(1); } + "I" | "INSERT" => self.mode = Mode::PolyInsert(*index), + "M" | "MOVE" => self.mode = Mode::PolyMove(*index), + "X" | "EXIT" => self.mode = Mode::Options, + _ => {} + } + Some(CmdResult::NeedPoint) + } Mode::MeshVertexMove(_) => None, Mode::MeshVertex(index) => { let (m, n) = mesh_size?; @@ -442,6 +548,19 @@ impl CadCommand for PeditCommand { }; } match up.as_str() { + "E" | "EDIT" if self.multiple.is_empty() => { self.mode = Mode::PolyVertex(0); Some(CmdResult::NeedPoint) } + "R" | "REVERSE" => Some(CmdResult::PeditOp { handle, op: PeditOp::Reverse }), + "L" | "LTYPE" | "LTYPEGEN" => { self.mode = Mode::AwaitLinetype; Some(CmdResult::NeedPoint) } + "U" | "UNDO" if self.undo_count > 0 => { + self.undo_count -= 1; + self.mesh_closed_history.pop(); + if let Some(previous) = self.multiple_history.pop() { + self.multiple = previous; + if !self.multiple.is_empty() { self.target = self.multiple.first().copied(); } + } + Some(CmdResult::UndoDocument) + } + "U" | "UNDO" => Some(CmdResult::NeedPoint), "X" | "EXIT" => Some(CmdResult::Cancel), "C" | "CLOSE" => Some(CmdResult::PeditOp { handle, @@ -475,21 +594,29 @@ impl CadCommand for PeditCommand { // Inline shorthand `W `. if let Some(rest) = up.strip_prefix("W ") { let w: f64 = rest.trim().replace(',', ".").parse().ok()?; - if w >= 0.0 { - return Some(CmdResult::PeditOp { + if w.is_finite() && w >= 0.0 { + return self.multiple_result(Some(CmdResult::PeditOp { handle, op: PeditOp::SetWidth(w), - }); + })); } } None } } } - } + }; + self.multiple_result(result) } fn on_point(&mut self, point: DVec3) -> CmdResult { + if let Mode::PolyMove(index) | Mode::PolyInsert(index) = self.mode { + if !point.is_finite() { return CmdResult::NeedPoint; } + let Some(handle) = self.target else { return CmdResult::Cancel; }; + let insert = matches!(self.mode, Mode::PolyInsert(_)); + self.mode = Mode::PolyVertex(index); + return CmdResult::PeditOp { handle, op: PeditOp::EditVertex { index, point, insert } }; + } if let Mode::MeshVertexMove(index) = self.mode { let Some(handle) = self.target else { return CmdResult::Cancel; @@ -507,6 +634,18 @@ impl CadCommand for PeditCommand { let mesh_size = self.mesh_size(); let vertex_default = self.mesh_vertex_default; match &mut self.mode { + Mode::MultipleGather => { + self.target = self.multiple.first().copied(); + if self.target.is_none() { return CmdResult::Cancel; } + self.mode = if self.multiple.iter().any(|handle| self.info.get(&handle.value()).is_some_and(|info| info.convertible)) { + Mode::MultipleConvert + } else { Mode::Options }; + CmdResult::NeedPoint + } + Mode::MultipleConvert => self.on_text_input("Y").unwrap_or(CmdResult::NeedPoint), + Mode::PolyVertex(_) => self.on_text_input("N").unwrap_or(CmdResult::NeedPoint), + Mode::PolyMove(_) | Mode::PolyInsert(_) => { self.mode = Mode::Options; CmdResult::NeedPoint } + Mode::AwaitLinetype => { self.mode = Mode::Options; CmdResult::NeedPoint } Mode::JoinGather(list) if list.len() >= 2 => CmdResult::JoinEntities(list.clone()), Mode::JoinGather(_) => { self.mode = Mode::Options; @@ -539,8 +678,12 @@ impl CadCommand for PeditCommand { #[derive(Clone)] pub enum PeditOp { + Multiple(Vec, Box), SetClosed(bool), SetWidth(f64), + SetLinetypeGeneration(bool), + EditVertex { index: usize, point: DVec3, insert: bool }, + Reverse, /// Replace the picked Line/Arc with an equivalent LwPolyline (#263). ConvertToPolyline, Fit, @@ -560,6 +703,54 @@ pub enum PeditOp { pub fn apply_pedit(entity: &mut EntityType, op: &PeditOp) -> bool { match op { + PeditOp::Multiple(_, _) => false, + PeditOp::EditVertex { index, point, insert } => { + if !point.is_finite() { return false; } + match entity { + EntityType::LwPolyline(polyline) => { + let plane = crate::entities::curve::ocs_plane(polyline.normal, polyline.elevation); + let Some(local) = plane.project([point.x, point.y, point.z]) else { return false; }; + if *index >= polyline.vertices.len() { return false; } + if *insert { + polyline.vertices.insert(index + 1, LwVertex::new(Vector2::new(local[0], local[1]))); + } else { polyline.vertices[*index].location = Vector2::new(local[0], local[1]); } + true + } + EntityType::Polyline2D(polyline) => { + let plane = crate::entities::curve::ocs_plane(polyline.normal, polyline.elevation); + let Some(local) = plane.project([point.x, point.y, point.z]) else { return false; }; + if *index >= polyline.vertices.len() { return false; } + let location = Vector3::new(local[0], local[1], polyline.elevation); + if *insert { + polyline.vertices.insert(index + 1, acadrust::entities::polyline::Vertex2D::new(location)); + } else { polyline.vertices[*index].location = location; } + true + } + _ => false, + } + } + PeditOp::Reverse => { + let Some(reversed) = super::reverse::ReverseCommand::reversed(entity) else { return false; }; + *entity = reversed; + true + } + PeditOp::SetLinetypeGeneration(enabled) => match entity { + EntityType::LwPolyline(polyline) => { + if polyline.plinegen == *enabled { return false; } + polyline.plinegen = *enabled; + true + } + EntityType::Polyline2D(polyline) => { + let flag = acadrust::entities::polyline::PolylineFlags::LINETYPE_CONTINUOUS; + let bits = polyline.flags.bits(); + if (bits & flag.bits() != 0) == *enabled { return false; } + polyline.flags = acadrust::entities::polyline::PolylineFlags::from_bits( + if *enabled { bits | flag.bits() } else { bits & !flag.bits() } + ); + true + } + _ => false, + }, PeditOp::SetClosed(closed) => match entity { EntityType::LwPolyline(p) => { p.is_closed = *closed; diff --git a/src/modules/draw/modify/reverse.rs b/src/modules/draw/modify/reverse.rs index 3d7e3d51e..ec1a6766f 100644 --- a/src/modules/draw/modify/reverse.rs +++ b/src/modules/draw/modify/reverse.rs @@ -54,7 +54,7 @@ impl ReverseCommand { } /// Build a reversed copy of `entity`, or `None` for an unsupported type. - fn reversed(entity: &EntityType) -> Option { + pub fn reversed(entity: &EntityType) -> Option { match entity { EntityType::Line(line) => { let mut out = line.clone(); From ebef847870c7de2568c7a23854a6f9a2583f753d Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:04:30 +0300 Subject: [PATCH 2/8] Edit individual polyline segment widths at the selected vertex --- src/modules/draw/modify/pedit.rs | 57 ++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/src/modules/draw/modify/pedit.rs b/src/modules/draw/modify/pedit.rs index 8f6c3214a..0b0d69878 100644 --- a/src/modules/draw/modify/pedit.rs +++ b/src/modules/draw/modify/pedit.rs @@ -39,6 +39,8 @@ enum Mode { PolyVertex(usize), PolyMove(usize), PolyInsert(usize), + PolyWidthStart(usize, f64), + PolyWidthEnd(usize, f64), /// Join: gathering additional segments; Enter merges. JoinGather(Vec), /// Polygon-mesh vertex navigation uses the mesh's row-major control net. @@ -141,6 +143,15 @@ impl PeditCommand { } } + fn vertex_width(&self, index: usize) -> f64 { + let entity = self.target.and_then(|handle| self.entities.get(&handle.value())); + match entity { + Some(EntityType::LwPolyline(p)) => if p.constant_width != 0.0 { p.constant_width } else { p.vertices.get(index).map_or(0.0, |v| v.start_width) }, + Some(EntityType::Polyline2D(p)) => p.vertices.get(index).map_or(p.start_width, |v| if v.start_width == 0.0 { p.start_width } else { v.start_width }), + _ => 0.0, + } + } + fn vertex_count(&self) -> usize { self.target.and_then(|handle| self.entities.get(&handle.value())).map_or(0, |entity| match entity { EntityType::LwPolyline(polyline) => polyline.vertices.len(), @@ -218,8 +229,10 @@ impl CadCommand for PeditCommand { vertex = index + 1 ) .into_owned(), - Mode::PolyVertex(index) => format!("PEDIT Vertex {} [Next/Previous/Insert/Move/eXit] :", index + 1), + Mode::PolyVertex(index) => format!("PEDIT Vertex {} [Next/Previous/Insert/Move/Width/eXit] :", index + 1), Mode::PolyMove(index) => format!("PEDIT Specify new location for vertex {}:", index + 1), + Mode::PolyWidthStart(_, width) => format!("PEDIT Specify starting width for next segment <{width}>:"), + Mode::PolyWidthEnd(_, width) => format!("PEDIT Specify ending width for next segment <{width}>:"), Mode::PolyInsert(index) => format!("PEDIT Specify location after vertex {}:", index + 1), Mode::Options => t!("PEDIT Enter option:").into_owned(), } @@ -279,7 +292,7 @@ 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::PolyVertex(_) => vec![CmdOption::new("Next", "N"), CmdOption::new("Previous", "P"), CmdOption::new("Insert", "I"), CmdOption::new("Move", "M"), CmdOption::new("Exit", "X")], + Mode::PolyVertex(_) => vec![CmdOption::new("Next", "N"), CmdOption::new("Previous", "P"), CmdOption::new("Insert", "I"), CmdOption::new("Move", "M"), CmdOption::new("Width", "W"), CmdOption::new("Exit", "X")], Mode::AwaitLinetype => vec![CmdOption::new("On", "ON"), CmdOption::new("Off", "OFF")], Mode::MeshVertexMove(_) => vec![], _ => vec![], @@ -379,6 +392,7 @@ impl CadCommand for PeditCommand { let up = text.trim().to_uppercase(); let mesh_size = self.mesh_size(); let vertex_count = self.vertex_count(); + let current_width = if let Mode::PolyVertex(index) = self.mode { self.vertex_width(index) } else { 0.0 }; let result = match &mut self.mode { Mode::PickTarget => { if matches!(up.as_str(), "M" | "MULTIPLE") { self.mode = Mode::MultipleGather; Some(CmdResult::NeedPoint) } else { None } @@ -432,6 +446,19 @@ impl CadCommand for PeditCommand { self.mode = Mode::Options; Some(CmdResult::PeditOp { handle: self.target?, op: PeditOp::SetLinetypeGeneration(enabled) }) } + Mode::PolyWidthStart(index, _) => { + let width = text.trim().parse::().ok()?; + if !width.is_finite() || width < 0.0 { return Some(CmdResult::NeedPoint); } + self.mode = Mode::PolyWidthEnd(*index, width); + Some(CmdResult::NeedPoint) + } + Mode::PolyWidthEnd(index, start) => { + let end = text.trim().parse::().ok()?; + if !end.is_finite() || end < 0.0 { return Some(CmdResult::NeedPoint); } + let (index, start) = (*index, *start); + self.mode = Mode::PolyVertex(index); + Some(CmdResult::PeditOp { handle: self.target?, op: PeditOp::SetVertexWidth { index, start, end } }) + } Mode::JoinGather(_) => None, Mode::PolyMove(_) | Mode::PolyInsert(_) => None, Mode::PolyVertex(index) => { @@ -440,6 +467,7 @@ impl CadCommand for PeditCommand { "P" | "PREVIOUS" => { *index = index.saturating_sub(1); } "I" | "INSERT" => self.mode = Mode::PolyInsert(*index), "M" | "MOVE" => self.mode = Mode::PolyMove(*index), + "W" | "WIDTH" => self.mode = Mode::PolyWidthStart(*index, current_width), "X" | "EXIT" => self.mode = Mode::Options, _ => {} } @@ -643,6 +671,7 @@ impl CadCommand for PeditCommand { CmdResult::NeedPoint } Mode::MultipleConvert => self.on_text_input("Y").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(_) => self.on_text_input("N").unwrap_or(CmdResult::NeedPoint), Mode::PolyMove(_) | Mode::PolyInsert(_) => { self.mode = Mode::Options; CmdResult::NeedPoint } Mode::AwaitLinetype => { self.mode = Mode::Options; CmdResult::NeedPoint } @@ -681,6 +710,7 @@ pub enum PeditOp { Multiple(Vec, Box), SetClosed(bool), SetWidth(f64), + SetVertexWidth { index: usize, start: f64, end: f64 }, SetLinetypeGeneration(bool), EditVertex { index: usize, point: DVec3, insert: bool }, Reverse, @@ -704,6 +734,29 @@ pub enum PeditOp { pub fn apply_pedit(entity: &mut EntityType, op: &PeditOp) -> bool { match op { PeditOp::Multiple(_, _) => false, + PeditOp::SetVertexWidth { index, start, end } => { + if !start.is_finite() || !end.is_finite() || *start < 0.0 || *end < 0.0 { return false; } + match entity { + EntityType::LwPolyline(p) => { + if *index >= p.vertices.len() { return false; } + if p.constant_width != 0.0 { + for v in &mut p.vertices { v.start_width = p.constant_width; v.end_width = p.constant_width; } + p.constant_width = 0.0; + } + p.vertices[*index].start_width = *start; p.vertices[*index].end_width = *end; true + } + EntityType::Polyline2D(p) => { + if *index >= p.vertices.len() { return false; } + for v in &mut p.vertices { + if v.start_width == 0.0 { v.start_width = p.start_width; } + if v.end_width == 0.0 { v.end_width = p.end_width; } + } + p.start_width = 0.0; p.end_width = 0.0; + p.vertices[*index].start_width = *start; p.vertices[*index].end_width = *end; true + } + _ => false, + } + } PeditOp::EditVertex { index, point, insert } => { if !point.is_finite() { return false; } match entity { From e3b1f5719ce957c3f89ad472a63ae578eea3504c Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:31:34 +0300 Subject: [PATCH 3/8] Edit polyline vertex ranges with deferred break and straighten --- src/app/command_driver.rs | 18 +++++++++ src/modules/draw/modify/pedit.rs | 68 ++++++++++++++++++++++++++++++-- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 7737ae7cc..4662f0bb6 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -3247,6 +3247,24 @@ impl OpenCADStudio { self.tabs[i].dirty = true; self.refresh_properties(); } + PeditOp::VertexRange { first, last, split } => { + let pieces = self.tabs[i].scene.document.get_entity(handle).and_then(|entity| { + crate::modules::draw::modify::pedit::edit_vertex_range(entity, *first, *last, *split) + }); + if let Some(mut pieces) = pieces { + self.push_undo_snapshot(i, "PEDIT"); + let source = pieces.remove(0); + self.tabs[i].scene.update_entity(source); + for piece in pieces { self.tabs[i].scene.add_entity(piece); } + let updated = self.tabs[i].scene.document.get_entity(handle).cloned(); + if let Some(command) = self.tabs[i].active_cmd.as_mut() { + if let Some(entity) = updated { command.inject_picked_entity(entity); } + command.on_pedit_applied(); + } + self.tabs[i].dirty = true; + self.refresh_properties(); + } else { self.command_line.push_error("PEDIT: invalid vertex range."); } + } // The convert replaces the entity (new handle). PeditOp::ConvertToPolyline => { let converted = self.tabs[i] diff --git a/src/modules/draw/modify/pedit.rs b/src/modules/draw/modify/pedit.rs index 0b0d69878..318021390 100644 --- a/src/modules/draw/modify/pedit.rs +++ b/src/modules/draw/modify/pedit.rs @@ -37,6 +37,7 @@ enum Mode { AwaitWidth, AwaitLinetype, PolyVertex(usize), + PolyRange { start: usize, end: usize, split: bool }, PolyMove(usize), PolyInsert(usize), PolyWidthStart(usize, f64), @@ -229,7 +230,8 @@ impl CadCommand for PeditCommand { vertex = index + 1 ) .into_owned(), - Mode::PolyVertex(index) => format!("PEDIT Vertex {} [Next/Previous/Insert/Move/Width/eXit] :", index + 1), + Mode::PolyVertex(index) => format!("PEDIT Vertex {} [Next/Previous/Break/Insert/Move/Straighten/Width/eXit] :", index + 1), + Mode::PolyRange { end, .. } => format!("PEDIT Vertex {} [Next/Previous/Go/eXit] :", end + 1), Mode::PolyMove(index) => format!("PEDIT Specify new location for vertex {}:", index + 1), Mode::PolyWidthStart(_, width) => format!("PEDIT Specify starting width for next segment <{width}>:"), Mode::PolyWidthEnd(_, width) => format!("PEDIT Specify ending width for next segment <{width}>:"), @@ -292,7 +294,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::PolyVertex(_) => vec![CmdOption::new("Next", "N"), CmdOption::new("Previous", "P"), CmdOption::new("Insert", "I"), CmdOption::new("Move", "M"), CmdOption::new("Width", "W"), CmdOption::new("Exit", "X")], + Mode::PolyVertex(_) => vec![CmdOption::new("Next", "N"), CmdOption::new("Previous", "P"), CmdOption::new("Break", "B"), CmdOption::new("Straighten", "S"), 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")], Mode::MeshVertexMove(_) => vec![], _ => vec![], @@ -369,6 +372,8 @@ impl CadCommand for PeditCommand { fn inject_picked_entity(&mut self, entity: EntityType) { self.entities.insert(entity.common().handle.value(), entity); + let count = self.vertex_count(); + if let Mode::PolyVertex(index) = &mut self.mode { *index = (*index).min(count.saturating_sub(1)); } } fn on_pedit_applied(&mut self) { @@ -461,10 +466,26 @@ impl CadCommand for PeditCommand { } Mode::JoinGather(_) => None, Mode::PolyMove(_) | Mode::PolyInsert(_) => None, + Mode::PolyRange { start, end, split } => { + match up.as_str() { + "N" | "NEXT" => { if *end + 1 < vertex_count { *end += 1; } } + "P" | "PREVIOUS" => { *end = end.saturating_sub(1); } + "X" | "EXIT" => self.mode = Mode::PolyVertex(*start), + "G" | "GO" => { + let (first, last, split) = ((*start).min(*end), (*start).max(*end), *split); + self.mode = Mode::PolyVertex(first); + return Some(CmdResult::PeditOp { handle: self.target?, op: PeditOp::VertexRange { first, last, split } }); + } + _ => {} + } + Some(CmdResult::NeedPoint) + } Mode::PolyVertex(index) => { match up.as_str() { "N" | "NEXT" => { if *index + 1 < vertex_count { *index += 1; } } "P" | "PREVIOUS" => { *index = index.saturating_sub(1); } + "B" | "BREAK" => self.mode = Mode::PolyRange { start: *index, end: *index, split: true }, + "S" | "STRAIGHTEN" => self.mode = Mode::PolyRange { start: *index, end: *index, split: false }, "I" | "INSERT" => self.mode = Mode::PolyInsert(*index), "M" | "MOVE" => self.mode = Mode::PolyMove(*index), "W" | "WIDTH" => self.mode = Mode::PolyWidthStart(*index, current_width), @@ -672,7 +693,7 @@ impl CadCommand for PeditCommand { } Mode::MultipleConvert => self.on_text_input("Y").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(_) => self.on_text_input("N").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 } Mode::AwaitLinetype => { self.mode = Mode::Options; CmdResult::NeedPoint } Mode::JoinGather(list) if list.len() >= 2 => CmdResult::JoinEntities(list.clone()), @@ -711,6 +732,7 @@ pub enum PeditOp { SetClosed(bool), SetWidth(f64), SetVertexWidth { index: usize, start: f64, end: f64 }, + VertexRange { first: usize, last: usize, split: bool }, SetLinetypeGeneration(bool), EditVertex { index: usize, point: DVec3, insert: bool }, Reverse, @@ -731,9 +753,47 @@ pub enum PeditOp { // ── Apply logic (pure entity edits; driver handles convert/break/marker) ── +/// Edit complete vertex records; no curve interpolation or geometric splitting +/// is needed because both range boundaries are existing vertices. +pub fn edit_vertex_range(entity: &EntityType, first: usize, last: usize, split: bool) -> Option> { + macro_rules! edit { + ($polyline:expr, $closed:expr, $variant:ident, $set_open:expr) => {{ + let polyline = $polyline; + if first > last || last >= polyline.vertices.len() { return None; } + if split && first == last && (first == 0 || last + 1 == polyline.vertices.len()) { return None; } + if !split { + if first == last { return None; } + let mut result = polyline.clone(); + result.vertices.drain(first + 1..last); + result.vertices[first].bulge = 0.0; + Some(vec![EntityType::$variant(result)]) + } else { + let mut vertices = polyline.vertices.clone(); + if $closed { vertices.push(vertices.first()?.clone()); } + let mut results = Vec::new(); + for part in [&vertices[..=first], &vertices[last..]] { + if part.len() < 2 { continue; } + let mut result = polyline.clone(); + result.vertices = part.to_vec(); + ($set_open)(&mut result); + results.push(EntityType::$variant(result)); + } + (!results.is_empty()).then_some(results) + } + }}; + } + match entity { + EntityType::LwPolyline(polyline) => edit!(polyline, polyline.is_closed, LwPolyline, + |value: &mut acadrust::entities::LwPolyline| { value.is_closed = false; }), + EntityType::Polyline2D(polyline) => edit!(polyline, polyline.flags.is_closed(), Polyline2D, + |value: &mut acadrust::entities::Polyline2D| { value.flags.set_closed(false); }), + _ => None, + } +} + pub fn apply_pedit(entity: &mut EntityType, op: &PeditOp) -> bool { match op { - PeditOp::Multiple(_, _) => false, + PeditOp::Multiple(_, _) | 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 { From ad9bf07cf7ac8c58d60a65d012efed2251b244c8 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:48:34 +0300 Subject: [PATCH 4/8] Fit polylines through kernel arcs and retain vertex tangent metadata --- src/modules/draw/modify/pedit.rs | 271 ++++++++++++++----------------- 1 file changed, 126 insertions(+), 145 deletions(-) diff --git a/src/modules/draw/modify/pedit.rs b/src/modules/draw/modify/pedit.rs index 318021390..fbf4f5dc6 100644 --- a/src/modules/draw/modify/pedit.rs +++ b/src/modules/draw/modify/pedit.rs @@ -5,7 +5,7 @@ use acadrust::types::{Vector2, Vector3}; use acadrust::{EntityType, Handle}; use cadkernel::geom2d::nurbs::clamped_uniform_knots; use cadkernel::geom2d::NurbsCurve; -use glam::{DVec2, DVec3}; +use glam::DVec3; use rustc_hash::FxHashMap as HashMap; use crate::command::{CadCommand, CmdResult}; @@ -37,6 +37,7 @@ enum Mode { AwaitWidth, AwaitLinetype, PolyVertex(usize), + PolyTangent(usize), PolyRange { start: usize, end: usize, split: bool }, PolyMove(usize), PolyInsert(usize), @@ -59,6 +60,7 @@ pub struct PeditCommand { info: HashMap, mode: Mode, undo_count: usize, + vertex_tangents: HashMap, mesh_smooth_type: acadrust::entities::polygon_mesh::SurfaceSmoothType, mesh_smooth_density: (i16, i16), mesh_vertex_default: isize, @@ -89,6 +91,7 @@ impl PeditCommand { info, mode: Mode::PickTarget, undo_count: 0, + vertex_tangents: HashMap::default(), mesh_smooth_type, mesh_smooth_density: ( surface_u_density.clamp(2, 200), @@ -230,8 +233,9 @@ impl CadCommand for PeditCommand { vertex = index + 1 ) .into_owned(), - Mode::PolyVertex(index) => format!("PEDIT Vertex {} [Next/Previous/Break/Insert/Move/Straighten/Width/eXit] :", index + 1), + Mode::PolyVertex(index) => format!("PEDIT Vertex {} [Next/Previous/Break/Insert/Move/Straighten/Tangent/Width/eXit] :", index + 1), Mode::PolyRange { end, .. } => format!("PEDIT Vertex {} [Next/Previous/Go/eXit] :", end + 1), + Mode::PolyTangent(_) => "PEDIT Specify direction of vertex tangent:".to_string(), Mode::PolyMove(index) => format!("PEDIT Specify new location for vertex {}:", index + 1), Mode::PolyWidthStart(_, width) => format!("PEDIT Specify starting width for next segment <{width}>:"), Mode::PolyWidthEnd(_, width) => format!("PEDIT Specify ending width for next segment <{width}>:"), @@ -294,7 +298,7 @@ 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::PolyVertex(_) => vec![CmdOption::new("Next", "N"), CmdOption::new("Previous", "P"), CmdOption::new("Break", "B"), CmdOption::new("Straighten", "S"), CmdOption::new("Insert", "I"), CmdOption::new("Move", "M"), CmdOption::new("Width", "W"), CmdOption::new("Exit", "X")], + 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")], Mode::MeshVertexMove(_) => vec![], @@ -389,7 +393,7 @@ impl CadCommand for PeditCommand { } fn wants_text_input(&self) -> bool { - !matches!(self.mode, Mode::PickTarget | Mode::MeshVertexMove(_) | Mode::PolyMove(_) | Mode::PolyInsert(_)) + !matches!(self.mode, Mode::PickTarget | Mode::MeshVertexMove(_) | Mode::PolyMove(_) | Mode::PolyInsert(_) | Mode::PolyTangent(_)) } fn on_text_input(&mut self, text: &str) -> Option { @@ -466,6 +470,13 @@ impl CadCommand for PeditCommand { } Mode::JoinGather(_) => None, Mode::PolyMove(_) | Mode::PolyInsert(_) => None, + Mode::PolyTangent(index) => { + let angle = text.trim().parse::().ok()?; + if !angle.is_finite() { return Some(CmdResult::NeedPoint); } + self.vertex_tangents.insert(*index, angle.to_radians().rem_euclid(TAU)); + self.mode = Mode::PolyVertex(*index); + Some(CmdResult::NeedPoint) + } Mode::PolyRange { start, end, split } => { match up.as_str() { "N" | "NEXT" => { if *end + 1 < vertex_count { *end += 1; } } @@ -484,6 +495,7 @@ impl CadCommand for PeditCommand { match up.as_str() { "N" | "NEXT" => { if *index + 1 < vertex_count { *index += 1; } } "P" | "PREVIOUS" => { *index = index.saturating_sub(1); } + "T" | "TANGENT" => self.mode = Mode::PolyTangent(*index), "B" | "BREAK" => self.mode = Mode::PolyRange { start: *index, end: *index, split: true }, "S" | "STRAIGHTEN" => self.mode = Mode::PolyRange { start: *index, end: *index, split: false }, "I" | "INSERT" => self.mode = Mode::PolyInsert(*index), @@ -629,7 +641,7 @@ impl CadCommand for PeditCommand { } "F" | "FIT" => Some(CmdResult::PeditOp { handle, - op: PeditOp::Fit, + op: PeditOp::FitWithTangents(self.vertex_tangents.iter().map(|(index, angle)| (*index, *angle)).collect()), }), "S" | "SPLINE" => Some(CmdResult::PeditOp { handle, @@ -659,6 +671,20 @@ impl CadCommand for PeditCommand { } fn on_point(&mut self, point: DVec3) -> CmdResult { + if let Mode::PolyTangent(index) = self.mode { + let Some(entity) = self.target.and_then(|handle| self.entities.get(&handle.value())) else { return CmdResult::NeedPoint; }; + let (normal, elevation, location) = match entity { + EntityType::LwPolyline(p) => { let Some(v) = p.vertices.get(index) else { return CmdResult::NeedPoint; }; (p.normal, p.elevation, [v.location.x, v.location.y]) } + EntityType::Polyline2D(p) => { let Some(v) = p.vertices.get(index) else { return CmdResult::NeedPoint; }; (p.normal, p.elevation, [v.location.x, v.location.y]) } + _ => return CmdResult::NeedPoint, + }; + let Some(local) = crate::entities::curve::ocs_plane(normal, elevation).project([point.x, point.y, point.z]) else { return CmdResult::NeedPoint; }; + let delta = cadkernel::geom2d::Vec2::new(local[0] - location[0], local[1] - location[1]); + if delta.length_squared() <= 1e-24 { return CmdResult::NeedPoint; } + self.vertex_tangents.insert(index, delta.angle().rem_euclid(TAU)); + self.mode = Mode::PolyVertex(index); + return CmdResult::NeedPoint; + } if let Mode::PolyMove(index) | Mode::PolyInsert(index) = self.mode { if !point.is_finite() { return CmdResult::NeedPoint; } let Some(handle) = self.target else { return CmdResult::Cancel; }; @@ -739,6 +765,7 @@ pub enum PeditOp { /// Replace the picked Line/Arc with an equivalent LwPolyline (#263). ConvertToPolyline, Fit, + FitWithTangents(Vec<(usize, f64)>), Spline, Decurve, SetMeshClosedM(bool), @@ -890,23 +917,20 @@ pub fn apply_pedit(entity: &mut EntityType, op: &PeditOp) -> bool { } _ => false, }, - PeditOp::Fit => match entity { - EntityType::LwPolyline(p) => fit_curve(p), - _ => false, - }, + PeditOp::Fit | PeditOp::FitWithTangents(_) => { + let overrides = match op { PeditOp::FitWithTangents(values) => values.as_slice(), _ => &[] }; + let Some(fitted) = fit_entity(entity, overrides) else { return false; }; + *entity = fitted; + true + } PeditOp::Spline => match entity { EntityType::LwPolyline(p) => spline_smooth(p), _ => false, }, - PeditOp::Decurve => match entity { - EntityType::LwPolyline(p) => { - for v in &mut p.vertices { - v.bulge = 0.0; - } - true - } - _ => false, - }, + PeditOp::Decurve => { + let Some(straight) = decurve_entity(entity) else { return false; }; + *entity = straight; true + } PeditOp::SetMeshClosedM(closed) => match entity { EntityType::PolygonMesh(mesh) => { if mesh.is_closed_m() == *closed { @@ -1094,139 +1118,96 @@ mod convert_tests { // ── Curve fitting ───────────────────────────────────────────────────────── -fn vert_xy(v: &LwVertex) -> DVec2 { - DVec2::new(v.location.x, v.location.y) -} - -/// Wrap an angle into (-pi, pi]. -fn wrap_angle(mut a: f64) -> f64 { - while a > std::f64::consts::PI { - a -= TAU; - } - while a <= -std::f64::consts::PI { - a += TAU; - } - a -} - -/// Bulge of the arc `a` -> `b` whose tangent AT `a` is `t` (entry form). -fn bulge_entry(a: DVec2, t: DVec2, b: DVec2) -> f64 { - let d = b - a; - if d.length_squared() < 1e-12 { - return 0.0; - } - (wrap_angle(d.y.atan2(d.x) - t.y.atan2(t.x)) / 2.0).tan() -} - -/// Bulge of the arc `a` -> `b` whose tangent AT `b` is `t` (exit form). -fn bulge_exit(a: DVec2, b: DVec2, t: DVec2) -> f64 { - let d = b - a; - if d.length_squared() < 1e-12 { - return 0.0; - } - (wrap_angle(t.y.atan2(t.x) - d.y.atan2(d.x)) / 2.0).tan() -} - -/// PEDIT Fit: replace every segment with a BIARC — two arcs that leave the -/// start vertex along its tangent, meet each other tangentially at an -/// inserted knee vertex, and arrive at the end vertex along ITS tangent. -/// Both segments at a vertex share that vertex's tangent (the average of the -/// neighbouring chords), so the whole run is tangent-continuous — arcs -/// mutually tangent everywhere, which a single arc per segment cannot do. -fn fit_curve(p: &mut acadrust::LwPolyline) -> bool { - let n = p.vertices.len(); - if n < 3 { - return false; - } - let pts: Vec = p.vertices.iter().map(vert_xy).collect(); - let chord = |i: usize| -> DVec2 { - let a = pts[i % n]; - let b = pts[(i + 1) % n]; - (b - a).normalize_or_zero() +fn fit_entity(entity: &EntityType, overrides: &[(usize, f64)]) -> Option { + use acadrust::entities::{Polyline2D, PolylineFlags, Vertex2D, VertexFlags}; + let mut polyline = match entity { + EntityType::Polyline2D(p) => p.clone(), + EntityType::LwPolyline(p) => { + let mut result = Polyline2D::new(); + result.common = p.common.clone(); result.normal = p.normal; + result.elevation = p.elevation; result.thickness = p.thickness; + result.flags = PolylineFlags::from_bits((p.is_closed as u16) | if p.plinegen { 128 } else { 0 }); + result.vertices = p.vertices.iter().map(|v| { + let mut vertex = Vertex2D::new(Vector3::new(v.location.x, v.location.y, p.elevation)); + vertex.start_width = if p.constant_width != 0.0 { p.constant_width } else { v.start_width }; + vertex.end_width = if p.constant_width != 0.0 { p.constant_width } else { v.end_width }; + vertex.id = v.vertex_id; + vertex + }).collect(); + result + } + _ => return None, }; - // Per-vertex tangents. - let tangent = |i: usize| -> DVec2 { - if !p.is_closed && i == 0 { - return chord(0); + // Refit the defining vertices, never the previously inserted fit knees. + let mut originals = defining_vertices(&polyline); + for vertex in &mut originals { + if vertex.start_width == 0.0 { vertex.start_width = polyline.start_width; } + if vertex.end_width == 0.0 { vertex.end_width = polyline.end_width; } + } + for &(index, angle) in overrides { + let vertex = originals.get_mut(index)?; + if !angle.is_finite() { return None; } + vertex.curve_tangent = angle.rem_euclid(TAU); + vertex.flags = VertexFlags::from_bits(vertex.flags.bits() | 2); + } + let points: Vec<_> = originals.iter().map(|v| [v.location.x, v.location.y]).collect(); + let directions: Vec<_> = originals.iter().map(|v| (v.flags.bits() & 2 != 0).then(|| [v.curve_tangent.cos(), v.curve_tangent.sin()])).collect(); + let fitted = cadkernel::geom2d::fit_arc_chain(&points, polyline.flags.is_closed(), &directions)?; + let mut vertices = Vec::with_capacity(fitted.len()); + for (i, fitted_vertex) in fitted.iter().enumerate() { + let original = &originals[fitted_vertex.source]; + let mut vertex = original.clone(); + vertex.location = Vector3::new(fitted_vertex.point[0], fitted_vertex.point[1], polyline.elevation); + vertex.bulge = fitted_vertex.bulge; + let end_fraction = fitted.get(i + 1).filter(|next| next.source == fitted_vertex.source).map_or(1.0, |next| next.fraction); + vertex.start_width = original.start_width + (original.end_width - original.start_width) * fitted_vertex.fraction; + vertex.end_width = original.start_width + (original.end_width - original.start_width) * end_fraction; + if fitted_vertex.inserted { vertex.flags = VertexFlags::EXTRA_VERTEX; vertex.id = 0; } + vertices.push(vertex); + } + polyline.start_width = originals.first()?.start_width; + polyline.end_width = originals.first()?.end_width; + polyline.vertices = vertices; + polyline.flags = PolylineFlags::from_bits((polyline.flags.bits() & !4) | 2); + Some(EntityType::Polyline2D(polyline)) +} + +fn defining_vertices(polyline: &acadrust::entities::Polyline2D) -> Vec { + polyline.vertices.iter().enumerate().filter(|(_, vertex)| vertex.flags.bits() & 1 == 0).map(|(index, vertex)| { + let mut original = vertex.clone(); + if let Some(knee) = polyline.vertices[index + 1..].iter().take_while(|next| next.flags.bits() & 1 != 0).last() { + original.end_width = knee.end_width; } - if !p.is_closed && i == n - 1 { - return chord(n - 2); - } - let prev = chord((i + n - 1) % n); - let cur = chord(i % n); - let sum = prev + cur; - if sum.length_squared() < 1e-12 { - cur - } else { - sum.normalize() + original + }).collect() +} + +fn decurve_entity(entity: &EntityType) -> Option { + let mut result = match entity { + EntityType::LwPolyline(p) => p.clone(), + EntityType::Polyline2D(p) => { + let mut result = acadrust::LwPolyline::new(); + result.common = p.common.clone(); result.normal = p.normal; + result.elevation = p.elevation; result.thickness = p.thickness; + result.is_closed = p.flags.is_closed(); result.plinegen = p.flags.bits() & 128 != 0; + result.vertices = defining_vertices(p).iter().map(|v| { + let mut vertex = LwVertex::new(Vector2::new(v.location.x, v.location.y)); + vertex.vertex_id = v.id; + vertex.start_width = if v.start_width == 0.0 { p.start_width } else { v.start_width }; + vertex.end_width = if v.end_width == 0.0 { p.end_width } else { v.end_width }; + vertex + }).collect(); + result } + _ => return None, }; - let seg_count = if p.is_closed { n } else { n - 1 }; - let mut out: Vec = Vec::with_capacity(seg_count * 2 + 1); - for i in 0..seg_count { - let p0 = pts[i % n]; - let p1 = pts[(i + 1) % n]; - let t0 = tangent(i % n); - let t1 = tangent((i + 1) % n); - let d = p1 - p0; - let src = &p.vertices[i % n]; - let mut push = |q: DVec2, bulge: f64| { - let mut v = LwVertex::new(Vector2::new(q.x, q.y)); - v.bulge = bulge; - v.start_width = src.start_width; - v.end_width = src.end_width; - out.push(v); - }; - if d.length_squared() < 1e-12 { - push(p0, 0.0); - continue; + for vertex in &mut result.vertices { vertex.bulge = 0.0; } + if let Some(width) = result.vertices.first().map(|v| v.start_width) { + if result.vertices.iter().all(|v| v.start_width == width && v.end_width == width) { + result.constant_width = width; } - // Both tangents already aligned with the chord: keep it straight. - let dn = d.normalize(); - if (dn - t0).length_squared() < 1e-12 && (dn - t1).length_squared() < 1e-12 { - push(p0, 0.0); - continue; - } - // Classic equal-parameter biarc: apexes A = p0 + k*t0, B = p1 - k*t1 - // and the knee M at their midpoint. Tangency at M needs |AB| = 2k, - // i.e. 2(1 - t0.t1) k^2 + 2 (d.(t0+t1)) k - |d|^2 = 0 — A and B are - // then the tangent-line apexes of their arcs, so both arcs' tangents - // at M run along AB and the pair is tangent-continuous. - let dot_tt = t0.dot(t1).clamp(-1.0, 1.0); - let b_lin = d.dot(t0 + t1); - let a_quad = 2.0 * (1.0 - dot_tt); - let k = if a_quad.abs() > 1e-9 { - let disc = b_lin * b_lin + a_quad * d.length_squared(); - if disc < 0.0 { - push(p0, bulge_entry(p0, t0, p1)); - continue; - } - (-b_lin + disc.sqrt()) / a_quad - } else if b_lin.abs() > 1e-9 { - // Parallel tangents: the quadratic degenerates to one root. - d.length_squared() / (2.0 * b_lin) - } else { - // Anti-parallel S with no along-tangent reach: symmetric split. - (d.length_squared() / 4.0).sqrt() - }; - if !k.is_finite() || k <= 1e-9 { - push(p0, bulge_entry(p0, t0, p1)); - continue; - } - let m = (p0 + t0 * k + p1 - t1 * k) * 0.5; - push(p0, bulge_entry(p0, t0, m)); - push(m, bulge_exit(m, p1, t1)); } - if !p.is_closed { - let mut last = p.vertices[n - 1].clone(); - last.bulge = 0.0; - out.push(last); - } - if out.len() < 2 { - return false; - } - p.vertices = out; - true + Some(EntityType::LwPolyline(result)) } /// PEDIT Spline: replace the shape with a sampled uniform cubic B-spline of From 499beb718f812c1c4f65db045fa893e624b0dbe0 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:50:25 +0300 Subject: [PATCH 5/8] Preserve polylines when fitting attributes are invalid --- src/modules/draw/modify/pedit.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/draw/modify/pedit.rs b/src/modules/draw/modify/pedit.rs index fbf4f5dc6..1878e01b9 100644 --- a/src/modules/draw/modify/pedit.rs +++ b/src/modules/draw/modify/pedit.rs @@ -680,7 +680,7 @@ impl CadCommand for PeditCommand { }; let Some(local) = crate::entities::curve::ocs_plane(normal, elevation).project([point.x, point.y, point.z]) else { return CmdResult::NeedPoint; }; let delta = cadkernel::geom2d::Vec2::new(local[0] - location[0], local[1] - location[1]); - if delta.length_squared() <= 1e-24 { return CmdResult::NeedPoint; } + if !delta.length_squared().is_finite() || delta.length_squared() <= 1e-24 { return CmdResult::NeedPoint; } self.vertex_tangents.insert(index, delta.angle().rem_euclid(TAU)); self.mode = Mode::PolyVertex(index); return CmdResult::NeedPoint; @@ -1144,6 +1144,8 @@ fn fit_entity(entity: &EntityType, overrides: &[(usize, f64)]) -> Option Date: Fri, 11 Sep 2026 11:51:54 +0300 Subject: [PATCH 6/8] Remap pending vertex tangents through polyline edits and undo --- src/modules/draw/modify/pedit.rs | 37 ++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/modules/draw/modify/pedit.rs b/src/modules/draw/modify/pedit.rs index 1878e01b9..4450f0313 100644 --- a/src/modules/draw/modify/pedit.rs +++ b/src/modules/draw/modify/pedit.rs @@ -61,6 +61,8 @@ pub struct PeditCommand { mode: Mode, undo_count: usize, vertex_tangents: HashMap, + pending_tangents: Option>, + tangent_history: Vec>, mesh_smooth_type: acadrust::entities::polygon_mesh::SurfaceSmoothType, mesh_smooth_density: (i16, i16), mesh_vertex_default: isize, @@ -92,6 +94,8 @@ impl PeditCommand { mode: Mode::PickTarget, undo_count: 0, vertex_tangents: HashMap::default(), + pending_tangents: None, + tangent_history: Vec::new(), mesh_smooth_type, mesh_smooth_density: ( surface_u_density.clamp(2, 200), @@ -147,6 +151,25 @@ impl PeditCommand { } } + fn mapped_tangents(&self, op: &PeditOp) -> Option> { + let count = self.vertex_count(); + match op { + PeditOp::Reverse => Some(self.vertex_tangents.iter().filter_map(|(&index, &angle)| + (index < count).then_some((count.saturating_sub(index + 1), (angle + std::f64::consts::PI).rem_euclid(TAU)))).collect()), + PeditOp::EditVertex { index, insert: true, .. } => Some(self.vertex_tangents.iter().map(|(&old, &angle)| + (if old > *index { old + 1 } else { old }, angle)).collect()), + PeditOp::VertexRange { first, last, split } => Some(self.vertex_tangents.iter().filter_map(|(&index, &angle)| { + if *split { + if *first > 0 { (index <= *first).then_some((index, angle)) } + else { (index >= *last).then_some((index.saturating_sub(*last), angle)) } + } else if index > *first && index < *last { None } + else { Some((if index >= *last { index.saturating_sub(last.saturating_sub(*first + 1)) } else { index }, angle)) } + }).collect()), + PeditOp::Fit | PeditOp::FitWithTangents(_) | PeditOp::Decurve | PeditOp::Spline => Some(HashMap::default()), + _ => None, + } + } + fn vertex_width(&self, index: usize) -> f64 { let entity = self.target.and_then(|handle| self.entities.get(&handle.value())); match entity { @@ -381,6 +404,8 @@ impl CadCommand for PeditCommand { } fn on_pedit_applied(&mut self) { + self.tangent_history.push(self.vertex_tangents.clone()); + if let Some(mapped) = self.pending_tangents.take() { self.vertex_tangents = mapped; } self.undo_count = self.undo_count.saturating_add(1); self.multiple_history.push(self.pending_multiple.take().unwrap_or_else(|| self.multiple.clone())); if let Some((m_direction, closed)) = self.pending_mesh_closed.take() { @@ -398,6 +423,7 @@ impl CadCommand for PeditCommand { fn on_text_input(&mut self, text: &str) -> Option { self.pending_mesh_closed = None; + self.pending_tangents = None; let up = text.trim().to_uppercase(); let mesh_size = self.mesh_size(); let vertex_count = self.vertex_count(); @@ -485,7 +511,9 @@ impl CadCommand for PeditCommand { "G" | "GO" => { let (first, last, split) = ((*start).min(*end), (*start).max(*end), *split); self.mode = Mode::PolyVertex(first); - return Some(CmdResult::PeditOp { handle: self.target?, op: PeditOp::VertexRange { first, last, split } }); + let op = PeditOp::VertexRange { first, last, split }; + self.pending_tangents = self.mapped_tangents(&op); + return Some(CmdResult::PeditOp { handle: self.target?, op }); } _ => {} } @@ -615,6 +643,7 @@ impl CadCommand for PeditCommand { "U" | "UNDO" if self.undo_count > 0 => { self.undo_count -= 1; self.mesh_closed_history.pop(); + if let Some(tangents) = self.tangent_history.pop() { self.vertex_tangents = tangents; } if let Some(previous) = self.multiple_history.pop() { self.multiple = previous; if !self.multiple.is_empty() { self.target = self.multiple.first().copied(); } @@ -667,10 +696,12 @@ impl CadCommand for PeditCommand { } } }; + if let Some(CmdResult::PeditOp { op, .. }) = &result { self.pending_tangents = self.mapped_tangents(op); } self.multiple_result(result) } fn on_point(&mut self, point: DVec3) -> CmdResult { + self.pending_tangents = None; if let Mode::PolyTangent(index) = self.mode { let Some(entity) = self.target.and_then(|handle| self.entities.get(&handle.value())) else { return CmdResult::NeedPoint; }; let (normal, elevation, location) = match entity { @@ -690,7 +721,9 @@ impl CadCommand for PeditCommand { let Some(handle) = self.target else { return CmdResult::Cancel; }; let insert = matches!(self.mode, Mode::PolyInsert(_)); self.mode = Mode::PolyVertex(index); - return CmdResult::PeditOp { handle, op: PeditOp::EditVertex { index, point, insert } }; + let op = PeditOp::EditVertex { index, point, insert }; + self.pending_tangents = self.mapped_tangents(&op); + return CmdResult::PeditOp { handle, op }; } if let Mode::MeshVertexMove(index) = self.mode { let Some(handle) = self.target else { From 5500272f314c6d93091c894235a3cb17bc8a8ef9 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 12:17:59 +0300 Subject: [PATCH 7/8] Allocate distinct handles for polyline break fragments --- src/app/command_driver.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 4662f0bb6..aed88b26a 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -3255,7 +3255,10 @@ impl OpenCADStudio { self.push_undo_snapshot(i, "PEDIT"); let source = pieces.remove(0); self.tabs[i].scene.update_entity(source); - for piece in pieces { self.tabs[i].scene.add_entity(piece); } + for mut piece in pieces { + piece.common_mut().handle = Handle::NULL; + self.tabs[i].scene.add_entity(piece); + } let updated = self.tabs[i].scene.document.get_entity(handle).cloned(); if let Some(command) = self.tabs[i].active_cmd.as_mut() { if let Some(entity) = updated { command.inject_picked_entity(entity); } From 356c8b5a7b5549458620ec98e0d05ebe44976902 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 11 Sep 2026 12:57:58 +0300 Subject: [PATCH 8/8] Keep multiple polyline editing active across selected joins --- src/app/command_driver.rs | 30 ++++++++++++++++++++++++++++++ src/modules/draw/modify/pedit.rs | 21 +++++++++++++++++---- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index aed88b26a..0017ad2b2 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -3216,6 +3216,36 @@ impl OpenCADStudio { apply_pedit, convert_to_polyline, PeditOp, }; match &op { + PeditOp::JoinSelection(handles) => { + 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 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::join::join_to_source(&source, &candidates) { + *result.common_mut() = source.common().clone(); + available.retain(|(handle, _)| !consumed.contains(handle)); + changes.push((source_handle, result, consumed)); + } + } + if !changes.is_empty() { + self.push_undo_snapshot(i, "PEDIT"); + for (source, replacement, consumed) in changes { + self.tabs[i].scene.erase_entities(&consumed); + self.tabs[i].scene.update_entity(replacement.clone()); + if let Some(command) = self.tabs[i].active_cmd.as_mut() { + for old in consumed { command.on_entity_replaced(old, &[source]); } + command.inject_picked_entity(replacement); + } + } + if let Some(command) = self.tabs[i].active_cmd.as_mut() { command.on_pedit_applied(); } + self.tabs[i].dirty = true; + self.refresh_properties(); + } + } PeditOp::Multiple(handles, operation) => { let replacements: Vec<_> = handles.iter().filter(|handle| !self.tabs[i].scene.is_layer_locked(**handle)).filter_map(|handle| { let original = self.tabs[i].scene.document.get_entity(*handle)?; diff --git a/src/modules/draw/modify/pedit.rs b/src/modules/draw/modify/pedit.rs index 4450f0313..963d9d350 100644 --- a/src/modules/draw/modify/pedit.rs +++ b/src/modules/draw/modify/pedit.rs @@ -31,6 +31,7 @@ enum Mode { PickTarget, MultipleGather, MultipleConvert, + MultipleJoin, Options, /// Picked a Line/Arc: asking "Turn it into one? [Yes/No]". ConvertPrompt(Handle), @@ -234,6 +235,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::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] :" @@ -392,6 +394,8 @@ impl CadCommand for PeditCommand { mesh_closed: None, }, ); + let mut seen = std::collections::HashSet::new(); + self.multiple.retain(|handle| seen.insert(*handle)); self.target = Some(nh); self.mode = Mode::Options; } @@ -433,6 +437,13 @@ impl CadCommand for PeditCommand { if matches!(up.as_str(), "M" | "MULTIPLE") { self.mode = Mode::MultipleGather; Some(CmdResult::NeedPoint) } else { None } } 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())); } + self.pending_multiple = Some(self.multiple.clone()); + self.mode = Mode::Options; + return Some(CmdResult::PeditOp { handle: self.target?, op: PeditOp::JoinSelection(self.multiple.clone()) }); + }, Mode::MultipleConvert => { match up.as_str() { "Y" | "YES" | "" => { @@ -443,8 +454,8 @@ impl CadCommand for PeditCommand { "N" | "NO" => { self.multiple.retain(|handle| self.info.get(&handle.value()).is_some_and(|info| info.is_poly)); self.target = self.multiple.first().copied(); - self.mode = Mode::Options; - Some(if self.target.is_some() { CmdResult::NeedPoint } else { CmdResult::Cancel }) + self.mode = if self.target.is_some() { Mode::Options } else { Mode::PickTarget }; + Some(CmdResult::NeedPoint) } _ => Some(CmdResult::NeedPoint), } @@ -665,7 +676,7 @@ impl CadCommand for PeditCommand { Some(CmdResult::NeedPoint) } "J" | "JOIN" => { - self.mode = Mode::JoinGather(vec![handle]); + self.mode = if self.multiple.is_empty() { Mode::JoinGather(vec![handle]) } else { Mode::MultipleJoin }; Some(CmdResult::NeedPoint) } "F" | "FIT" => Some(CmdResult::PeditOp { @@ -751,6 +762,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::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 } @@ -788,6 +800,7 @@ impl CadCommand for PeditCommand { #[derive(Clone)] pub enum PeditOp { Multiple(Vec, Box), + JoinSelection(Vec), SetClosed(bool), SetWidth(f64), SetVertexWidth { index: usize, start: f64, end: f64 }, @@ -853,7 +866,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::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 {