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 Seven 2026-07-22 20:06:04 +03:00
commit e7eb84b514
7 changed files with 704 additions and 111 deletions

View file

@ -404,6 +404,42 @@ fn tapered_band_verts(pline: &LwPolyline) -> Option<Vec<([f64; 2], f64, f64, f64
}
}
/// Split at vertex `idx`: a closed polyline re-opens there (one piece); an
/// open one splits into two (interior vertices only). `None` when invalid.
pub(crate) fn break_at_vertex(
p: &acadrust::LwPolyline,
idx: usize,
) -> Option<Vec<acadrust::EntityType>> {
use acadrust::EntityType;
let n = p.vertices.len();
if n < 3 || idx >= n {
return None;
}
if p.is_closed {
let mut verts = Vec::with_capacity(n + 1);
verts.extend_from_slice(&p.vertices[idx..]);
verts.extend_from_slice(&p.vertices[..=idx]);
let mut out = p.clone();
out.common.handle = acadrust::Handle::NULL;
out.is_closed = false;
out.vertices = verts;
return Some(vec![EntityType::LwPolyline(out)]);
}
if idx == 0 || idx == n - 1 {
return None;
}
let mut a = p.clone();
a.common.handle = acadrust::Handle::NULL;
a.vertices = p.vertices[..=idx].to_vec();
if let Some(last) = a.vertices.last_mut() {
last.bulge = 0.0;
}
let mut b = p.clone();
b.common.handle = acadrust::Handle::NULL;
b.vertices = p.vertices[idx..].to_vec();
Some(vec![EntityType::LwPolyline(a), EntityType::LwPolyline(b)])
}
fn grips(pline: &LwPolyline) -> Vec<GripDef> {
let elev = pline.elevation;
let n = pline.vertices.len();
@ -678,8 +714,10 @@ impl crate::entities::traits::Grippable for LwPolyline {
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
let n = self.vertices.len();
if grip_id < n {
// Vertex grip.
return vec![
// Vertex grip. Break only where a split is possible: any vertex
// of a closed polyline, interior vertices of an open one.
let breakable = n >= 3 && (self.is_closed || (grip_id > 0 && grip_id < n - 1));
let mut items = vec![
GripMenuItem {
label: "Stretch",
action: GripMenuAction::Stretch,
@ -693,6 +731,13 @@ impl crate::entities::traits::Grippable for LwPolyline {
action: GripMenuAction::RemoveVertex,
},
];
if breakable {
items.push(GripMenuItem {
label: "Break",
action: GripMenuAction::BreakVertex,
});
}
return items;
}
// Segment midpoint grip.
let seg = grip_id - n;