forked from 1b-agent/opencadstudio-upstream
fix(trim): handle polyline arc segments and preserve curvature
- Intersect cutting boundaries against polyline arc segments via KernelArc instead of chord lines - Project click points onto BulgeArc geometry to evaluate accurate click parameters - Proportionally scale and preserve bulges on surviving partial arc segments via extract_sub_polyline - Update crossing trim and hover wire preview to support polyline arc geometry
This commit is contained in:
1 parent
21e085db7e
commit
f79bad8adf
1 file changed
+458
-136
+458
-136
@@ -30,7 +30,7 @@ use glam::DVec3;
|
||||
use cadkernel::geom2d::nurbs::clamped_uniform_knots;
|
||||
use cadkernel::geom2d::{
|
||||
intersect as kernel_intersect, trim_spans as kernel_trim_spans, Arc as KernelArc,
|
||||
Circle as KernelCircle, Curve, Extent as KernelExtent,
|
||||
BulgeArc, Circle as KernelCircle, Curve, Extent as KernelExtent,
|
||||
Ellipse as KernelEllipse, EllipseArc as KernelEllipseArc, Line as KernelLine,
|
||||
NurbsCurve, Ray as KernelRay, Tolerance as KernelTolerance, XLine as KernelXLine,
|
||||
};
|
||||
@@ -395,6 +395,39 @@ fn line_seg_ts(ax: f64, ay: f64, bx: f64, by: f64, target: Handle, geos: &[Geo])
|
||||
)
|
||||
}
|
||||
|
||||
fn polyline_seg_ts(
|
||||
p0: [f64; 2],
|
||||
p1: [f64; 2],
|
||||
bulge: f64,
|
||||
target: Handle,
|
||||
geos: &[Geo],
|
||||
) -> Vec<f64> {
|
||||
if let Some(ba) = BulgeArc::from_bulge(p0, p1, bulge) {
|
||||
let (from, to) = if ba.sweep >= 0.0 {
|
||||
(ba.start_angle, ba.start_angle + ba.sweep)
|
||||
} else {
|
||||
(ba.end_angle, ba.end_angle - ba.sweep)
|
||||
};
|
||||
let arc_curve = Curve::Arc(KernelArc {
|
||||
centre: ba.center,
|
||||
radius: ba.radius,
|
||||
start_angle: from,
|
||||
end_angle: to,
|
||||
});
|
||||
let ts = cut_params(&arc_curve, target, geos);
|
||||
let mut u_ts: Vec<f64> = if ba.sweep >= 0.0 {
|
||||
ts
|
||||
} else {
|
||||
ts.into_iter().map(|t| (1.0 - t).clamp(0.0, 1.0)).collect()
|
||||
};
|
||||
u_ts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
u_ts.dedup_by(|a, b| (*a - *b).abs() < 1e-6);
|
||||
u_ts
|
||||
} else {
|
||||
line_seg_ts(p0[0], p0[1], p1[0], p1[1], target, geos)
|
||||
}
|
||||
}
|
||||
|
||||
fn arc_seg_ts(
|
||||
cx: f64,
|
||||
cy: f64,
|
||||
@@ -846,11 +879,121 @@ fn trim_circle(orig: &CircleEnt, ts: &[f64], t_click: f64) -> Vec<EntityType> {
|
||||
vec![EntityType::Arc(arc)]
|
||||
}
|
||||
|
||||
/// Extract a sub-polyline between parameter `s0` and `s1` (`s0 < s1`).
|
||||
/// Preserves and proportionally scales arc bulges on partial segments.
|
||||
fn extract_sub_polyline(poly: &LwPolyline, s0: f64, s1: f64) -> Option<LwPolyline> {
|
||||
if s1 <= s0 + 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let n = poly.vertices.len();
|
||||
if n < 2 {
|
||||
return None;
|
||||
}
|
||||
let closed = poly.is_closed;
|
||||
let seg_count = if closed { n } else { n - 1 };
|
||||
if seg_count == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let vx = |i: usize| -> [f64; 2] {
|
||||
let v = &poly.vertices[i % n];
|
||||
[v.location.x, v.location.y]
|
||||
};
|
||||
let seg_bulge = |i: usize| -> f64 {
|
||||
poly.vertices[i % n].bulge
|
||||
};
|
||||
|
||||
let mut raw_verts: Vec<(f64, f64, f64)> = Vec::new();
|
||||
let mut curr = s0;
|
||||
while curr < s1 - 1e-9 {
|
||||
let seg_idx_unwrapped = curr.floor() as usize;
|
||||
let next_int = (seg_idx_unwrapped + 1) as f64;
|
||||
let end_curr = s1.min(next_int);
|
||||
let seg = seg_idx_unwrapped % seg_count;
|
||||
let u_a = (curr - seg_idx_unwrapped as f64).clamp(0.0, 1.0);
|
||||
let u_b = (end_curr - seg_idx_unwrapped as f64).clamp(0.0, 1.0);
|
||||
if u_b > u_a + 1e-9 {
|
||||
let p0 = vx(seg);
|
||||
let p1 = vx(seg + 1);
|
||||
let b = seg_bulge(seg);
|
||||
let (start_pt, sub_bulge) = if let Some(ba) = BulgeArc::from_bulge(p0, p1, b) {
|
||||
let sp = ba.sample(u_a);
|
||||
let sub_b = if (u_a - 0.0).abs() < 1e-9 && (u_b - 1.0).abs() < 1e-9 {
|
||||
b
|
||||
} else {
|
||||
let sweep = ba.sweep * (u_b - u_a);
|
||||
(sweep * 0.25).tan()
|
||||
};
|
||||
(sp, sub_b)
|
||||
} else {
|
||||
let sp = [
|
||||
p0[0] + u_a * (p1[0] - p0[0]),
|
||||
p0[1] + u_a * (p1[1] - p0[1]),
|
||||
];
|
||||
(sp, 0.0)
|
||||
};
|
||||
raw_verts.push((start_pt[0], start_pt[1], sub_bulge));
|
||||
}
|
||||
curr = end_curr;
|
||||
}
|
||||
|
||||
// End point of the last sub-segment
|
||||
let end_pt = {
|
||||
let seg_idx_unwrapped = (s1 - 1e-9).floor() as usize;
|
||||
let seg = seg_idx_unwrapped % seg_count;
|
||||
let u = (s1 - seg_idx_unwrapped as f64).clamp(0.0, 1.0);
|
||||
let p0 = vx(seg);
|
||||
let p1 = vx(seg + 1);
|
||||
let b = seg_bulge(seg);
|
||||
if let Some(ba) = BulgeArc::from_bulge(p0, p1, b) {
|
||||
ba.sample(u)
|
||||
} else {
|
||||
[p0[0] + u * (p1[0] - p0[0]), p0[1] + u * (p1[1] - p0[1])]
|
||||
}
|
||||
};
|
||||
raw_verts.push((end_pt[0], end_pt[1], 0.0));
|
||||
|
||||
// Filter out coincident consecutive points to prevent degenerate zero-length segments
|
||||
let mut clean_verts: Vec<(f64, f64, f64)> = Vec::new();
|
||||
for (x, y, b) in raw_verts {
|
||||
if let Some(last) = clean_verts.last_mut() {
|
||||
if (last.0 - x).hypot(last.1 - y) < 1e-6 {
|
||||
if last.2 == 0.0 {
|
||||
last.2 = b;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
clean_verts.push((x, y, b));
|
||||
}
|
||||
|
||||
if let Some(last) = clean_verts.last_mut() {
|
||||
last.2 = 0.0;
|
||||
}
|
||||
|
||||
if clean_verts.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut new_poly = poly.clone();
|
||||
new_poly.common.handle = Handle::NULL;
|
||||
new_poly.is_closed = false;
|
||||
new_poly.vertices = clean_verts
|
||||
.into_iter()
|
||||
.map(|(x, y, b)| {
|
||||
let mut v = LwVertex::from_coords(x, y);
|
||||
v.bulge = b;
|
||||
v
|
||||
})
|
||||
.collect();
|
||||
Some(new_poly)
|
||||
}
|
||||
|
||||
/// 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).
|
||||
/// the surviving piece(s). Bulges on surviving arc segments are preserved
|
||||
/// with proportionally scaled curvature.
|
||||
fn trim_lwpolyline(poly: &LwPolyline, cx: f64, cy: f64, geos: &[Geo]) -> Option<Vec<EntityType>> {
|
||||
let handle = poly.common.handle;
|
||||
let n = poly.vertices.len();
|
||||
@@ -861,26 +1004,23 @@ fn trim_lwpolyline(poly: &LwPolyline, cx: f64, cy: f64, geos: &[Geo]) -> Option<
|
||||
let seg_count = if closed { n } else { n - 1 };
|
||||
let total = seg_count as f64;
|
||||
|
||||
let vx = |i: usize| -> (f64, f64) {
|
||||
let vx = |i: usize| -> [f64; 2] {
|
||||
let v = &poly.vertices[i % n];
|
||||
(v.location.x, v.location.y)
|
||||
[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))
|
||||
let seg_bulge = |i: usize| -> f64 {
|
||||
poly.vertices[i % n].bulge
|
||||
};
|
||||
|
||||
// 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));
|
||||
let p0 = vx(i);
|
||||
let p1 = vx(i + 1);
|
||||
let b = seg_bulge(i);
|
||||
for u in polyline_seg_ts(p0, p1, b, handle, geos) {
|
||||
let param = i as f64 + u.clamp(0.0, 1.0);
|
||||
cuts.push(if closed { param.rem_euclid(total) } else { param });
|
||||
}
|
||||
}
|
||||
cuts.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
|
||||
@@ -892,50 +1032,40 @@ fn trim_lwpolyline(poly: &LwPolyline, cx: f64, cy: f64, geos: &[Geo]) -> Option<
|
||||
// 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)
|
||||
let p0 = vx(i);
|
||||
let p1 = vx(i + 1);
|
||||
let b = seg_bulge(i);
|
||||
let (d, u) = if let Some(ba) = BulgeArc::from_bulge(p0, p1, b) {
|
||||
let angle = (cy - ba.center[1]).atan2(cx - ba.center[0]);
|
||||
let travelled = if ba.sweep >= 0.0 {
|
||||
(angle - ba.start_angle).rem_euclid(TAU)
|
||||
} else {
|
||||
-((ba.start_angle - angle).rem_euclid(TAU))
|
||||
};
|
||||
let u = (travelled / ba.sweep).clamp(0.0, 1.0);
|
||||
let pt = ba.sample(u);
|
||||
let dist2 = (pt[0] - cx).powi(2) + (pt[1] - cy).powi(2);
|
||||
(dist2, u)
|
||||
} else {
|
||||
0.0
|
||||
let (dx, dy) = (p1[0] - p0[0], p1[1] - p0[1]);
|
||||
let len2 = dx * dx + dy * dy;
|
||||
let u = if len2 > 1e-12 {
|
||||
(((cx - p0[0]) * dx + (cy - p0[1]) * dy) / len2).clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let px = p0[0] + u * dx;
|
||||
let py = p0[1] + u * dy;
|
||||
let dist2 = (px - cx).powi(2) + (py - cy).powi(2);
|
||||
(dist2, u)
|
||||
};
|
||||
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();
|
||||
let mut out: Vec<EntityType> = Vec::new();
|
||||
if closed {
|
||||
if cuts.len() < 2 {
|
||||
return None;
|
||||
@@ -955,37 +1085,29 @@ fn trim_lwpolyline(poly: &LwPolyline, cx: f64, cy: f64, geos: &[Geo]) -> Option<
|
||||
while s1 <= hi {
|
||||
s1 += total;
|
||||
}
|
||||
pieces.push(emit(hi, s1, true, true));
|
||||
if let Some(piece) = extract_sub_polyline(poly, hi, s1) {
|
||||
out.push(EntityType::LwPolyline(piece));
|
||||
}
|
||||
} 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(piece) = extract_sub_polyline(poly, 0.0, lo) {
|
||||
out.push(EntityType::LwPolyline(piece));
|
||||
}
|
||||
}
|
||||
if let Some(hi) = hi {
|
||||
pieces.push(emit(hi, total, true, false));
|
||||
if let Some(piece) = extract_sub_polyline(poly, hi, total) {
|
||||
out.push(EntityType::LwPolyline(piece));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
if out.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(out)
|
||||
}
|
||||
Some(out)
|
||||
}
|
||||
|
||||
// ── Extend helpers ────────────────────────────────────────────────────────
|
||||
@@ -1269,16 +1391,11 @@ fn entity_pts(e: &EntityType) -> Vec<[f32; 3]> {
|
||||
EntityType::Spline(s) => spline_pts_wire(s),
|
||||
EntityType::LwPolyline(p) => {
|
||||
let elev = p.elevation as f32;
|
||||
let n = p.vertices.len();
|
||||
let seg_count = if p.is_closed { n } else { n.saturating_sub(1) };
|
||||
let mut pts = Vec::with_capacity(seg_count * 2);
|
||||
for i in 0..seg_count {
|
||||
let v0 = &p.vertices[i];
|
||||
let v1 = &p.vertices[(i + 1) % n];
|
||||
pts.push([v0.location.x as f32, v0.location.y as f32, elev]);
|
||||
pts.push([v1.location.x as f32, v1.location.y as f32, elev]);
|
||||
}
|
||||
pts
|
||||
let xy_pts = preview_sample_xy(e);
|
||||
xy_pts
|
||||
.into_iter()
|
||||
.map(|pt| [pt[0] as f32, pt[1] as f32, elev])
|
||||
.collect()
|
||||
}
|
||||
// For preview, show a 20-unit section of semi-infinite results
|
||||
EntityType::Ray(r) => {
|
||||
@@ -1380,28 +1497,63 @@ fn crossing_trim_lwpolyline(
|
||||
let v = &poly.vertices[i % n];
|
||||
[v.location.x, v.location.y]
|
||||
};
|
||||
let seg_bulge = |i: usize| -> f64 {
|
||||
poly.vertices[i % n].bulge
|
||||
};
|
||||
|
||||
let mut removed = Vec::<(f64, f64)>::new();
|
||||
for i in 0..seg_count {
|
||||
let a = vertex_xy(i);
|
||||
let b = vertex_xy(i + 1);
|
||||
let Some((inside_lo, inside_hi)) = segment_window_range(a, b, window) else {
|
||||
let bulge = seg_bulge(i);
|
||||
let window_range = if let Some(ba) = BulgeArc::from_bulge(a, b, bulge) {
|
||||
let steps = 32usize;
|
||||
let mut min_t: Option<f64> = None;
|
||||
let mut max_t: Option<f64> = None;
|
||||
for s in 0..=steps {
|
||||
let t = s as f64 / steps as f64;
|
||||
let pt = ba.sample(t);
|
||||
if pt[0] >= window.min[0] - 1e-9 && pt[0] <= window.max[0] + 1e-9
|
||||
&& pt[1] >= window.min[1] - 1e-9 && pt[1] <= window.max[1] + 1e-9
|
||||
{
|
||||
min_t = Some(min_t.map_or(t, |m: f64| m.min(t)));
|
||||
max_t = Some(max_t.map_or(t, |m: f64| m.max(t)));
|
||||
}
|
||||
}
|
||||
match (min_t, max_t) {
|
||||
(Some(lo), Some(hi)) => Some((lo, hi)),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
segment_window_range(a, b, window)
|
||||
};
|
||||
let Some((inside_lo, inside_hi)) = window_range else {
|
||||
continue;
|
||||
};
|
||||
let cuts = line_seg_ts(a[0], a[1], b[0], b[1], handle, geos);
|
||||
let cuts = polyline_seg_ts(a, b, bulge, handle, geos);
|
||||
if cuts.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dx = b[0] - a[0];
|
||||
let dy = b[1] - a[1];
|
||||
let len2 = dx * dx + dy * dy;
|
||||
let projected = if len2 > 1e-12 {
|
||||
((window.pick[0] - a[0]) * dx + (window.pick[1] - a[1]) * dy) / len2
|
||||
let pick_t = if let Some(ba) = BulgeArc::from_bulge(a, b, bulge) {
|
||||
let angle = (window.pick[1] - ba.center[1]).atan2(window.pick[0] - ba.center[0]);
|
||||
let travelled = if ba.sweep >= 0.0 {
|
||||
(angle - ba.start_angle).rem_euclid(TAU)
|
||||
} else {
|
||||
-((ba.start_angle - angle).rem_euclid(TAU))
|
||||
};
|
||||
(travelled / ba.sweep).clamp(inside_lo, inside_hi)
|
||||
} else {
|
||||
(inside_lo + inside_hi) * 0.5
|
||||
let dx = b[0] - a[0];
|
||||
let dy = b[1] - a[1];
|
||||
let len2 = dx * dx + dy * dy;
|
||||
let projected = if len2 > 1e-12 {
|
||||
((window.pick[0] - a[0]) * dx + (window.pick[1] - a[1]) * dy) / len2
|
||||
} else {
|
||||
(inside_lo + inside_hi) * 0.5
|
||||
};
|
||||
projected.clamp(inside_lo, inside_hi)
|
||||
};
|
||||
let pick_t = projected.clamp(inside_lo, inside_hi);
|
||||
|
||||
let mut bounds = vec![0.0];
|
||||
bounds.extend(cuts);
|
||||
@@ -1433,49 +1585,6 @@ fn crossing_trim_lwpolyline(
|
||||
merged.push(span);
|
||||
}
|
||||
|
||||
let point_at = |t: 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 a = vertex_xy(i);
|
||||
let b = vertex_xy(i + 1);
|
||||
[a[0] + u * (b[0] - a[0]), a[1] + u * (b[1] - a[1])]
|
||||
};
|
||||
let emit = |s0: f64, s1: f64| -> Option<EntityType> {
|
||||
if s1 - s0 <= 1e-6 {
|
||||
return None;
|
||||
}
|
||||
let mut vertices = Vec::<LwVertex>::new();
|
||||
let start = point_at(s0);
|
||||
vertices.push(LwVertex::from_coords(start[0], start[1]));
|
||||
let mut k = s0.floor() as i64 + 1;
|
||||
while (k as f64) < s1 - 1e-9 {
|
||||
vertices.push(poly.vertices[(k as usize) % n]);
|
||||
k += 1;
|
||||
}
|
||||
if let Some(last) = vertices.last_mut() {
|
||||
last.bulge = 0.0;
|
||||
}
|
||||
let end = point_at(s1);
|
||||
if vertices.last().is_none_or(|v| {
|
||||
(v.location.x - end[0]).hypot(v.location.y - end[1]) > 1e-6
|
||||
}) {
|
||||
vertices.push(LwVertex::from_coords(end[0], end[1]));
|
||||
}
|
||||
if vertices.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let mut piece = poly.clone();
|
||||
piece.common.handle = Handle::NULL;
|
||||
piece.is_closed = false;
|
||||
piece.vertices = vertices;
|
||||
Some(EntityType::LwPolyline(piece))
|
||||
};
|
||||
|
||||
let mut kept = Vec::<(f64, f64)>::new();
|
||||
if closed {
|
||||
for i in 0..merged.len() {
|
||||
@@ -1503,7 +1612,7 @@ fn crossing_trim_lwpolyline(
|
||||
|
||||
Some(
|
||||
kept.into_iter()
|
||||
.filter_map(|(start, end)| emit(start, end))
|
||||
.filter_map(|(start, end)| extract_sub_polyline(poly, start, end).map(EntityType::LwPolyline))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
@@ -3488,6 +3597,219 @@ mod tests {
|
||||
assert!(trim_circle(&c, &[], 0.3).is_empty());
|
||||
assert!(trim_circle(&c, &[0.4], 0.3).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_polyline_arc_segment_preserves_bulge() {
|
||||
use std::f64::consts::FRAC_PI_8;
|
||||
// Arc from (0, 0) to (2, 0) with bulge 1.0 (semicircle sweeping CCW under the chord)
|
||||
let mut poly = LwPolyline::new();
|
||||
poly.common.handle = Handle::new(10);
|
||||
let mut v0 = LwVertex::from_coords(0.0, 0.0);
|
||||
v0.bulge = 1.0;
|
||||
let v1 = LwVertex::from_coords(2.0, 0.0);
|
||||
poly.vertices = vec![v0, v1];
|
||||
|
||||
// Vertical cutter line at x = 1.0 crossing the arc at (1.0, -1.0)
|
||||
let mut cutter = LineEnt::new();
|
||||
cutter.start = Vector3::new(1.0, -2.0, 0.0);
|
||||
cutter.end = Vector3::new(1.0, 2.0, 0.0);
|
||||
cutter.common.handle = Handle::new(20);
|
||||
|
||||
let all = vec![EntityType::LwPolyline(poly), EntityType::Line(cutter)];
|
||||
let geos = build_geos(&all);
|
||||
|
||||
// Click first half near (0.5, -0.8) -> first half removed, second half kept from (1.0, -1.0) to (2.0, 0.0)
|
||||
let res = pick_trim_at(&all, &geos, Handle::new(10), 0.5, -0.8).expect("should trim polyline arc");
|
||||
assert_eq!(res.len(), 1);
|
||||
match &res[0] {
|
||||
EntityType::LwPolyline(p) => {
|
||||
assert_eq!(p.vertices.len(), 2);
|
||||
let p0 = &p.vertices[0];
|
||||
let p1 = &p.vertices[1];
|
||||
assert!((p0.location.x - 1.0).abs() < 1e-5, "expected x=1.0, got {}", p0.location.x);
|
||||
assert!((p0.location.y + 1.0).abs() < 1e-5, "expected y=-1.0, got {}", p0.location.y);
|
||||
assert!((p1.location.x - 2.0).abs() < 1e-5, "expected x=2.0, got {}", p1.location.x);
|
||||
assert!(p1.location.y.abs() < 1e-5, "expected y=0.0, got {}", p1.location.y);
|
||||
// The surviving quarter circle should have bulge = tan(pi / 8)
|
||||
let expected_bulge = FRAC_PI_8.tan();
|
||||
assert!(
|
||||
(p0.bulge - expected_bulge).abs() < 1e-4,
|
||||
"surviving arc should retain quarter-circle bulge {}, got {}",
|
||||
expected_bulge,
|
||||
p0.bulge
|
||||
);
|
||||
assert_eq!(p1.bulge, 0.0);
|
||||
}
|
||||
other => panic!("expected LwPolyline, got {other:?}"),
|
||||
}
|
||||
|
||||
// Click second half near (1.5, -0.8) -> second half removed, first half kept from (0.0, 0.0) to (1.0, -1.0)
|
||||
let res2 = pick_trim_at(&all, &geos, Handle::new(10), 1.5, -0.8).expect("should trim polyline arc");
|
||||
assert_eq!(res2.len(), 1);
|
||||
match &res2[0] {
|
||||
EntityType::LwPolyline(p) => {
|
||||
assert_eq!(p.vertices.len(), 2);
|
||||
let p0 = &p.vertices[0];
|
||||
let p1 = &p.vertices[1];
|
||||
assert!(p0.location.x.abs() < 1e-5, "expected x=0.0, got {}", p0.location.x);
|
||||
assert!(p0.location.y.abs() < 1e-5, "expected y=0.0, got {}", p0.location.y);
|
||||
assert!((p1.location.x - 1.0).abs() < 1e-5, "expected x=1.0, got {}", p1.location.x);
|
||||
assert!((p1.location.y + 1.0).abs() < 1e-5, "expected y=-1.0, got {}", p1.location.y);
|
||||
let expected_bulge = FRAC_PI_8.tan();
|
||||
assert!(
|
||||
(p0.bulge - expected_bulge).abs() < 1e-4,
|
||||
"surviving arc should retain quarter-circle bulge {}, got {}",
|
||||
expected_bulge,
|
||||
p0.bulge
|
||||
);
|
||||
assert_eq!(p1.bulge, 0.0);
|
||||
}
|
||||
other => panic!("expected LwPolyline, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_multi_segment_polyline_with_arc() {
|
||||
use std::f64::consts::FRAC_PI_8;
|
||||
// Segment 0: straight line from (-2, 0) to (0, 0)
|
||||
// Segment 1: arc from (0, 0) to (2, 0) with bulge 1.0
|
||||
let mut poly = LwPolyline::new();
|
||||
poly.common.handle = Handle::new(30);
|
||||
let v0 = LwVertex::from_coords(-2.0, 0.0);
|
||||
let mut v1 = LwVertex::from_coords(0.0, 0.0);
|
||||
v1.bulge = 1.0;
|
||||
let v2 = LwVertex::from_coords(2.0, 0.0);
|
||||
poly.vertices = vec![v0, v1, v2];
|
||||
|
||||
// Vertical cutter at x = 1.0
|
||||
let mut cutter = LineEnt::new();
|
||||
cutter.start = Vector3::new(1.0, -2.0, 0.0);
|
||||
cutter.end = Vector3::new(1.0, 2.0, 0.0);
|
||||
cutter.common.handle = Handle::new(40);
|
||||
|
||||
let all = vec![EntityType::LwPolyline(poly), EntityType::Line(cutter)];
|
||||
let geos = build_geos(&all);
|
||||
|
||||
// Click the far end of the arc near (1.5, -0.8) -> removes end, keeps straight seg + first half of arc
|
||||
let res = pick_trim_at(&all, &geos, Handle::new(30), 1.5, -0.8).expect("should trim");
|
||||
assert_eq!(res.len(), 1);
|
||||
match &res[0] {
|
||||
EntityType::LwPolyline(p) => {
|
||||
assert_eq!(p.vertices.len(), 3);
|
||||
// Vertex 0: (-2, 0), straight
|
||||
assert!((p.vertices[0].location.x + 2.0).abs() < 1e-5);
|
||||
assert!(p.vertices[0].bulge.abs() < 1e-5);
|
||||
// Vertex 1: (0, 0), arc with quarter-turn bulge
|
||||
assert!(p.vertices[1].location.x.abs() < 1e-5);
|
||||
let expected_bulge = FRAC_PI_8.tan();
|
||||
assert!((p.vertices[1].bulge - expected_bulge).abs() < 1e-4);
|
||||
// Vertex 2: (1, -1), end
|
||||
assert!((p.vertices[2].location.x - 1.0).abs() < 1e-5);
|
||||
assert!((p.vertices[2].location.y + 1.0).abs() < 1e-5);
|
||||
}
|
||||
other => panic!("expected LwPolyline, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_negative_bulge_polyline_arc() {
|
||||
use std::f64::consts::FRAC_PI_8;
|
||||
// Negative bulge means CW arc: from (0, 0) to (2, 0) arches above the chord (y > 0)
|
||||
let mut poly = LwPolyline::new();
|
||||
poly.common.handle = Handle::new(50);
|
||||
let mut v0 = LwVertex::from_coords(0.0, 0.0);
|
||||
v0.bulge = -1.0;
|
||||
let v1 = LwVertex::from_coords(2.0, 0.0);
|
||||
poly.vertices = vec![v0, v1];
|
||||
|
||||
// Vertical cutter at x = 1.0 crossing at (1.0, 1.0)
|
||||
let mut cutter = LineEnt::new();
|
||||
cutter.start = Vector3::new(1.0, -2.0, 0.0);
|
||||
cutter.end = Vector3::new(1.0, 2.0, 0.0);
|
||||
cutter.common.handle = Handle::new(60);
|
||||
|
||||
let all = vec![EntityType::LwPolyline(poly), EntityType::Line(cutter)];
|
||||
let geos = build_geos(&all);
|
||||
|
||||
// Click first half near (0.5, 0.8) -> removes first half, keeps second half from (1.0, 1.0) to (2.0, 0.0)
|
||||
let res = pick_trim_at(&all, &geos, Handle::new(50), 0.5, 0.8).expect("should trim CW arc");
|
||||
assert_eq!(res.len(), 1);
|
||||
match &res[0] {
|
||||
EntityType::LwPolyline(p) => {
|
||||
assert_eq!(p.vertices.len(), 2);
|
||||
let p0 = &p.vertices[0];
|
||||
let p1 = &p.vertices[1];
|
||||
assert!((p0.location.x - 1.0).abs() < 1e-5);
|
||||
assert!((p0.location.y - 1.0).abs() < 1e-5);
|
||||
assert!((p1.location.x - 2.0).abs() < 1e-5);
|
||||
assert!(p1.location.y.abs() < 1e-5);
|
||||
let expected_bulge = -FRAC_PI_8.tan();
|
||||
assert!(
|
||||
(p0.bulge - expected_bulge).abs() < 1e-4,
|
||||
"expected negative bulge {}, got {}",
|
||||
expected_bulge,
|
||||
p0.bulge
|
||||
);
|
||||
}
|
||||
other => panic!("expected LwPolyline, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim_closed_polyline_with_arc() {
|
||||
use std::f64::consts::FRAC_PI_8;
|
||||
// Closed polyline:
|
||||
// Vertex 0: (0, 0), bulge 1.0 (arc to (2, 0) below chord, peak at y=-1)
|
||||
// Vertex 1: (2, 0), bulge 0.0 (straight to (2, 2))
|
||||
// Vertex 2: (2, 2), bulge 0.0 (straight to (0, 2))
|
||||
// Vertex 3: (0, 2), bulge 0.0 (straight back to (0, 0))
|
||||
let mut poly = LwPolyline::new();
|
||||
poly.common.handle = Handle::new(70);
|
||||
poly.is_closed = true;
|
||||
let mut v0 = LwVertex::from_coords(0.0, 0.0);
|
||||
v0.bulge = 1.0;
|
||||
let v1 = LwVertex::from_coords(2.0, 0.0);
|
||||
let v2 = LwVertex::from_coords(2.0, 2.0);
|
||||
let v3 = LwVertex::from_coords(0.0, 2.0);
|
||||
poly.vertices = vec![v0, v1, v2, v3];
|
||||
|
||||
// Cutter 1: x = 1.0 cuts the arc at (1.0, -1.0)
|
||||
let mut c1 = LineEnt::new();
|
||||
c1.start = Vector3::new(1.0, -3.0, 0.0);
|
||||
c1.end = Vector3::new(1.0, -0.5, 0.0);
|
||||
c1.common.handle = Handle::new(80);
|
||||
|
||||
// Cutter 2: y = 1.0 cuts vertical edges at x=0 and x=2
|
||||
let mut c2 = LineEnt::new();
|
||||
c2.start = Vector3::new(-1.0, 1.0, 0.0);
|
||||
c2.end = Vector3::new(3.0, 1.0, 0.0);
|
||||
c2.common.handle = Handle::new(81);
|
||||
|
||||
let all = vec![
|
||||
EntityType::LwPolyline(poly),
|
||||
EntityType::Line(c1),
|
||||
EntityType::Line(c2),
|
||||
];
|
||||
let geos = build_geos(&all);
|
||||
|
||||
// Click first half of arc near (0.5, -0.8):
|
||||
// Nearest cuts are at (1.0, -1.0) on seg 0 and at (0.0, 1.0) on seg 3.
|
||||
// Trim removes that corner/arc portion and leaves an open polyline
|
||||
let res = pick_trim_at(&all, &geos, Handle::new(70), 0.5, -0.8).expect("should trim closed polyline");
|
||||
assert_eq!(res.len(), 1);
|
||||
match &res[0] {
|
||||
EntityType::LwPolyline(p) => {
|
||||
assert!(!p.is_closed);
|
||||
// Starts at (1.0, -1.0) with remaining quarter-circle arc
|
||||
let start_v = &p.vertices[0];
|
||||
assert!((start_v.location.x - 1.0).abs() < 1e-5);
|
||||
assert!((start_v.location.y + 1.0).abs() < 1e-5);
|
||||
let expected_bulge = FRAC_PI_8.tan();
|
||||
assert!((start_v.bulge - expected_bulge).abs() < 1e-4);
|
||||
}
|
||||
other => panic!("expected LwPolyline, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
Reference in new issue
Block a user