From 1870b8420ffa5fdf11e235d9cc8e5d13b8852d53 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 22 Jul 2026 11:17:01 +0300 Subject: [PATCH] feat(pline): Undo option removes the last segment (#352) U / UNDO (typed or via the prompt button) pops the last placed vertex without ending the command, repeatably down to and past the start point. The exit tangent of the remaining last segment is recomputed from its bulge so a following Arc segment stays tangent-continuous. New CmdResult::RemoveLiveEntity takes the live polyline out of the document when Undo drops it below two vertices; the next point re-creates it under the same undo unit. Closes #352 Co-Authored-By: Claude Fable 5 --- src/app/command_driver.rs | 13 +++++++++ src/command.rs | 3 +++ src/modules/draw/draw/polyline.rs | 44 +++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index b9fdc9c38..7972d7d17 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -808,6 +808,19 @@ impl OpenCADStudio { } } } + CmdResult::RemoveLiveEntity(handle) => { + // The command backed off below a valid entity (PLINE Undo at + // one remaining vertex): take the live entity out of the + // document and keep prompting. No undo bookkeeping — the + // create's snapshot already covers the whole in-progress + // object as one unit. + self.tabs[i].scene.erase_entities(&[handle]); + self.tabs[i].dirty = true; + let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); + if let Some(p) = prompt { + self.command_line.push_info(&p); + } + } CmdResult::Cancel => { self.tabs[i].scene.clear_preview_wire(); self.tabs[i].active_cmd = None; diff --git a/src/command.rs b/src/command.rs index 696998a57..e5eecc4e1 100644 --- a/src/command.rs +++ b/src/command.rs @@ -983,6 +983,9 @@ pub enum CmdResult { entity: EntityType, finish: bool, }, + /// Remove the live entity from the document but keep the command running — + /// PLINE's Undo popping back below the two vertices an entity needs. + RemoveLiveEntity(Handle), /// Suspends command execution, moves it to suspended_cmd, and opens the text editor for the given handle. SuspendForTextEdit { handle: Handle }, /// Requests a standard document-level undo while keeping the command active. diff --git a/src/modules/draw/draw/polyline.rs b/src/modules/draw/draw/polyline.rs index 02beb6f0d..0d2cf7e9a 100644 --- a/src/modules/draw/draw/polyline.rs +++ b/src/modules/draw/draw/polyline.rs @@ -144,6 +144,20 @@ fn compute_bulge(a: DVec2, tangent: DVec2, b: DVec2) -> f64 { (arc_angle / 4.0).tan() } +/// Exit tangent of the segment `a` → `b` with `bulge` (0 = straight line): +/// the chord direction rotated by half the arc sweep (the chord bisects the +/// entry/exit tangents of a bulge arc). Used to restore tangent continuity +/// after Undo pops a segment. +fn seg_exit_tangent(a: DVec3, b: DVec3, bulge: f64) -> Option { + let d = DVec2::new(b.x - a.x, b.y - a.y); + if d.length_squared() < 1e-10 { + return None; + } + // Full sweep is 4·atan(bulge); the exit tangent sits half that past the chord. + let ang = d.y.atan2(d.x) + 2.0 * bulge.atan(); + Some(Vec2::new(ang.cos() as f32, ang.sin() as f32)) +} + /// Update `tangent` after an arc segment described by `bulge` from `a` to `b`. fn update_tangent_after_arc(tangent: &mut Option, bulge: f64) { let Some(t) = *tangent else { @@ -235,6 +249,7 @@ impl CadCommand for PlineCommand { CmdOption::new("Arc", "A"), CmdOption::new("Line", "L"), CmdOption::new("Close", "C"), + CmdOption::new("Undo", "U"), CmdOption::enter("Done"), ] } @@ -330,6 +345,35 @@ impl CadCommand for PlineCommand { Some(CmdResult::NeedPoint) } "C" | "CLOSE" => Some(self.sync_live(true, true)), + // Undo: drop the last placed vertex but keep drawing (#352). + // Repeatable — each U backs off one more segment, down to (and + // including) the start point. + "U" | "UNDO" => { + if self.vertices.is_empty() { + return Some(CmdResult::NeedPoint); + } + self.vertices.pop(); + self.bulges.pop(); + // Restore the exit tangent of the segment that is now last so + // a following Arc segment stays tangent-continuous. + let n = self.vertices.len(); + self.last_tangent = if n >= 2 { + seg_exit_tangent(self.vertices[n - 2], self.vertices[n - 1], self.bulges[n - 2]) + } else { + None + }; + Some(match n { + // Start point undone too — back to "Specify start point". + 0 => CmdResult::NeedPoint, + // One vertex left: the live entity needs two, so it leaves + // the document until the next point re-creates it. + 1 => match self.live_handle.take() { + Some(h) => CmdResult::RemoveLiveEntity(h), + None => CmdResult::NeedPoint, + }, + _ => self.sync_live(false, false), + }) + } _ => None, } }