diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 88e05d7e1..fd52dcd54 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -4,6 +4,68 @@ use acadrust::Handle; use iced::Task; impl OpenCADStudio { + /// Point supplied by a bare Enter before LINE/PLINE's first click. Prefer + /// the current endpoint of the most recently created path drawable in the + /// active space. A loaded drawing has no runtime anchor, so recover its + /// newest line/arc/polyline endpoint. An empty space starts at the active + /// UCS origin (0,0,0 in user coordinates). + fn default_draw_start(&self, i: usize) -> glam::DVec3 { + let tab = &self.tabs[i]; + let endpoint = |entity: &acadrust::EntityType| { + let last_grip = match entity { + acadrust::EntityType::Line(line) => { + return Some(glam::DVec3::new( + line.end.x, + line.end.y, + line.end.z, + )); + } + acadrust::EntityType::Arc(_) => Some(2), + acadrust::EntityType::LwPolyline(polyline) => { + polyline.vertices.len().checked_sub(1) + } + acadrust::EntityType::Polyline(polyline) => { + polyline.vertices.len().checked_sub(1) + } + acadrust::EntityType::Polyline2D(polyline) => { + polyline.vertices.len().checked_sub(1) + } + acadrust::EntityType::Polyline3D(polyline) => { + polyline.vertices.len().checked_sub(1) + } + _ => None, + }?; + crate::scene::view::dispatch::grips(entity) + .into_iter() + .find(|grip| grip.id == last_grip) + .map(|grip| grip.world) + }; + + if let Some(handle) = tab.last_draw_anchor { + if tab.scene.entity_belongs_to_active_space(handle) { + if let Some(point) = tab.scene.document.get_entity(handle).and_then(endpoint) { + return point; + } + } + } + + let recovered = tab + .scene + .document + .entities() + .filter_map(|entity| { + let handle = entity.common().handle; + tab.scene + .entity_belongs_to_active_space(handle) + .then(|| endpoint(entity).map(|point| (handle, point))) + .flatten() + }) + .max_by_key(|(handle, _)| handle.value()) + .map(|(_, point)| point); + + recovered.unwrap_or_else(|| tab.ucs_origin_world()) + } + /// Drop cursor-relative state that was computed in the drawing space being /// left. This is also used by MVIEW, whose command object survives its /// intentional paper/model round-trip while its old-space overlays cannot. @@ -169,11 +231,31 @@ impl OpenCADStudio { } } let i = self.active_tab; + let default_start = matches!(&input, StepInput::Enter) + && self.tabs[i] + .active_cmd + .as_ref() + .is_some_and(|command| command.enter_accepts_default_start()); + let input = if default_start { + StepInput::Point(self.default_draw_start(i)) + } else { + input + }; if let StepInput::Point(point) = &input { if !self.command_point_allowed(i, *point) { return Task::none(); } } + if default_start { + let StepInput::Point(point) = &input else { + unreachable!("default command start must be a point"); + }; + self.last_point = Some(*point); + self.dyn_user_reshaped = false; + self.sync_dyn_fields(); + self.reset_tracking_after_point(); + self.push_ucs_to_cmd(i); + } let ctrl = self.ctrl_down; let shift = self.shift_down; let result: Option = { @@ -1245,6 +1327,15 @@ impl OpenCADStudio { entity, finish, } => { + let tracks_draw_anchor = matches!( + &entity, + acadrust::EntityType::Line(_) + | acadrust::EntityType::Arc(_) + | acadrust::EntityType::LwPolyline(_) + | acadrust::EntityType::Polyline(_) + | acadrust::EntityType::Polyline2D(_) + | acadrust::EntityType::Polyline3D(_) + ); // Replace the live entity's geometry in place, preserving its // handle and layer (the fresh entity from the command carries // defaults — a NULL handle would desync it from the document @@ -1263,6 +1354,9 @@ impl OpenCADStudio { .bump_entities(&[(handle, crate::scene::ChangeKind::Modified)]); self.tabs[i].dirty = true; } + if tracks_draw_anchor { + self.tabs[i].last_draw_anchor = Some(handle); + } if finish { self.finish_live_entity_history(i, handle); self.tabs[i].scene.clear_preview_wire(); @@ -1292,6 +1386,12 @@ impl OpenCADStudio { // document, drop its provisional history entry and keep // prompting. A later second point creates one fresh entry. self.tabs[i].scene.erase_entities(&[handle]); + if self.tabs[i] + .last_draw_anchor + .is_some_and(|anchor_handle| anchor_handle == handle) + { + self.tabs[i].last_draw_anchor = None; + } self.discard_last_undo_entry(i); self.tabs[i].dirty = true; let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); diff --git a/src/app/document.rs b/src/app/document.rs index 63d89f4ec..3224f0e16 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -132,6 +132,9 @@ pub(super) struct DocumentTab { /// "Previous" keyword at any Select objects prompt (#426). pub(super) prev_selection: Vec, pub(super) last_cmd: Option, + /// Most recently created path drawable. A fresh LINE/PLINE can accept its + /// current final endpoint with Enter before the first click. + pub(super) last_draw_anchor: Option, pub(super) snap_result: Option, pub(super) active_grip: Option, pub(super) selected_grips: Vec, @@ -437,6 +440,7 @@ impl DocumentTab { layers: LayerPanel::default(), active_cmd: None, last_cmd: None, + last_draw_anchor: None, snap_result: None, active_grip: None, selected_grips: vec![], diff --git a/src/app/properties.rs b/src/app/properties.rs index 8046f9745..97d475d7f 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -1775,6 +1775,16 @@ impl OpenCADStudio { mut entity: acadrust::EntityType, ) -> Option { let i = self.active_tab; + let tracks_draw_anchor = self.tabs[i].active_cmd.is_some() + && matches!( + &entity, + acadrust::EntityType::Line(_) + | acadrust::EntityType::Arc(_) + | acadrust::EntityType::LwPolyline(_) + | acadrust::EntityType::Polyline(_) + | acadrust::EntityType::Polyline2D(_) + | acadrust::EntityType::Polyline3D(_) + ); let layer = &self.tabs[i].active_layer; if layer != "0" || entity.as_entity().layer().is_empty() { entity.as_entity_mut().set_layer(layer.clone()); @@ -1946,7 +1956,7 @@ impl OpenCADStudio { } - if matches!(&entity, acadrust::EntityType::Viewport(_)) + let new_handle = if matches!(&entity, acadrust::EntityType::Viewport(_)) && self.tabs[i].scene.current_layout != "Model" { // Assign a unique viewport ID (max existing id + 1, min 2). @@ -1999,7 +2009,14 @@ impl OpenCADStudio { } } else { Some(self.tabs[i].scene.add_entity(entity)) + }; + + if tracks_draw_anchor { + if let Some(handle) = new_handle { + self.tabs[i].last_draw_anchor = Some(handle); + } } + new_handle } } diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 3d391316e..ff2010e2d 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -356,11 +356,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } if text.is_empty() { - let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_enter()); - if let Some(r) = result { - return self.apply_cmd_result(r); - } - return Task::none(); + return self.feed_command(crate::command::StepInput::Enter); } // OTRACK: while aligned to a tracking ray, a bare distance @@ -494,11 +490,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { } let i = self.active_tab; if self.tabs[i].active_cmd.is_some() { - let result = self.tabs[i].active_cmd.as_mut().map(|c| c.on_enter()); - if let Some(r) = result { - return self.apply_cmd_result(r); - } - Task::none() + self.feed_command(crate::command::StepInput::Enter) } else if let Some(cmd) = self.tabs[i].last_cmd.clone() { self.dispatch_command(&cmd) } else { diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 75fe0917a..a0b3ddfa2 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -247,7 +247,7 @@ impl OpenCADStudio { /// point has been committed to the active command. Temporary tracking /// points are reset on every input so they don't pile up across a /// multi-point command and overwhelm the next pick (issue #85). - fn reset_tracking_after_point(&mut self) { + pub(in crate::app) fn reset_tracking_after_point(&mut self) { self.snapper.clear_tracking(); self.otrack_active = None; } diff --git a/src/command.rs b/src/command.rs index af7bd19ab..5199b2963 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1279,6 +1279,14 @@ pub trait CadCommand: Send { /// Called when the user presses Enter (finalize / next option). fn on_enter(&mut self) -> CmdResult; + /// Whether a bare Enter should supply the drawing's continuation point as + /// this command's first point instead of calling [`Self::on_enter`]. Draw + /// commands opt in only while their first point is still unset; later + /// Enter presses retain their normal finish/cancel meaning. + fn enter_accepts_default_start(&self) -> bool { + false + } + /// Called when the user presses Escape (cancel). #[allow(dead_code)] fn on_escape(&mut self) -> CmdResult { diff --git a/src/modules/draw/draw/arc.rs b/src/modules/draw/draw/arc.rs index baaa3e5f3..e6e48e932 100644 --- a/src/modules/draw/draw/arc.rs +++ b/src/modules/draw/draw/arc.rs @@ -369,6 +369,9 @@ impl CadCommand for ArcCommand { fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } + fn enter_accepts_default_start(&self) -> bool { + self.step == 0 + } fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } @@ -467,6 +470,9 @@ impl CadCommand for Arc3PCommand { fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } + fn enter_accepts_default_start(&self) -> bool { + self.pts.is_empty() + } fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } @@ -564,6 +570,9 @@ impl CadCommand for ArcSCECommand { fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } + fn enter_accepts_default_start(&self) -> bool { + self.step == 0 + } fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } @@ -664,6 +673,9 @@ impl CadCommand for ArcSCACommand { fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } + fn enter_accepts_default_start(&self) -> bool { + self.step == 0 + } fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } @@ -787,6 +799,9 @@ impl CadCommand for ArcSCLCommand { fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } + fn enter_accepts_default_start(&self) -> bool { + self.step == 0 + } fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } @@ -886,6 +901,9 @@ impl CadCommand for ArcSEACommand { fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } + fn enter_accepts_default_start(&self) -> bool { + self.step == 0 + } fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } @@ -961,6 +979,9 @@ impl CadCommand for ArcSERCommand { fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } + fn enter_accepts_default_start(&self) -> bool { + self.step == 0 + } fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } @@ -1074,6 +1095,9 @@ impl CadCommand for ArcSEDCommand { fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } + fn enter_accepts_default_start(&self) -> bool { + self.step == 0 + } fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } @@ -1160,6 +1184,9 @@ impl CadCommand for ArcCSACommand { fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } + fn enter_accepts_default_start(&self) -> bool { + self.step == 0 + } fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } @@ -1279,6 +1306,9 @@ impl CadCommand for ArcCSLCommand { fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } + fn enter_accepts_default_start(&self) -> bool { + self.step == 0 + } fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } diff --git a/src/modules/draw/draw/line.rs b/src/modules/draw/draw/line.rs index 303102552..a3d982456 100644 --- a/src/modules/draw/draw/line.rs +++ b/src/modules/draw/draw/line.rs @@ -150,6 +150,10 @@ impl CadCommand for LineCommand { CmdResult::Cancel } + fn enter_accepts_default_start(&self) -> bool { + self.points.is_empty() + } + fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } diff --git a/src/modules/draw/draw/polyline.rs b/src/modules/draw/draw/polyline.rs index 16d366bec..8fe6c90d5 100644 --- a/src/modules/draw/draw/polyline.rs +++ b/src/modules/draw/draw/polyline.rs @@ -355,6 +355,10 @@ impl CadCommand for PlineCommand { } } + fn enter_accepts_default_start(&self) -> bool { + self.vertices.is_empty() + } + fn on_escape(&mut self) -> CmdResult { match self.live_handle { Some(handle) => CmdResult::FinalizeLiveEntity(handle), diff --git a/src/modules/draw/draw/spline.rs b/src/modules/draw/draw/spline.rs index 43f9ad88c..8d20390a5 100644 --- a/src/modules/draw/draw/spline.rs +++ b/src/modules/draw/draw/spline.rs @@ -143,6 +143,10 @@ impl CadCommand for SplineCommand { } } + fn enter_accepts_default_start(&self) -> bool { + self.pts.is_empty() + } + fn on_escape(&mut self) -> CmdResult { match self.build(false) { Some(e) => CmdResult::CommitAndExit(e),