From 4923e4c9da95813bbb6ed364343feaf346aa071c Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:14:48 +0300 Subject: [PATCH] feat(boundary): complete region creation workflow --- src/app/commands/draw.rs | 17 +- src/modules/draw/draw/hatch.rs | 383 +++++++++++++++++++++++++++++++-- src/scene/boundary.rs | 324 +++++++++++++++++++++++++++- src/scene/mod.rs | 5 +- 4 files changed, 705 insertions(+), 24 deletions(-) diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index c26073815..245dd26e0 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -676,8 +676,21 @@ impl OpenCADStudio { "BOUNDARY" => { use crate::modules::draw::draw::hatch::BoundaryCommand; - let outlines = self.tabs[i].scene.closed_outlines(); - let new_cmd = BoundaryCommand::new(outlines); + let plane = if self.tabs[i].editing_model_space() { + self.tabs[i].ucs_xform().working_plane() + } else { + crate::command::WorkingPlane::default() + }; + let sources = self.tabs[i] + .scene + .boundary_sources_on_plane(plane, 1.0e-6); + let selected = self.tabs[i] + .scene + .selected_entities() + .iter() + .map(|(handle, _)| *handle) + .collect(); + let new_cmd = BoundaryCommand::new(sources, selected, plane); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } diff --git a/src/modules/draw/draw/hatch.rs b/src/modules/draw/draw/hatch.rs index 614fee11b..61fe02eac 100644 --- a/src/modules/draw/draw/hatch.rs +++ b/src/modules/draw/draw/hatch.rs @@ -9,7 +9,7 @@ // Click a point INSIDE a closed region → boundary auto-detected. // Type "S" to switch to manual vertex-picking mode (HATCH/GRADIENT only). -use crate::command::{CadCommand, CmdResult}; +use crate::command::{CadCommand, CmdResult, WorkingPlane}; use crate::modules::IconKind; use crate::scene::model::hatch_model::{HatchModel, HatchPattern, PatFamily}; use crate::scene::model::wire_model::WireModel; @@ -850,20 +850,195 @@ impl CadCommand for GradientCommand { // ── BOUNDARY command ─────────────────────────────────────────────────────── pub struct BoundaryCommand { + sources: rustc_hash::FxHashMap, outlines: Vec>, + point_regions: Vec>>, + object_regions: Vec>>, + selected_objects: Vec, + mode: BoundaryMode, + output: BoundaryOutput, + island_style: BoundaryIslandStyle, + gap_tolerance: f64, + plane: WorkingPlane, missed: bool, } +#[derive(Clone, Copy, PartialEq, Eq)] +enum BoundaryMode { + PickInside, + SelectObjects, + GapTolerance { return_to_selection: bool }, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum BoundaryOutput { + Polyline, + Region, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum BoundaryIslandStyle { + Normal, + Outer, + Ignore, +} + impl BoundaryCommand { - pub fn new(outlines: Vec>) -> Self { - Self { + pub fn new( + sources: rustc_hash::FxHashMap, + selected_objects: Vec, + plane: WorkingPlane, + ) -> Self { + let selected_objects: Vec<_> = selected_objects + .into_iter() + .filter(|handle| sources.contains_key(handle)) + .collect(); + let has_selection = !selected_objects.is_empty(); + let outlines = crate::scene::boundary_faces(&sources, 1.0e-6); + let mut command = Self { + sources, outlines, + point_regions: Vec::new(), + object_regions: Vec::new(), + selected_objects: Vec::new(), + mode: if has_selection { + BoundaryMode::SelectObjects + } else { + BoundaryMode::PickInside + }, + output: BoundaryOutput::Polyline, + island_style: BoundaryIslandStyle::Normal, + gap_tolerance: 1.0e-6, + plane, missed: false, + }; + if has_selection { + command.set_object_selection(selected_objects); + } + command + } + + fn set_object_selection(&mut self, handles: Vec) { + let selected_sources: rustc_hash::FxHashMap<_, _> = handles + .iter() + .filter_map(|handle| self.sources.get(handle).cloned().map(|source| (*handle, source))) + .collect(); + self.object_regions = crate::scene::boundary_faces( + &selected_sources, + self.gap_tolerance, + ) + .into_iter() + .map(|ring| vec![ring]) + .collect(); + self.missed = !handles.is_empty() && self.object_regions.is_empty(); + self.selected_objects = handles; + } + + fn rebuild_faces(&mut self) { + self.outlines = crate::scene::boundary_faces(&self.sources, self.gap_tolerance); + if !self.selected_objects.is_empty() { + self.set_object_selection(self.selected_objects.clone()); + } + self.point_regions.clear(); + } + + fn region_count(&self) -> usize { + self.point_regions.len() + self.object_regions.len() + } + + fn regions(&self) -> Vec>> { + let mut regions = Vec::new(); + for region in self.point_regions.iter().chain(self.object_regions.iter()) { + if !regions.iter().any(|existing| existing == region) { + regions.push(region.clone()); + } + } + regions + } + + fn output_label(&self) -> &'static str { + match self.output { + BoundaryOutput::Polyline => "Polyline", + BoundaryOutput::Region => "Region", + } + } + + fn island_label(&self) -> &'static str { + match self.island_style { + BoundaryIslandStyle::Normal => "Normal", + BoundaryIslandStyle::Outer => "Outer", + BoundaryIslandStyle::Ignore => "Ignore", + } + } + + fn picked_region(&self, point: [f64; 2]) -> Option>> { + let mut containing: Vec<(usize, f64)> = self + .outlines + .iter() + .enumerate() + .filter(|(_, outline)| point_in_polygon(point, outline)) + .map(|(index, outline)| (index, polygon_area(outline))) + .collect(); + containing.sort_by(|left, right| left.1.total_cmp(&right.1)); + let outer_index = containing.first()?.0; + let outer = &self.outlines[outer_index]; + let mut rings = vec![outer.clone()]; + if self.island_style == BoundaryIslandStyle::Ignore { + return Some(rings); + } + for (index, candidate) in self.outlines.iter().enumerate() { + if index == outer_index + || !polygon_contains_polygon(outer, candidate) + || point_in_polygon(point, candidate) + { + continue; + } + if self.island_style == BoundaryIslandStyle::Outer { + let has_intermediate = self.outlines.iter().enumerate().any(|(middle, other)| { + middle != index + && middle != outer_index + && polygon_contains_polygon(outer, other) + && polygon_contains_polygon(other, candidate) + }); + if has_intermediate { + continue; + } + } + rings.push(candidate.clone()); + } + Some(rings) + } + + fn add_point_region(&mut self, region: Vec>) { + if !self.point_regions.iter().any(|existing| existing == ®ion) { + self.point_regions.push(region); + } + } + + fn make_entities(&self) -> Vec { + let regions = self.regions(); + match self.output { + BoundaryOutput::Polyline => crate::scene::boundary_polyline_entities( + ®ions, + self.plane, + &self.sources, + self.gap_tolerance, + ), + BoundaryOutput::Region => crate::scene::boundary_region_entities( + ®ions, + self.plane, + &self.sources, + self.gap_tolerance, + ), } } } impl CadCommand for BoundaryCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn name(&self) -> &'static str { "BOUNDARY" } @@ -874,29 +1049,199 @@ impl CadCommand for BoundaryCommand { } else { String::new() }; - t!("BOUNDARY Pick internal point:%{miss}", miss = miss).into_owned() - } - - fn on_point(&mut self, pt: DVec3) -> CmdResult { - let xy = [pt.x, pt.y]; - match resolve_hatch_rings(&self.outlines, xy) { - Some(rings) => { - self.missed = false; - CmdResult::CommitEntitiesAndExit(crate::scene::boundary_entities(&rings)) - } - None => { - self.missed = true; - CmdResult::NeedPoint - } + match self.mode { + BoundaryMode::PickInside => t!( + "BOUNDARY Pick internal point (%{count} regions, %{output}, islands: %{islands}; Enter to apply):%{miss}", + count = self.region_count(), + output = self.output_label(), + islands = self.island_label(), + miss = miss + ) + .into_owned(), + BoundaryMode::SelectObjects => t!( + "BOUNDARY Select boundary objects (%{objects} objects, %{count} regions; Enter to apply):%{miss}", + objects = self.selected_objects.len(), + count = self.region_count(), + miss = miss + ) + .into_owned(), + BoundaryMode::GapTolerance { .. } => t!( + "BOUNDARY Enter gap tolerance <%{tolerance}>:", + tolerance = self.gap_tolerance + ) + .into_owned(), } } - fn on_enter(&mut self) -> CmdResult { - CmdResult::Cancel + fn options(&self) -> Vec { + use crate::command::CmdOption; + if matches!(self.mode, BoundaryMode::GapTolerance { .. }) { + return Vec::new(); + } + let mut options = vec![ + CmdOption::new( + if matches!(self.mode, BoundaryMode::SelectObjects) { + "Pick internal points" + } else { + "Select objects" + }, + if matches!(self.mode, BoundaryMode::SelectObjects) { "I" } else { "O" }, + ), + CmdOption::new( + &format!("Object type: {}", self.output_label()), + "T", + ), + CmdOption::new( + &format!("Island detection: {}", self.island_label()), + "S", + ), + CmdOption::new("Gap tolerance", "G"), + ]; + if self.region_count() > 0 { + options.push(CmdOption::enter("Accept")); + } + options } + + fn on_point(&mut self, pt: DVec3) -> CmdResult { + if matches!(self.mode, BoundaryMode::PickInside) { + let local = self.plane.to_local(pt); + match self.picked_region([local.x, local.y]) { + Some(region) => { + self.missed = false; + self.add_point_region(region); + } + None => self.missed = true, + } + } + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + if let BoundaryMode::GapTolerance { return_to_selection } = self.mode { + self.mode = if return_to_selection { + BoundaryMode::SelectObjects + } else { + BoundaryMode::PickInside + }; + return CmdResult::NeedPoint; + } + let entities = self.make_entities(); + if entities.is_empty() { + CmdResult::Cancel + } else { + CmdResult::CommitEntitiesAndExit(entities) + } + } + fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } + + fn wants_text_input(&self) -> bool { + true + } + + fn on_text_input(&mut self, text: &str) -> Option { + if let BoundaryMode::GapTolerance { return_to_selection } = self.mode { + if let Ok(value) = text.trim().parse::() { + if value.is_finite() && value > 0.0 { + self.gap_tolerance = value; + self.rebuild_faces(); + self.missed = false; + } + } + self.mode = if return_to_selection { + BoundaryMode::SelectObjects + } else { + BoundaryMode::PickInside + }; + return Some(CmdResult::NeedPoint); + } + match text.trim().to_ascii_uppercase().as_str() { + "O" | "OBJECT" | "OBJECTS" => { + self.mode = BoundaryMode::SelectObjects; + self.missed = false; + } + "I" | "INTERNAL" | "POINTS" => { + self.mode = BoundaryMode::PickInside; + self.missed = false; + } + "T" | "TYPE" => { + self.output = match self.output { + BoundaryOutput::Polyline => BoundaryOutput::Region, + BoundaryOutput::Region => BoundaryOutput::Polyline, + }; + } + "POLYLINE" => self.output = BoundaryOutput::Polyline, + "REGION" => self.output = BoundaryOutput::Region, + "S" | "ISLAND" | "ISLANDS" => { + self.island_style = match self.island_style { + BoundaryIslandStyle::Normal => BoundaryIslandStyle::Outer, + BoundaryIslandStyle::Outer => BoundaryIslandStyle::Ignore, + BoundaryIslandStyle::Ignore => BoundaryIslandStyle::Normal, + }; + self.point_regions.clear(); + } + "NORMAL" => self.island_style = BoundaryIslandStyle::Normal, + "OUTER" => self.island_style = BoundaryIslandStyle::Outer, + "IGNORE" => self.island_style = BoundaryIslandStyle::Ignore, + "G" | "GAP" | "TOLERANCE" => { + self.mode = BoundaryMode::GapTolerance { + return_to_selection: matches!(self.mode, BoundaryMode::SelectObjects), + }; + } + _ => return None, + } + Some(CmdResult::NeedPoint) + } + + fn is_selection_gathering(&self) -> bool { + matches!(self.mode, BoundaryMode::SelectObjects) + } + + fn selection_forces_add(&self) -> bool { + matches!(self.mode, BoundaryMode::SelectObjects) + } + + fn on_selection_complete(&mut self, handles: Vec) -> CmdResult { + if matches!(self.mode, BoundaryMode::SelectObjects) { + self.set_object_selection(handles); + CmdResult::NeedPoint + } else { + CmdResult::Cancel + } + } + + fn on_undo_step(&mut self) -> Option { + self.point_regions.pop().map(|_| CmdResult::NeedPoint) + } + + fn on_mouse_move(&mut self, pt: DVec3) -> Option { + if !matches!(self.mode, BoundaryMode::PickInside) { + return None; + } + let local = self.plane.to_local(pt); + let region = self.picked_region([local.x, local.y])?; + let mut points = Vec::new(); + for ring in region { + if !points.is_empty() { + points.push([f64::NAN; 3]); + } + for [x, y] in &ring { + points.push(self.plane.to_world(DVec3::new(*x, *y, 0.0)).to_array()); + } + if let Some([x, y]) = ring.first() { + points.push(self.plane.to_world(DVec3::new(*x, *y, 0.0)).to_array()); + } + } + Some(WireModel::solid_f64( + "boundary_preview".into(), + points, + WireModel::CYAN, + false, + )) + } } diff --git a/src/scene/boundary.rs b/src/scene/boundary.rs index 1e0c12ce0..2299b9de9 100644 --- a/src/scene/boundary.rs +++ b/src/scene/boundary.rs @@ -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, + pub curve: Option, +} + /// 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,63 @@ fn wire_segments(wire: &WireModel) -> Vec { segments } +fn wire_segments_on_plane( + wire: &WireModel, + plane: WorkingPlane, + tolerance: f64, +) -> Vec { + let mut segments = Vec::new(); + let mut previous: Option = 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_curve_on_plane( + entity: &EntityType, + plane: WorkingPlane, + tolerance: f64, +) -> Option { + let planar = crate::entities::curve::entity_curve(entity)?; + 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 None; + } + 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(), + }) +} + fn ring_seed(model: &HatchModel, wanted: usize) -> Option<[f64; 2]> { let mut ring = Vec::new(); let mut index = 0usize; @@ -164,6 +229,213 @@ pub(crate) fn boundary_entities(rings: &[Vec<[f64; 2]>]) -> Vec, + tolerance: f64, +) -> Option { + 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 (&handle, source) in sources { + 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(handle); + best_overlap = overlap; + } + } + } + } + if best.is_some() { + return best; + } + let midpoint = [ + (edge.start[0] + edge.end[0]) * 0.5, + (edge.start[1] + edge.end[1]) * 0.5, + ]; + sources + .iter() + .filter_map(|(&handle, source)| { + let curve = source.curve.as_ref()?; + Some((handle, distance_to(curve, midpoint))) + }) + .filter(|(_, distance)| *distance <= tolerance * 4.0) + .min_by(|left, right| left.1.total_cmp(&right.1)) + .map(|(handle, _)| handle) +} + +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, + tolerance: f64, +) -> (Vec<[f64; 2]>, Vec>) { + let count = ring.len(); + if count < 3 { + return (ring.to_vec(), Vec::new()); + } + let edge_sources: Vec<_> = (0..count) + .map(|index| { + edge_source( + 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_sources[(index + count - 1) % count] + .and_then(|handle| sources.get(&handle)?.curve.as_ref()); + let outgoing = edge_sources[index] + .and_then(|handle| sources.get(&handle)?.curve.as_ref()); + match (incoming, outgoing) { + (Some(a), Some(b)) if !std::ptr::eq(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_sources) +} + +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(); + } + if matches!(curve, Curve::Line(_)) { + return 0.0; + } + let chord = [end[0] - start[0], end[1] - start[1]]; + let squared = chord[0] * chord[0] + chord[1] * chord[1]; + if squared <= 1.0e-20 { + return 0.0; + } + let midpoint = [(start[0] + end[0]) * 0.5, (start[1] + end[1]) * 0.5]; + let on_curve = project_to_curve(curve, midpoint); + let relative = [on_curve[0] - start[0], on_curve[1] - start[1]]; + let bulge = 2.0 * (chord[0] * relative[1] - chord[1] * relative[0]) / squared; + if bulge.is_finite() { bulge } else { 0.0 } +} + +fn boundary_polyline( + ring: &[[f64; 2]], + plane: WorkingPlane, + sources: &rustc_hash::FxHashMap, + tolerance: f64, +) -> Option { + let (points, edge_sources) = 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_sources + .get(index) + .and_then(|handle| handle.and_then(|value| sources.get(&value))) + .and_then(|source| source.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>], + plane: WorkingPlane, + sources: &rustc_hash::FxHashMap, + tolerance: f64, +) -> Vec { + regions + .iter() + .flat_map(|region| region.iter()) + .filter_map(|ring| boundary_polyline(ring, plane, sources, tolerance)) + .collect() +} + +pub(crate) fn boundary_region_entities( + regions: &[Vec>], + plane: WorkingPlane, + sources: &rustc_hash::FxHashMap, + tolerance: f64, +) -> Vec { + regions + .iter() + .filter_map(|region| { + let mut wires = Vec::new(); + for ring in region { + let (points, _) = refined_boundary_ring(ring, sources, tolerance); + if points.len() < 3 { + continue; + } + let mut wire = acadrust::entities::Wire::new(); + wire.points = points + .into_iter() + .map(|[x, y]| { + let world = plane.to_world(glam::DVec3::new(x, y, 0.0)); + acadrust::types::Vector3::new(world.x, world.y, world.z) + }) + .collect(); + wires.push(wire); + } + let first = wires.first()?.points.first().copied()?; + let mut region = acadrust::entities::Region::new(); + region.point_of_reference = first; + region.wires = wires; + Some(EntityType::Region(region)) + }) + .collect() +} + impl Scene { fn associative_boundary_segments(&self, handles: &[Handle]) -> Vec { self.wire_models_for(handles) @@ -340,4 +612,52 @@ impl Scene { } sources } + + /// Boundary candidates expressed in the active working plane. Rendered + /// segments cover transformed/block geometry; direct curve entities also + /// retain their exact kernel curve so output polylines can restore arc + /// bulges instead of saving the display tessellation as straight chords. + pub fn boundary_sources_on_plane( + &self, + plane: WorkingPlane, + tolerance: f64, + ) -> rustc_hash::FxHashMap { + let mut sources: rustc_hash::FxHashMap = + rustc_hash::FxHashMap::default(); + for wire in self.entity_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(), + curve: None, + }) + .segments + .extend(segments); + } + for (&handle, source) in &mut sources { + source.curve = self + .document + .get_entity(handle) + .and_then(|entity| entity_curve_on_plane(entity, plane, tolerance)); + } + sources + } +} + +pub(crate) fn boundary_faces( + sources: &rustc_hash::FxHashMap, + tolerance: f64, +) -> Vec> { + let segments: Vec<_> = sources + .values() + .flat_map(|source| source.segments.iter().copied()) + .collect(); + bounded_faces(&segments, Tolerance::new(tolerance)) } diff --git a/src/scene/mod.rs b/src/scene/mod.rs index ad546ffb8..7ee3ff4ec 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -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, boundary_region_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