diff --git a/assets/icons/constrain/smooth.svg b/assets/icons/constrain/smooth.svg index 5986ce9d0..7f5425a8a 100644 --- a/assets/icons/constrain/smooth.svg +++ b/assets/icons/constrain/smooth.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 178b41a0a..d8e1a035b 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -3141,9 +3141,24 @@ impl OpenCADStudio { &refs, driving_param.as_ref(), ) { - self.tabs[i].active_cmd = None; + let keep_smooth_selection = kind + == crate::scene::parametric_constraints::ConstraintKind::Smooth + && self.tabs[i] + .active_cmd + .as_ref() + .is_some_and(|command| command.name() == "GCSMOOTH"); + if !keep_smooth_selection { + self.tabs[i].active_cmd = None; + } self.tabs[i].snap_result = None; self.command_line.push_error(message); + if keep_smooth_selection { + if let Some(prompt) = + self.tabs[i].active_cmd.as_ref().map(|command| command.prompt()) + { + self.command_line.push_info(&prompt); + } + } return Task::none(); } let scope = self.tabs[i].current_parametric_scope(); diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 4c747e0a1..2289dbf09 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -1206,17 +1206,38 @@ impl OpenCADStudio { } } - "SMOOTHCONSTRAINT" => { + "GCSMOOTH" | "SMOOTHCONSTRAINT" => { + use crate::modules::parametric::SmoothConstraintCommand; let handles = self.tabs[i].scene.selected_handles_in_order(); if handles.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let sel = SelectObjectsCommand::new(cmd); - self.command_line.push_info(&sel.prompt()); - self.tabs[i].active_cmd = Some(Box::new(sel)); + 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 Some(refs) = self.tabs[i].scene.smooth_constraint_refs(&handles) else { - self.command_line - .push_output("Select one open spline and one open target curve."); + 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 { + 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.", + ); + 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; diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index 751d2e5d2..ffaf6cd59 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -1049,6 +1049,7 @@ impl OpenCADStudio { | "CONSTRAINTSOLVEMODE" | "CONSTRAINTINFER" | "CONSTRAINTBARDISPLAY" + | "CONSTRAINTBARMODE" ) => { return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i); @@ -1071,7 +1072,7 @@ impl OpenCADStudio { let value = it.next().map(|s| s.trim().to_string()); if name.is_empty() || name == "?" { self.command_line.push_info( - crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(), + crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY CONSTRAINTBARMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(), ); } else { if name == "CETRANSPARENCY" { @@ -1267,15 +1268,23 @@ impl OpenCADStudio { } if matches!( name.as_str(), - "CONSTRAINTSOLVEMODE" | "CONSTRAINTINFER" | "CONSTRAINTBARDISPLAY" + "CONSTRAINTSOLVEMODE" + | "CONSTRAINTINFER" + | "CONSTRAINTBARDISPLAY" + | "CONSTRAINTBARMODE" ) { let current = match name.as_str() { "CONSTRAINTSOLVEMODE" => i16::from(self.constraint_solve_mode), "CONSTRAINTINFER" => i16::from(self.constraint_infer), "CONSTRAINTBARDISPLAY" => self.constraint_bar_display, + "CONSTRAINTBARMODE" => self.constraint_bar_mode, _ => unreachable!(), }; - let maximum = if name == "CONSTRAINTBARDISPLAY" { 3 } else { 1 }; + let maximum = match name.as_str() { + "CONSTRAINTBARDISPLAY" => 3, + "CONSTRAINTBARMODE" => 4095, + _ => 1, + }; if let Some(value) = &value { match value .parse::() @@ -1291,6 +1300,7 @@ impl OpenCADStudio { "CONSTRAINTBARDISPLAY" => { self.constraint_bar_display = mode } + "CONSTRAINTBARMODE" => self.constraint_bar_mode = mode, _ => unreachable!(), } self.persist_settings_if_changed(); diff --git a/src/app/mod.rs b/src/app/mod.rs index fab5a9ecb..fa607fdeb 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -578,6 +578,7 @@ pub(super) struct OpenCADStudio { pub constraint_solve_mode: bool, pub constraint_infer: bool, pub constraint_bar_display: i16, + pub constraint_bar_mode: i16, /// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command); /// 0 disables autosave. pub savetime_min: i32, @@ -3834,6 +3835,7 @@ impl OpenCADStudio { constraint_solve_mode: true, constraint_infer: false, constraint_bar_display: 3, + constraint_bar_mode: 4095, savetime_min: 10, default_bg_color: None, default_paper_bg_color: None, diff --git a/src/app/settings.rs b/src/app/settings.rs index 0c1670994..a1f7b43bf 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -423,6 +423,9 @@ pub struct UserSettings { /// Constraint bar display bit mask: 1 after applying, 2 on selection. #[serde(default = "default_constraint_bar_display")] pub constraint_bar_display: i16, + /// Geometric constraint type bit mask (1..2048, combined; default all). + #[serde(default = "default_constraint_bar_mode")] + pub constraint_bar_mode: i16, /// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command). /// 0 disables autosave. pub savetime_min: i32, @@ -526,6 +529,10 @@ fn default_constraint_bar_display() -> i16 { 3 } +fn default_constraint_bar_mode() -> i16 { + 4095 +} + fn deserialize_clipromptlines<'de, D>(de: D) -> Result where D: serde::Deserializer<'de>, @@ -589,6 +596,7 @@ impl Default for UserSettings { constraint_solve_mode: true, constraint_infer: false, constraint_bar_display: 3, + constraint_bar_mode: 4095, savetime_min: 10, default_save_format: crate::io::DEFAULT_SAVE_FORMAT.to_string(), pick_add: true, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index c4edecfa0..c0c4d0d8a 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -519,6 +519,7 @@ impl OpenCADStudio { constraint_solve_mode: self.constraint_solve_mode, constraint_infer: self.constraint_infer, constraint_bar_display: self.constraint_bar_display, + constraint_bar_mode: self.constraint_bar_mode, savetime_min: self.savetime_min, default_save_format: self.default_save_format.clone(), pick_add: self.pick_add, @@ -615,6 +616,7 @@ impl OpenCADStudio { self.constraint_solve_mode = s.constraint_solve_mode; self.constraint_infer = s.constraint_infer; self.constraint_bar_display = s.constraint_bar_display.clamp(0, 3); + self.constraint_bar_mode = s.constraint_bar_mode.clamp(0, 4095); self.savetime_min = s.savetime_min; self.default_save_format = crate::io::canonical_save_format(&s.default_save_format).to_string(); diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 8478a94b9..e0ff77876 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -3036,6 +3036,7 @@ impl OpenCADStudio { vp_size, self.show_constraint_values, self.constraint_bar_display, + self.constraint_bar_mode, cursor, )?; let set = self.tabs[i].scene.parametric_constraint_set(scope)?; @@ -4643,6 +4644,7 @@ properties={:.1}ms picked={}", canvas_sz, self.show_constraint_values, self.constraint_bar_display, + self.constraint_bar_mode, p_full, ) }) @@ -6591,6 +6593,7 @@ mod selection_preview_tests { (800.0, 600.0), true, 3, + 4095, )[0].1; let point = (-30..=30).find_map(|y| { (-30..=30).find_map(|x| { diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index e239808b9..d1ecc1648 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -830,6 +830,7 @@ bg={bg_ms:.1}ms n={view_count}" sel_ref.vp_size, self.show_constraint_values, self.constraint_bar_display, + self.constraint_bar_mode, ) .into_iter() .map(|(id, point, direction, label, is_conflicting, hover_points)| { diff --git a/src/modules/parametric/geom_constraint.rs b/src/modules/parametric/geom_constraint.rs index 199912c87..ecf0a57fd 100644 --- a/src/modules/parametric/geom_constraint.rs +++ b/src/modules/parametric/geom_constraint.rs @@ -19,7 +19,7 @@ impl GeomConstraintCommand { "P" | "PERPENDICULAR" => "GCPERPENDICULAR", "PA" | "PARALLEL" => "PCONSTRAINT", "T" | "TANGENT" => "TCONSTRAINT", - "SM" | "SMOOTH" => "SMOOTHCONSTRAINT", + "SM" | "SMOOTH" => "GCSMOOTH", "C" | "COINCIDENT" => "GCCOINCIDENT", "CON" | "CONCENTRIC" => "NCONSTRAINT", "COL" | "COLLINEAR" | "COLINEAR" => "LCONSTRAINT", diff --git a/src/modules/parametric/mod.rs b/src/modules/parametric/mod.rs index 056c17686..4b5d4e65e 100644 --- a/src/modules/parametric/mod.rs +++ b/src/modules/parametric/mod.rs @@ -8,6 +8,7 @@ mod geom_constraint; #[path = "perpendicular.rs"] mod perpendicular_command; mod point_on_entity; +mod smooth; #[path = "tangent.rs"] mod tangent_command; mod tools; @@ -19,6 +20,7 @@ pub use perpendicular_command::{PerpendicularConstraintCommand, PerpendicularPic pub use point_on_entity::{ center_point_tool, midpoint_tool, point_on_curve_tool, PointOnEntityConstraintCommand, }; +pub use smooth::SmoothConstraintCommand; pub use tangent_command::TangentConstraintCommand; pub use tools::{ colinear, concentric, equal, fixed, horizontal, normal, parallel, perpendicular, symmetric, @@ -62,7 +64,7 @@ impl CadModule for ParametricModule { RibbonItem::LargeTool(colinear::tool()), RibbonItem::LargeTool(perpendicular::tool()), RibbonItem::LargeTool(command( - "SMOOTHCONSTRAINT", "Smooth", + "GCSMOOTH", "Smooth", include_bytes!("../../../assets/icons/constrain/smooth.svg"), )), RibbonItem::LargeTool(concentric::tool()), @@ -136,7 +138,7 @@ impl CadModule for ParametricModule { inventory::submit!(crate::command::CommandRegistration { names: &[ - "AUTOCONSTRAIN", "CONSTRAINTSETTINGS", "SMOOTHCONSTRAINT", "GCSHOW", "GCHIDE", "GCRESET", + "AUTOCONSTRAIN", "CONSTRAINTSETTINGS", "GCSMOOTH", "SMOOTHCONSTRAINT", "GCSHOW", "GCHIDE", "GCRESET", "GCSHOWALL", "GCHIDEALL", "DCSHOW", "DCHIDE", "DCSHOWALL", "DCHIDEALL", "DCCONVERT", "DELCONSTRAINT", ] @@ -168,7 +170,7 @@ mod tests { groups[0].tools.iter().map(item_id).collect::>(), [ "AUTOCONSTRAIN", "CCONSTRAINT", "PCONSTRAINT", "TCONSTRAINT", - "LCONSTRAINT", "QCONSTRAINT", "SMOOTHCONSTRAINT", "NCONSTRAINT", + "LCONSTRAINT", "QCONSTRAINT", "GCSMOOTH", "NCONSTRAINT", "HCONSTRAINT", "SYCONSTRAINT", "FXCONSTRAINT", "VCONSTRAINT", "ECONSTRAINT", "GCVISIBILITY", "GCSHOWALL", "GCHIDEALL", ] diff --git a/src/modules/parametric/smooth.rs b/src/modules/parametric/smooth.rs new file mode 100644 index 000000000..dc81a158f --- /dev/null +++ b/src/modules/parametric/smooth.rs @@ -0,0 +1,231 @@ +use acadrust::{EntityType, Handle}; +use glam::DVec3; + +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, + } + } + + fn point(point: acadrust::types::Vector3) -> DVec3 { + DVec3::new(point.x, point.y, point.z) + } + + fn nearest_marker(points: &[acadrust::types::Vector3], point: DVec3) -> Option { + points + .iter() + .enumerate() + .min_by(|(_, left), (_, right)| { + (Self::point(**left) - point) + .length_squared() + .total_cmp(&(Self::point(**right) - point).length_squared()) + }) + .map(|(index, _)| index as i32) + } + + fn source_reference(entity: &EntityType, handle: Handle, point: DVec3) -> Option { + let EntityType::Spline(spline) = entity else { + return None; + }; + if spline.flags.closed || spline.flags.periodic { + return None; + } + let points = crate::scene::dimension_assoc::source_points(entity); + let endpoints = [*points.first()?, *points.last()?]; + let marker = Self::nearest_marker(&endpoints, point)?; + 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(), + ) + } +} + +impl CadCommand for SmoothConstraintCommand { + fn name(&self) -> &'static str { + "GCSMOOTH" + } + + 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() + } + } + + fn needs_entity_pick(&self) -> bool { + true + } + + fn entity_pick_highlights_hover(&self) -> bool { + true + } + + fn inject_before_entity_pick(&self) -> bool { + true + } + + fn inject_picked_entity(&mut self, entity: EntityType) { + self.picked_entity = Some(entity); + } + + fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult { + if handle.is_null() { + return CmdResult::NeedPoint; + } + 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(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, + driving_param: None, + label: "Smooth constraint", + } + } + + fn on_point(&mut self, _point: DVec3) -> CmdResult { + CmdResult::NeedPoint + } + + fn on_enter(&mut self) -> CmdResult { + CmdResult::Cancel + } + + fn on_escape(&mut self) -> CmdResult { + CmdResult::Cancel + } +} diff --git a/src/scene/dwg_native_constraints.rs b/src/scene/dwg_native_constraints.rs index f4d755674..b89216bac 100644 --- a/src/scene/dwg_native_constraints.rs +++ b/src/scene/dwg_native_constraints.rs @@ -771,7 +771,13 @@ fn constraint_node( ) -> Option { let refs: &[ParametricRef] = &constraint.refs; if constraint.kind == ConstraintKind::Smooth { - let [first, second] = refs else { return None }; + let (first, second, second_curve_ref) = match refs { + [first, second] => (*first, *second, ParametricRef::whole(second.entity)), + [first, second, second_curve] if second.entity == second_curve.entity => { + (*first, *second, *second_curve) + } + _ => return None, + }; let endpoint_parameter = |reference: ParametricRef| { let entity = document.get_entity(reference.entity)?; let EntityType::Spline(spline) = entity else { @@ -791,15 +797,15 @@ fn constraint_node( }?; Some(Some(value)) }; - let first_parameter = endpoint_parameter(*first)?; - let second_parameter = endpoint_parameter(*second)?; + let first_parameter = endpoint_parameter(first)?; + let second_parameter = endpoint_parameter(second)?; if first_parameter.is_none() && second_parameter.is_none() { return None; } let first_curve = builder.geometry_node(first.entity)?; - let second_curve = builder.geometry_node(second.entity)?; - let first_point = builder.ref_node(*first)?; - let second_point = builder.ref_node(*second)?; + let second_curve = builder.ref_node(second_curve_ref)?; + let first_point = builder.ref_node(first)?; + let second_point = builder.ref_node(second)?; let coincidence = builder.alloc_node_id(); builder.push_node( @@ -2130,7 +2136,7 @@ pub(super) fn native_constraint_set( owned_constraint_ids, .. } if kind == ConstraintKind::Smooth => { - let endpoints = owned_constraint_ids + let mut endpoints: Vec = owned_constraint_ids .iter() .filter_map(|id| group.nodes.iter().find(|child| child.node_id == *id)) .find(|child| { @@ -2146,6 +2152,23 @@ pub(super) fn native_constraint_set( .collect() }) .unwrap_or_default(); + if let Some(target) = endpoints.get(1).copied() { + let segment = owned_constraint_ids + .iter() + .filter_map(|id| group.nodes.iter().find(|child| child.node_id == *id)) + .filter(|child| { + child.class_name.eq_ignore_ascii_case("AcTangentConstraint") + }) + .flat_map(|child| child.connections.iter()) + .filter_map(|id| refs.get(id).copied()) + .find(|reference| { + reference.entity == target.entity + && reference.segment_index().is_some() + }); + if let Some(segment) = segment { + endpoints.push(segment); + } + } (endpoints, Vec::new()) } _ => ( @@ -2192,6 +2215,7 @@ pub(super) fn native_constraint_set( ConstraintKind::DistanceDirected => matches!(targets.len(), 2 | 3), ConstraintKind::EqualDistance => targets.len() == 4, ConstraintKind::RigidSet => !targets.is_empty() && rigid_points.len() >= 2, + ConstraintKind::Smooth => matches!(targets.len(), 2 | 3), _ => targets.len() == 2, }; if !valid_target_count { diff --git a/src/scene/parametric_constraints.rs b/src/scene/parametric_constraints.rs index 467f8d779..3d0473217 100644 --- a/src/scene/parametric_constraints.rs +++ b/src/scene/parametric_constraints.rs @@ -687,6 +687,27 @@ pub(crate) fn parametric_curve_ref_for_pick( } impl ConstraintKind { + /// Per-kind visibility bit used by CONSTRAINTBARMODE for the standard + /// geometric-constraint family. Helper relations which have no standard + /// bit stay visible under the normal display policy. + pub const fn bar_mode_bit(self) -> Option { + match self { + Self::Horizontal => Some(1), + Self::Vertical => Some(2), + Self::Perpendicular => Some(4), + Self::Parallel => Some(8), + Self::Tangent => Some(16), + Self::Smooth => Some(32), + Self::Coincident => Some(64), + Self::Concentric => Some(128), + Self::Colinear => Some(256), + Self::Symmetric => Some(512), + Self::Equal => Some(1024), + Self::Fixed => Some(2048), + _ => None, + } + } + pub const fn label(self) -> &'static str { match self { Self::Coincident => "Coincident", @@ -1083,10 +1104,15 @@ impl super::Scene { &self, scope: ParametricScope, id: ConstraintId, + kind: ConstraintKind, related_entity_selected: bool, display_mode: i16, + bar_mode: i16, ) -> bool { self.is_parametric_constraint_visible(scope, id) + && kind + .bar_mode_bit() + .is_none_or(|bit| bar_mode & bit != 0) && (self.shown_parametric_constraints.contains(&(scope, id)) || (display_mode & 2 != 0 && related_entity_selected)) } @@ -1437,40 +1463,6 @@ impl super::Scene { inferred } - pub fn smooth_constraint_refs(&self, handles: &[Handle]) -> Option> { - let [first, second] = handles else { - return None; - }; - let first_entity = self.document.get_entity(*first)?; - let second_entity = self.document.get_entity(*second)?; - let (spline_handle, spline, target_handle, target) = match (first_entity, second_entity) { - (acadrust::EntityType::Spline(spline), target) => (*first, spline, *second, target), - (target, acadrust::EntityType::Spline(spline)) => (*second, spline, *first, target), - _ => return None, - }; - if spline.flags.closed || spline.flags.periodic { - return None; - } - let spline_points = - super::dimension_assoc::source_points(&acadrust::EntityType::Spline(spline.clone())); - let target_points = super::dimension_assoc::source_points(target); - let spline_ends = [*spline_points.first()?, *spline_points.last()?]; - let target_ends = [*target_points.first()?, *target_points.last()?]; - let mut best = (f64::INFINITY, 0, 0); - for (source_marker, source) in spline_ends.iter().enumerate() { - for (target_marker, target) in target_ends.iter().enumerate() { - let distance = (*source - *target).length_squared(); - if distance < best.0 { - best = (distance, source_marker, target_marker); - } - } - } - Some(vec![ - ParametricRef::point(spline_handle, best.1 as i32), - ParametricRef::point(target_handle, best.2 as i32), - ]) - } - /// The constraint set for `scope`, if one has been created. pub fn parametric_constraint_set( &self, @@ -1521,6 +1513,7 @@ impl super::Scene { vp_size: (f32, f32), show_values: bool, display_mode: i16, + bar_mode: i16, ) -> Vec<( ConstraintId, iced::Point, @@ -1555,7 +1548,14 @@ impl super::Scene { .iter() .any(|reference| self.selected.contains(&reference.entity) || self.preview_hidden.contains(&reference.entity)); - self.should_display_parametric_constraint(scope, c.id, selected, display_mode) + self.should_display_parametric_constraint( + scope, + c.id, + c.kind, + selected, + display_mode, + bar_mode, + ) }) .flat_map(|c| { let is_conflicting = set.conflicts.iter().any(|(id, _)| *id == c.id); @@ -1627,6 +1627,7 @@ impl super::Scene { vp_size: (f32, f32), show_values: bool, display_mode: i16, + bar_mode: i16, p: iced::Point, ) -> Option { let placements = self.constraint_glyph_placements_screen( @@ -1634,6 +1635,7 @@ impl super::Scene { vp_size, show_values, display_mode, + bar_mode, ); let glyphs: Vec<(iced::Point, [f32; 2], String, bool)> = placements .iter() @@ -2213,18 +2215,34 @@ mod tests { vec![ParametricRef::whole(h(1))], None, ); - assert!(!scene.should_display_parametric_constraint(scope, id, false, 3)); - assert!(scene.should_display_parametric_constraint(scope, id, true, 2)); - assert!(!scene.should_display_parametric_constraint(scope, id, true, 1)); + assert!(!scene.should_display_parametric_constraint( + scope, id, ConstraintKind::Horizontal, false, 3, 4095 + )); + assert!(scene.should_display_parametric_constraint( + scope, id, ConstraintKind::Horizontal, true, 2, 4095 + )); + assert!(!scene.should_display_parametric_constraint( + scope, id, ConstraintKind::Horizontal, true, 1, 4095 + )); scene.note_parametric_constraint_applied(scope, id, 1); - assert!(scene.should_display_parametric_constraint(scope, id, false, 0)); + assert!(scene.should_display_parametric_constraint( + scope, id, ConstraintKind::Horizontal, false, 0, 4095 + )); scene.set_parametric_constraint_visibility(scope, None, false, false); - assert!(!scene.should_display_parametric_constraint(scope, id, true, 3)); + assert!(!scene.should_display_parametric_constraint( + scope, id, ConstraintKind::Horizontal, true, 3, 4095 + )); scene.set_parametric_constraint_visibility(scope, None, false, true); - assert!(scene.should_display_parametric_constraint(scope, id, false, 0)); + assert!(scene.should_display_parametric_constraint( + scope, id, ConstraintKind::Horizontal, false, 0, 4095 + )); scene.note_parametric_constraint_applied(scope, id, 0); - assert!(!scene.should_display_parametric_constraint(scope, id, false, 3)); - assert!(scene.should_display_parametric_constraint(scope, id, true, 2)); + assert!(!scene.should_display_parametric_constraint( + scope, id, ConstraintKind::Horizontal, false, 3, 4095 + )); + assert!(scene.should_display_parametric_constraint( + scope, id, ConstraintKind::Horizontal, true, 2, 4095 + )); } #[test] @@ -2238,7 +2256,9 @@ mod tests { ); scene.hidden_parametric_constraints.insert((scope, id)); - assert!(!scene.should_display_parametric_constraint(scope, id, true, 2)); + assert!(!scene.should_display_parametric_constraint( + scope, id, ConstraintKind::Horizontal, true, 2, 4095 + )); } #[test] diff --git a/src/scene/parametric_solve.rs b/src/scene/parametric_solve.rs index 30e56a8fc..642b26925 100644 --- a/src/scene/parametric_solve.rs +++ b/src/scene/parametric_solve.rs @@ -1697,26 +1697,167 @@ fn build_constraint( } } -fn smooth_target_jet(entity: &EntityType, marker: i32) -> Option { - let parameter = match marker { - 0 => 0.0, - 1 => 1.0, - _ => return None, - }; - let curve = crate::entities::curve::entity_curve(entity)?; - if curve.is_closed() { - return None; +fn smooth_refs(refs: &[ParametricRef]) -> Option<(ParametricRef, ParametricRef, ParametricRef)> { + match refs { + [source, target_endpoint] => Some(( + *source, + *target_endpoint, + ParametricRef::whole(target_endpoint.entity), + )), + [source, target_endpoint, target_curve] + if target_endpoint.entity == target_curve.entity => + { + Some((*source, *target_endpoint, *target_curve)) + } + _ => None, } - let step = if marker == 0 { 1.0e-4 } else { -1.0e-4 }; +} + +fn smooth_target_planar_curve( + entity: &EntityType, + curve_reference: ParametricRef, +) -> Option { + let planar = crate::entities::curve::entity_curve(entity)?; + let Some(segment) = curve_reference.segment_index() else { + return (!planar.is_closed()).then_some(planar); + }; + let curve = planar.curve.segments().into_iter().nth(segment)?; + Some(cadkernel::space::PlanarCurve::new(planar.plane, curve)) +} + +fn smooth_endpoint_parameter( + entity: &EntityType, + endpoint_reference: ParametricRef, + curve: &cadkernel::space::PlanarCurve, +) -> Option { + let marker = endpoint_reference.marker?; + let endpoint = super::parametric_constraints::resolve_point(entity, marker)?; + let endpoint = cadkernel::space::Vec3::new(endpoint.x, endpoint.y, endpoint.z); + let start = cadkernel::space::Vec3::from(curve.point_at(0.0)); + let end = cadkernel::space::Vec3::from(curve.point_at(1.0)); + Some(if endpoint.distance(start) <= endpoint.distance(end) { + 0.0 + } else { + 1.0 + }) +} + +fn smooth_target_jet( + entity: &EntityType, + endpoint_reference: ParametricRef, + curve_reference: ParametricRef, +) -> Option { + if curve_reference.segment_index().is_none() { + if let EntityType::Spline(spline) = entity { + let endpoint = match endpoint_reference.marker { + Some(0) => cadkernel::space::SplineEnd::Start, + Some(1) => cadkernel::space::SplineEnd::End, + _ => return None, + }; + let nurbs = crate::entities::spline::nurbs3(spline)?; + return cadkernel::space::CurveJet::from_nurbs(&nurbs, endpoint); + } + } + + let curve = smooth_target_planar_curve(entity, curve_reference)?; + let parameter = smooth_endpoint_parameter(entity, endpoint_reference, &curve)?; + let point = curve.point_at(parameter); let tangent = curve.tangent_at(parameter); - let next = curve.point_at(parameter + step); - let third = curve.point_at(parameter + 2.0 * step); - Some(cadkernel::space::CurveJet { - point, - tangent, - curvature: cadkernel::space::curve::curvature_through(point, next, third), - }) + match &curve.curve { + cadkernel::geom2d::Curve::Line(_) => { + cadkernel::space::CurveJet::linear(point, tangent) + } + cadkernel::geom2d::Curve::Arc(arc) => cadkernel::space::CurveJet::circular( + point, + tangent, + curve.plane.point_at(arc.centre), + ), + _ => None, + } +} + +fn smooth_spline_plane(spline: &acadrust::entities::Spline) -> Option { + let source = if crate::entities::spline::uses_fit_method(spline) { + &spline.fit_points + } else { + &spline.control_points + }; + let origin = source.first()?; + let origin_vector = cadkernel::space::Vec3::new(origin.x, origin.y, origin.z); + let along = source + .iter() + .skip(1) + .map(|point| cadkernel::space::Vec3::new(point.x, point.y, point.z) - origin_vector) + .find(|vector| vector.length_squared() > 1.0e-24)?; + let normal = source + .iter() + .skip(1) + .map(|point| cadkernel::space::Vec3::new(point.x, point.y, point.z) - origin_vector) + .map(|vector| along.cross(vector)) + .find(|vector| vector.length_squared() > 1.0e-24)?; + let plane = cadkernel::space::Plane::orthonormal( + origin_vector.to_array(), + along.to_array(), + normal.to_array(), + )?; + let points: Vec<_> = source.iter().map(|point| [point.x, point.y, point.z]).collect(); + let tolerance = cadkernel::space::coplanarity_tolerance(&points); + source + .iter() + .all(|point| plane.contains([point.x, point.y, point.z], tolerance)) + .then_some(plane) +} + +fn smooth_entity_plane( + entity: &EntityType, + curve_reference: ParametricRef, +) -> Option { + if let EntityType::Spline(spline) = entity { + return crate::entities::curve::entity_curve(entity) + .map(|curve| curve.plane) + .or_else(|| smooth_spline_plane(spline)); + } + smooth_target_planar_curve(entity, curve_reference).map(|curve| curve.plane) +} + +fn smooth_refs_share_plane( + document: &acadrust::CadDocument, + source_ref: ParametricRef, + target_curve_ref: ParametricRef, +) -> bool { + let Some(source_entity) = document.get_entity(source_ref.entity) else { + return false; + }; + let Some(source_plane) = smooth_entity_plane(source_entity, ParametricRef::whole(source_ref.entity)) + else { + return false; + }; + let Some(target_entity) = document.get_entity(target_curve_ref.entity) else { + return false; + }; + let Some(target_plane) = smooth_entity_plane(target_entity, target_curve_ref) else { + return false; + }; + let points = [source_plane.origin, target_plane.origin]; + let tolerance = cadkernel::space::coplanarity_tolerance(&points); + if let EntityType::Line(line) = target_entity { + return source_plane + .contains([line.start.x, line.start.y, line.start.z], tolerance) + && source_plane + .contains([line.end.x, line.end.y, line.end.z], tolerance); + } + let (Some(source_normal), Some(target_normal)) = + (source_plane.normal(), target_plane.normal()) + else { + return false; + }; + cadkernel::space::Vec3::from(source_normal) + .cross(cadkernel::space::Vec3::from(target_normal)) + .length() + <= 1.0e-9 + && source_plane.contains(target_plane.origin, tolerance) + && target_plane.contains(source_plane.origin, tolerance) } fn apply_smooth_constraints( @@ -1729,7 +1870,9 @@ fn apply_smooth_constraints( .iter() .filter(|constraint| constraint.enabled && constraint.kind == ConstraintKind::Smooth) { - let [source_ref, target_ref] = constraint.refs.as_slice() else { + let Some((source_ref, target_ref, target_curve_ref)) = + smooth_refs(&constraint.refs) + else { continue; }; let source = results @@ -1750,10 +1893,7 @@ fn apply_smooth_constraints( Some(1) => cadkernel::space::SplineEnd::End, _ => continue, }; - let Some(target_marker) = target_ref.marker else { - continue; - }; - let Some(target_jet) = smooth_target_jet(&target, target_marker) else { + let Some(target_jet) = smooth_target_jet(&target, target_ref, target_curve_ref) else { continue; }; let Some(curve) = crate::entities::spline::nurbs3(&spline) else { @@ -2871,13 +3011,8 @@ impl Scene { refs: &[ParametricRef], driving_param: Option<&super::named_parameters::DrivingValue>, ) -> Result<(), &'static str> { - if !refs_share_supported_plane(&self.document, refs) { - return Err( - "Constraint references must be supported entities on the same world-XY plane.", - ); - } if kind == ConstraintKind::Smooth { - let [source_ref, target_ref] = refs else { + let Some((source_ref, target_ref, target_curve_ref)) = smooth_refs(refs) else { return Err("Smooth requires one spline endpoint and one target endpoint."); }; if source_ref.entity == target_ref.entity { @@ -2895,13 +3030,16 @@ impl Scene { Some(1) => cadkernel::space::SplineEnd::End, _ => return Err("Smooth requires a spline endpoint."), }; - let Some(target_marker) = target_ref.marker else { + let Some(_) = target_ref.marker else { return Err("Smooth requires a target endpoint."); }; let Some(target) = self.document.get_entity(target_ref.entity) else { return Err("Smooth target does not exist."); }; - let Some(target_jet) = smooth_target_jet(target, target_marker) else { + if !smooth_refs_share_plane(&self.document, source_ref, target_curve_ref) { + return Err("Smooth requires both curves to lie on the same plane."); + } + let Some(target_jet) = smooth_target_jet(target, target_ref, target_curve_ref) else { return Err("The selected target does not support endpoint smoothing."); }; let Some(curve) = crate::entities::spline::nurbs3(spline) else { @@ -2912,6 +3050,11 @@ impl Scene { } return Ok(()); } + if !refs_share_supported_plane(&self.document, refs) { + return Err( + "Constraint references must be supported entities on the same world-XY plane.", + ); + } let validation_target = match driving_param { Some(super::named_parameters::DrivingValue::Named(name)) if !self.named_parameters.contains(name) => diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 37274b4a8..906261a99 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -38,12 +38,36 @@ fn constraint_glyph_size(label: &str) -> Size { if is_compact_coincident_glyph(label) { return Size::new(COINCIDENT_GLYPH_SIZE, COINCIDENT_GLYPH_SIZE); } + if label == "G²" { + let side = CONSTRAINT_GLYPH_SIZE + CONSTRAINT_GLYPH_PAD_Y * 2.0; + return Size::new(side, side); + } let w = label.chars().count() as f32 * CONSTRAINT_GLYPH_SIZE * 0.62 + CONSTRAINT_GLYPH_PAD_X * 2.0; let h = CONSTRAINT_GLYPH_SIZE + CONSTRAINT_GLYPH_PAD_Y * 2.0; Size::new(w, h) } +fn draw_smooth_constraint_glyph(frame: &mut canvas::Frame, center: Point, color: Color) { + let curve = canvas::Path::new(|builder| { + for step in 0..=16 { + let x = -7.0 + step as f32 * 0.875; + let y = 0.012 * x * x * x; + let point = Point::new(center.x + x, center.y - y); + if step == 0 { + builder.move_to(point); + } else { + builder.line_to(point); + } + } + }); + frame.stroke( + &curve, + canvas::Stroke::default().with_color(color).with_width(1.45), + ); + frame.fill(&canvas::Path::circle(center, 1.35), color); +} + fn draw_tangent_constraint_glyph( frame: &mut canvas::Frame, center: Point, @@ -1905,6 +1929,8 @@ impl canvas::Program for SelectionCanvas { ); if label == "T" { draw_tangent_constraint_glyph(&mut frame, glyph_center, fg); + } else if label == "G²" { + draw_smooth_constraint_glyph(&mut frame, glyph_center, fg); } else { frame.fill_text(canvas::Text { content: label.clone(),