forked from 1b-agent/opencadstudio-upstream
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:
1 parent
06a0fc4ddf
commit
397fb38292
6 files changed
+34
-346
No files matched your search
@@ -19,13 +19,11 @@ use acadrust::entities::{
|
||||
Spline as SplineEnt, XLine as XLineEnt,
|
||||
};
|
||||
use acadrust::{EntityType, Handle};
|
||||
use cavalier_contours::core::math::Vector2 as CavVector2;
|
||||
use cavalier_contours::polyline::internal::pline_offset::{
|
||||
create_raw_offset_polyline, slices_from_dual_raw_offsets, stitch_slices_together,
|
||||
};
|
||||
use cavalier_contours::polyline::{
|
||||
seg_tangent_vector, PlineOffsetOptions, PlineSource, PlineSourceMut,
|
||||
Polyline as CavPolyline,
|
||||
// Polyline offsetting, and the angle normalisation that goes with it, come
|
||||
// from the kernel; only the entity conversion stays here.
|
||||
use acadrust::kernel::geom2d::{
|
||||
normalize_angle as norm_rad, offset_polyline, Polyline as KernelPolyline,
|
||||
PolylineVertex as KernelVertex,
|
||||
};
|
||||
use glam::{DVec3, Vec3};
|
||||
use crate::t;
|
||||
@@ -48,26 +46,6 @@ pub fn tool() -> ToolDef {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Geometry helpers ────────────────────────────────────────────────────────
|
||||
|
||||
/// Infinite-line intersection in 2D. Returns the point or None if parallel.
|
||||
fn isect_lines(p0: [f64; 2], p1: [f64; 2], q0: [f64; 2], q1: [f64; 2]) -> Option<[f64; 2]> {
|
||||
let dx = p1[0] - p0[0];
|
||||
let dy = p1[1] - p0[1];
|
||||
let ex = q1[0] - q0[0];
|
||||
let ey = q1[1] - q0[1];
|
||||
let det = dx * ey - dy * ex;
|
||||
if det.abs() < 1e-10 {
|
||||
return None;
|
||||
}
|
||||
let t = ((q0[0] - p0[0]) * ey - (q0[1] - p0[1]) * ex) / det;
|
||||
Some([p0[0] + t * dx, p0[1] + t * dy])
|
||||
}
|
||||
|
||||
fn norm_rad(a: f64) -> f64 {
|
||||
((a % TAU) + TAU) % TAU
|
||||
}
|
||||
|
||||
// ── Line offset ────────────────────────────────────────────────────────────
|
||||
|
||||
fn offset_line(l: &LineEnt, dist: f64, side_pt: Vec3) -> Option<EntityType> {
|
||||
@@ -178,248 +156,35 @@ fn offset_arc(a: &ArcEnt, dist: f64, side_pt: Vec3) -> Option<EntityType> {
|
||||
// remaining slices. This can legitimately return several disconnected
|
||||
// polylines.
|
||||
|
||||
const OFFSET_POS_EPS: f64 = 1e-5;
|
||||
const OFFSET_JOIN_EPS: f64 = 1e-4;
|
||||
|
||||
/// Convert an acad LWPOLYLINE to the line/arc representation used by the
|
||||
/// topology pass. Coordinates are translated and divided by `dist`, so the
|
||||
/// offset passed to the algorithm is always ±1. This avoids fixed-epsilon
|
||||
/// failures on tiny drawings and on UTM-scale coordinates.
|
||||
fn normalized_offset_source(
|
||||
p: &LwPolyline,
|
||||
dist: f64,
|
||||
) -> Option<(CavPolyline<f64>, [f64; 2])> {
|
||||
let first = p.vertices.first()?;
|
||||
let origin = [first.location.x, first.location.y];
|
||||
let normalize = |point: [f64; 2]| {
|
||||
[
|
||||
(point[0] - origin[0]) / dist,
|
||||
(point[1] - origin[1]) / dist,
|
||||
]
|
||||
};
|
||||
|
||||
let n = p.vertices.len();
|
||||
if n < 2 {
|
||||
return None;
|
||||
}
|
||||
let segment_count = if p.is_closed { n } else { n - 1 };
|
||||
let mut source = if p.is_closed {
|
||||
CavPolyline::new_closed()
|
||||
} else {
|
||||
CavPolyline::new()
|
||||
};
|
||||
|
||||
for index in 0..segment_count {
|
||||
let start = &p.vertices[index];
|
||||
let end = &p.vertices[(index + 1) % n];
|
||||
let p0 = [start.location.x, start.location.y];
|
||||
let p1 = [end.location.x, end.location.y];
|
||||
let bulge = if start.bulge.is_finite() {
|
||||
start.bulge
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let q0 = normalize(p0);
|
||||
|
||||
// CavalierContours represents arcs up to a half turn per segment.
|
||||
// Split major bulge arcs at their exact midpoint; both halves retain
|
||||
// the original circle and traversal direction.
|
||||
if bulge.abs() > 1.0 {
|
||||
if let Some(arc) =
|
||||
crate::entities::common::BulgeArc::from_bulge(p0, p1, bulge)
|
||||
{
|
||||
let half_bulge = (arc.sweep / 8.0).tan();
|
||||
let midpoint = normalize(arc.sample(0.5));
|
||||
source.add(q0[0], q0[1], half_bulge);
|
||||
source.add(midpoint[0], midpoint[1], half_bulge);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
source.add(q0[0], q0[1], bulge);
|
||||
}
|
||||
|
||||
if !p.is_closed {
|
||||
let last = &p.vertices[n - 1];
|
||||
let point = normalize([last.location.x, last.location.y]);
|
||||
source.add(point[0], point[1], 0.0);
|
||||
}
|
||||
|
||||
let source = source
|
||||
.remove_repeat_pos(OFFSET_POS_EPS)
|
||||
.unwrap_or(source);
|
||||
(source.vertex_count() >= 2).then_some((source, origin))
|
||||
}
|
||||
|
||||
/// CavalierContours deliberately connects diverging line offsets with a round
|
||||
/// arc. OFFSETGAPTYPE=0 (and OpenCADStudio's previous behavior) instead extends
|
||||
/// the two lines to a sharp projected intersection. Replace only those
|
||||
/// generated line-line connection arcs before the self-intersection pass.
|
||||
fn sharpen_line_connections(raw: &mut CavPolyline<f64>, source: &CavPolyline<f64>) {
|
||||
loop {
|
||||
let count = raw.vertex_data.len();
|
||||
if count < 4 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut changed = false;
|
||||
for index in 0..count {
|
||||
if !raw.is_closed && index == 0 {
|
||||
continue;
|
||||
}
|
||||
let next = if index + 1 < count {
|
||||
index + 1
|
||||
} else if raw.is_closed {
|
||||
0
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let after = if next + 1 < count {
|
||||
next + 1
|
||||
} else if raw.is_closed {
|
||||
0
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
let previous = if index > 0 {
|
||||
index - 1
|
||||
} else if raw.is_closed {
|
||||
count - 1
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let arc_start = raw.vertex_data[index];
|
||||
let arc_end = raw.vertex_data[next];
|
||||
if arc_start.bulge.abs() < OFFSET_POS_EPS
|
||||
|| raw.vertex_data[previous].bulge.abs() >= OFFSET_POS_EPS
|
||||
|| arc_end.bulge.abs() >= OFFSET_POS_EPS
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(connection) = crate::entities::common::BulgeArc::from_bulge(
|
||||
[arc_start.x, arc_start.y],
|
||||
[arc_end.x, arc_end.y],
|
||||
arc_start.bulge,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
if (connection.radius - 1.0).abs() > OFFSET_JOIN_EPS {
|
||||
continue;
|
||||
}
|
||||
let generated_at_source_vertex = source.iter_vertexes().any(|vertex| {
|
||||
let dx = vertex.x - connection.center[0];
|
||||
let dy = vertex.y - connection.center[1];
|
||||
dx * dx + dy * dy <= OFFSET_JOIN_EPS * OFFSET_JOIN_EPS
|
||||
});
|
||||
if !generated_at_source_vertex {
|
||||
continue;
|
||||
}
|
||||
|
||||
let before = raw.vertex_data[previous];
|
||||
let after_vertex = raw.vertex_data[after];
|
||||
let Some(point) = isect_lines(
|
||||
[before.x, before.y],
|
||||
[arc_start.x, arc_start.y],
|
||||
[arc_end.x, arc_end.y],
|
||||
[after_vertex.x, after_vertex.y],
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
raw.vertex_data[index].x = point[0];
|
||||
raw.vertex_data[index].y = point[1];
|
||||
raw.vertex_data[index].bulge = 0.0;
|
||||
raw.vertex_data.remove(next);
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if !changed {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cleaned_parallel_offset(
|
||||
source: &CavPolyline<f64>,
|
||||
signed_offset: f64,
|
||||
) -> Vec<CavPolyline<f64>> {
|
||||
let options = PlineOffsetOptions {
|
||||
handle_self_intersects: true,
|
||||
pos_equal_eps: OFFSET_POS_EPS,
|
||||
slice_join_eps: OFFSET_JOIN_EPS,
|
||||
offset_dist_eps: OFFSET_JOIN_EPS,
|
||||
..Default::default()
|
||||
};
|
||||
let source_index = source.create_approx_aabb_index();
|
||||
let mut raw: CavPolyline<f64> =
|
||||
create_raw_offset_polyline(source, signed_offset, OFFSET_POS_EPS);
|
||||
if raw.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut dual: CavPolyline<f64> =
|
||||
create_raw_offset_polyline(source, -signed_offset, OFFSET_POS_EPS);
|
||||
sharpen_line_connections(&mut raw, source);
|
||||
sharpen_line_connections(&mut dual, source);
|
||||
|
||||
let slices = slices_from_dual_raw_offsets(
|
||||
source,
|
||||
&raw,
|
||||
&dual,
|
||||
&source_index,
|
||||
signed_offset,
|
||||
&options,
|
||||
);
|
||||
stitch_slices_together::<_, f64, CavPolyline<f64>>(
|
||||
&raw,
|
||||
&slices,
|
||||
source.is_closed(),
|
||||
raw.vertex_count(),
|
||||
&options,
|
||||
)
|
||||
}
|
||||
|
||||
fn offset_lwpolylines(p: &LwPolyline, dist: f64, side_pt: Vec3) -> Vec<EntityType> {
|
||||
let dist = dist.abs();
|
||||
if dist < 1e-12 {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some((source, origin)) = normalized_offset_source(p, dist) else {
|
||||
return Vec::new();
|
||||
let source = KernelPolyline {
|
||||
closed: p.is_closed,
|
||||
vertices: p
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|v| KernelVertex {
|
||||
position: [v.location.x, v.location.y],
|
||||
bulge: if v.bulge.is_finite() { v.bulge } else { 0.0 },
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
let side = CavVector2::new(
|
||||
(side_pt.x as f64 - origin[0]) / dist,
|
||||
(side_pt.y as f64 - origin[1]) / dist,
|
||||
);
|
||||
let Some(closest) = source.closest_point(side, OFFSET_POS_EPS) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let start_index = closest.seg_start_index;
|
||||
let tangent = seg_tangent_vector(
|
||||
source.at(start_index),
|
||||
source.at(source.next_wrapping_index(start_index)),
|
||||
closest.seg_point,
|
||||
);
|
||||
let toward_pick = side - closest.seg_point;
|
||||
let cross = tangent.x * toward_pick.y - tangent.y * toward_pick.x;
|
||||
let signed_offset = if cross >= 0.0 { 1.0 } else { -1.0 };
|
||||
|
||||
cleaned_parallel_offset(&source, signed_offset)
|
||||
offset_polyline(&source, dist, [side_pt.x as f64, side_pt.y as f64])
|
||||
.into_iter()
|
||||
.filter(|result| result.vertex_count() >= 2)
|
||||
.map(|result| {
|
||||
let mut new_polyline = p.clone();
|
||||
new_polyline.common.handle = Handle::NULL;
|
||||
new_polyline.is_closed = result.is_closed();
|
||||
new_polyline.is_closed = result.closed;
|
||||
new_polyline.vertices = result
|
||||
.iter_vertexes()
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|vertex| {
|
||||
let mut output = LwVertex::from_coords(
|
||||
origin[0] + vertex.x * dist,
|
||||
origin[1] + vertex.y * dist,
|
||||
);
|
||||
let mut output =
|
||||
LwVertex::from_coords(vertex.position[0], vertex.position[1]);
|
||||
output.bulge = vertex.bulge;
|
||||
output
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user