Merge pull request #1352 from ramox81/codex/symmetric-constraint-parity

Complete Symmetric constraint behavior
This commit is contained in:
Hakan Seven committed 2026-09-18 18:52:41 +03:00
commit da02e38208
11 files changed
+755 -68

No files matched your search

+133 -2
View File
@@ -150,9 +150,14 @@ impl OpenCADStudio {
entity, target.handle, target.grip_id,
)).unwrap_or_default()
}).collect();
let retain_size = self.constraint_solve_mode
&& !driven_refs.is_empty()
&& driven_refs.iter().all(|reference| reference.marker.is_some());
let solved = self.tabs[i].scene.solve_parametric_constraints_preview(
&touched, &driven_refs,
self.constraint_solve_mode && !driven_refs.is_empty(), &self.grip_originals,
&touched,
&driven_refs,
retain_size,
&self.grip_originals,
);
for (handle, entity) in solved {
if let Some(slot) = self.tabs[i].scene.document.get_entity_mut(handle) {
@@ -3388,6 +3393,132 @@ impl OpenCADStudio {
self.commit_undo_delta(i, pd);
}
}
CmdResult::AddSymmetricConstraint {
selection,
axis,
label,
} => {
use crate::command::SymmetricConstraintSelection;
use crate::scene::parametric_constraints::ConstraintKind;
let scope = self.tabs[i].current_parametric_scope();
let to_world = |point: glam::DVec3| {
acadrust::types::Vector3::new(point.x, point.y, point.z)
};
let resolve_point = |pick: crate::command::CoincidentPick| {
if let Some(handle) = pick.handle {
crate::scene::parametric_constraints::nearest_parametric_point_on_entity(
&self.tabs[i].scene.document,
scope,
handle,
to_world(pick.point),
)
} else {
crate::scene::parametric_constraints::nearest_parametric_point(
&self.tabs[i].scene.document,
scope,
to_world(pick.point),
None,
)
}
};
let (first, second) = match selection {
SymmetricConstraintSelection::Objects(first, second) => (first, second),
SymmetricConstraintSelection::Points(first, second) => {
let (Some(first), Some(second)) =
(resolve_point(first), resolve_point(second))
else {
self.command_line.push_error(
"Symmetric: select supported endpoints, centers, midpoints, or vertices.",
);
return Task::none();
};
(first, second)
}
};
if first == second
|| axis.entity == first.entity
|| axis.entity == second.entity
{
self.command_line
.push_error("Symmetric: select two different references and a separate line axis.");
return Task::none();
}
let refs = vec![first, second, axis];
if let Err(message) = self.tabs[i].scene.validate_parametric_constraint(
ConstraintKind::Symmetric,
&refs,
None,
) {
self.command_line.push_error(message);
return Task::none();
}
let duplicate = self.tabs[i]
.scene
.parametric_constraint_set(scope)
.is_some_and(|set| {
set.constraints.iter().any(|constraint| {
constraint.enabled
&& constraint.kind == ConstraintKind::Symmetric
&& matches!(constraint.refs.as_slice(), [a, b, m]
if *m == axis
&& ((*a == first && *b == second)
|| (*a == second && *b == first)))
})
});
if duplicate {
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.command_line
.push_output("The Symmetric constraint already exists.");
return Task::none();
}
let constraints_before = self.tabs[i]
.scene
.parametric_constraint_set(scope)
.cloned()
.unwrap_or_else(|| {
crate::scene::parametric_constraints::ParametricConstraintSet::new(scope)
});
let mut touched = Vec::new();
for reference in &refs {
if !touched.contains(&reference.entity) {
touched.push(reference.entity);
}
}
let pending = self.begin_undo(i, label, touched.len(), true);
self.tabs[i]
.scene
.record_undo_parametric_constraints_before(scope, constraints_before);
let id = self.tabs[i].scene.parametric_constraint_set_mut(scope).add(
ConstraintKind::Symmetric,
refs,
None,
);
self.tabs[i].scene.note_parametric_constraint_applied(
scope,
id,
self.constraint_bar_display,
);
let changes = touched
.into_iter()
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
.collect::<Vec<_>>();
self.tabs[i].scene.bump_entities_with_initial_parametric_policy(
&changes,
&[first, axis],
false,
);
self.tabs[i].dirty = true;
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.command_line
.push_output("Symmetric constraint applied.");
self.refresh_properties();
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
CmdResult::AddPerpendicularConstraint {
first,
second,
+33 -20
View File
@@ -1618,27 +1618,40 @@ impl OpenCADStudio {
"SYCONSTRAINT" => {
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));
} else if handles.len() != 3 {
self.command_line.push_output(
"Select exactly three entities (two circles/arcs, then the mirror line), then run this constraint again.",
);
use crate::modules::parametric::SymmetricConstraintCommand;
let command = SymmetricConstraintCommand::new();
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
} else {
use crate::command::CmdResult;
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef};
return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint {
kind: ConstraintKind::Symmetric,
refs: vec![
ParametricRef::center(handles[0]),
ParametricRef::center(handles[1]),
ParametricRef::whole(handles[2]),
],
driving_param: None,
label: "Symmetric constraint",
}));
use crate::command::{
CmdResult, SymmetricConstraintSelection,
};
use crate::modules::parametric::SymmetricConstraintCommand;
let refs = (handles.len() == 3).then(|| {
let first = self.tabs[i].scene.document.get_entity(handles[0])?;
let second = self.tabs[i].scene.document.get_entity(handles[1])?;
let axis = self.tabs[i].scene.document.get_entity(handles[2])?;
SymmetricConstraintCommand::preselected_refs(
(first, handles[0]),
(second, handles[1]),
(axis, handles[2]),
)
}).flatten();
if let Some([first, second, axis]) = refs {
return Some(self.apply_cmd_result(CmdResult::AddSymmetricConstraint {
selection: SymmetricConstraintSelection::Objects(first, second),
axis,
label: "Symmetric constraint",
}));
}
self.tabs[i].scene.deselect_all();
self.command_line.push_error(
"Symmetric: select two compatible objects followed by a line axis.",
);
let command = SymmetricConstraintCommand::new();
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
}
+12 -2
View File
@@ -2791,7 +2791,16 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={}
handles: &[Handle],
driven_refs: &[crate::scene::parametric_constraints::ParametricRef],
) {
self.invalidate_property_targets_with_originals(i, handles, driven_refs, &[]);
let retain_size = self.constraint_solve_mode
&& !driven_refs.is_empty()
&& driven_refs.iter().all(|reference| reference.marker.is_some());
self.invalidate_property_targets_with_originals(
i,
handles,
driven_refs,
retain_size,
&[],
);
}
pub(super) fn invalidate_property_targets_with_originals(
@@ -2799,6 +2808,7 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={}
i: usize,
handles: &[Handle],
driven_refs: &[crate::scene::parametric_constraints::ParametricRef],
retain_size: bool,
retained_originals: &[(Handle, acadrust::EntityType)],
) {
let mut context_object_changed = false;
@@ -2835,7 +2845,7 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={}
.bump_entities_with_parametric_originals(
&changes,
driven_refs,
self.constraint_solve_mode && !driven_refs.is_empty(),
retain_size,
retained_originals,
);
}
+22 -12
View File
@@ -3263,15 +3263,17 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
self.tabs[i].properties.active_field = None;
let handles = self.property_target_handles(i);
if !handles.is_empty() {
let driven_marker = match field {
"start_x" | "start_y" | "start_z" => Some(0),
"end_x" | "end_y" | "end_z" => Some(1),
"center_x" | "center_y" | "center_z" => Some(-3),
let driven_reference = match field {
"start_x" | "start_y" | "start_z" => Some(Some(0)),
"end_x" | "end_y" | "end_z" => Some(Some(1)),
"center_x" | "center_y" | "center_z" => Some(Some(-3)),
"radius" | "diameter" | "circumference" | "area"
| "major_r" | "minor_r" | "ratio" => Some(None),
_ => None,
};
let retained_originals: Vec<_> = if self.constraint_solve_mode
&& driven_marker.is_some()
{
let retain_size = self.constraint_solve_mode
&& driven_reference.is_some_and(|marker| marker.is_some());
let retained_originals: Vec<_> = if retain_size {
handles
.iter()
.filter_map(|handle| {
@@ -3602,13 +3604,20 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
}
}
}
let driven_refs: Vec<_> = driven_marker
let driven_refs: Vec<_> = driven_reference
.into_iter()
.flat_map(|marker| {
handles.iter().copied().map(move |handle| {
crate::scene::parametric_constraints::ParametricRef::point(
handle, marker,
)
handles.iter().copied().map(move |handle| match marker {
Some(marker) => {
crate::scene::parametric_constraints::ParametricRef::point(
handle, marker,
)
}
None => {
crate::scene::parametric_constraints::ParametricRef::whole(
handle,
)
}
})
})
.collect();
@@ -3616,6 +3625,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
i,
&handles,
&driven_refs,
retain_size,
&retained_originals,
);
self.tabs[i].dirty = true;