feat(trim): trim closed/open polylines (#65)
TRIM had no LwPolyline arm in its pick dispatch, so clicking a polyline segment did nothing (only EXTEND handled polylines). Add trim_lwpolyline: parameterise the polyline by segment+u, find boundary crossings per segment, and remove the span containing the click between the adjacent cuts. A closed polyline needs >=2 cuts and yields the surviving open arc; an open one yields the surviving piece(s). Fully-surviving segment bulges are kept; the partial ends become straight. Hover now previews the trim too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
3c97111d8f
commit
534535be98
1 changed files with 166 additions and 1 deletions
|
|
@ -11,7 +11,7 @@
|
||||||
use std::f64::consts::TAU;
|
use std::f64::consts::TAU;
|
||||||
|
|
||||||
use acadrust::entities::{
|
use acadrust::entities::{
|
||||||
Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline, Ray as RayEnt,
|
Arc as ArcEnt, Ellipse as EllipseEnt, Line as LineEnt, LwPolyline, LwVertex, Ray as RayEnt,
|
||||||
Spline as SplineEnt, XLine as XLineEnt,
|
Spline as SplineEnt, XLine as XLineEnt,
|
||||||
};
|
};
|
||||||
use acadrust::types::Vector3;
|
use acadrust::types::Vector3;
|
||||||
|
|
@ -1223,6 +1223,148 @@ fn trim_arc(orig: &ArcEnt, ts: &[f64], t_click: f64) -> Vec<EntityType> {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Trim a clicked LwPolyline: remove the portion containing the click, bounded
|
||||||
|
/// by the nearest boundary intersections on each side. A closed polyline needs
|
||||||
|
/// ≥2 cuts and becomes an open polyline (the surviving arc); an open one yields
|
||||||
|
/// the surviving piece(s). Bulges on fully-surviving segments are kept; the
|
||||||
|
/// partial end segments at a cut become straight (issue #65).
|
||||||
|
fn trim_lwpolyline(poly: &LwPolyline, cx: f64, cy: f64, geos: &[Geo]) -> Option<Vec<EntityType>> {
|
||||||
|
let handle = poly.common.handle;
|
||||||
|
let n = poly.vertices.len();
|
||||||
|
if n < 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let closed = poly.is_closed;
|
||||||
|
let seg_count = if closed { n } else { n - 1 };
|
||||||
|
let total = seg_count as f64;
|
||||||
|
|
||||||
|
let vx = |i: usize| -> (f64, f64) {
|
||||||
|
let v = &poly.vertices[i % n];
|
||||||
|
(v.location.x, v.location.y)
|
||||||
|
};
|
||||||
|
let point_at = |t: f64| -> (f64, f64) {
|
||||||
|
let tt = if closed { t.rem_euclid(total) } else { t.clamp(0.0, total) };
|
||||||
|
let i = (tt.floor() as usize).min(seg_count.saturating_sub(1));
|
||||||
|
let u = tt - i as f64;
|
||||||
|
let (ax, ay) = vx(i);
|
||||||
|
let (bx, by) = vx(i + 1);
|
||||||
|
(ax + u * (bx - ax), ay + u * (by - ay))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Boundary cuts as global params (segment index + local u).
|
||||||
|
let mut cuts: Vec<f64> = Vec::new();
|
||||||
|
for i in 0..seg_count {
|
||||||
|
let (ax, ay) = vx(i);
|
||||||
|
let (bx, by) = vx(i + 1);
|
||||||
|
for u in line_seg_ts(ax, ay, bx, by, handle, geos) {
|
||||||
|
cuts.push(i as f64 + u.clamp(0.0, 1.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cuts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||||
|
cuts.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
|
||||||
|
if cuts.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click param: nearest point on the polyline.
|
||||||
|
let mut best = (f64::INFINITY, 0.0_f64);
|
||||||
|
for i in 0..seg_count {
|
||||||
|
let (ax, ay) = vx(i);
|
||||||
|
let (bx, by) = vx(i + 1);
|
||||||
|
let (dx, dy) = (bx - ax, by - ay);
|
||||||
|
let len2 = dx * dx + dy * dy;
|
||||||
|
let u = if len2 > 1e-12 {
|
||||||
|
(((cx - ax) * dx + (cy - ay) * dy) / len2).clamp(0.0, 1.0)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
let (px, py) = (ax + u * dx, ay + u * dy);
|
||||||
|
let d = (px - cx).powi(2) + (py - cy).powi(2);
|
||||||
|
if d < best.0 {
|
||||||
|
best = (d, i as f64 + u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let t_click = best.1;
|
||||||
|
|
||||||
|
// Emit the surviving sub-polyline from param `s0` to `s1` (s1 > s0), with
|
||||||
|
// both ends treated as cut points (straight) and interior vertices keeping
|
||||||
|
// their original bulge.
|
||||||
|
let emit = |s0: f64, s1: f64, start_cut: bool, end_cut: bool| -> Vec<(f64, f64, f64)> {
|
||||||
|
let mut o: Vec<(f64, f64, f64)> = Vec::new();
|
||||||
|
let (sx, sy) = point_at(s0);
|
||||||
|
let s_idx = (s0.floor() as usize) % seg_count;
|
||||||
|
let s_bulge = if start_cut { 0.0 } else { poly.vertices[s_idx % n].bulge };
|
||||||
|
o.push((sx, sy, s_bulge));
|
||||||
|
let mut k = s0.floor() as i64 + 1;
|
||||||
|
while (k as f64) < s1 - 1e-9 {
|
||||||
|
let idx = (k as usize) % n;
|
||||||
|
let seg = (k as usize) % seg_count;
|
||||||
|
o.push((vx(idx).0, vx(idx).1, poly.vertices[seg].bulge));
|
||||||
|
k += 1;
|
||||||
|
}
|
||||||
|
if end_cut {
|
||||||
|
if let Some(l) = o.last_mut() {
|
||||||
|
l.2 = 0.0; // outgoing toward the cut is partial → straight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let (ex, ey) = point_at(s1);
|
||||||
|
o.push((ex, ey, 0.0));
|
||||||
|
o
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut pieces: Vec<Vec<(f64, f64, f64)>> = Vec::new();
|
||||||
|
if closed {
|
||||||
|
if cuts.len() < 2 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let hi = cuts
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.find(|&c| c > t_click + 1e-9)
|
||||||
|
.unwrap_or(cuts[0] + total);
|
||||||
|
let lo = cuts
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.rev()
|
||||||
|
.find(|&c| c < t_click - 1e-9)
|
||||||
|
.unwrap_or(cuts[cuts.len() - 1] - total);
|
||||||
|
let mut s1 = lo;
|
||||||
|
while s1 <= hi {
|
||||||
|
s1 += total;
|
||||||
|
}
|
||||||
|
pieces.push(emit(hi, s1, true, true));
|
||||||
|
} else {
|
||||||
|
let lo = cuts.iter().cloned().rev().find(|&c| c < t_click - 1e-9);
|
||||||
|
let hi = cuts.iter().cloned().find(|&c| c > t_click + 1e-9);
|
||||||
|
if let Some(lo) = lo {
|
||||||
|
pieces.push(emit(0.0, lo, false, true));
|
||||||
|
}
|
||||||
|
if let Some(hi) = hi {
|
||||||
|
pieces.push(emit(hi, total, true, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out: Vec<EntityType> = Vec::new();
|
||||||
|
for verts in pieces {
|
||||||
|
if verts.len() < 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut np = poly.clone();
|
||||||
|
np.common.handle = Handle::NULL;
|
||||||
|
np.is_closed = false;
|
||||||
|
np.vertices = verts
|
||||||
|
.into_iter()
|
||||||
|
.map(|(x, y, b)| {
|
||||||
|
let mut v = LwVertex::from_coords(x, y);
|
||||||
|
v.bulge = b;
|
||||||
|
v
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
out.push(EntityType::LwPolyline(np));
|
||||||
|
}
|
||||||
|
Some(out)
|
||||||
|
}
|
||||||
|
|
||||||
// ── Extend helpers ────────────────────────────────────────────────────────
|
// ── Extend helpers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Extend the first or last segment of an LwPolyline to the nearest boundary.
|
/// Extend the first or last segment of an LwPolyline to the nearest boundary.
|
||||||
|
|
@ -1946,6 +2088,12 @@ impl CadCommand for TrimCommand {
|
||||||
.unwrap_or(0.5);
|
.unwrap_or(0.5);
|
||||||
Some(trim_spline(s, &ts, t_click))
|
Some(trim_spline(s, &ts, t_click))
|
||||||
}
|
}
|
||||||
|
Some(EntityType::LwPolyline(p)) => {
|
||||||
|
match trim_lwpolyline(p, pt.x as f64, pt.y as f64, &self.geos) {
|
||||||
|
Some(v) => Some(v),
|
||||||
|
None => return CmdResult::NeedPoint,
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -2207,6 +2355,23 @@ impl CadCommand for TrimCommand {
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
Some(EntityType::LwPolyline(p)) => {
|
||||||
|
let Some(survivors) = trim_lwpolyline(p, pt.x as f64, pt.y as f64, &self.geos)
|
||||||
|
else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let orig = WireModel::solid("trim_rm".into(), entity_pts(entity.unwrap()), DIM_RED, false);
|
||||||
|
let mut out = vec![orig];
|
||||||
|
for (i, ent) in survivors.iter().enumerate() {
|
||||||
|
out.push(WireModel::solid(
|
||||||
|
format!("trim_keep_{i}"),
|
||||||
|
entity_pts(ent),
|
||||||
|
WireModel::CYAN,
|
||||||
|
false,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue