forked from 1b-agent/opencadstudio-upstream
feat(boundary): trace exact planar loops
This commit is contained in:
commit
0e944c19a6
4 files changed
+685
-24
No files matched your search
+307
-2
@@ -1,10 +1,18 @@
|
||||
use super::*;
|
||||
|
||||
use cadkernel::geom2d::{
|
||||
bounded_faces, contains, distance_to, segment_crossing, triangulate, Curve, Line,
|
||||
SegmentCrossing, Tolerance,
|
||||
bounded_faces, contains, distance_to, intersect, segment_crossing, triangulate, Curve, Line,
|
||||
SegmentCrossing, Tolerance, Transform as CurveTransform,
|
||||
};
|
||||
|
||||
use crate::command::WorkingPlane;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BoundarySource {
|
||||
pub segments: Vec<Line>,
|
||||
pub curves: Vec<Curve>,
|
||||
}
|
||||
|
||||
/// How far apart two points may be and still be taken for the same one.
|
||||
///
|
||||
/// The boundary search runs on already-tessellated wire geometry, so the
|
||||
@@ -38,6 +46,73 @@ fn wire_segments(wire: &WireModel) -> Vec<Line> {
|
||||
segments
|
||||
}
|
||||
|
||||
fn wire_segments_on_plane(
|
||||
wire: &WireModel,
|
||||
plane: WorkingPlane,
|
||||
tolerance: f64,
|
||||
) -> Vec<Line> {
|
||||
let mut segments = Vec::new();
|
||||
let mut previous: Option<glam::DVec3> = None;
|
||||
for (index, high) in wire.points.iter().copied().enumerate() {
|
||||
if !high.iter().all(|value| value.is_finite()) {
|
||||
previous = None;
|
||||
continue;
|
||||
}
|
||||
let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]);
|
||||
let world = glam::DVec3::new(
|
||||
high[0] as f64 + low[0] as f64,
|
||||
high[1] as f64 + low[1] as f64,
|
||||
high[2] as f64 + low[2] as f64,
|
||||
);
|
||||
let current = plane.to_local(world);
|
||||
if current.z.abs() > tolerance {
|
||||
previous = None;
|
||||
continue;
|
||||
}
|
||||
if let Some(start) = previous {
|
||||
let length = (current.x - start.x).hypot(current.y - start.y);
|
||||
if length > tolerance {
|
||||
segments.push(Line {
|
||||
start: [start.x, start.y],
|
||||
end: [current.x, current.y],
|
||||
});
|
||||
}
|
||||
}
|
||||
previous = Some(current);
|
||||
}
|
||||
segments
|
||||
}
|
||||
|
||||
fn entity_curves_on_plane(
|
||||
entity: &EntityType,
|
||||
plane: WorkingPlane,
|
||||
tolerance: f64,
|
||||
) -> Vec<Curve> {
|
||||
let Some(planar) = crate::entities::curve::entity_curve(entity) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let source = planar.plane;
|
||||
let origin = plane.to_local(glam::DVec3::from_array(source.origin));
|
||||
let x_axis = plane.vector_to_local(glam::DVec3::from_array(source.x_axis));
|
||||
let y_axis = plane.vector_to_local(glam::DVec3::from_array(source.y_axis));
|
||||
if origin.z.abs() > tolerance || x_axis.z.abs() > 1.0e-9 || y_axis.z.abs() > 1.0e-9 {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(curve) = planar.curve.transformed(&CurveTransform {
|
||||
x_axis: [x_axis.x, x_axis.y].into(),
|
||||
y_axis: [y_axis.x, y_axis.y].into(),
|
||||
origin: [origin.x, origin.y].into(),
|
||||
}) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let segments = curve.segments();
|
||||
if segments.is_empty() {
|
||||
vec![curve]
|
||||
} else {
|
||||
segments
|
||||
}
|
||||
}
|
||||
|
||||
fn ring_seed(model: &HatchModel, wanted: usize) -> Option<[f64; 2]> {
|
||||
let mut ring = Vec::new();
|
||||
let mut index = 0usize;
|
||||
@@ -164,6 +239,175 @@ pub(crate) fn boundary_entities(rings: &[Vec<[f64; 2]>]) -> Vec<acadrust::Entity
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn edge_curve(
|
||||
edge: Line,
|
||||
sources: &rustc_hash::FxHashMap<Handle, BoundarySource>,
|
||||
tolerance: f64,
|
||||
) -> Option<Curve> {
|
||||
let edge_length = (edge.end[0] - edge.start[0]).hypot(edge.end[1] - edge.start[1]);
|
||||
let mut best = None;
|
||||
let mut best_overlap = 0.0;
|
||||
for source in sources.values() {
|
||||
for segment in &source.segments {
|
||||
if let SegmentCrossing::Overlap { a, .. } =
|
||||
segment_crossing(edge, *segment, Tolerance::new(tolerance))
|
||||
{
|
||||
let overlap = (a[1] - a[0]).abs() * edge_length;
|
||||
if overlap > best_overlap {
|
||||
best = Some(source);
|
||||
best_overlap = overlap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let candidates: Vec<&Curve> = match best {
|
||||
Some(source) => source.curves.iter().collect(),
|
||||
None => sources
|
||||
.values()
|
||||
.flat_map(|source| source.curves.iter())
|
||||
.filter(|curve| {
|
||||
distance_to(curve, edge.start).min(distance_to(curve, edge.end))
|
||||
<= tolerance * 4.0
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
candidates
|
||||
.into_iter()
|
||||
.min_by(|left, right| {
|
||||
let error = |curve: &Curve| {
|
||||
distance_to(curve, edge.start) + distance_to(curve, edge.end)
|
||||
};
|
||||
error(left).total_cmp(&error(right))
|
||||
})
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn nearest_crossing(a: &Curve, b: &Curve, point: [f64; 2], tolerance: f64) -> Option<[f64; 2]> {
|
||||
intersect(a, b, Tolerance::new(tolerance))
|
||||
.into_iter()
|
||||
.min_by(|left, right| {
|
||||
let distance = |candidate: [f64; 2]| {
|
||||
(candidate[0] - point[0]).hypot(candidate[1] - point[1])
|
||||
};
|
||||
distance(left.point).total_cmp(&distance(right.point))
|
||||
})
|
||||
.map(|crossing| crossing.point)
|
||||
}
|
||||
|
||||
fn project_to_curve(curve: &Curve, point: [f64; 2]) -> [f64; 2] {
|
||||
curve.point_at(curve.parameter_at(point))
|
||||
}
|
||||
|
||||
fn refined_boundary_ring(
|
||||
ring: &[[f64; 2]],
|
||||
sources: &rustc_hash::FxHashMap<Handle, BoundarySource>,
|
||||
tolerance: f64,
|
||||
) -> (Vec<[f64; 2]>, Vec<Option<Curve>>) {
|
||||
let count = ring.len();
|
||||
if count < 3 {
|
||||
return (ring.to_vec(), Vec::new());
|
||||
}
|
||||
let edge_curves: Vec<_> = (0..count)
|
||||
.map(|index| {
|
||||
edge_curve(
|
||||
Line {
|
||||
start: ring[index],
|
||||
end: ring[(index + 1) % count],
|
||||
},
|
||||
sources,
|
||||
tolerance,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let points = (0..count)
|
||||
.map(|index| {
|
||||
let point = ring[index];
|
||||
let incoming = edge_curves[(index + count - 1) % count].as_ref();
|
||||
let outgoing = edge_curves[index].as_ref();
|
||||
match (incoming, outgoing) {
|
||||
(Some(a), Some(b)) if a != b => nearest_crossing(a, b, point, tolerance)
|
||||
.unwrap_or_else(|| project_to_curve(b, point)),
|
||||
(Some(curve), _) | (_, Some(curve)) => project_to_curve(curve, point),
|
||||
_ => point,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
(points, edge_curves)
|
||||
}
|
||||
|
||||
fn normalized_delta(from: f64, to: f64) -> f64 {
|
||||
(to - from + std::f64::consts::PI)
|
||||
.rem_euclid(std::f64::consts::TAU)
|
||||
- std::f64::consts::PI
|
||||
}
|
||||
|
||||
fn curve_edge_bulge(curve: &Curve, start: [f64; 2], end: [f64; 2]) -> f64 {
|
||||
let direct = match curve {
|
||||
Curve::Circle(circle) => Some((circle.centre, circle.radius)),
|
||||
Curve::Arc(arc) => Some((arc.centre, arc.radius)),
|
||||
_ => None,
|
||||
};
|
||||
if let Some((centre, radius)) = direct {
|
||||
if radius.abs() <= 1.0e-12 {
|
||||
return 0.0;
|
||||
}
|
||||
let start_angle = (start[1] - centre[1]).atan2(start[0] - centre[0]);
|
||||
let end_angle = (end[1] - centre[1]).atan2(end[0] - centre[0]);
|
||||
return (normalized_delta(start_angle, end_angle) * 0.25).tan();
|
||||
}
|
||||
0.0
|
||||
}
|
||||
|
||||
fn boundary_polyline(
|
||||
ring: &[[f64; 2]],
|
||||
plane: WorkingPlane,
|
||||
sources: &rustc_hash::FxHashMap<Handle, BoundarySource>,
|
||||
tolerance: f64,
|
||||
) -> Option<EntityType> {
|
||||
let (points, edge_curves) = refined_boundary_ring(ring, sources, tolerance);
|
||||
if points.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
let mut polyline = acadrust::entities::LwPolyline::new();
|
||||
polyline.is_closed = true;
|
||||
polyline.vertices = points
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, point)| {
|
||||
let mut vertex = acadrust::entities::LwVertex::new(acadrust::types::Vector2::new(
|
||||
point[0], point[1],
|
||||
));
|
||||
vertex.bulge = edge_curves
|
||||
.get(index)
|
||||
.and_then(|curve| curve.as_ref())
|
||||
.map(|curve| {
|
||||
curve_edge_bulge(curve, *point, points[(index + 1) % points.len()])
|
||||
})
|
||||
.unwrap_or(0.0);
|
||||
vertex
|
||||
})
|
||||
.collect();
|
||||
Some(plane.place_entity(EntityType::LwPolyline(polyline)))
|
||||
}
|
||||
|
||||
pub(crate) fn boundary_polyline_entities(
|
||||
regions: &[Vec<Vec<[f64; 2]>>],
|
||||
plane: WorkingPlane,
|
||||
sources: &rustc_hash::FxHashMap<Handle, BoundarySource>,
|
||||
tolerance: f64,
|
||||
) -> Vec<EntityType> {
|
||||
let mut rings = Vec::new();
|
||||
for ring in regions.iter().flat_map(|region| region.iter()) {
|
||||
if !rings.iter().any(|existing| *existing == ring) {
|
||||
rings.push(ring);
|
||||
}
|
||||
}
|
||||
rings
|
||||
.into_iter()
|
||||
.filter_map(|ring| boundary_polyline(ring, plane, sources, tolerance))
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
fn associative_boundary_segments(&self, handles: &[Handle]) -> Vec<Line> {
|
||||
self.wire_models_for(handles)
|
||||
@@ -340,4 +584,65 @@ impl Scene {
|
||||
}
|
||||
sources
|
||||
}
|
||||
|
||||
/// Boundary candidates in the active working plane, with exact curves
|
||||
/// where the source entity exposes them.
|
||||
pub fn boundary_sources_on_plane(
|
||||
&self,
|
||||
plane: WorkingPlane,
|
||||
tolerance: f64,
|
||||
) -> rustc_hash::FxHashMap<Handle, BoundarySource> {
|
||||
let mut sources: rustc_hash::FxHashMap<Handle, BoundarySource> =
|
||||
rustc_hash::FxHashMap::default();
|
||||
let wires = if let Some(viewport) = self.active_viewport {
|
||||
self.model_wires_for_viewport_arc(viewport, 0.0)
|
||||
} else if self.current_layout == "Model" {
|
||||
let camera = self.camera.borrow().clone();
|
||||
self.model_tile_wires_arc(0, &camera, 1.0, 1.0)
|
||||
} else {
|
||||
self.paper_sheet_wires_arc()
|
||||
};
|
||||
for wire in wires.iter() {
|
||||
let Some(handle) = Self::handle_from_wire_name(&wire.name) else {
|
||||
continue;
|
||||
};
|
||||
let segments = wire_segments_on_plane(wire, plane, tolerance);
|
||||
if segments.is_empty() {
|
||||
continue;
|
||||
}
|
||||
sources
|
||||
.entry(handle)
|
||||
.or_insert_with(|| BoundarySource {
|
||||
segments: Vec::new(),
|
||||
curves: Vec::new(),
|
||||
})
|
||||
.segments
|
||||
.extend(segments);
|
||||
}
|
||||
for (&handle, source) in &mut sources {
|
||||
source.curves = self
|
||||
.document
|
||||
.get_entity(handle)
|
||||
.map(|entity| entity_curves_on_plane(entity, plane, tolerance))
|
||||
.unwrap_or_default();
|
||||
source.curves.retain(|curve| {
|
||||
source.segments.iter().any(|segment| {
|
||||
distance_to(curve, segment.start).max(distance_to(curve, segment.end))
|
||||
<= tolerance * 4.0
|
||||
})
|
||||
});
|
||||
}
|
||||
sources
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn boundary_faces(
|
||||
sources: &rustc_hash::FxHashMap<Handle, BoundarySource>,
|
||||
tolerance: f64,
|
||||
) -> Vec<Vec<[f64; 2]>> {
|
||||
let segments: Vec<_> = sources
|
||||
.values()
|
||||
.flat_map(|source| source.segments.iter().copied())
|
||||
.collect();
|
||||
bounded_faces(&segments, Tolerance::new(tolerance))
|
||||
}
|
||||
+4
-1
@@ -33,7 +33,10 @@ mod project;
|
||||
mod scene_markers;
|
||||
mod selection;
|
||||
|
||||
pub(crate) use boundary::{boundary_entities, ring_source_handles};
|
||||
pub(crate) use boundary::{
|
||||
boundary_entities, boundary_faces, boundary_polyline_entities, ring_source_handles,
|
||||
BoundarySource,
|
||||
};
|
||||
|
||||
// Parallel tessellation free functions live in `convert::tess` (alongside the
|
||||
// other tessellation code); re-exported here so this root and sibling topic
|
||||
|
||||
Reference in new issue
Block a user