feat(boundary): complete region creation workflow
This commit is contained in:
parent
dcedf945ea
commit
4923e4c9da
4 changed files with 700 additions and 19 deletions
|
|
@ -676,8 +676,21 @@ impl OpenCADStudio {
|
||||||
|
|
||||||
"BOUNDARY" => {
|
"BOUNDARY" => {
|
||||||
use crate::modules::draw::draw::hatch::BoundaryCommand;
|
use crate::modules::draw::draw::hatch::BoundaryCommand;
|
||||||
let outlines = self.tabs[i].scene.closed_outlines();
|
let plane = if self.tabs[i].editing_model_space() {
|
||||||
let new_cmd = BoundaryCommand::new(outlines);
|
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.command_line.push_info(&new_cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
// Click a point INSIDE a closed region → boundary auto-detected.
|
// Click a point INSIDE a closed region → boundary auto-detected.
|
||||||
// Type "S" to switch to manual vertex-picking mode (HATCH/GRADIENT only).
|
// 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::modules::IconKind;
|
||||||
use crate::scene::model::hatch_model::{HatchModel, HatchPattern, PatFamily};
|
use crate::scene::model::hatch_model::{HatchModel, HatchPattern, PatFamily};
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
|
|
@ -850,20 +850,195 @@ impl CadCommand for GradientCommand {
|
||||||
// ── BOUNDARY command ───────────────────────────────────────────────────────
|
// ── BOUNDARY command ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
pub struct BoundaryCommand {
|
pub struct BoundaryCommand {
|
||||||
|
sources: rustc_hash::FxHashMap<Handle, crate::scene::BoundarySource>,
|
||||||
outlines: Vec<Vec<[f64; 2]>>,
|
outlines: Vec<Vec<[f64; 2]>>,
|
||||||
|
point_regions: Vec<Vec<Vec<[f64; 2]>>>,
|
||||||
|
object_regions: Vec<Vec<Vec<[f64; 2]>>>,
|
||||||
|
selected_objects: Vec<Handle>,
|
||||||
|
mode: BoundaryMode,
|
||||||
|
output: BoundaryOutput,
|
||||||
|
island_style: BoundaryIslandStyle,
|
||||||
|
gap_tolerance: f64,
|
||||||
|
plane: WorkingPlane,
|
||||||
missed: bool,
|
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 {
|
impl BoundaryCommand {
|
||||||
pub fn new(outlines: Vec<Vec<[f64; 2]>>) -> Self {
|
pub fn new(
|
||||||
Self {
|
sources: rustc_hash::FxHashMap<Handle, crate::scene::BoundarySource>,
|
||||||
|
selected_objects: Vec<Handle>,
|
||||||
|
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,
|
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,
|
missed: false,
|
||||||
|
};
|
||||||
|
if has_selection {
|
||||||
|
command.set_object_selection(selected_objects);
|
||||||
|
}
|
||||||
|
command
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_object_selection(&mut self, handles: Vec<Handle>) {
|
||||||
|
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<Vec<Vec<[f64; 2]>>> {
|
||||||
|
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<Vec<Vec<[f64; 2]>>> {
|
||||||
|
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<Vec<[f64; 2]>>) {
|
||||||
|
if !self.point_regions.iter().any(|existing| existing == ®ion) {
|
||||||
|
self.point_regions.push(region);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_entities(&self) -> Vec<acadrust::EntityType> {
|
||||||
|
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 {
|
impl CadCommand for BoundaryCommand {
|
||||||
|
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||||
|
self.plane = plane;
|
||||||
|
}
|
||||||
|
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
"BOUNDARY"
|
"BOUNDARY"
|
||||||
}
|
}
|
||||||
|
|
@ -874,29 +1049,199 @@ impl CadCommand for BoundaryCommand {
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
};
|
};
|
||||||
t!("BOUNDARY Pick internal point:%{miss}", miss = miss).into_owned()
|
match self.mode {
|
||||||
}
|
BoundaryMode::PickInside => t!(
|
||||||
|
"BOUNDARY Pick internal point (%{count} regions, %{output}, islands: %{islands}; Enter to apply):%{miss}",
|
||||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
count = self.region_count(),
|
||||||
let xy = [pt.x, pt.y];
|
output = self.output_label(),
|
||||||
match resolve_hatch_rings(&self.outlines, xy) {
|
islands = self.island_label(),
|
||||||
Some(rings) => {
|
miss = miss
|
||||||
self.missed = false;
|
)
|
||||||
CmdResult::CommitEntitiesAndExit(crate::scene::boundary_entities(&rings))
|
.into_owned(),
|
||||||
}
|
BoundaryMode::SelectObjects => t!(
|
||||||
None => {
|
"BOUNDARY Select boundary objects (%{objects} objects, %{count} regions; Enter to apply):%{miss}",
|
||||||
self.missed = true;
|
objects = self.selected_objects.len(),
|
||||||
CmdResult::NeedPoint
|
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 {
|
fn options(&self) -> Vec<crate::command::CmdOption> {
|
||||||
CmdResult::Cancel
|
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 {
|
fn on_escape(&mut self) -> CmdResult {
|
||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn wants_text_input(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
|
if let BoundaryMode::GapTolerance { return_to_selection } = self.mode {
|
||||||
|
if let Ok(value) = text.trim().parse::<f64>() {
|
||||||
|
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<Handle>) -> CmdResult {
|
||||||
|
if matches!(self.mode, BoundaryMode::SelectObjects) {
|
||||||
|
self.set_object_selection(handles);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
} else {
|
||||||
|
CmdResult::Cancel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_undo_step(&mut self) -> Option<CmdResult> {
|
||||||
|
self.point_regions.pop().map(|_| CmdResult::NeedPoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||||
|
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,
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use cadkernel::geom2d::{
|
use cadkernel::geom2d::{
|
||||||
bounded_faces, contains, distance_to, segment_crossing, triangulate, Curve, Line,
|
bounded_faces, contains, distance_to, intersect, segment_crossing, triangulate, Curve, Line,
|
||||||
SegmentCrossing, Tolerance,
|
SegmentCrossing, Tolerance, Transform as CurveTransform,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::command::WorkingPlane;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct BoundarySource {
|
||||||
|
pub segments: Vec<Line>,
|
||||||
|
pub curve: Option<Curve>,
|
||||||
|
}
|
||||||
|
|
||||||
/// How far apart two points may be and still be taken for the same one.
|
/// 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
|
/// The boundary search runs on already-tessellated wire geometry, so the
|
||||||
|
|
@ -38,6 +46,63 @@ fn wire_segments(wire: &WireModel) -> Vec<Line> {
|
||||||
segments
|
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_curve_on_plane(
|
||||||
|
entity: &EntityType,
|
||||||
|
plane: WorkingPlane,
|
||||||
|
tolerance: f64,
|
||||||
|
) -> Option<Curve> {
|
||||||
|
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]> {
|
fn ring_seed(model: &HatchModel, wanted: usize) -> Option<[f64; 2]> {
|
||||||
let mut ring = Vec::new();
|
let mut ring = Vec::new();
|
||||||
let mut index = 0usize;
|
let mut index = 0usize;
|
||||||
|
|
@ -164,6 +229,213 @@ pub(crate) fn boundary_entities(rings: &[Vec<[f64; 2]>]) -> Vec<acadrust::Entity
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn edge_source(
|
||||||
|
edge: Line,
|
||||||
|
sources: &rustc_hash::FxHashMap<Handle, BoundarySource>,
|
||||||
|
tolerance: f64,
|
||||||
|
) -> Option<Handle> {
|
||||||
|
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<Handle, BoundarySource>,
|
||||||
|
tolerance: f64,
|
||||||
|
) -> (Vec<[f64; 2]>, Vec<Option<Handle>>) {
|
||||||
|
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<Handle, BoundarySource>,
|
||||||
|
tolerance: f64,
|
||||||
|
) -> Option<EntityType> {
|
||||||
|
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<Vec<[f64; 2]>>],
|
||||||
|
plane: WorkingPlane,
|
||||||
|
sources: &rustc_hash::FxHashMap<Handle, BoundarySource>,
|
||||||
|
tolerance: f64,
|
||||||
|
) -> Vec<EntityType> {
|
||||||
|
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<Vec<[f64; 2]>>],
|
||||||
|
plane: WorkingPlane,
|
||||||
|
sources: &rustc_hash::FxHashMap<Handle, BoundarySource>,
|
||||||
|
tolerance: f64,
|
||||||
|
) -> Vec<EntityType> {
|
||||||
|
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 {
|
impl Scene {
|
||||||
fn associative_boundary_segments(&self, handles: &[Handle]) -> Vec<Line> {
|
fn associative_boundary_segments(&self, handles: &[Handle]) -> Vec<Line> {
|
||||||
self.wire_models_for(handles)
|
self.wire_models_for(handles)
|
||||||
|
|
@ -340,4 +612,52 @@ impl Scene {
|
||||||
}
|
}
|
||||||
sources
|
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<Handle, BoundarySource> {
|
||||||
|
let mut sources: rustc_hash::FxHashMap<Handle, BoundarySource> =
|
||||||
|
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<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))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,10 @@ mod project;
|
||||||
mod scene_markers;
|
mod scene_markers;
|
||||||
mod selection;
|
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
|
// Parallel tessellation free functions live in `convert::tess` (alongside the
|
||||||
// other tessellation code); re-exported here so this root and sibling topic
|
// other tessellation code); re-exported here so this root and sibling topic
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue