diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index d8e1a035b..10de341fe 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -849,6 +849,36 @@ impl OpenCADStudio { /// command, so `was_active` is false and their selection is preserved. pub(super) fn apply_cmd_result(&mut self, mut result: CmdResult) -> Task { let i = self.active_tab; + if let CmdResult::AddParametricConstraint { + kind, + refs, + .. + } = &mut result + { + if *kind == crate::scene::parametric_constraints::ConstraintKind::Smooth + && refs.len() == 1 + { + let source = refs[0]; + let scope = self.tabs[i].current_parametric_scope(); + let Some(resolved) = self.tabs[i] + .scene + .smooth_refs_from_connected_spline(scope, source.entity, source.marker) + else { + self.tabs[i].snap_result = None; + self.command_line + .push_error("No valid constraint point found."); + if let Some(prompt) = self.tabs[i] + .active_cmd + .as_ref() + .map(|command| command.prompt()) + { + self.command_line.push_info(&prompt); + } + return Task::none(); + }; + *refs = resolved; + } + } if let CmdResult::ReturnPoint(pt) = result { self.tabs[i].scene.clear_preview_wire(); if let Some(mut suspended) = self.tabs[i].suspended_cmd.take() { diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 2289dbf09..1d3981574 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -1213,41 +1213,32 @@ impl OpenCADStudio { let command = SmoothConstraintCommand::new(); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); - } else if handles.len() != 2 { - self.tabs[i].scene.deselect_all(); - self.command_line.push_error( - "Smooth requires an open spline first and a target curve second.", - ); - let command = SmoothConstraintCommand::new(); - self.command_line.push_info(&command.prompt()); - self.tabs[i].active_cmd = Some(Box::new(command)); } else { - let first = self.tabs[i].scene.document.get_entity(handles[0]); - let second = self.tabs[i].scene.document.get_entity(handles[1]); - let refs = first.zip(second).and_then(|(first, second)| { - SmoothConstraintCommand::preselected_refs( - (first, handles[0]), - (second, handles[1]), - ) - }); - let Some(refs) = refs else { + let scope = self.tabs[i].current_parametric_scope(); + let refs = (handles.len() == 1) + .then(|| { + self.tabs[i] + .scene + .smooth_refs_from_connected_spline(scope, handles[0], None) + }) + .flatten(); + if let Some(refs) = refs { + use crate::command::CmdResult; + use crate::scene::parametric_constraints::ConstraintKind; + return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint { + kind: ConstraintKind::Smooth, + refs, + driving_param: None, + label: "Smooth constraint", + })); + } else { self.tabs[i].scene.deselect_all(); - self.command_line.push_error( - "Smooth requires an open spline first and a line, arc, polyline segment or open spline second.", - ); + self.command_line + .push_error("No valid constraint point found."); let command = SmoothConstraintCommand::new(); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); - return None; - }; - use crate::command::CmdResult; - use crate::scene::parametric_constraints::ConstraintKind; - return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint { - kind: ConstraintKind::Smooth, - refs, - driving_param: None, - label: "Smooth constraint", - })); + } } } diff --git a/src/modules/parametric/smooth.rs b/src/modules/parametric/smooth.rs index dc81a158f..3df5b0966 100644 --- a/src/modules/parametric/smooth.rs +++ b/src/modules/parametric/smooth.rs @@ -5,21 +5,13 @@ use crate::command::{CadCommand, CmdResult}; use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef}; use crate::t; -#[derive(Clone, Copy)] -struct SmoothTarget { - endpoint: ParametricRef, - curve: ParametricRef, -} - pub struct SmoothConstraintCommand { - source: Option, picked_entity: Option, } impl SmoothConstraintCommand { pub fn new() -> Self { Self { - source: None, picked_entity: None, } } @@ -53,106 +45,8 @@ impl SmoothConstraintCommand { Some(ParametricRef::point(handle, marker)) } - fn target_reference(entity: &EntityType, handle: Handle, point: DVec3) -> Option { - match entity { - EntityType::Line(_) | EntityType::Arc(_) => { - let points = crate::scene::dimension_assoc::source_points(entity); - let endpoints = [*points.first()?, *points.last()?]; - let marker = Self::nearest_marker(&endpoints, point)?; - Some(SmoothTarget { - endpoint: ParametricRef::point(handle, marker), - curve: ParametricRef::whole(handle), - }) - } - EntityType::Spline(spline) if !spline.flags.closed && !spline.flags.periodic => { - let points = crate::scene::dimension_assoc::source_points(entity); - let endpoints = [*points.first()?, *points.last()?]; - let marker = Self::nearest_marker(&endpoints, point)?; - Some(SmoothTarget { - endpoint: ParametricRef::point(handle, marker), - curve: ParametricRef::whole(handle), - }) - } - EntityType::LwPolyline(_) | EntityType::Polyline2D(_) => { - let (source, _, _) = crate::scene::centerline::picked_source(entity, handle, point)?; - let segment = usize::try_from(source.segment_index).ok()?; - let points = crate::scene::dimension_assoc::source_points(entity); - let closed = match entity { - EntityType::LwPolyline(polyline) => polyline.is_closed, - EntityType::Polyline2D(polyline) => polyline.is_closed(), - _ => false, - }; - let end = if segment + 1 < points.len() { - segment + 1 - } else if closed { - 0 - } else { - return None; - }; - let candidates = [points.get(segment).copied()?, points.get(end).copied()?]; - let side = Self::nearest_marker(&candidates, point)? as usize; - let vertex = if side == 0 { segment } else { end }; - Some(SmoothTarget { - endpoint: ParametricRef::point(handle, vertex as i32), - curve: ParametricRef::segment(handle, segment), - }) - } - _ => None, - } - } - - pub fn preselected_refs( - first: (&EntityType, Handle), - second: (&EntityType, Handle), - ) -> Option> { - let source_points = crate::scene::dimension_assoc::source_points(first.0); - let source_ends = [*source_points.first()?, *source_points.last()?]; - let source_point = source_ends - .iter() - .copied() - .min_by(|left, right| { - let target_points = crate::scene::dimension_assoc::source_points(second.0); - let distance = |source: acadrust::types::Vector3| { - target_points - .iter() - .map(|target| (source - *target).length_squared()) - .fold(f64::INFINITY, f64::min) - }; - distance(*left).total_cmp(&distance(*right)) - })?; - let source = Self::source_reference(first.0, first.1, Self::point(source_point))?; - - let target_points = crate::scene::dimension_assoc::source_points(second.0); - let target_point = target_points - .iter() - .copied() - .min_by(|left, right| { - (Self::point(*left) - Self::point(source_point)) - .length_squared() - .total_cmp( - &(Self::point(*right) - Self::point(source_point)).length_squared(), - ) - })?; - let target = Self::target_reference(second.0, second.1, Self::point(target_point))?; - let mut refs = vec![source, target.endpoint]; - if target.curve.marker.is_some() { - refs.push(target.curve); - } - Some(refs) - } - - fn invalid_source() -> CmdResult { - CmdResult::ReportError( - t!("Invalid first selection for Smooth. Select an endpoint of an open spline.") - .into_owned(), - ) - } - - fn invalid_target() -> CmdResult { - CmdResult::ReportError( - t!("Invalid second selection for Smooth. Select an endpoint of a line, arc, polyline segment or open spline.") - .into_owned(), - ) + fn invalid_selection() -> CmdResult { + CmdResult::ReportError(t!("No valid constraint point found.").into_owned()) } } @@ -162,11 +56,7 @@ impl CadCommand for SmoothConstraintCommand { } fn prompt(&self) -> String { - if self.source.is_some() { - t!("GCSMOOTH Select second curve endpoint:").into_owned() - } else { - t!("GCSMOOTH Select first spline curve endpoint:").into_owned() - } + t!("GCSMOOTH Select first spline curve:").into_owned() } fn needs_entity_pick(&self) -> bool { @@ -192,26 +82,14 @@ impl CadCommand for SmoothConstraintCommand { let Some(entity) = self.picked_entity.take() else { return CmdResult::NeedPoint; }; - let Some(source) = self.source else { - let Some(source) = Self::source_reference(&entity, handle, point) else { - return Self::invalid_source(); - }; - self.source = Some(source); - return CmdResult::NeedPoint; + let Some(source) = Self::source_reference(&entity, handle, point) else { + return Self::invalid_selection(); }; - let Some(target) = Self::target_reference(&entity, handle, point) else { - return Self::invalid_target(); - }; - if source.entity == target.endpoint.entity { - return Self::invalid_target(); - } - let mut refs = vec![source, target.endpoint]; - if target.curve.marker.is_some() { - refs.push(target.curve); - } CmdResult::AddParametricConstraint { kind: ConstraintKind::Smooth, - refs, + // The host resolves the target from an existing Coincident + // relation. GCSMOOTH itself accepts only the spline selection. + refs: vec![source], driving_param: None, label: "Smooth constraint", } diff --git a/src/scene/parametric_constraints.rs b/src/scene/parametric_constraints.rs index 3d0473217..637d52c5b 100644 --- a/src/scene/parametric_constraints.rs +++ b/src/scene/parametric_constraints.rs @@ -1382,6 +1382,56 @@ impl super::Scene { ), }) .collect(); + if settings + .enabled + .contains(&crate::app::settings::AutoConstraintKind::Coincident) + { + for refs in self.inferred_coincident_constraints_with_tolerance( + scope, + handles, + settings.distance_tolerance, + ) { + let duplicate = mapped.iter().any(|(kind, current)| { + *kind == ConstraintKind::Coincident + && (current == &refs + || (current.len() == 2 + && refs.len() == 2 + && current[0] == refs[1] + && current[1] == refs[0])) + }); + if !duplicate { + mapped.push((ConstraintKind::Coincident, refs)); + } + } + let priority = |kind: ConstraintKind| { + let auto_kind = match kind { + ConstraintKind::Coincident => { + crate::app::settings::AutoConstraintKind::Coincident + } + ConstraintKind::Colinear => crate::app::settings::AutoConstraintKind::Collinear, + ConstraintKind::Parallel => crate::app::settings::AutoConstraintKind::Parallel, + ConstraintKind::Perpendicular => { + crate::app::settings::AutoConstraintKind::Perpendicular + } + ConstraintKind::Tangent => crate::app::settings::AutoConstraintKind::Tangent, + ConstraintKind::Concentric => { + crate::app::settings::AutoConstraintKind::Concentric + } + ConstraintKind::Horizontal => { + crate::app::settings::AutoConstraintKind::Horizontal + } + ConstraintKind::Vertical => crate::app::settings::AutoConstraintKind::Vertical, + ConstraintKind::Equal => crate::app::settings::AutoConstraintKind::Equal, + _ => return usize::MAX, + }; + settings + .priority + .iter() + .position(|candidate| *candidate == auto_kind) + .unwrap_or(usize::MAX) + }; + mapped.sort_by_key(|(kind, _)| priority(*kind)); + } if let Some(existing) = self.parametric_constraint_set(scope) { mapped.retain(|(kind, refs)| { !existing.constraints.iter().any(|constraint| { @@ -1409,6 +1459,19 @@ impl super::Scene { &self, scope: ParametricScope, handles: &[Handle], + ) -> Vec> { + self.inferred_coincident_constraints_with_tolerance( + scope, + handles, + COINCIDENT_EPSILON_SQ.sqrt(), + ) + } + + fn inferred_coincident_constraints_with_tolerance( + &self, + scope: ParametricScope, + handles: &[Handle], + distance_tolerance: f64, ) -> Vec> { let owner = scope.owner_handle(&self.document); let mut sources = Vec::new(); @@ -1437,7 +1500,7 @@ impl super::Scene { for second in first + 1..sources.len() { if sources[first].0.entity == sources[second].0.entity || (sources[first].1 - sources[second].1).length_squared() - > COINCIDENT_EPSILON_SQ + > distance_tolerance.max(0.0).powi(2) { continue; } @@ -1463,6 +1526,104 @@ impl super::Scene { inferred } + /// Resolve the target of GCSMOOTH through an existing Coincident + /// relation. The command selects only the spline; the connected endpoint + /// identifies both the target entity and the target point. + pub(crate) fn smooth_refs_from_connected_spline( + &self, + scope: ParametricScope, + spline_handle: Handle, + preferred_marker: Option, + ) -> Option> { + let spline = self.document.get_entity(spline_handle)?; + let acadrust::EntityType::Spline(spline) = spline else { + return None; + }; + if spline.flags.closed || spline.flags.periodic { + return None; + } + let markers: Vec = match preferred_marker { + Some(marker @ (0 | 1)) => vec![marker], + _ => vec![0, 1], + }; + let set = self.parametric_constraint_set(scope)?; + for marker in markers { + let source = ParametricRef::point(spline_handle, marker); + for constraint in set.constraints.iter().filter(|constraint| { + constraint.enabled + && constraint.kind == ConstraintKind::Coincident + && constraint.refs.len() == 2 + }) { + let target = if constraint.refs[0] == source { + constraint.refs[1] + } else if constraint.refs[1] == source { + constraint.refs[0] + } else { + continue; + }; + if target.entity == spline_handle || target.marker.is_none() { + continue; + } + let Some(target_entity) = self.document.get_entity(target.entity) else { + continue; + }; + let target_curve = match target_entity { + acadrust::EntityType::Line(_) + | acadrust::EntityType::Arc(_) + | acadrust::EntityType::Spline(_) => ParametricRef::whole(target.entity), + acadrust::EntityType::LwPolyline(polyline) => { + let Some(vertex) = target.marker.and_then(|marker| usize::try_from(marker).ok()) + else { + continue; + }; + let count = polyline.vertices.len(); + if count < 2 || vertex >= count { + continue; + } + let segment = if vertex == 0 { + 0 + } else if vertex + 1 == count && !polyline.is_closed { + vertex - 1 + } else { + vertex.saturating_sub(1) + }; + ParametricRef::segment(target.entity, segment) + } + acadrust::EntityType::Polyline2D(polyline) => { + let Some(vertex) = target.marker.and_then(|marker| usize::try_from(marker).ok()) + else { + continue; + }; + let count = polyline.vertices.len(); + if count < 2 || vertex >= count { + continue; + } + let segment = if vertex == 0 { + 0 + } else if vertex + 1 == count && !polyline.is_closed() { + vertex - 1 + } else { + vertex.saturating_sub(1) + }; + ParametricRef::segment(target.entity, segment) + } + _ => continue, + }; + let mut refs = vec![source, target]; + if target_curve.marker.is_some() { + refs.push(target_curve); + } + if self + .validate_parametric_constraint(ConstraintKind::Smooth, &refs, None) + .is_ok() + { + return Some(refs); + } + } + } + None + } + /// The constraint set for `scope`, if one has been created. pub fn parametric_constraint_set( &self,