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 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Fable 5 committed 2026-07-22 11:17:01 +03:00
1 parent e009d2d7a3
commit 1870b8420f
3 files changed
+60

No files matched your search

+13
View File
@@ -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;
+3
View File
@@ -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.
+44
View File
@@ -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<Vec2> {
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<Vec2>, 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,
}
}