refactor(offset): take polyline offsetting from cadkernel

The offset core moves out: normalising the source, the cavalier_contours
call, the sharp-corner join fixup and the conversion back. What stays is
entity work — reading an LwPolyline in, writing offset LwPolylines out,
and the per-type offsets for lines, arcs, circles, ellipses and splines,
which are analytic and never needed the polyline machinery.

`BulgeArc` moves with it, since the offset preprocessing splits over-half-
turn arcs and cannot work without it. `entities::common` now re-exports it
from the kernel, so the twelve modules already reaching for
`entities::common::BulgeArc` are untouched.

`norm_rad` was a fourth copy of angle normalisation, after the three
removed from trim, fillet and explode. It now aliases the kernel's.

cavalier_contours leaves this crate's manifest: nothing here calls it any
more, and it arrives through the kernel's `offset` feature instead, which
acadifc forwards.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5 committed 2026-08-08 16:01:43 +03:00
1 parent 06a0fc4ddf
commit 397fb38292
6 files changed
+34 -346

No files matched your search

+5 -83
View File
@@ -582,90 +582,12 @@ pub fn parse_angle_deg(value: &str) -> Option<f64> {
Some(if neg { -total } else { total })
}
/// Bulge → arc geometry for a polyline segment.
/// Bulge → arc geometry for a polyline segment, from the kernel.
///
/// DXF/DWG polyline arcs are encoded as a bulge factor on each vertex —
/// `bulge = tan(theta/4)` where `theta` is the included angle of the arc
/// from `p0` to `p1`. Sign convention: positive bulge = CCW from p0 to p1,
/// negative = CW. `|bulge| = 1` is a half-circle.
///
/// This struct centralises the (formerly duplicated) math that takes
/// `(p0, p1, bulge)` and produces the canonical `(center, radius,
/// start_angle, sweep)` quadruple. Callsites pick the fields they need.
#[derive(Clone, Copy, Debug)]
pub struct BulgeArc {
pub center: [f64; 2],
pub radius: f64,
/// Angle from center to p0 (atan2, range -π..π).
pub start_angle: f64,
/// Angle from center to p1 (atan2, range -π..π).
pub end_angle: f64,
/// Signed sweep from p0 to p1. Positive ⇒ CCW (bulge > 0),
/// negative ⇒ CW (bulge < 0). For exact half-turns the sign of
/// `bulge` decides the direction.
pub sweep: f64,
}
impl BulgeArc {
/// Build from endpoints + bulge. Returns `None` for degenerate input
/// (chord ≈ 0 or |bulge| ≈ 0).
pub fn from_bulge(p0: [f64; 2], p1: [f64; 2], bulge: f64) -> Option<Self> {
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 || bulge.abs() < 1e-12 {
return None;
}
let b = bulge;
let b2 = b * b;
// r = chord · (1 + b²) / (4·|b|)
let r = chord_len * (1.0 + b2) / (4.0 * b.abs());
// d_perp = signed distance from chord midpoint to arc center
// = r · (1 - b²) / (1 + b²) = r · cos(theta/2)
let d_perp = r * (1.0 - b2) / (1.0 + b2);
let mx = (p0[0] + p1[0]) * 0.5;
let my = (p0[1] + p1[1]) * 0.5;
// Left perpendicular to chord (90° CCW).
let perp_x = -chord_y / chord_len;
let perp_y = chord_x / chord_len;
let sign = b.signum();
let cx = mx + sign * d_perp * perp_x;
let cy = my + sign * d_perp * perp_y;
let a0 = (p0[1] - cy).atan2(p0[0] - cx);
let a1 = (p1[1] - cy).atan2(p1[0] - cx);
// Wrap sweep to match bulge sign: bulge>0 ⇒ positive (CCW),
// bulge<0 ⇒ negative (CW). Falls back to ±τ for half-turns.
const TAU: f64 = std::f64::consts::TAU;
let mut sweep = a1 - a0;
if bulge > 0.0 {
if sweep <= 0.0 {
sweep += TAU;
}
} else if sweep >= 0.0 {
sweep -= TAU;
}
if sweep.abs() < 1e-9 {
sweep = if bulge > 0.0 { TAU } else { -TAU };
}
Some(Self {
center: [cx, cy],
radius: r,
start_angle: a0,
end_angle: a1,
sweep,
})
}
/// Sample a point on the arc at parameter `t ∈ [0, 1]`. `t=0` ↦ p0,
/// `t=1` ↦ p1, walks along the signed sweep direction.
pub fn sample(&self, t: f64) -> [f64; 2] {
let a = self.start_angle + self.sweep * t;
[
self.center[0] + self.radius * a.cos(),
self.center[1] + self.radius * a.sin(),
]
}
}
/// Re-exported rather than imported at each call site so the twelve modules
/// that already reach for `entities::common::BulgeArc` keep working, and so
/// there is one obvious place to see that the maths moved out.
pub use acadrust::kernel::geom2d::BulgeArc;
/// Triangulate the solid bands a `wide_fills` returns into the flat WCS f64
/// triangle list `TruckEntity::pick_tris` carries, so a wide polyline is