feat(pedit): convert prompt, join, fit/spline/decurve; join fixes (#263)

PEDIT reworked around a mode machine:

- Picking a line/arc asks "Object is not a polyline. Turn it into one?
  [Yes/No] <Y>" — Yes converts in place (arc keeps its bulge) and
  editing continues on the result. Pickfirst: an already-selected
  polyline skips the select step, a selected line/arc goes straight to
  the convert prompt.
- Join gathers with the normal selection system (single picks AND
  window/crossing boxes) and merges through the JOIN machinery.
- Fit replaces every segment with a classic equal-parameter BIARC: two
  arcs per segment meeting tangentially at an inserted knee, both
  segments at a vertex sharing that vertex's tangent — the whole run is
  tangent-continuous (verified: zero tangent jump on L/closed/zigzag/S
  shapes). Spline resamples the vertex frame as a uniform cubic
  B-spline (8 samples per span); Decurve straightens every bulge.
- Earlier hardening kept: undo snapshot before the mutation, immediate
  repaint, polyline-only pick validation.
- Vertex editing lives on grips, so no Edit-vertex submode; instead the
  vertex grip hover menu gains Break — a closed polyline opens at the
  vertex, an open one splits into two (interior vertices only),
  handled driver-side as an entity replacement.

JOIN itself: segs_of now emits one segment per span of an OPEN
polyline (bulges kept), so polylines merge with their neighbours —
PEDIT Join's set always contains the target polyline and used to abort
every time; plain JOIN had the same gap. JOIN also honours pickfirst:
launched with two or more objects selected it joins them immediately.

Closes #263

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Fable 5 committed 2026-07-22 20:06:04 +03:00
1 parent b32a9b4083
commit e7eb84b514
7 files changed
+705 -112

No files matched your search

+51 -18
View File
@@ -1277,25 +1277,58 @@ impl OpenCADStudio {
self.restore_pre_cmd_tangent();
}
CmdResult::PeditOp { handle, op } => {
use crate::modules::draw::modify::pedit::apply_pedit;
let changed = self.tabs[i]
.scene
.document
.get_entity_mut(handle)
.map(|e| apply_pedit(e, &op))
.unwrap_or(false);
if changed {
self.push_undo_snapshot(i, "PEDIT");
self.tabs[i].dirty = true;
self.command_line.push_output("PEDIT: applied.");
self.refresh_properties();
} else {
self.command_line
.push_error("PEDIT: operation not applicable to this entity.");
use crate::modules::draw::modify::pedit::{
apply_pedit, convert_to_polyline, PeditOp,
};
match &op {
// The convert replaces the entity (new handle).
PeditOp::ConvertToPolyline => {
let converted = self.tabs[i]
.scene
.document
.get_entity(handle)
.and_then(convert_to_polyline);
match converted {
Some(pl) => {
return self.apply_cmd_result(CmdResult::ReplaceEntity(
handle,
vec![pl],
));
}
None => self
.command_line
.push_error("PEDIT: cannot convert this entity."),
}
}
_ => {
// Snapshot BEFORE the mutation — an after-the-fact
// snapshot records the changed state and undo no-ops.
self.push_undo_snapshot(i, "PEDIT");
let changed = self.tabs[i]
.scene
.document
.get_entity_mut(handle)
.map(|e| apply_pedit(e, &op))
.unwrap_or(false);
if changed {
self.tabs[i].dirty = true;
// Repaint immediately (wide-band fill included).
self.tabs[i].scene.mark_entity_dirty(handle);
self.tabs[i].scene.refresh_fill_model(handle);
self.tabs[i].scene.bump_geometry_no_blocks();
self.command_line.push_output("PEDIT: applied.");
self.refresh_properties();
} else {
let _ = self.tabs[i].history.undo_stack.pop();
self.command_line
.push_error("PEDIT: operation not applicable to this entity.");
}
}
}
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
if let Some(p) = prompt {
self.command_line.push_info(&p);
}
// Keep command active — user may apply more ops
self.command_line
.push_info("PEDIT Enter option [C=Close O=Open W=Width X=Exit]:");
}
CmdResult::JoinEntities(handles) => {
use crate::modules::draw::modify::join::join_entities;
+43 -2
View File
@@ -223,6 +223,15 @@ impl OpenCADStudio {
// ── Break / Join ─────────────────────────────────────────────────
"JOIN" => {
use crate::modules::draw::modify::join::JoinCommand;
// Pickfirst: with objects already selected, join them right
// away instead of asking for a selection again.
let selected: Vec<acadrust::Handle> =
self.tabs[i].scene.selected.iter().copied().collect();
if selected.len() >= 2 {
let task =
self.apply_cmd_result(crate::command::CmdResult::JoinEntities(selected));
return Some(task);
}
let cmd = JoinCommand::new();
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
@@ -243,8 +252,40 @@ impl OpenCADStudio {
}
"PEDIT" => {
use crate::modules::draw::modify::pedit::PeditCommand;
let cmd_obj = PeditCommand::new();
use crate::modules::draw::modify::pedit::{PeditCommand, PeditTarget};
let info = self.tabs[i]
.scene
.document
.entities()
.filter_map(|e| {
let h = e.common().handle.value();
match e {
acadrust::EntityType::LwPolyline(_)
| acadrust::EntityType::Polyline2D(_) => Some((
h,
PeditTarget {
is_poly: true,
convertible: false,
},
)),
acadrust::EntityType::Line(_) | acadrust::EntityType::Arc(_) => {
Some((
h,
PeditTarget {
is_poly: false,
convertible: true,
},
))
}
_ => None,
}
})
.collect();
// Pickfirst: an already-selected polyline (or line/arc, via
// the convert prompt) skips the select step.
let preselected: Vec<acadrust::Handle> =
self.tabs[i].scene.selected.iter().copied().collect();
let cmd_obj = PeditCommand::new(info).with_preselection(&preselected);
self.command_line.push_info(&cmd_obj.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd_obj));
}
+27
View File
@@ -935,6 +935,33 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
self.command_line.push_info(&format!("{label}:"));
return self.focus_cmd_input();
}
// Break at vertex replaces the entity with the split pieces —
// structural, so it can't ride the in-place apply below.
if matches!(item.action, GripMenuAction::BreakVertex) {
let pieces = match self.tabs[i].scene.document.get_entity(popup.handle) {
Some(acadrust::EntityType::LwPolyline(p)) => {
crate::entities::lwpolyline::break_at_vertex(p, popup.grip_id)
}
_ => None,
};
match pieces {
Some(pieces) => {
self.push_undo_snapshot(i, "BREAK");
self.tabs[i].scene.erase_entities(&[popup.handle]);
for e in pieces {
self.tabs[i].scene.add_entity(e);
}
self.tabs[i].dirty = true;
self.refresh_selected_grips();
self.refresh_properties();
self.command_line.push_output("Polyline broken at vertex.");
}
None => self
.command_line
.push_error("Cannot break at this vertex."),
}
return Task::none();
}
// One-shot action — apply immediately.
self.push_undo_snapshot(i, item.label);
// For Add Leader, the new arrow becomes the last grip; remember