forked from 1b-agent/opencadstudio-upstream
Equal: apply each Multiple pick at once, take typed coordinates as picks
Multiple now follows the reference flow: every pick after the first is made equal right away under `Select object to make equal to first:` and Enter ends the command with `Length of objects made equal` (or `Radius …`). A typed coordinate at one of the command's object prompts picks the object under that point instead of being refused as a point. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
8520ee45e8
commit
e32e7e68e4
3 files changed
+122
-105
No files matched your search
+75
-27
@@ -767,6 +767,21 @@ impl OpenCADStudio {
|
|||||||
if self.command_point_allowed(i, wcs) {
|
if self.command_point_allowed(i, wcs) {
|
||||||
self.last_point = Some(wcs);
|
self.last_point = Some(wcs);
|
||||||
self.push_ucs_to_cmd(i);
|
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 self.feed_command(StepInput::Point(wcs));
|
||||||
}
|
}
|
||||||
return Task::none();
|
return Task::none();
|
||||||
@@ -3268,39 +3283,26 @@ impl OpenCADStudio {
|
|||||||
CmdResult::AddEqualConstraint {
|
CmdResult::AddEqualConstraint {
|
||||||
first,
|
first,
|
||||||
others,
|
others,
|
||||||
|
multiple,
|
||||||
label,
|
label,
|
||||||
} => {
|
} => {
|
||||||
use crate::modules::parametric::EqualConstraintCommand;
|
use crate::modules::parametric::EqualConstraintCommand;
|
||||||
use crate::scene::parametric_constraints::{
|
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();
|
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<ParametricRef> = Vec::new();
|
let mut followers: Vec<ParametricRef> = Vec::new();
|
||||||
for other in others {
|
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];
|
let refs = [first, other];
|
||||||
if self
|
if other == first
|
||||||
.tabs[i]
|
|| self
|
||||||
.scene
|
.tabs[i]
|
||||||
.validate_parametric_constraint(ConstraintKind::Equal, &refs, None)
|
.scene
|
||||||
.is_err()
|
.validate_parametric_constraint(ConstraintKind::Equal, &refs, None)
|
||||||
|
.is_err()
|
||||||
|| equal_size_follower(&self.tabs[i].scene.document, first, other)
|
|| equal_size_follower(&self.tabs[i].scene.document, first, other)
|
||||||
.is_none()
|
.is_none()
|
||||||
{
|
{
|
||||||
@@ -3323,12 +3325,25 @@ impl OpenCADStudio {
|
|||||||
.push_error("The constraint already exists on the selected objects.");
|
.push_error("The constraint already exists on the selected objects.");
|
||||||
continue;
|
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;
|
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() {
|
if followers.is_empty() {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
@@ -3379,7 +3394,6 @@ impl OpenCADStudio {
|
|||||||
self.constraint_solve_mode,
|
self.constraint_solve_mode,
|
||||||
);
|
);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.command_line.push_output("Equal constraint applied.");
|
|
||||||
self.refresh_properties();
|
self.refresh_properties();
|
||||||
if let Some(pd) = pending {
|
if let Some(pd) = pending {
|
||||||
self.commit_undo_delta(i, pd);
|
self.commit_undo_delta(i, pd);
|
||||||
@@ -7749,6 +7763,40 @@ impl OpenCADStudio {
|
|||||||
/// returning the new root handle. `allocate_handle` advances the document's
|
/// returning the new root handle. `allocate_handle` advances the document's
|
||||||
/// handle counter — `next_handle()` only peeks, so reusing it would hand every
|
/// handle counter — `next_handle()` only peeks, so reusing it would hand every
|
||||||
/// object the same handle and collapse the dictionary chain.
|
/// 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<Handle> {
|
||||||
|
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(
|
fn recreate_ext_subtree(
|
||||||
doc: &mut acadrust::CadDocument,
|
doc: &mut acadrust::CadDocument,
|
||||||
cap: &crate::app::ClipExtObjects,
|
cap: &crate::app::ClipExtObjects,
|
||||||
|
|||||||
@@ -1598,6 +1598,9 @@ pub enum CmdResult {
|
|||||||
AddEqualConstraint {
|
AddEqualConstraint {
|
||||||
first: crate::scene::parametric_constraints::ParametricRef,
|
first: crate::scene::parametric_constraints::ParametricRef,
|
||||||
others: Vec<crate::scene::parametric_constraints::ParametricRef>,
|
others: Vec<crate::scene::parametric_constraints::ParametricRef>,
|
||||||
|
/// 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,
|
label: &'static str,
|
||||||
},
|
},
|
||||||
/// Adds a point or object symmetry relation around a picked line. The
|
/// Adds a point or object symmetry relation around a picked line. The
|
||||||
@@ -2438,6 +2441,11 @@ pub trait CadCommand: Send {
|
|||||||
false
|
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.
|
/// Include filled hatch / DXF SOLID regions in the entity hit-test.
|
||||||
///
|
///
|
||||||
/// Most entity-pick commands operate on curve geometry and intentionally
|
/// Most entity-pick commands operate on curve geometry and intentionally
|
||||||
|
|||||||
@@ -13,19 +13,18 @@ enum Step {
|
|||||||
First { multiple: bool },
|
First { multiple: bool },
|
||||||
/// `Select second object:` — the one that takes the first's size.
|
/// `Select second object:` — the one that takes the first's size.
|
||||||
Second(ParametricRef),
|
Second(ParametricRef),
|
||||||
/// `Select objects to make equal to the first:` — a selection set,
|
/// `Select object to make equal to first:` — every pick follows the
|
||||||
/// applied on Enter.
|
/// first at once, Enter ends the command.
|
||||||
Others(ParametricRef),
|
Others(ParametricRef),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Interactive front end for the Equal geometric constraint: the first
|
/// Interactive front end for the Equal geometric constraint: the first
|
||||||
/// object sets the size, the second (or, with Multiple, every object of a
|
/// object sets the size, the second (or, with Multiple, each further pick)
|
||||||
/// set) takes its length or radius. Picks are made inside the command; a
|
/// takes its length or radius. Objects are picked inside the command; a
|
||||||
/// selection made before it starts is not used.
|
/// selection made before it starts is not used.
|
||||||
pub struct EqualConstraintCommand {
|
pub struct EqualConstraintCommand {
|
||||||
step: Step,
|
step: Step,
|
||||||
picked_entity: Option<EntityType>,
|
picked_entity: Option<EntityType>,
|
||||||
others: Vec<Handle>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EqualConstraintCommand {
|
impl EqualConstraintCommand {
|
||||||
@@ -39,7 +38,6 @@ impl EqualConstraintCommand {
|
|||||||
Self {
|
Self {
|
||||||
step: Step::First { multiple: false },
|
step: Step::First { multiple: false },
|
||||||
picked_entity: None,
|
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<ParametricRef> {
|
|
||||||
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 {
|
fn report(message: &str) -> CmdResult {
|
||||||
CmdResult::ReportError(message.to_string())
|
CmdResult::ReportError(message.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish(first: ParametricRef, others: Vec<ParametricRef>) -> CmdResult {
|
fn finish(first: ParametricRef, others: Vec<ParametricRef>, multiple: bool) -> CmdResult {
|
||||||
CmdResult::AddEqualConstraint {
|
CmdResult::AddEqualConstraint {
|
||||||
first,
|
first,
|
||||||
others,
|
others,
|
||||||
|
multiple,
|
||||||
label: "Equal constraint",
|
label: "Equal constraint",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,13 +89,7 @@ impl CadCommand for EqualConstraintCommand {
|
|||||||
}
|
}
|
||||||
Step::First { multiple: true } => "GCEQUAL Select first object:".to_string(),
|
Step::First { multiple: true } => "GCEQUAL Select first object:".to_string(),
|
||||||
Step::Second(_) => "GCEQUAL Select second object:".to_string(),
|
Step::Second(_) => "GCEQUAL Select second object:".to_string(),
|
||||||
Step::Others(_) if self.others.is_empty() => {
|
Step::Others(_) => "GCEQUAL Select object to make equal to first:".to_string(),
|
||||||
"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()
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,13 +119,17 @@ impl CadCommand for EqualConstraintCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn needs_entity_pick(&self) -> bool {
|
fn needs_entity_pick(&self) -> bool {
|
||||||
!matches!(self.step, Step::Others(_))
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn entity_pick_accepts_points(&self) -> bool {
|
fn entity_pick_accepts_points(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn typed_point_picks_entity(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
fn entity_pick_highlights_hover(&self) -> bool {
|
fn entity_pick_highlights_hover(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -174,21 +142,6 @@ impl CadCommand for EqualConstraintCommand {
|
|||||||
self.picked_entity = Some(entity);
|
self.picked_entity = Some(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_selection_gathering(&self) -> bool {
|
|
||||||
matches!(self.step, Step::Others(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> 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 {
|
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
|
||||||
if handle.is_null() {
|
if handle.is_null() {
|
||||||
return Self::report(Self::NO_OBJECT);
|
return Self::report(Self::NO_OBJECT);
|
||||||
@@ -208,27 +161,23 @@ impl CadCommand for EqualConstraintCommand {
|
|||||||
self.step = Step::Others(reference);
|
self.step = Step::Others(reference);
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
Step::Second(first) => {
|
Step::Second(first) | Step::Others(first) if first == reference => {
|
||||||
if first == reference {
|
Self::report(Self::SAME_OBJECT)
|
||||||
return Self::report(Self::SAME_OBJECT);
|
|
||||||
}
|
|
||||||
Self::finish(first, vec![reference])
|
|
||||||
}
|
}
|
||||||
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 {
|
fn on_point(&mut self, _point: DVec3) -> CmdResult {
|
||||||
Self::report(Self::NO_OBJECT)
|
Self::report(Self::NO_OBJECT)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
match self.step {
|
match self.step {
|
||||||
Step::Others(first) if !self.others.is_empty() => Self::finish(
|
// Multiple ends with its summary line; the host prints it.
|
||||||
first,
|
Step::Others(first) => Self::finish(first, Vec::new(), true),
|
||||||
self.others.iter().map(|handle| ParametricRef::whole(*handle)).collect(),
|
|
||||||
),
|
|
||||||
_ => CmdResult::Cancel,
|
_ => CmdResult::Cancel,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -261,26 +210,38 @@ mod tests {
|
|||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
));
|
));
|
||||||
command.inject_picked_entity(line());
|
command.inject_picked_entity(line());
|
||||||
let CmdResult::AddEqualConstraint { first, others, .. } =
|
let CmdResult::AddEqualConstraint {
|
||||||
command.on_entity_pick(Handle::new(8), DVec3::ZERO)
|
first,
|
||||||
|
others,
|
||||||
|
multiple,
|
||||||
|
..
|
||||||
|
} = command.on_entity_pick(Handle::new(8), DVec3::ZERO)
|
||||||
else {
|
else {
|
||||||
panic!("the second pick must create the constraint");
|
panic!("the second pick must create the constraint");
|
||||||
};
|
};
|
||||||
assert_eq!(first, ParametricRef::whole(Handle::new(7)));
|
assert_eq!(first, ParametricRef::whole(Handle::new(7)));
|
||||||
assert_eq!(others, vec![ParametricRef::whole(Handle::new(8))]);
|
assert_eq!(others, vec![ParametricRef::whole(Handle::new(8))]);
|
||||||
|
assert!(!multiple);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn multiple_applies_the_gathered_set_on_enter() {
|
fn multiple_applies_each_pick_and_ends_on_enter() {
|
||||||
let mut command = EqualConstraintCommand::new();
|
let mut command = EqualConstraintCommand::new();
|
||||||
assert!(matches!(command.on_text_input("M"), Some(CmdResult::NeedPoint)));
|
assert!(matches!(command.on_text_input("M"), Some(CmdResult::NeedPoint)));
|
||||||
command.inject_picked_entity(line());
|
command.inject_picked_entity(line());
|
||||||
command.on_entity_pick(Handle::new(7), DVec3::ZERO);
|
command.on_entity_pick(Handle::new(7), DVec3::ZERO);
|
||||||
assert!(command.is_selection_gathering());
|
command.inject_picked_entity(line());
|
||||||
command.on_selection_complete(vec![Handle::new(7), Handle::new(8), Handle::new(9)]);
|
let CmdResult::AddEqualConstraint {
|
||||||
let CmdResult::AddEqualConstraint { others, .. } = command.on_enter() else {
|
others, multiple, ..
|
||||||
panic!("Enter must apply the set");
|
} = 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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user