feat(draw): continue paths on initial Enter

Start LINE, PLINE, ARC, and SPLINE from the latest path endpoint when Enter accepts the first point. Fall back to the active UCS origin.\n\nRefs #610
This commit is contained in:
Hakan Seven committed 2026-08-01 19:09:19 +03:00
1 parent 796d7d91d0
commit dccaf95f49
10 files changed
+175 -12

No files matched your search

+100
View File
@@ -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<CmdResult> = {
@@ -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());
+4
View File
@@ -132,6 +132,9 @@ pub(super) struct DocumentTab {
/// "Previous" keyword at any Select objects prompt (#426).
pub(super) prev_selection: Vec<acadrust::Handle>,
pub(super) last_cmd: Option<String>,
/// 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<Handle>,
pub(super) snap_result: Option<SnapResult>,
pub(super) active_grip: Option<GripEdit>,
pub(super) selected_grips: Vec<GripDef>,
@@ -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![],
+18 -1
View File
@@ -1775,6 +1775,16 @@ impl OpenCADStudio {
mut entity: acadrust::EntityType,
) -> Option<Handle> {
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
}
}
+2 -10
View File
@@ -356,11 +356,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
}
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<Message> {
}
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 {
+1 -1
View File
@@ -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;
}
+8
View File
@@ -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 {
+30
View File
@@ -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
}
+4
View File
@@ -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
}
+4
View File
@@ -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),
+4
View File
@@ -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),