forked from 1b-agent/opencadstudio-upstream
Merge pull request #852 from ramox81/codex/complete-hatch-workflow
This commit is contained in:
commit
bd5f9436ea
18 files changed
+1537
-481
No files matched your search
+401
-128
@@ -1,8 +1,9 @@
|
||||
use super::*;
|
||||
|
||||
use cadkernel::geom2d::{
|
||||
bounded_faces, contains, distance_to, intersect, segment_crossing, triangulate, Curve, Line,
|
||||
SegmentCrossing, Tolerance, Transform as CurveTransform,
|
||||
bounded_faces, closest_point, contains, distance_to, intersect, ring_nesting_depths,
|
||||
segment_crossing, signed_area, triangulate, Curve, Line, SegmentCrossing, Tolerance,
|
||||
Transform as CurveTransform,
|
||||
};
|
||||
|
||||
use crate::command::WorkingPlane;
|
||||
@@ -13,39 +14,9 @@ pub struct BoundarySource {
|
||||
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
|
||||
/// 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.
|
||||
/// Boundary welding tolerance for tessellated wires.
|
||||
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 wire_segments_on_plane(
|
||||
wire: &WireModel,
|
||||
plane: WorkingPlane,
|
||||
@@ -113,22 +84,12 @@ fn entity_curves_on_plane(
|
||||
}
|
||||
}
|
||||
|
||||
fn ring_seed(model: &HatchModel, wanted: usize) -> Option<[f64; 2]> {
|
||||
fn hatch_path_seed(path: &acadrust::entities::BoundaryPath) -> 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;
|
||||
}
|
||||
for edge in &path.edges {
|
||||
let curve = crate::entities::hatch::edge_curve(edge)?;
|
||||
let points = curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE);
|
||||
ring.extend(points.into_iter().skip(usize::from(!ring.is_empty())));
|
||||
}
|
||||
if ring.len() < 3 {
|
||||
return None;
|
||||
@@ -136,15 +97,16 @@ fn ring_seed(model: &HatchModel, wanted: usize) -> Option<[f64; 2]> {
|
||||
let (points, triangles) = triangulate(&ring, &[]);
|
||||
if let Some(triangle) = triangles.first() {
|
||||
let [a, b, c] = triangle.map(|vertex| points[vertex]);
|
||||
return Some([
|
||||
Some([
|
||||
(a[0] + b[0] + c[0]) / 3.0,
|
||||
(a[1] + b[1] + c[1]) / 3.0,
|
||||
]);
|
||||
])
|
||||
} else {
|
||||
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,
|
||||
])
|
||||
}
|
||||
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> {
|
||||
@@ -181,7 +143,7 @@ fn matching_face(faces: &[Vec<[f64; 2]>], seed: Option<[f64; 2]>) -> Option<&Vec
|
||||
|
||||
pub(crate) fn ring_source_handles(
|
||||
ring: &[[f64; 2]],
|
||||
sources: &rustc_hash::FxHashMap<acadrust::Handle, Vec<Line>>,
|
||||
sources: &rustc_hash::FxHashMap<acadrust::Handle, BoundarySource>,
|
||||
) -> Vec<acadrust::Handle> {
|
||||
let mut handles = rustc_hash::FxHashSet::default();
|
||||
for (&start, &end) in ring
|
||||
@@ -191,8 +153,8 @@ pub(crate) fn ring_source_handles(
|
||||
{
|
||||
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| {
|
||||
for (&handle, source) in sources {
|
||||
if source.segments.iter().any(|line| {
|
||||
matches!(
|
||||
segment_crossing(edge, *line, Tolerance::new(WELD_TOLERANCE)),
|
||||
SegmentCrossing::Overlap { a, .. }
|
||||
@@ -208,7 +170,220 @@ pub(crate) fn ring_source_handles(
|
||||
handles
|
||||
}
|
||||
|
||||
pub(crate) fn boundary_entities(rings: &[Vec<[f64; 2]>]) -> Vec<acadrust::EntityType> {
|
||||
fn curve_forward(curve: &Curve, start: [f64; 2], next: [f64; 2]) -> bool {
|
||||
let a = curve.parameter_at(start);
|
||||
let b = curve.parameter_at(next);
|
||||
let mut delta = b - a;
|
||||
if curve.is_closed() {
|
||||
if delta > 0.5 {
|
||||
delta -= 1.0;
|
||||
} else if delta < -0.5 {
|
||||
delta += 1.0;
|
||||
}
|
||||
}
|
||||
delta >= 0.0
|
||||
}
|
||||
|
||||
fn stored_arc_angles(start: f64, end: f64, counter_clockwise: bool, whole: bool) -> (f64, f64) {
|
||||
let stored_start = if counter_clockwise { start } else { -start }
|
||||
.rem_euclid(std::f64::consts::TAU);
|
||||
let sweep = if whole {
|
||||
std::f64::consts::TAU
|
||||
} else if counter_clockwise {
|
||||
(end - start).rem_euclid(std::f64::consts::TAU)
|
||||
} else {
|
||||
(start - end).rem_euclid(std::f64::consts::TAU)
|
||||
};
|
||||
(stored_start, stored_start + sweep)
|
||||
}
|
||||
|
||||
fn exact_boundary_edge(
|
||||
curve: Option<&Curve>,
|
||||
start: [f64; 2],
|
||||
end: [f64; 2],
|
||||
next: [f64; 2],
|
||||
whole_curve: bool,
|
||||
) -> acadrust::entities::BoundaryEdge {
|
||||
use acadrust::entities::{
|
||||
BoundaryEdge, CircularArcEdge, EllipticArcEdge, LineEdge, SplineEdge,
|
||||
};
|
||||
use acadrust::types::{Vector2, Vector3};
|
||||
|
||||
let Some(curve) = curve else {
|
||||
return BoundaryEdge::Line(LineEdge {
|
||||
start: Vector2::new(start[0], start[1]),
|
||||
end: Vector2::new(end[0], end[1]),
|
||||
});
|
||||
};
|
||||
let forward = curve_forward(curve, start, next);
|
||||
match curve {
|
||||
Curve::Line(_) => BoundaryEdge::Line(LineEdge {
|
||||
start: Vector2::new(start[0], start[1]),
|
||||
end: Vector2::new(end[0], end[1]),
|
||||
}),
|
||||
Curve::Circle(circle) => {
|
||||
let true_start = (start[1] - circle.centre[1]).atan2(start[0] - circle.centre[0]);
|
||||
let true_end = (end[1] - circle.centre[1]).atan2(end[0] - circle.centre[0]);
|
||||
let (start_angle, end_angle) =
|
||||
stored_arc_angles(true_start, true_end, forward, whole_curve);
|
||||
BoundaryEdge::CircularArc(CircularArcEdge {
|
||||
center: Vector2::new(circle.centre[0], circle.centre[1]),
|
||||
radius: circle.radius,
|
||||
start_angle,
|
||||
end_angle,
|
||||
counter_clockwise: forward,
|
||||
})
|
||||
}
|
||||
Curve::Arc(arc) => {
|
||||
let true_start = (start[1] - arc.centre[1]).atan2(start[0] - arc.centre[0]);
|
||||
let true_end = (end[1] - arc.centre[1]).atan2(end[0] - arc.centre[0]);
|
||||
let (start_angle, end_angle) =
|
||||
stored_arc_angles(true_start, true_end, forward, whole_curve);
|
||||
BoundaryEdge::CircularArc(CircularArcEdge {
|
||||
center: Vector2::new(arc.centre[0], arc.centre[1]),
|
||||
radius: arc.radius,
|
||||
start_angle,
|
||||
end_angle,
|
||||
counter_clockwise: forward,
|
||||
})
|
||||
}
|
||||
Curve::Ellipse(arc) => {
|
||||
let ellipse = arc.ellipse;
|
||||
let true_start = arc.start_parameter + curve.parameter_at(start) * arc.sweep();
|
||||
let true_end = arc.start_parameter + curve.parameter_at(end) * arc.sweep();
|
||||
let (start_parameter, end_parameter) =
|
||||
stored_arc_angles(true_start, true_end, forward, whole_curve);
|
||||
BoundaryEdge::EllipticArc(EllipticArcEdge {
|
||||
center: Vector2::new(ellipse.centre[0], ellipse.centre[1]),
|
||||
major_axis_endpoint: Vector2::new(
|
||||
ellipse.major_axis[0] * ellipse.major_radius,
|
||||
ellipse.major_axis[1] * ellipse.major_radius,
|
||||
),
|
||||
minor_axis_ratio: ellipse.minor_radius / ellipse.major_radius,
|
||||
start_angle: start_parameter,
|
||||
end_angle: end_parameter,
|
||||
counter_clockwise: forward,
|
||||
})
|
||||
}
|
||||
Curve::Nurbs(source) => {
|
||||
let trimmed = if whole_curve {
|
||||
Some(if forward { source.clone() } else { source.reversed() })
|
||||
} else {
|
||||
source.trimmed(source.parameter_at(start), source.parameter_at(end))
|
||||
};
|
||||
let Some(trimmed) = trimmed else {
|
||||
return BoundaryEdge::Line(LineEdge {
|
||||
start: Vector2::new(start[0], start[1]),
|
||||
end: Vector2::new(end[0], end[1]),
|
||||
});
|
||||
};
|
||||
let rational = trimmed.is_rational();
|
||||
BoundaryEdge::Spline(SplineEdge {
|
||||
degree: trimmed.degree() as i32,
|
||||
rational,
|
||||
periodic: trimmed.is_closed(),
|
||||
knots: trimmed.knots().to_vec(),
|
||||
control_points: trimmed
|
||||
.control_points()
|
||||
.iter()
|
||||
.zip(trimmed.weights())
|
||||
.map(|(point, weight)| {
|
||||
Vector3::new(point[0], point[1], if rational { *weight } else { 1.0 })
|
||||
})
|
||||
.collect(),
|
||||
fit_points: Vec::new(),
|
||||
start_tangent: Vector2::new(0.0, 0.0),
|
||||
end_tangent: Vector2::new(0.0, 0.0),
|
||||
})
|
||||
}
|
||||
Curve::Polyline(_) | Curve::Ray(_) | Curve::XLine(_) => {
|
||||
BoundaryEdge::Line(LineEdge {
|
||||
start: Vector2::new(start[0], start[1]),
|
||||
end: Vector2::new(end[0], end[1]),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuild detected rings from exact source curves.
|
||||
pub(crate) fn exact_hatch_paths(
|
||||
rings: &[Vec<[f64; 2]>],
|
||||
exterior: &[bool],
|
||||
sources: &rustc_hash::FxHashMap<Handle, BoundarySource>,
|
||||
tolerance: f64,
|
||||
) -> Vec<acadrust::entities::BoundaryPath> {
|
||||
use acadrust::entities::{BoundaryPath, BoundaryPathFlags};
|
||||
|
||||
rings
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(ring_index, ring)| {
|
||||
let (points, curves) = refined_boundary_ring(ring, sources, tolerance);
|
||||
let count = points.len();
|
||||
if count < 3 {
|
||||
return None;
|
||||
}
|
||||
let handles = ring_source_handles(ring, sources);
|
||||
let mut bits = 0;
|
||||
if exterior.get(ring_index).copied().unwrap_or(ring_index == 0) {
|
||||
bits |= BoundaryPathFlags::OUTERMOST.bits();
|
||||
}
|
||||
if !handles.is_empty() {
|
||||
bits |= BoundaryPathFlags::EXTERNAL.bits();
|
||||
}
|
||||
let mut path = BoundaryPath::with_flags(BoundaryPathFlags::from_bits(bits));
|
||||
|
||||
let all_same = curves.first().is_some_and(|first| {
|
||||
first.is_some() && curves.iter().all(|curve| curve == first)
|
||||
});
|
||||
if all_same {
|
||||
let curve = curves[0].as_ref();
|
||||
path.add_edge(exact_boundary_edge(
|
||||
curve,
|
||||
points[0],
|
||||
points[0],
|
||||
points[1],
|
||||
true,
|
||||
));
|
||||
} else {
|
||||
let start_index = (0..count)
|
||||
.find(|index| curves[*index] != curves[(*index + count - 1) % count])
|
||||
.unwrap_or(0);
|
||||
let mut consumed = 0usize;
|
||||
while consumed < count {
|
||||
let edge_index = (start_index + consumed) % count;
|
||||
let curve = curves.get(edge_index).and_then(Option::as_ref);
|
||||
let mut length = 1usize;
|
||||
if curve.is_some() {
|
||||
while consumed + length < count
|
||||
&& curves[(edge_index + length) % count].as_ref() == curve
|
||||
{
|
||||
length += 1;
|
||||
}
|
||||
}
|
||||
let end_index = (edge_index + length) % count;
|
||||
path.add_edge(exact_boundary_edge(
|
||||
curve,
|
||||
points[edge_index],
|
||||
points[end_index],
|
||||
points[(edge_index + 1) % count],
|
||||
false,
|
||||
));
|
||||
consumed += length;
|
||||
}
|
||||
}
|
||||
for handle in handles {
|
||||
path.add_boundary_handle(handle);
|
||||
}
|
||||
Some(path)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn boundary_entities(
|
||||
rings: &[Vec<[f64; 2]>],
|
||||
plane: WorkingPlane,
|
||||
) -> Vec<acadrust::EntityType> {
|
||||
rings
|
||||
.iter()
|
||||
.filter_map(|ring| {
|
||||
@@ -234,7 +409,7 @@ pub(crate) fn boundary_entities(rings: &[Vec<[f64; 2]>]) -> Vec<acadrust::Entity
|
||||
acadrust::entities::LwVertex::new(acadrust::types::Vector2::new(x, y))
|
||||
})
|
||||
.collect();
|
||||
Some(acadrust::EntityType::LwPolyline(polyline))
|
||||
Some(plane.place_entity(acadrust::EntityType::LwPolyline(polyline)))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -295,7 +470,7 @@ fn nearest_crossing(a: &Curve, b: &Curve, point: [f64; 2], tolerance: f64) -> Op
|
||||
}
|
||||
|
||||
fn project_to_curve(curve: &Curve, point: [f64; 2]) -> [f64; 2] {
|
||||
curve.point_at(curve.parameter_at(point))
|
||||
closest_point(curve, point).point
|
||||
}
|
||||
|
||||
fn refined_boundary_ring(
|
||||
@@ -408,12 +583,61 @@ pub(crate) fn boundary_polyline_entities(
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn boundary_entities_from_sources(
|
||||
rings: &[Vec<[f64; 2]>],
|
||||
plane: WorkingPlane,
|
||||
sources: &rustc_hash::FxHashMap<Handle, BoundarySource>,
|
||||
tolerance: f64,
|
||||
) -> Vec<EntityType> {
|
||||
rings
|
||||
.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)
|
||||
pub(crate) fn edit_hatch_boundary_handles(
|
||||
&mut self,
|
||||
hatch_handle: Handle,
|
||||
handles: &[Handle],
|
||||
add: bool,
|
||||
) -> bool {
|
||||
let Some(EntityType::Hatch(hatch)) = self.document.get_entity_mut(hatch_handle) else {
|
||||
return false;
|
||||
};
|
||||
let Some(path) = hatch.paths.first_mut() else {
|
||||
return false;
|
||||
};
|
||||
if add {
|
||||
for handle in handles.iter().copied().filter(|handle| handle.is_valid()) {
|
||||
if !path.boundary_handles.contains(&handle) {
|
||||
path.boundary_handles.push(handle);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
path.boundary_handles.retain(|handle| !handles.contains(handle));
|
||||
}
|
||||
if path.boundary_handles.is_empty() {
|
||||
path.flags.set_external(false);
|
||||
} else {
|
||||
path.flags.set_external(true);
|
||||
}
|
||||
hatch.is_associative = hatch
|
||||
.paths
|
||||
.iter()
|
||||
.flat_map(wire_segments)
|
||||
.collect()
|
||||
.any(|candidate| !candidate.boundary_handles.is_empty());
|
||||
self.associative_hatch_source_cache.borrow_mut().take();
|
||||
if add && !handles.is_empty() {
|
||||
let changes: Vec<_> = handles
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|handle| (handle, ChangeKind::Modified))
|
||||
.collect();
|
||||
self.refresh_associative_hatches(&changes);
|
||||
} else {
|
||||
self.refresh_fill_model(hatch_handle);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn associative_hatch_dependents(
|
||||
@@ -470,15 +694,7 @@ impl Scene {
|
||||
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();
|
||||
let seeds = hatch.paths.iter().map(hatch_path_seed).collect::<Vec<_>>();
|
||||
(handle, hatch.clone(), seeds)
|
||||
})
|
||||
})
|
||||
@@ -486,10 +702,13 @@ impl Scene {
|
||||
|
||||
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 storage = crate::entities::curve::ocs_plane(hatch.normal, hatch.elevation);
|
||||
let plane = WorkingPlane::new(
|
||||
glam::DVec3::from_array(storage.origin),
|
||||
glam::DVec3::from_array(storage.x_axis),
|
||||
glam::DVec3::from_array(storage.y_axis),
|
||||
);
|
||||
let all_sources = self.boundary_sources_on_plane(plane, WELD_TOLERANCE);
|
||||
let mut modified = false;
|
||||
let mut association_changed = false;
|
||||
for (index, path) in hatch.paths.iter_mut().enumerate() {
|
||||
@@ -503,21 +722,41 @@ impl Scene {
|
||||
let old_count = path.boundary_handles.len();
|
||||
path.boundary_handles
|
||||
.retain(|source| self.document.get_entity(*source).is_some());
|
||||
if path.boundary_handles.is_empty() {
|
||||
path.flags.set_external(false);
|
||||
}
|
||||
association_changed |= path.boundary_handles.len() != old_count;
|
||||
modified |= association_changed;
|
||||
let segments = self.associative_boundary_segments(&path.boundary_handles);
|
||||
let sources: rustc_hash::FxHashMap<_, _> = path
|
||||
.boundary_handles
|
||||
.iter()
|
||||
.filter_map(|source| {
|
||||
all_sources
|
||||
.get(source)
|
||||
.cloned()
|
||||
.map(|geometry| (*source, geometry))
|
||||
})
|
||||
.collect();
|
||||
let segments: Vec<_> = sources
|
||||
.values()
|
||||
.flat_map(|source| source.segments.iter().copied())
|
||||
.collect();
|
||||
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,
|
||||
),
|
||||
)];
|
||||
let exterior = [path.flags.is_outermost()];
|
||||
if let Some(exact) = exact_hatch_paths(
|
||||
std::slice::from_ref(face),
|
||||
&exterior,
|
||||
&sources,
|
||||
WELD_TOLERANCE,
|
||||
)
|
||||
.into_iter()
|
||||
.next()
|
||||
{
|
||||
path.edges = exact.edges;
|
||||
}
|
||||
modified = true;
|
||||
}
|
||||
hatch.is_associative = hatch
|
||||
@@ -543,48 +782,6 @@ impl Scene {
|
||||
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
|
||||
}
|
||||
|
||||
/// Boundary candidates in the active working plane, with exact curves
|
||||
/// where the source entity exposes them.
|
||||
pub fn boundary_sources_on_plane(
|
||||
@@ -636,6 +833,82 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
|
||||
fn hatch_path_geometry(
|
||||
path: &acadrust::entities::BoundaryPath,
|
||||
) -> (Vec<[f64; 2]>, Vec<f64>) {
|
||||
let edges: Vec<_> = path
|
||||
.edges
|
||||
.iter()
|
||||
.filter_map(crate::entities::hatch::edge_curve)
|
||||
.map(|curve| curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE))
|
||||
.collect();
|
||||
super::entity::chain_path_edges_with_directions(edges)
|
||||
}
|
||||
|
||||
pub(crate) fn hatch_path_ring(path: &acadrust::entities::BoundaryPath) -> Option<Vec<[f64; 2]>> {
|
||||
let (ring, _) = hatch_path_geometry(path);
|
||||
(ring.len() >= 3).then_some(ring)
|
||||
}
|
||||
|
||||
pub(crate) fn hatch_path_directions(path: &acadrust::entities::BoundaryPath) -> Vec<f64> {
|
||||
hatch_path_geometry(path).1
|
||||
}
|
||||
|
||||
pub(crate) fn hatch_boundary_rings(hatch: &acadrust::entities::Hatch) -> Vec<Vec<[f64; 2]>> {
|
||||
hatch.paths.iter().filter_map(hatch_path_ring).collect()
|
||||
}
|
||||
|
||||
pub(crate) fn separated_hatch_path_groups(
|
||||
hatch: &acadrust::entities::Hatch,
|
||||
) -> Vec<Vec<acadrust::entities::BoundaryPath>> {
|
||||
let items: Vec<_> = hatch
|
||||
.paths
|
||||
.iter()
|
||||
.filter_map(|path| hatch_path_ring(path).map(|ring| (path.clone(), ring)))
|
||||
.collect();
|
||||
let rings: Vec<_> = items.iter().map(|(_, ring)| ring.clone()).collect();
|
||||
let depths = ring_nesting_depths(&rings);
|
||||
let outer_indices: Vec<_> = depths
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, depth)| (*depth == 0).then_some(index))
|
||||
.collect();
|
||||
let mut groups: Vec<_> = outer_indices
|
||||
.iter()
|
||||
.map(|index| vec![items[*index].0.clone()])
|
||||
.collect();
|
||||
for (index, (path, ring)) in items.iter().enumerate() {
|
||||
if depths.get(index) == Some(&0) {
|
||||
continue;
|
||||
}
|
||||
let Some(seed) = ring.first().copied() else {
|
||||
continue;
|
||||
};
|
||||
let owner = outer_indices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, outer)| {
|
||||
contains(
|
||||
&face_curves(&rings[**outer]),
|
||||
seed,
|
||||
Tolerance::new(WELD_TOLERANCE),
|
||||
)
|
||||
})
|
||||
.min_by(|(_, left), (_, right)| {
|
||||
signed_area(&rings[**left])
|
||||
.abs()
|
||||
.total_cmp(&signed_area(&rings[**right]).abs())
|
||||
})
|
||||
.map(|(group, _)| group);
|
||||
if let Some(owner) = owner {
|
||||
groups[owner].push(path.clone());
|
||||
} else {
|
||||
groups.push(vec![path.clone()]);
|
||||
}
|
||||
}
|
||||
groups
|
||||
}
|
||||
|
||||
pub(crate) fn boundary_faces(
|
||||
sources: &rustc_hash::FxHashMap<Handle, BoundarySource>,
|
||||
tolerance: f64,
|
||||
|
||||
+122
-83
@@ -1,41 +1,33 @@
|
||||
// Auto-split from scene/mod.rs. Pure text-move; behaviour unchanged.
|
||||
use super::*;
|
||||
|
||||
/// Convert a HATCH's own resolved pattern line (world-unit `offset` = step to
|
||||
/// the next parallel line, plus a base angle) into a render `PatFamily` whose
|
||||
/// geometry is already final — the HatchModel that carries it uses scale 1 and
|
||||
/// angle_offset 0 (see `prebaked` in `hatch_model_from_dxf`). The world-space
|
||||
/// offset is rotated into the line's local frame so `pattern_segments` and the
|
||||
/// GPU shader, which rotate `(dx, dy)` back out by the family angle, reproduce
|
||||
/// the exact stored step. `x0/y0` are filled in by the caller (from the stored
|
||||
/// `base_point`, relative to `world_origin`) once the boundary anchor is known;
|
||||
/// they set the pattern origin, observable for dashed / offset patterns.
|
||||
/// Order a hatch boundary path's sampled edges into one tip-to-tail loop.
|
||||
///
|
||||
/// Real files do not store boundary edges as a sequential walk: associative
|
||||
/// hatches list them in boundary-source-entity order, with arbitrary
|
||||
/// direction — the next edge in the list may attach to either end of the
|
||||
/// chain built so far, or belong to the far side of the loop entirely.
|
||||
/// Concatenating them verbatim draws a self-crossing "bowtie" outline and
|
||||
/// flips the even-odd fill over the wrong region.
|
||||
///
|
||||
/// Greedy nearest-endpoint assembly: keep the chain open at both ends and, at
|
||||
/// each step, attach the unused edge whose endpoint lies closest to either
|
||||
/// end (reversing / prepending as needed). Distance comparison, no tolerance:
|
||||
/// a correctly-ordered file matches at distance 0 and reproduces exactly.
|
||||
fn chain_path_edges(mut polys: Vec<Vec<[f64; 2]>>) -> Vec<[f64; 2]> {
|
||||
/// Order sampled boundary edges into one tip-to-tail loop.
|
||||
pub(super) fn chain_path_edges(polys: Vec<Vec<[f64; 2]>>) -> Vec<[f64; 2]> {
|
||||
chain_path_edges_with_directions(polys).0
|
||||
}
|
||||
|
||||
pub(super) fn chain_path_edges_with_directions(
|
||||
polys: Vec<Vec<[f64; 2]>>,
|
||||
) -> (Vec<[f64; 2]>, Vec<f64>) {
|
||||
let d2 = |a: [f64; 2], b: [f64; 2]| (a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2);
|
||||
polys.retain(|p| !p.is_empty());
|
||||
let mut directions = vec![0.0; polys.len()];
|
||||
let mut polys: Vec<_> = polys
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter(|(_, points)| !points.is_empty())
|
||||
.collect();
|
||||
if polys.is_empty() {
|
||||
return Vec::new();
|
||||
return (Vec::new(), directions);
|
||||
}
|
||||
let mut chain: std::collections::VecDeque<[f64; 2]> = polys.swap_remove(0).into();
|
||||
let (first_index, first) = polys.swap_remove(0);
|
||||
directions[first_index] = 1.0;
|
||||
let mut chain: std::collections::VecDeque<[f64; 2]> = first.into();
|
||||
while !polys.is_empty() {
|
||||
let head = *chain.front().unwrap();
|
||||
let tail = *chain.back().unwrap();
|
||||
// (distance, index, reverse-points, attach-at-front)
|
||||
let mut best = (f64::MAX, 0usize, false, false);
|
||||
for (i, p) in polys.iter().enumerate() {
|
||||
for (i, (_, p)) in polys.iter().enumerate() {
|
||||
let s = p[0];
|
||||
let e = *p.last().unwrap();
|
||||
for c in [
|
||||
@@ -50,7 +42,8 @@ fn chain_path_edges(mut polys: Vec<Vec<[f64; 2]>>) -> Vec<[f64; 2]> {
|
||||
}
|
||||
}
|
||||
let (_, idx, rev, at_front) = best;
|
||||
let mut p = polys.swap_remove(idx);
|
||||
let (original_index, mut p) = polys.swap_remove(idx);
|
||||
directions[original_index] = if rev { -1.0 } else { 1.0 };
|
||||
if rev {
|
||||
p.reverse();
|
||||
}
|
||||
@@ -71,7 +64,7 @@ fn chain_path_edges(mut polys: Vec<Vec<[f64; 2]>>) -> Vec<[f64; 2]> {
|
||||
chain.extend(it);
|
||||
}
|
||||
}
|
||||
chain.into()
|
||||
(chain.into(), directions)
|
||||
}
|
||||
|
||||
fn family_from_stored_line(
|
||||
@@ -1533,6 +1526,8 @@ impl Scene {
|
||||
fill_plane_boundary,
|
||||
boundary_exterior: None,
|
||||
boundary_sources: None,
|
||||
boundary_paths: None,
|
||||
style: acadrust::entities::HatchStyleType::Normal,
|
||||
pattern: model::hatch_model::HatchPattern::Solid,
|
||||
name: "WIPEOUT_FILL".into(),
|
||||
color,
|
||||
@@ -1684,6 +1679,7 @@ impl Scene {
|
||||
}
|
||||
|
||||
let mut rings = Vec::new();
|
||||
let mut local_rings = Vec::new();
|
||||
let mut ring_sources = Vec::new();
|
||||
|
||||
for path in &dxf.paths {
|
||||
@@ -1698,26 +1694,27 @@ impl Scene {
|
||||
for edge in &path.edges {
|
||||
if let Some(curve) = crate::entities::hatch::edge_curve(edge) {
|
||||
edge_polys.push(
|
||||
curve
|
||||
.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE)
|
||||
.into_iter()
|
||||
.map(|point| to_xy(point[0], point[1]))
|
||||
.collect(),
|
||||
curve.tessellate_angle(cadkernel::tessellation::DEFAULT_ANGLE),
|
||||
);
|
||||
}
|
||||
}
|
||||
let mut ring = chain_path_edges(edge_polys);
|
||||
if ring.is_empty() {
|
||||
let mut local_ring = chain_path_edges(edge_polys);
|
||||
if local_ring.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if ring.len() >= 3 {
|
||||
let first = ring[0];
|
||||
let last = *ring.last().unwrap();
|
||||
if local_ring.len() >= 3 {
|
||||
let first = local_ring[0];
|
||||
let last = *local_ring.last().unwrap();
|
||||
if (first[0] - last[0]).abs() > 1e-5 || (first[1] - last[1]).abs() > 1e-5 {
|
||||
ring.push(first);
|
||||
local_ring.push(first);
|
||||
}
|
||||
}
|
||||
let ring = local_ring
|
||||
.iter()
|
||||
.map(|point| to_xy(point[0], point[1]))
|
||||
.collect();
|
||||
rings.push(ring);
|
||||
local_rings.push(local_ring);
|
||||
ring_sources.push(path.boundary_handles.clone());
|
||||
}
|
||||
|
||||
@@ -1727,9 +1724,15 @@ impl Scene {
|
||||
|
||||
let depths = cadkernel::geom2d::ring_nesting_depths(&rings);
|
||||
let mut boundary = Vec::new();
|
||||
let mut local_boundary = Vec::new();
|
||||
let mut boundary_exterior = Vec::new();
|
||||
let mut boundary_sources = Vec::new();
|
||||
for ((ring, sources), depth) in rings.into_iter().zip(ring_sources).zip(depths) {
|
||||
for (((ring, local_ring), sources), depth) in rings
|
||||
.into_iter()
|
||||
.zip(local_rings)
|
||||
.zip(ring_sources)
|
||||
.zip(depths)
|
||||
{
|
||||
let keep = match dxf.style {
|
||||
acadrust::entities::HatchStyleType::Normal => true,
|
||||
acadrust::entities::HatchStyleType::Outer => depth <= 1,
|
||||
@@ -1740,8 +1743,14 @@ impl Scene {
|
||||
}
|
||||
if !boundary.is_empty() {
|
||||
boundary.push([f64::NAN, f64::NAN]);
|
||||
local_boundary.push([f32::NAN, f32::NAN]);
|
||||
}
|
||||
boundary.extend(ring);
|
||||
local_boundary.extend(
|
||||
local_ring
|
||||
.into_iter()
|
||||
.map(|[x, y]| [x as f32, y as f32]),
|
||||
);
|
||||
boundary_exterior.push(depth == 0);
|
||||
boundary_sources.push(sources);
|
||||
}
|
||||
@@ -1941,14 +1950,21 @@ impl Scene {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let storage = crate::entities::curve::ocs_plane(dxf.normal, dxf.elevation);
|
||||
Some(HatchModel {
|
||||
render_instance: None,
|
||||
boundary: std::sync::Arc::new(boundary_f32),
|
||||
boundary_wcs: None,
|
||||
fill_plane: None,
|
||||
fill_plane_boundary: None,
|
||||
fill_plane: Some(model::hatch_model::FillPlane {
|
||||
origin: storage.origin,
|
||||
x_axis: storage.x_axis,
|
||||
y_axis: storage.y_axis,
|
||||
}),
|
||||
fill_plane_boundary: Some(std::sync::Arc::new(local_boundary)),
|
||||
boundary_exterior: Some(std::sync::Arc::new(boundary_exterior)),
|
||||
boundary_sources: Some(std::sync::Arc::new(boundary_sources)),
|
||||
boundary_paths: Some(std::sync::Arc::new(dxf.paths.clone())),
|
||||
style: dxf.style,
|
||||
pattern,
|
||||
name,
|
||||
// A gradient starts from its first stop; other fills use the
|
||||
@@ -2267,6 +2283,8 @@ impl Scene {
|
||||
fill_plane_boundary: None,
|
||||
boundary_exterior: None,
|
||||
boundary_sources: None,
|
||||
boundary_paths: None,
|
||||
style: acadrust::entities::HatchStyleType::Normal,
|
||||
pattern: model::hatch_model::HatchPattern::Solid,
|
||||
name: "SOLID".into(),
|
||||
color,
|
||||
@@ -2286,14 +2304,24 @@ impl Scene {
|
||||
entity_style: Option<(acadrust::types::Color, acadrust::types::Transparency)>,
|
||||
) -> Handle {
|
||||
let mut dxf = DxfHatch::new();
|
||||
dxf.style = model.style;
|
||||
if let Some(plane) = model.fill_plane {
|
||||
let x = glam::DVec3::from_array(plane.x_axis);
|
||||
let y = glam::DVec3::from_array(plane.y_axis);
|
||||
let normal = x.cross(y).normalize_or(glam::DVec3::Z);
|
||||
dxf.normal = acadrust::types::Vector3::new(normal.x, normal.y, normal.z);
|
||||
dxf.elevation = glam::DVec3::from_array(plane.origin).dot(normal);
|
||||
}
|
||||
dxf.is_solid = matches!(
|
||||
model.pattern,
|
||||
crate::scene::model::hatch_model::HatchPattern::Solid
|
||||
);
|
||||
// Prefer exact command geometry; otherwise reconstruct every ring from
|
||||
// the render offsets without dropping its separators.
|
||||
// Build one DXF path per NaN-separated ring and retain each outer/hole role.
|
||||
let reconstructed_wcs: Vec<[f64; 2]> = if model.boundary_wcs.is_none() {
|
||||
// Persist analytic command geometry when available.
|
||||
if let Some(paths) = model.boundary_paths.as_deref() {
|
||||
dxf.paths = paths.clone();
|
||||
} else {
|
||||
// Otherwise reconstruct every ring from render offsets.
|
||||
let reconstructed_wcs: Vec<[f64; 2]> = if model.boundary_wcs.is_none() {
|
||||
let [wx, wy] = model.world_origin;
|
||||
model
|
||||
.boundary
|
||||
@@ -2306,18 +2334,18 @@ impl Scene {
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let wcs = model
|
||||
.boundary_wcs
|
||||
.as_deref()
|
||||
.map(|points| points.as_slice())
|
||||
.unwrap_or(reconstructed_wcs.as_slice());
|
||||
let mut ring: Vec<Vector2> = Vec::new();
|
||||
let mut first = true;
|
||||
let mut ring_index = 0usize;
|
||||
let mut push_ring = |r: &mut Vec<Vector2>, is_outer: bool, index: usize| {
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let wcs = model
|
||||
.boundary_wcs
|
||||
.as_deref()
|
||||
.map(|points| points.as_slice())
|
||||
.unwrap_or(reconstructed_wcs.as_slice());
|
||||
let mut ring: Vec<Vector2> = Vec::new();
|
||||
let mut first = true;
|
||||
let mut ring_index = 0usize;
|
||||
let mut push_ring = |r: &mut Vec<Vector2>, is_outer: bool, index: usize| {
|
||||
if !r.is_empty() {
|
||||
let edge = PolylineEdge::new(std::mem::take(r), true);
|
||||
let handles: Vec<_> = model
|
||||
@@ -2345,31 +2373,32 @@ impl Scene {
|
||||
}
|
||||
dxf.paths.push(path);
|
||||
}
|
||||
};
|
||||
for &[x, y] in wcs {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
ring.push(Vector2::new(x, y));
|
||||
} else {
|
||||
let is_outer = model
|
||||
.boundary_exterior
|
||||
.as_deref()
|
||||
.and_then(|roles| roles.get(ring_index))
|
||||
.copied()
|
||||
.unwrap_or(first);
|
||||
first = false;
|
||||
if !ring.is_empty() {
|
||||
push_ring(&mut ring, is_outer, ring_index);
|
||||
ring_index += 1;
|
||||
};
|
||||
for &[x, y] in wcs {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
ring.push(Vector2::new(x, y));
|
||||
} else {
|
||||
let is_outer = model
|
||||
.boundary_exterior
|
||||
.as_deref()
|
||||
.and_then(|roles| roles.get(ring_index))
|
||||
.copied()
|
||||
.unwrap_or(first);
|
||||
first = false;
|
||||
if !ring.is_empty() {
|
||||
push_ring(&mut ring, is_outer, ring_index);
|
||||
ring_index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
let is_outer = model
|
||||
.boundary_exterior
|
||||
.as_deref()
|
||||
.and_then(|roles| roles.get(ring_index))
|
||||
.copied()
|
||||
.unwrap_or(first);
|
||||
push_ring(&mut ring, is_outer, ring_index);
|
||||
}
|
||||
let is_outer = model
|
||||
.boundary_exterior
|
||||
.as_deref()
|
||||
.and_then(|roles| roles.get(ring_index))
|
||||
.copied()
|
||||
.unwrap_or(first);
|
||||
push_ring(&mut ring, is_outer, ring_index);
|
||||
dxf.is_associative = dxf
|
||||
.paths
|
||||
.iter()
|
||||
@@ -2379,6 +2408,16 @@ impl Scene {
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let pattern_origin = model
|
||||
.fill_plane_boundary
|
||||
.as_deref()
|
||||
.and_then(|points| {
|
||||
points
|
||||
.iter()
|
||||
.find(|point| point[0].is_finite() && point[1].is_finite())
|
||||
})
|
||||
.map(|point| [point[0] as f64, point[1] as f64])
|
||||
.unwrap_or(model.world_origin);
|
||||
if let crate::scene::model::hatch_model::HatchPattern::Pattern(families) = &model.pattern {
|
||||
let mut pattern = acadrust::entities::HatchPattern::new(&model.name);
|
||||
let rotation = model.angle_offset as f64;
|
||||
@@ -2396,10 +2435,10 @@ impl Scene {
|
||||
pattern.lines.push(acadrust::entities::HatchPatternLine {
|
||||
angle,
|
||||
base_point: Vector2::new(
|
||||
model.world_origin[0]
|
||||
pattern_origin[0]
|
||||
+ base_x * rotation_cos
|
||||
- base_y * rotation_sin,
|
||||
model.world_origin[1]
|
||||
pattern_origin[1]
|
||||
+ base_x * rotation_sin
|
||||
+ base_y * rotation_cos,
|
||||
),
|
||||
|
||||
+5
-2
@@ -37,8 +37,9 @@ mod scene_markers;
|
||||
mod selection;
|
||||
|
||||
pub(crate) use boundary::{
|
||||
boundary_entities, boundary_faces, boundary_polyline_entities, ring_source_handles,
|
||||
BoundarySource,
|
||||
boundary_entities, boundary_entities_from_sources, boundary_faces,
|
||||
boundary_polyline_entities, exact_hatch_paths, hatch_boundary_rings, hatch_path_directions,
|
||||
hatch_path_ring, ring_source_handles, separated_hatch_path_groups, BoundarySource,
|
||||
};
|
||||
|
||||
// Parallel tessellation free functions live in `convert::tess` (alongside the
|
||||
@@ -3227,6 +3228,8 @@ impl Scene {
|
||||
fill_plane_boundary: None,
|
||||
boundary_exterior: None,
|
||||
boundary_sources: None,
|
||||
boundary_paths: None,
|
||||
style: acadrust::entities::HatchStyleType::Normal,
|
||||
pattern: crate::scene::model::hatch_model::HatchPattern::Solid,
|
||||
name: "SOLID".to_string(),
|
||||
color: self.paper_bg_color,
|
||||
|
||||
@@ -203,13 +203,17 @@ pub struct HatchModel {
|
||||
/// rebuilt from a DXF entity — `add_hatch` then reconstructs the persisted
|
||||
/// vertices from `boundary` + `world_origin` instead.
|
||||
pub boundary_wcs: Option<Arc<Vec<[f64; 2]>>>,
|
||||
/// Optional 3-D placement used by wipeout fills.
|
||||
/// Optional 3-D placement for planar fills.
|
||||
pub fill_plane: Option<FillPlane>,
|
||||
pub fill_plane_boundary: Option<Arc<Vec<[f32; 2]>>>,
|
||||
/// Per-ring DXF role, aligned with the NaN-separated boundary paths.
|
||||
pub boundary_exterior: Option<Arc<Vec<bool>>>,
|
||||
/// Source entity handles for each boundary ring.
|
||||
pub boundary_sources: Option<Arc<Vec<Vec<acadrust::Handle>>>>,
|
||||
/// Exact boundary paths retained for persistence and editing.
|
||||
pub boundary_paths: Option<Arc<Vec<acadrust::entities::BoundaryPath>>>,
|
||||
/// Island handling used by the persisted hatch entity.
|
||||
pub style: acadrust::entities::HatchStyleType,
|
||||
/// Fill pattern.
|
||||
pub pattern: HatchPattern,
|
||||
/// Catalog name for this pattern (e.g. "ANSI31", "SOLID", "LINEAR").
|
||||
|
||||
@@ -162,6 +162,8 @@ impl Scene {
|
||||
fill_plane_boundary: None,
|
||||
boundary_exterior: None,
|
||||
boundary_sources: None,
|
||||
boundary_paths: None,
|
||||
style: acadrust::entities::HatchStyleType::Normal,
|
||||
pattern: HatchPattern::Solid,
|
||||
name: "AREA_PREVIEW".into(),
|
||||
color: [0.0; 4],
|
||||
|
||||
Reference in new issue
Block a user