diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 8b80557e9..16d57ae5f 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -417,10 +417,13 @@ impl OpenCADStudio { } } if let StepInput::SelectionComplete(handles) = &input { + let exclude_locked = self.tabs[i].active_cmd.as_ref() + .is_some_and(|command| command.selection_entities_exclude_locked()); let entities = { let scene = &self.tabs[i].scene; handles .iter() + .filter(|handle| !exclude_locked || !scene.is_layer_locked(**handle)) .filter_map(|handle| { scene.document.get_entity(*handle).cloned().map(|entity| { let surface_area = scene diff --git a/src/command.rs b/src/command.rs index 177d25894..919c869e4 100644 --- a/src/command.rs +++ b/src/command.rs @@ -2301,6 +2301,9 @@ pub trait CadCommand: Send { CmdResult::Cancel } + /// Exclude locked-layer entities from injected selection geometry. + fn selection_entities_exclude_locked(&self) -> bool { false } + fn inject_selection_entities(&mut self, _entities: Vec) {} fn area_preview_regions(&self) -> Option> { diff --git a/src/modules/draw/modify/splinedit.rs b/src/modules/draw/modify/splinedit.rs index c549aba05..5e260b414 100644 --- a/src/modules/draw/modify/splinedit.rs +++ b/src/modules/draw/modify/splinedit.rs @@ -33,6 +33,7 @@ enum Step { SelectSpline, Options, Refine, + Join, Add, Delete, Elevate, @@ -48,11 +49,12 @@ pub struct SplineditCommand { pending: Option, history: Vec<(acadrust::Handle, acadrust::entities::Spline)>, pick_context: Option, + join_candidates: Vec, } impl SplineditCommand { pub fn new() -> Self { - Self { step: Step::SelectSpline, handle: acadrust::Handle::NULL, spline: None, pending: None, history: Vec::new(), pick_context: None } + Self { step: Step::SelectSpline, handle: acadrust::Handle::NULL, spline: None, pending: None, history: Vec::new(), pick_context: None, join_candidates: Vec::new() } } fn picked_vertex(&self, point: DVec3) -> Option { @@ -83,6 +85,24 @@ impl SplineditCommand { CmdResult::ReplaceManyContinue(vec![(self.handle, vec![EntityType::Spline(spline)])]) } + fn finish_join(&mut self) -> CmdResult { + self.step = Step::Options; + let candidates = std::mem::take(&mut self.join_candidates); + let Some(source) = self.spline.as_ref() else { return CmdResult::NeedPoint; }; + let selected: Vec<_> = candidates.iter().filter(|item| item.handle != self.handle) + .map(|item| (item.handle, &item.entity)).collect(); + let Some((EntityType::Spline(mut spline), consumed)) = super::join::join_to_source( + &EntityType::Spline(source.clone()), &selected) else { return CmdResult::NeedPoint; }; + spline.dwg_flags1 &= !1; + spline.dxf_flags &= !(32 | 1024); + self.pending = Some(spline.clone()); + let mut replacements = vec![(self.handle, vec![EntityType::Spline(spline)])]; + replacements.extend(consumed.into_iter().map(|handle| (handle, Vec::new()))); + // The host stores one document snapshot, including every consumed curve. + // The existing command-local Undo restores that snapshot and the source cache. + CmdResult::ReplaceManyContinue(replacements) + } + fn refined(&self, point: Option, degree: Option) -> Option { let source = self.spline.as_ref()?; let planar = crate::entities::curve::entity_curve(&EntityType::Spline(source.clone()))?; @@ -114,7 +134,8 @@ impl CadCommand for SplineditCommand { match self.step { Step::SelectSpline => crate::t!("SPLINEDIT Select spline:").into_owned(), Step::Options if self.closed() => crate::t!("SPLINEDIT [Open/Move vertex/Refine/rEverse/Undo/eXit] :").into_owned(), - Step::Options => crate::t!("SPLINEDIT [Close/Move vertex/Refine/rEverse/Undo/eXit] :").into_owned(), + Step::Options => crate::t!("SPLINEDIT [Close/Join/Move vertex/Refine/rEverse/Undo/eXit] :").into_owned(), + Step::Join => crate::t!("SPLINEDIT Select any open curves to join to source:").into_owned(), Step::Refine => crate::t!("SPLINEDIT [Add/Delete/Elevate order/Move/Weight/eXit] :").into_owned(), Step::Add => crate::t!("SPLINEDIT Specify a point on the spline :").into_owned(), Step::Delete => crate::t!("SPLINEDIT Specify control vertex to delete:").into_owned(), @@ -127,12 +148,26 @@ impl CadCommand for SplineditCommand { fn options(&self) -> Vec { use crate::command::CmdOption; match self.step { - Step::Options => vec![if self.closed() { CmdOption::new("Open", "O") } else { CmdOption::new("Close", "C") }, CmdOption::new("Move vertex", "M"), CmdOption::new("Refine", "R"), CmdOption::new("Reverse", "E"), CmdOption::new("Undo", "U"), CmdOption::new("Exit", "X")], + Step::Options => { + let mut options = vec![if self.closed() { CmdOption::new("Open", "O") } else { CmdOption::new("Close", "C") }]; + if !self.closed() { options.push(CmdOption::new("Join", "J")); } + options.extend([CmdOption::new("Move vertex", "M"), CmdOption::new("Refine", "R"), CmdOption::new("Reverse", "E"), CmdOption::new("Undo", "U"), CmdOption::new("Exit", "X")]); + options + }, Step::Refine => vec![CmdOption::new("Add", "A"), CmdOption::new("Delete", "D"), CmdOption::new("Elevate order", "E"), CmdOption::new("Move", "M"), CmdOption::new("Weight", "W"), CmdOption::new("Exit", "X")], Step::Move { .. } | Step::Weight { .. } => vec![CmdOption::new("Next", "N"), CmdOption::new("Previous", "P"), CmdOption::new("Select point", "S"), CmdOption::new("Exit", "X")], _ => Vec::new(), } } + fn is_selection_gathering(&self) -> bool { matches!(self.step, Step::Join) } + fn selection_entities_exclude_locked(&self) -> bool { matches!(self.step, Step::Join) } + fn inject_selection_entities(&mut self, entities: Vec) { + if matches!(self.step, Step::Join) { self.join_candidates = entities; } + } + fn on_selection_complete(&mut self, handles: Vec) -> CmdResult { + self.join_candidates.retain(|item| handles.contains(&item.handle) && item.handle != self.handle); + CmdResult::NeedPoint + } fn needs_entity_pick(&self) -> bool { matches!(self.step, Step::SelectSpline) } fn inject_before_entity_pick(&self) -> bool { true } fn inject_picked_entity(&mut self, entity: EntityType) { @@ -152,12 +187,13 @@ impl CadCommand for SplineditCommand { } } } - fn wants_text_input(&self) -> bool { !matches!(self.step, Step::SelectSpline | Step::Add | Step::Delete | Step::Move { .. } | Step::SelectVertex { .. }) } + fn wants_text_input(&self) -> bool { !matches!(self.step, Step::SelectSpline | Step::Join | Step::Add | Step::Delete | Step::Move { .. } | Step::SelectVertex { .. }) } fn on_text_input(&mut self, text: &str) -> Option { let upper = text.trim().to_uppercase(); if upper.is_empty() { return Some(self.on_enter()); } match self.step { Step::Options => match upper.as_str() { + "J" | "JOIN" if !self.closed() => { self.join_candidates.clear(); self.step = Step::Join; }, "R" | "REFINE" => self.step = Step::Refine, "M" | "MOVE" => self.step = Step::Move { index: 0, refine: false }, "X" | "EXIT" => return Some(CmdResult::Cancel), @@ -277,6 +313,7 @@ impl CadCommand for SplineditCommand { fn on_enter(&mut self) -> CmdResult { match self.step { Step::SelectSpline | Step::Options => CmdResult::Cancel, + Step::Join => self.finish_join(), Step::Refine => { self.step = Step::Options; CmdResult::NeedPoint } Step::Add | Step::Delete | Step::SelectVertex { .. } => { self.step = Step::Refine; CmdResult::NeedPoint } Step::Elevate => { @@ -286,6 +323,13 @@ impl CadCommand for SplineditCommand { Step::Move { .. } | Step::Weight { .. } => self.on_text_input("N").unwrap_or(CmdResult::NeedPoint), } } + fn on_escape(&mut self) -> CmdResult { + if matches!(self.step, Step::Join) { + self.join_candidates.clear(); + self.step = Step::Options; + CmdResult::NeedPoint + } else { CmdResult::Cancel } + } fn on_preview_wires(&mut self, _pt: DVec3) -> Vec { vec![] } } /// Apply a spline operation (CLOSE/OPEN/REVERSE) to a spline entity.