343 lines
12 KiB
Rust
343 lines
12 KiB
Rust
use super::*;
|
|
|
|
use cadkernel::geom2d::{
|
|
bounded_faces, contains, distance_to, segment_crossing, triangulate, Curve, Line,
|
|
SegmentCrossing, Tolerance,
|
|
};
|
|
|
|
/// 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
|
|
/// input is a chord approximation of the drawn curves to begin with; this
|
|
/// only has to be coarse enough to close the gaps that leaves and fine
|
|
/// enough not to weld genuinely separate corners together.
|
|
const WELD_TOLERANCE: f64 = 1.0e-6;
|
|
|
|
fn wire_segments(wire: &WireModel) -> Vec<Line> {
|
|
let mut segments = Vec::new();
|
|
let mut previous: Option<[f64; 2]> = None;
|
|
|
|
for (index, high) in wire.points.iter().copied().enumerate() {
|
|
if !high[0].is_finite() || !high[1].is_finite() {
|
|
previous = None;
|
|
continue;
|
|
}
|
|
let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]);
|
|
let current = [
|
|
high[0] as f64 + low[0] as f64,
|
|
high[1] as f64 + low[1] as f64,
|
|
];
|
|
if let Some(start) = previous {
|
|
let (dx, dy) = (current[0] - start[0], current[1] - start[1]);
|
|
if dx.hypot(dy) > WELD_TOLERANCE {
|
|
segments.push(Line { start, end: current });
|
|
}
|
|
}
|
|
previous = Some(current);
|
|
}
|
|
segments
|
|
}
|
|
|
|
fn ring_seed(model: &HatchModel, wanted: usize) -> Option<[f64; 2]> {
|
|
let mut ring = Vec::new();
|
|
let mut index = 0usize;
|
|
for &[x, y] in model.boundary.iter() {
|
|
if x.is_finite() && y.is_finite() {
|
|
if index == wanted {
|
|
ring.push([
|
|
model.world_origin[0] + x as f64,
|
|
model.world_origin[1] + y as f64,
|
|
]);
|
|
}
|
|
} else if index == wanted {
|
|
break;
|
|
} else {
|
|
index += 1;
|
|
}
|
|
}
|
|
if ring.len() < 3 {
|
|
return None;
|
|
}
|
|
let (points, triangles) = triangulate(&ring, &[]);
|
|
if let Some(triangle) = triangles.first() {
|
|
let [a, b, c] = triangle.map(|vertex| points[vertex]);
|
|
return Some([
|
|
(a[0] + b[0] + c[0]) / 3.0,
|
|
(a[1] + b[1] + c[1]) / 3.0,
|
|
]);
|
|
}
|
|
Some([
|
|
ring.iter().map(|point| point[0]).sum::<f64>() / ring.len() as f64,
|
|
ring.iter().map(|point| point[1]).sum::<f64>() / ring.len() as f64,
|
|
])
|
|
}
|
|
|
|
fn face_curves(face: &[[f64; 2]]) -> Vec<Curve> {
|
|
face.iter()
|
|
.copied()
|
|
.zip(face.iter().copied().cycle().skip(1))
|
|
.take(face.len())
|
|
.map(|(start, end)| Curve::Line(Line { start, end }))
|
|
.collect()
|
|
}
|
|
|
|
fn matching_face(faces: &[Vec<[f64; 2]>], seed: Option<[f64; 2]>) -> Option<&Vec<[f64; 2]>> {
|
|
if faces.len() == 1 {
|
|
return faces.first();
|
|
}
|
|
let seed = seed?;
|
|
let tolerance = Tolerance::new(WELD_TOLERANCE);
|
|
if let Some(face) = faces.iter().find(|face| {
|
|
let curves = face_curves(face);
|
|
contains(&curves, seed, tolerance)
|
|
}) {
|
|
return Some(face);
|
|
}
|
|
faces.iter().min_by(|a, b| {
|
|
let nearest = |face: &Vec<[f64; 2]>| {
|
|
face_curves(face)
|
|
.iter()
|
|
.map(|curve| distance_to(curve, seed))
|
|
.fold(f64::INFINITY, f64::min)
|
|
};
|
|
nearest(a).total_cmp(&nearest(b))
|
|
})
|
|
}
|
|
|
|
pub(crate) fn ring_source_handles(
|
|
ring: &[[f64; 2]],
|
|
sources: &rustc_hash::FxHashMap<acadrust::Handle, Vec<Line>>,
|
|
) -> Vec<acadrust::Handle> {
|
|
let mut handles = rustc_hash::FxHashSet::default();
|
|
for (&start, &end) in ring
|
|
.iter()
|
|
.zip(ring.iter().cycle().skip(1))
|
|
.take(ring.len())
|
|
{
|
|
let edge = Line { start, end };
|
|
let edge_length = (end[0] - start[0]).hypot(end[1] - start[1]);
|
|
for (&handle, lines) in sources {
|
|
if lines.iter().any(|line| {
|
|
matches!(
|
|
segment_crossing(edge, *line, Tolerance::new(WELD_TOLERANCE)),
|
|
SegmentCrossing::Overlap { a, .. }
|
|
if (a[1] - a[0]).abs() * edge_length > WELD_TOLERANCE
|
|
)
|
|
}) {
|
|
handles.insert(handle);
|
|
}
|
|
}
|
|
}
|
|
let mut handles: Vec<_> = handles.into_iter().collect();
|
|
handles.sort_by_key(|handle| handle.value());
|
|
handles
|
|
}
|
|
|
|
pub(crate) fn boundary_entities(rings: &[Vec<[f64; 2]>]) -> Vec<acadrust::EntityType> {
|
|
rings
|
|
.iter()
|
|
.filter_map(|ring| {
|
|
let mut points = Vec::with_capacity(ring.len());
|
|
for &point in ring {
|
|
if point.iter().all(|value| value.is_finite())
|
|
&& points.last() != Some(&point)
|
|
{
|
|
points.push(point);
|
|
}
|
|
}
|
|
if points.len() > 1 && points.first() == points.last() {
|
|
points.pop();
|
|
}
|
|
if points.len() < 3 {
|
|
return None;
|
|
}
|
|
let mut polyline = acadrust::entities::LwPolyline::new();
|
|
polyline.is_closed = true;
|
|
polyline.vertices = points
|
|
.into_iter()
|
|
.map(|[x, y]| {
|
|
acadrust::entities::LwVertex::new(acadrust::types::Vector2::new(x, y))
|
|
})
|
|
.collect();
|
|
Some(acadrust::EntityType::LwPolyline(polyline))
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
impl Scene {
|
|
fn associative_boundary_segments(&self, handles: &[Handle]) -> Vec<Line> {
|
|
self.wire_models_for(handles)
|
|
.iter()
|
|
.flat_map(wire_segments)
|
|
.collect()
|
|
}
|
|
|
|
fn associative_hatch_dependents(
|
|
&self,
|
|
changed: &rustc_hash::FxHashSet<Handle>,
|
|
) -> Vec<Handle> {
|
|
if self.associative_hatch_source_cache.borrow().is_none() {
|
|
let mut index: HashMap<Handle, Vec<Handle>> = HashMap::default();
|
|
for entity in self.document.entities() {
|
|
let EntityType::Hatch(hatch) = entity else {
|
|
continue;
|
|
};
|
|
if !hatch.is_associative {
|
|
continue;
|
|
}
|
|
for source in hatch
|
|
.paths
|
|
.iter()
|
|
.flat_map(|path| path.boundary_handles.iter().copied())
|
|
{
|
|
let dependents = index.entry(source).or_default();
|
|
if !dependents.contains(&hatch.common.handle) {
|
|
dependents.push(hatch.common.handle);
|
|
}
|
|
}
|
|
}
|
|
*self.associative_hatch_source_cache.borrow_mut() = Some(index);
|
|
}
|
|
let cache = self.associative_hatch_source_cache.borrow();
|
|
let index = cache.as_ref().expect("associative hatch index");
|
|
let mut handles = rustc_hash::FxHashSet::default();
|
|
for source in changed {
|
|
if let Some(dependents) = index.get(source) {
|
|
handles.extend(dependents.iter().copied());
|
|
}
|
|
}
|
|
handles.into_iter().collect()
|
|
}
|
|
|
|
pub(crate) fn refresh_associative_hatches(
|
|
&mut self,
|
|
changes: &[(Handle, ChangeKind)],
|
|
) -> Vec<(Handle, ChangeKind)> {
|
|
if changes.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
let changed: rustc_hash::FxHashSet<_> =
|
|
changes.iter().map(|(handle, _)| *handle).collect();
|
|
let candidates: Vec<_> = self
|
|
.associative_hatch_dependents(&changed)
|
|
.into_iter()
|
|
.filter_map(|handle| {
|
|
let EntityType::Hatch(hatch) = self.document.get_entity(handle)? else {
|
|
return None;
|
|
};
|
|
Some({
|
|
let seeds = self
|
|
.hatches
|
|
.get(&hatch.common.handle)
|
|
.map(|model| {
|
|
(0..hatch.paths.len())
|
|
.map(|index| ring_seed(model, index))
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.unwrap_or_default();
|
|
(handle, hatch.clone(), seeds)
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
let mut refreshed = Vec::new();
|
|
for (handle, mut hatch, seeds) in candidates {
|
|
let normal = hatch.normal;
|
|
if normal.x.abs() > 1.0e-8 || normal.y.abs() > 1.0e-8 {
|
|
continue;
|
|
}
|
|
let mut modified = false;
|
|
let mut association_changed = false;
|
|
for (index, path) in hatch.paths.iter_mut().enumerate() {
|
|
if !path
|
|
.boundary_handles
|
|
.iter()
|
|
.any(|source| changed.contains(source))
|
|
{
|
|
continue;
|
|
}
|
|
let old_count = path.boundary_handles.len();
|
|
path.boundary_handles
|
|
.retain(|source| self.document.get_entity(*source).is_some());
|
|
association_changed |= path.boundary_handles.len() != old_count;
|
|
modified |= association_changed;
|
|
let segments = self.associative_boundary_segments(&path.boundary_handles);
|
|
let faces = bounded_faces(&segments, Tolerance::new(WELD_TOLERANCE));
|
|
let Some(face) = matching_face(&faces, seeds.get(index).copied().flatten()) else {
|
|
continue;
|
|
};
|
|
path.edges = vec![acadrust::entities::hatch::BoundaryEdge::Polyline(
|
|
acadrust::entities::hatch::PolylineEdge::new(
|
|
face.iter()
|
|
.map(|point| acadrust::types::Vector2::new(point[0], point[1]))
|
|
.collect(),
|
|
true,
|
|
),
|
|
)];
|
|
modified = true;
|
|
}
|
|
hatch.is_associative = hatch
|
|
.paths
|
|
.iter()
|
|
.any(|path| !path.boundary_handles.is_empty());
|
|
if !modified {
|
|
continue;
|
|
}
|
|
if association_changed {
|
|
self.associative_hatch_source_cache.borrow_mut().take();
|
|
}
|
|
if self.is_recording_undo() {
|
|
let before = self.document.get_entity_arc(handle);
|
|
self.record_undo_before(handle, before);
|
|
}
|
|
if let Some(slot) = self.document.get_entity_mut(handle) {
|
|
*slot = EntityType::Hatch(hatch);
|
|
self.refresh_fill_model(handle);
|
|
refreshed.push((handle, ChangeKind::Modified));
|
|
}
|
|
}
|
|
refreshed
|
|
}
|
|
|
|
/// Build closed planar regions from the visible wire geometry.
|
|
///
|
|
/// Source entities do not need to be closed individually. Intersections are
|
|
/// inserted as temporary graph vertices and
|
|
/// the bounded faces of that planar graph are returned as hatch candidates.
|
|
///
|
|
/// Curved entities participate through their already-tessellated WireModel
|
|
/// geometry, so arcs, circles, ellipses and splines can take part in the
|
|
/// boundary search without modifying the source entities.
|
|
///
|
|
/// The arrangement itself is the kernel's: splitting at crossings, welding
|
|
/// coincident ends and tracing the bounded faces is the same problem a
|
|
/// B-rep boolean solves in a face's parameter space, and it is solved
|
|
/// once. What stays here is reading the wires — which is where the
|
|
/// drawing's own conventions live.
|
|
pub fn hatch_boundary_outlines(&self) -> Vec<Vec<[f64; 2]>> {
|
|
let mut segments = Vec::<Line>::new();
|
|
|
|
for wire in self.entity_wires().iter() {
|
|
segments.extend(wire_segments(wire));
|
|
}
|
|
|
|
bounded_faces(&segments, Tolerance::new(WELD_TOLERANCE))
|
|
}
|
|
|
|
/// Tessellated boundary segments grouped by their selectable entity.
|
|
pub fn hatch_boundary_sources(
|
|
&self,
|
|
) -> rustc_hash::FxHashMap<acadrust::Handle, Vec<Line>> {
|
|
let mut sources = rustc_hash::FxHashMap::default();
|
|
for wire in self.entity_wires().iter() {
|
|
let Some(handle) = Self::handle_from_wire_name(&wire.name) else {
|
|
continue;
|
|
};
|
|
sources
|
|
.entry(handle)
|
|
.or_insert_with(Vec::new)
|
|
.extend(wire_segments(wire));
|
|
}
|
|
sources
|
|
}
|
|
}
|