diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 4f4ef4d01..f3e63ac44 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -767,6 +767,21 @@ impl OpenCADStudio { if self.command_point_allowed(i, wcs) { self.last_point = Some(wcs); self.push_ucs_to_cmd(i); + // A typed coordinate at an object prompt is a pick at + // that point, as in the reference. + let picks_entity = self.tabs[i] + .active_cmd + .as_ref() + .is_some_and(|command| command.typed_point_picks_entity()); + if picks_entity { + let owner = self.tabs[i] + .current_parametric_scope() + .owner_handle(&self.tabs[i].scene.document); + let handle = + entity_at_typed_point(&self.tabs[i].scene.document, owner, wcs) + .unwrap_or(Handle::NULL); + return self.feed_command(StepInput::EntityPick(handle, wcs)); + } return self.feed_command(StepInput::Point(wcs)); } return Task::none(); @@ -3268,39 +3283,26 @@ impl OpenCADStudio { CmdResult::AddEqualConstraint { first, others, + multiple, label, } => { use crate::modules::parametric::EqualConstraintCommand; use crate::scene::parametric_constraints::{ - equal_size_follower, ConstraintKind, ParametricRef, + equal_size, equal_size_follower, ConstraintKind, EqualSize, ParametricRef, }; let scope = self.tabs[i].current_parametric_scope(); + // Enter ends a Multiple flow with the reference's summary line. + let finishing = multiple && others.is_empty(); let mut followers: Vec = Vec::new(); for other in others { - // A Multiple set arrives as whole objects. - let resolved = if other.marker.is_none() { - self.tabs[i] - .scene - .document - .get_entity(other.entity) - .and_then(|entity| { - EqualConstraintCommand::whole_reference(entity, other.entity) - }) - } else { - Some(other) - }; - let Some(other) = resolved.filter(|other| *other != first) else { - self.command_line - .push_error(EqualConstraintCommand::INVALID_OBJECT); - continue; - }; let refs = [first, other]; - if self - .tabs[i] - .scene - .validate_parametric_constraint(ConstraintKind::Equal, &refs, None) - .is_err() + if other == first + || self + .tabs[i] + .scene + .validate_parametric_constraint(ConstraintKind::Equal, &refs, None) + .is_err() || equal_size_follower(&self.tabs[i].scene.document, first, other) .is_none() { @@ -3323,12 +3325,25 @@ impl OpenCADStudio { .push_error("The constraint already exists on the selected objects."); continue; } - if !followers.contains(&other) { - followers.push(other); + followers.push(other); + } + if multiple && !finishing { + if let Some(prompt) = + self.tabs[i].active_cmd.as_ref().map(|command| command.prompt()) + { + self.command_line.push_info(&prompt); } + } else { + self.tabs[i].active_cmd = None; } - self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; + if finishing { + let summary = match equal_size(&self.tabs[i].scene.document, first) { + Some(EqualSize::Radius(_)) => "Radius of objects made equal", + _ => "Length of objects made equal", + }; + self.command_line.push_output(summary); + } if followers.is_empty() { return Task::none(); } @@ -3379,7 +3394,6 @@ impl OpenCADStudio { self.constraint_solve_mode, ); self.tabs[i].dirty = true; - self.command_line.push_output("Equal constraint applied."); self.refresh_properties(); if let Some(pd) = pending { self.commit_undo_delta(i, pd); @@ -7749,6 +7763,40 @@ impl OpenCADStudio { /// returning the new root handle. `allocate_handle` advances the document's /// handle counter — `next_handle()` only peeks, so reusing it would hand every /// object the same handle and collapse the dictionary chain. +/// The entity a typed coordinate lands on while an object is asked for: +/// the nearest planar curve of the edited space within a small share of +/// that space's extent, the way a pick box takes the object under a click. +fn entity_at_typed_point( + document: &acadrust::CadDocument, + owner: Handle, + point: glam::DVec3, +) -> Option { + let mut extent = 0.0f64; + let mut nearest: Option<(f64, Handle)> = None; + for entity in document.entities() { + let common = entity.common(); + if common.owner_handle != owner { + continue; + } + let bounds = entity.as_entity().bounding_box(); + extent = extent + .max((bounds.max.x - bounds.min.x).abs()) + .max((bounds.max.y - bounds.min.y).abs()); + let Some(distance) = + crate::scene::viewport_dimension_pick::planar_pick_distance(entity, point) + else { + continue; + }; + if nearest.is_none_or(|(best, _)| distance < best) { + nearest = Some((distance, common.handle)); + } + } + let tolerance = (extent * 0.002).max(1.0e-9); + nearest + .filter(|(distance, _)| *distance <= tolerance) + .map(|(_, handle)| handle) +} + fn recreate_ext_subtree( doc: &mut acadrust::CadDocument, cap: &crate::app::ClipExtObjects, diff --git a/src/command.rs b/src/command.rs index 1ee244ffe..fda0b82cf 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1598,6 +1598,9 @@ pub enum CmdResult { AddEqualConstraint { first: crate::scene::parametric_constraints::ParametricRef, others: Vec, + /// A Multiple flow: the command stays for more picks; an empty + /// `others` is its Enter and prints the summary line. + multiple: bool, label: &'static str, }, /// Adds a point or object symmetry relation around a picked line. The @@ -2438,6 +2441,11 @@ pub trait CadCommand: Send { false } + /// Take a typed coordinate at an object prompt as a pick at that point. + fn typed_point_picks_entity(&self) -> bool { + false + } + /// Include filled hatch / DXF SOLID regions in the entity hit-test. /// /// Most entity-pick commands operate on curve geometry and intentionally diff --git a/src/modules/parametric/equal.rs b/src/modules/parametric/equal.rs index ea547c850..fde973082 100644 --- a/src/modules/parametric/equal.rs +++ b/src/modules/parametric/equal.rs @@ -13,19 +13,18 @@ enum Step { First { multiple: bool }, /// `Select second object:` — the one that takes the first's size. Second(ParametricRef), - /// `Select objects to make equal to the first:` — a selection set, - /// applied on Enter. + /// `Select object to make equal to first:` — every pick follows the + /// first at once, Enter ends the command. Others(ParametricRef), } /// Interactive front end for the Equal geometric constraint: the first -/// object sets the size, the second (or, with Multiple, every object of a -/// set) takes its length or radius. Picks are made inside the command; a +/// object sets the size, the second (or, with Multiple, each further pick) +/// takes its length or radius. Objects are picked inside the command; a /// selection made before it starts is not used. pub struct EqualConstraintCommand { step: Step, picked_entity: Option, - others: Vec, } impl EqualConstraintCommand { @@ -39,7 +38,6 @@ impl EqualConstraintCommand { Self { step: Step::First { multiple: false }, picked_entity: None, - others: Vec::new(), } } @@ -65,43 +63,15 @@ impl EqualConstraintCommand { } } - /// An object of a Multiple set: a whole line, circle or arc, a - /// one-segment polyline as its segment. - pub fn whole_reference(entity: &EntityType, handle: Handle) -> Option { - match entity { - EntityType::Line(_) | EntityType::Circle(_) | EntityType::Arc(_) => { - Some(ParametricRef::whole(handle)) - } - EntityType::LwPolyline(polyline) => { - let count = if polyline.is_closed { - polyline.vertices.len() - } else { - polyline.vertices.len().saturating_sub(1) - }; - (count == 1 && HorizontalConstraintCommand::segment_is_straight(entity, 0)) - .then_some(ParametricRef::segment(handle, 0)) - } - EntityType::Polyline2D(polyline) => { - let count = if polyline.is_closed() { - polyline.vertices.len() - } else { - polyline.vertices.len().saturating_sub(1) - }; - (count == 1 && HorizontalConstraintCommand::segment_is_straight(entity, 0)) - .then_some(ParametricRef::segment(handle, 0)) - } - _ => None, - } - } - fn report(message: &str) -> CmdResult { CmdResult::ReportError(message.to_string()) } - fn finish(first: ParametricRef, others: Vec) -> CmdResult { + fn finish(first: ParametricRef, others: Vec, multiple: bool) -> CmdResult { CmdResult::AddEqualConstraint { first, others, + multiple, label: "Equal constraint", } } @@ -119,13 +89,7 @@ impl CadCommand for EqualConstraintCommand { } Step::First { multiple: true } => "GCEQUAL Select first object:".to_string(), Step::Second(_) => "GCEQUAL Select second object:".to_string(), - Step::Others(_) if self.others.is_empty() => { - "GCEQUAL Select objects to make equal to the first:".to_string() - } - Step::Others(_) => format!( - "GCEQUAL Select objects to make equal to the first ({} selected, Enter to apply):", - self.others.len() - ), + Step::Others(_) => "GCEQUAL Select object to make equal to first:".to_string(), } } @@ -155,13 +119,17 @@ impl CadCommand for EqualConstraintCommand { } fn needs_entity_pick(&self) -> bool { - !matches!(self.step, Step::Others(_)) + true } fn entity_pick_accepts_points(&self) -> bool { true } + fn typed_point_picks_entity(&self) -> bool { + true + } + fn entity_pick_highlights_hover(&self) -> bool { true } @@ -174,21 +142,6 @@ impl CadCommand for EqualConstraintCommand { self.picked_entity = Some(entity); } - fn is_selection_gathering(&self) -> bool { - matches!(self.step, Step::Others(_)) - } - - fn on_selection_complete(&mut self, handles: Vec) -> CmdResult { - if let Step::Others(first) = self.step { - // The first object sets the size; it cannot also follow itself. - self.others = handles - .into_iter() - .filter(|handle| *handle != first.entity) - .collect(); - } - CmdResult::NeedPoint - } - fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult { if handle.is_null() { return Self::report(Self::NO_OBJECT); @@ -208,27 +161,23 @@ impl CadCommand for EqualConstraintCommand { self.step = Step::Others(reference); CmdResult::NeedPoint } - Step::Second(first) => { - if first == reference { - return Self::report(Self::SAME_OBJECT); - } - Self::finish(first, vec![reference]) + Step::Second(first) | Step::Others(first) if first == reference => { + Self::report(Self::SAME_OBJECT) } - Step::Others(_) => CmdResult::NeedPoint, + Step::Second(first) => Self::finish(first, vec![reference], false), + Step::Others(first) => Self::finish(first, vec![reference], true), } } - // An empty-space click while an object is asked for. + // An empty-space click, or a typed coordinate with nothing under it. fn on_point(&mut self, _point: DVec3) -> CmdResult { Self::report(Self::NO_OBJECT) } fn on_enter(&mut self) -> CmdResult { match self.step { - Step::Others(first) if !self.others.is_empty() => Self::finish( - first, - self.others.iter().map(|handle| ParametricRef::whole(*handle)).collect(), - ), + // Multiple ends with its summary line; the host prints it. + Step::Others(first) => Self::finish(first, Vec::new(), true), _ => CmdResult::Cancel, } } @@ -261,26 +210,38 @@ mod tests { CmdResult::NeedPoint )); command.inject_picked_entity(line()); - let CmdResult::AddEqualConstraint { first, others, .. } = - command.on_entity_pick(Handle::new(8), DVec3::ZERO) + let CmdResult::AddEqualConstraint { + first, + others, + multiple, + .. + } = command.on_entity_pick(Handle::new(8), DVec3::ZERO) else { panic!("the second pick must create the constraint"); }; assert_eq!(first, ParametricRef::whole(Handle::new(7))); assert_eq!(others, vec![ParametricRef::whole(Handle::new(8))]); + assert!(!multiple); } #[test] - fn multiple_applies_the_gathered_set_on_enter() { + fn multiple_applies_each_pick_and_ends_on_enter() { let mut command = EqualConstraintCommand::new(); assert!(matches!(command.on_text_input("M"), Some(CmdResult::NeedPoint))); command.inject_picked_entity(line()); command.on_entity_pick(Handle::new(7), DVec3::ZERO); - assert!(command.is_selection_gathering()); - command.on_selection_complete(vec![Handle::new(7), Handle::new(8), Handle::new(9)]); - let CmdResult::AddEqualConstraint { others, .. } = command.on_enter() else { - panic!("Enter must apply the set"); + command.inject_picked_entity(line()); + let CmdResult::AddEqualConstraint { + others, multiple, .. + } = command.on_entity_pick(Handle::new(8), DVec3::ZERO) + else { + panic!("a Multiple pick must apply at once"); }; - assert_eq!(others.len(), 2); + assert_eq!(others, vec![ParametricRef::whole(Handle::new(8))]); + assert!(multiple); + let CmdResult::AddEqualConstraint { others, .. } = command.on_enter() else { + panic!("Enter must end the Multiple flow through the host"); + }; + assert!(others.is_empty()); } }