Join compatible curves during spline editing with grouped undo

This commit is contained in:
ramox81 committed 2026-09-11 12:04:54 +03:00
1 parent b51672470d
commit 57c9e93d0c
3 files changed
+54 -4

No files matched your search

+3
View File
@@ -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
+3
View File
@@ -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<SelectionEntity>) {}
fn area_preview_regions(&self) -> Option<Vec<AreaPreviewRegion>> {
+48 -4
View File
@@ -33,6 +33,7 @@ enum Step {
SelectSpline,
Options,
Refine,
Join,
Add,
Delete,
Elevate,
@@ -48,11 +49,12 @@ pub struct SplineditCommand {
pending: Option<acadrust::entities::Spline>,
history: Vec<(acadrust::Handle, acadrust::entities::Spline)>,
pick_context: Option<crate::command::PointPickContext>,
join_candidates: Vec<crate::command::SelectionEntity>,
}
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<usize> {
@@ -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<DVec3>, degree: Option<usize>) -> Option<acadrust::entities::Spline> {
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] <eXit>:").into_owned(),
Step::Options => crate::t!("SPLINEDIT [Close/Move vertex/Refine/rEverse/Undo/eXit] <eXit>:").into_owned(),
Step::Options => crate::t!("SPLINEDIT [Close/Join/Move vertex/Refine/rEverse/Undo/eXit] <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] <eXit>:").into_owned(),
Step::Add => crate::t!("SPLINEDIT Specify a point on the spline <exit>:").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<crate::command::CmdOption> {
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<crate::command::SelectionEntity>) {
if matches!(self.step, Step::Join) { self.join_candidates = entities; }
}
fn on_selection_complete(&mut self, handles: Vec<acadrust::Handle>) -> 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<CmdResult> {
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<WireModel> { vec![] }
}
/// Apply a spline operation (CLOSE/OPEN/REVERSE) to a spline entity.