forked from 1b-agent/opencadstudio-upstream
fix(fillet): polyline re-fillet replaces the old arc, open ends close, arc segments are not filleted as chords
Fixes #1422 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
0bfd5dcc52
commit
1db1ea0140
1 file changed
+80
-31
@@ -817,6 +817,14 @@ fn compute_fillet_entities(
|
|||||||
radius: f64,
|
radius: f64,
|
||||||
) -> Option<(EntityType, EntityType, Option<EntityType>)> {
|
) -> Option<(EntityType, EntityType, Option<EntityType>)> {
|
||||||
let z = e1.elevation();
|
let z = e1.elevation();
|
||||||
|
// A polyline arc segment would be filleted as its straight chord.
|
||||||
|
let picks_arc_segment = |entity: &FilletEntity| {
|
||||||
|
matches!(entity, FilletEntity::LwPoly { poly, seg_idx, .. }
|
||||||
|
if poly.vertices.get(*seg_idx).is_some_and(|vertex| vertex.bulge.abs() > 1e-12))
|
||||||
|
};
|
||||||
|
if picks_arc_segment(e1) || picks_arc_segment(e2) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
match (e1, e2) {
|
match (e1, e2) {
|
||||||
// ── Line × Line ───────────────────────────────────────────────────
|
// ── Line × Line ───────────────────────────────────────────────────
|
||||||
@@ -848,24 +856,39 @@ fn compute_fillet_entities(
|
|||||||
..
|
..
|
||||||
},
|
},
|
||||||
) if h1 == h2 => {
|
) if h1 == h2 => {
|
||||||
// Adjacent segments share a corner vertex — fillet that corner.
|
|
||||||
let (low, high) = if *s1 < *s2 { (*s1, *s2) } else { (*s2, *s1) };
|
|
||||||
let n = p1.vertices.len();
|
let n = p1.vertices.len();
|
||||||
// The wrap-around corner of a closed polyline joins the last
|
let closed = p1.is_closed;
|
||||||
// segment (n-1) and the first (0); their shared vertex is v0.
|
let seg_count = if closed { n } else { n.saturating_sub(1) };
|
||||||
let wrap = p1.is_closed && low == 0 && high == n.saturating_sub(1);
|
let next = |seg: usize| if closed { (seg + 1) % n } else { seg + 1 };
|
||||||
// Segments must be consecutive, or the wrap-around pair above.
|
let straight = |seg: usize| seg < seg_count && p1.vertices[seg].bulge.abs() < 1e-12;
|
||||||
if !(high == low + 1 || wrap) {
|
// `before` runs into the corner and `after` leaves it. Between them
|
||||||
|
// sits nothing (a sharp corner), an old fillet arc being replaced,
|
||||||
|
// or — on an open polyline — the gap between its two ends, which
|
||||||
|
// the fillet closes. Arc segments themselves are never filleted:
|
||||||
|
// treating one as its chord bent the result across the shape.
|
||||||
|
enum Between {
|
||||||
|
Corner,
|
||||||
|
Arc(usize),
|
||||||
|
OpenEnds,
|
||||||
|
}
|
||||||
|
let order = |a: usize, b: usize| {
|
||||||
|
if next(a) == b {
|
||||||
|
Some((a, b, Between::Corner))
|
||||||
|
} else if next(a) < seg_count
|
||||||
|
&& next(next(a)) == b
|
||||||
|
&& !straight(next(a))
|
||||||
|
{
|
||||||
|
Some((a, b, Between::Arc(next(a))))
|
||||||
|
} else if !closed && seg_count >= 2 && a == seg_count - 1 && b == 0 {
|
||||||
|
Some((a, b, Between::OpenEnds))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let (before_seg, after_seg, between) = order(*s1, *s2).or_else(|| order(*s2, *s1))?;
|
||||||
|
if !straight(before_seg) || !straight(after_seg) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
// `before_seg` ends at the shared corner vertex, `after_seg` starts
|
|
||||||
// there. For the wrap corner that is seg n-1 → v0 → seg 0; for a
|
|
||||||
// normal corner it is seg low → v[high] → seg high.
|
|
||||||
let (before_seg, after_seg, corner_idx) = if wrap {
|
|
||||||
(high, low, 0)
|
|
||||||
} else {
|
|
||||||
(low, high, high)
|
|
||||||
};
|
|
||||||
let l1 = lwpoly_seg_as_line(p1, before_seg);
|
let l1 = lwpoly_seg_as_line(p1, before_seg);
|
||||||
let l2 = lwpoly_seg_as_line(p1, after_seg);
|
let l2 = lwpoly_seg_as_line(p1, after_seg);
|
||||||
// Re-map click to whichever segment each was picked on.
|
// Re-map click to whichever segment each was picked on.
|
||||||
@@ -874,23 +897,49 @@ fn compute_fillet_entities(
|
|||||||
} else {
|
} else {
|
||||||
(click2, click1)
|
(click2, click1)
|
||||||
};
|
};
|
||||||
match compute_fillet(&l1, c1, &l2, c2, radius)? {
|
let (EntityType::Line(tl1), EntityType::Line(tl2), maybe_arc) =
|
||||||
(EntityType::Line(tl1), EntityType::Line(tl2), maybe_arc) => {
|
compute_fillet(&l1, c1, &l2, c2, radius)?
|
||||||
let t1 = [tl1.end.x, tl1.end.y]; // trimmed end of seg before corner
|
else {
|
||||||
let t2 = [tl2.start.x, tl2.start.y]; // trimmed start of seg after corner
|
return None;
|
||||||
let bulge = if let Some(EntityType::Arc(ref fa)) = maybe_arc {
|
};
|
||||||
// center from arc entity
|
let t1 = [tl1.end.x, tl1.end.y]; // trimmed end of seg before corner
|
||||||
compute_bulge(t1, t2, [fa.center.x, fa.center.y])
|
let t2 = [tl2.start.x, tl2.start.y]; // trimmed start of seg after corner
|
||||||
} else {
|
let bulge = match maybe_arc {
|
||||||
0.0 // r=0, sharp corner
|
Some(EntityType::Arc(ref fa)) => compute_bulge(t1, t2, [fa.center.x, fa.center.y]),
|
||||||
};
|
_ => 0.0, // r=0, sharp corner
|
||||||
let new_poly = lwpoly_replace_corner(p1, corner_idx, t1, t2, bulge);
|
};
|
||||||
let et = EntityType::LwPolyline(new_poly);
|
let new_poly = match between {
|
||||||
// Return same rebuilt poly for both slots; caller uses only h1.
|
Between::Corner => lwpoly_replace_corner(p1, after_seg, t1, t2, bulge),
|
||||||
Some((et.clone(), et, None))
|
// The old arc runs from its own vertex to the `after` vertex:
|
||||||
|
// move both onto the new tangent points.
|
||||||
|
Between::Arc(arc_seg) => {
|
||||||
|
let mut poly = p1.clone();
|
||||||
|
poly.common.handle = Handle::NULL;
|
||||||
|
poly.vertices[arc_seg].location.x = t1[0];
|
||||||
|
poly.vertices[arc_seg].location.y = t1[1];
|
||||||
|
poly.vertices[arc_seg].bulge = bulge;
|
||||||
|
poly.vertices[after_seg].location.x = t2[0];
|
||||||
|
poly.vertices[after_seg].location.y = t2[1];
|
||||||
|
poly
|
||||||
}
|
}
|
||||||
_ => None,
|
// The last vertex becomes the start of the fillet and the
|
||||||
}
|
// closing segment is the fillet itself.
|
||||||
|
Between::OpenEnds => {
|
||||||
|
let mut poly = p1.clone();
|
||||||
|
poly.common.handle = Handle::NULL;
|
||||||
|
let last = n - 1;
|
||||||
|
poly.vertices[last].location.x = t1[0];
|
||||||
|
poly.vertices[last].location.y = t1[1];
|
||||||
|
poly.vertices[last].bulge = bulge;
|
||||||
|
poly.vertices[0].location.x = t2[0];
|
||||||
|
poly.vertices[0].location.y = t2[1];
|
||||||
|
poly.is_closed = true;
|
||||||
|
poly
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let et = EntityType::LwPolyline(new_poly);
|
||||||
|
// Return same rebuilt poly for both slots; caller uses only h1.
|
||||||
|
Some((et.clone(), et, None))
|
||||||
}
|
}
|
||||||
// ── LwPoly × LwPoly (different entities) ──────────────────────────
|
// ── LwPoly × LwPoly (different entities) ──────────────────────────
|
||||||
(
|
(
|
||||||
|
|||||||
Reference in new issue
Block a user