refactor(common): unify bulge → arc math into BulgeArc

The same DXF/DWG bulge → (center, radius, sweep) formula was inlined
in six places (hatch outline, polyline tess + truck path, lwpolyline
×4, scene hatch fill, explode bulge_to_arc) — each with subtly
different sweep-direction handling for the bulge sign.

Add `entities::common::BulgeArc { center, radius, start_angle,
end_angle, sweep }` with one `from_bulge(p0, p1, bulge)` constructor
and a `sample(t)` helper. Callsites collapse to a few lines.

Net: -121 lines, one canonical sweep-direction convention shared by
all consumers.

array.rs (the path-along-curve f32 helper) keeps its inline math —
its sign convention is inverted relative to the others and changing
it without exercising the array-path command risks regressing the
output.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.7 committed 2026-05-23 23:42:26 +03:00
1 parent e78321190a
commit 5e36e85e07
6 files changed
+175 -296

No files matched your search

+10 -41
View File
@@ -217,47 +217,16 @@ fn bulge_to_arc(
elevation: f64,
common_src: &EntityCommon,
) -> Option<EntityType> {
let chord_x = p1[0] - p0[0];
let chord_y = p1[1] - p0[1];
let chord_len = (chord_x * chord_x + chord_y * chord_y).sqrt();
if chord_len < 1e-12 {
return None;
}
let ba = crate::entities::common::BulgeArc::from_bulge(p0, p1, bulge)?;
// Included angle = 4 * atan(bulge)
let b = bulge;
let b2 = b * b;
// Radius: r = chord * (1 + b²) / (4 * |b|)
let r = chord_len * (1.0 + b2) / (4.0 * b.abs());
// Perpendicular distance from midpoint to center:
// d = r * cos(theta/2), where cos(theta/2) = (1 - b²) / (1 + b²)
let d = r * (1.0 - b2) / (1.0 + b2);
// Midpoint
let mx = (p0[0] + p1[0]) * 0.5;
let my = (p0[1] + p1[1]) * 0.5;
// Left-perpendicular direction (rotate chord 90° CCW)
let perp_x = -chord_y / chord_len;
let perp_y = chord_x / chord_len;
// Positive bulge → center is to the left of chord direction (CCW arc)
let sign = b.signum();
let cx = mx + sign * d * perp_x;
let cy = my + sign * d * perp_y;
// Angles from center to p0 and p1
let a0_rad = (p0[1] - cy).atan2(p0[0] - cx);
let a1_rad = (p1[1] - cy).atan2(p1[0] - cx);
// acadrust arcs are always CCW: positive bulge → CCW from p0 to p1
// Negative bulge → CW from p0 to p1 = CCW from p1 to p0
let (start_angle, end_angle) = if b > 0.0 {
(norm_angle(a0_rad), norm_angle(a1_rad))
// acadrust Arc is always CCW from start_angle to end_angle. Negative
// bulge means the polyline goes p0→p1 the CW way around the centre,
// which is the same circular arc traversed p1→p0 the CCW way — so
// swap endpoints when bulge < 0.
let (start_angle, end_angle) = if bulge > 0.0 {
(norm_angle(ba.start_angle), norm_angle(ba.end_angle))
} else {
(norm_angle(a1_rad), norm_angle(a0_rad))
(norm_angle(ba.end_angle), norm_angle(ba.start_angle))
};
let mut common = common_src.clone();
@@ -265,8 +234,8 @@ fn bulge_to_arc(
let arc = ArcEnt {
common,
center: Vector3::new(cx, cy, elevation),
radius: r,
center: Vector3::new(ba.center[0], ba.center[1], elevation),
radius: ba.radius,
start_angle,
end_angle,
..ArcEnt::new()