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

Generated
+2 -2
View File
@@ -922,7 +922,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cadkernel"
version = "0.1.0"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=d2a288b#d2a288b5f397ad7941719119f216a048d6cdcc6f"
source = "git+https://github.com/ramox81/cadkernel.git?rev=0493b59#0493b59a06980e3b5b07bf3c73df0725a2e56c86"
dependencies = [
"acadrust",
"cavalier_contours",
@@ -933,7 +933,7 @@ dependencies = [
[[package]]
name = "cadkernel-constraints"
version = "0.1.0"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=d2a288b#d2a288b5f397ad7941719119f216a048d6cdcc6f"
source = "git+https://github.com/ramox81/cadkernel.git?rev=0493b59#0493b59a06980e3b5b07bf3c73df0725a2e56c86"
dependencies = [
"nalgebra",
]
+2 -2
View File
@@ -34,8 +34,8 @@ clap = { version = "4", features = ["derive", "string"] }
env_logger = "0.11"
log = "0.4"
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "8a28c21", features = ["serde"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "d2a288b", features = ["acis", "offset"] }
cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "d2a288b" }
cadkernel = { git = "https://github.com/ramox81/cadkernel.git", rev = "0493b59", features = ["acis", "offset"] }
cadkernel-constraints = { git = "https://github.com/ramox81/cadkernel.git", rev = "0493b59" }
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
flate2 = "1"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
+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;
+19
View File
@@ -1324,6 +1324,18 @@ pub enum HorizontalConstraintSelection {
Points(CoincidentPick, CoincidentPick),
}
/// The two input forms accepted by the Symmetric geometric constraint.
/// Object picks retain their curve or segment references; point picks are
/// resolved by the host against the live document.
#[derive(Clone, Copy, Debug)]
pub enum SymmetricConstraintSelection {
Objects(
crate::scene::parametric_constraints::ParametricRef,
crate::scene::parametric_constraints::ParametricRef,
),
Points(CoincidentPick, CoincidentPick),
}
/// Construction options shared by SWEEP creation and its live preview.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SweepOptions {
@@ -1578,6 +1590,13 @@ pub enum CmdResult {
direction: acadrust::types::Vector3,
label: &'static str,
},
/// Adds a point or object symmetry relation around a picked line. The
/// first reference and axis remain fixed during initial placement.
AddSymmetricConstraint {
selection: SymmetricConstraintSelection,
axis: crate::scene::parametric_constraints::ParametricRef,
label: &'static str,
},
/// Adds an ordered perpendicular relation. The first picked direction and
/// the second direction's start point stay fixed during the initial solve;
/// those temporary anchors are not persisted as geometric constraints.
+3
View File
@@ -13,6 +13,8 @@ mod horizontal_command;
mod perpendicular_command;
mod point_on_entity;
mod smooth;
#[path = "symmetric.rs"]
mod symmetric_command;
#[path = "tangent.rs"]
mod tangent_command;
mod tools;
@@ -28,6 +30,7 @@ pub use point_on_entity::{
center_point_tool, midpoint_tool, point_on_curve_tool, PointOnEntityConstraintCommand,
};
pub use smooth::SmoothConstraintCommand;
pub use symmetric_command::SymmetricConstraintCommand;
pub use tangent_command::TangentConstraintCommand;
pub use tools::{
colinear, concentric as concentric_tool, equal, fixed, horizontal, normal, parallel,
+360
View File
@@ -0,0 +1,360 @@
use acadrust::{EntityType, Handle};
use glam::DVec3;
use crate::command::{
CadCommand, CmdOption, CmdResult, CoincidentPick, SymmetricConstraintSelection,
};
use crate::scene::parametric_constraints::{is_parametric_point_near, ParametricRef};
#[derive(Clone, Copy, PartialEq, Eq)]
enum ObjectFamily {
Line,
Circular,
Ellipse,
}
#[derive(Clone, Copy)]
struct ObjectPick {
reference: ParametricRef,
family: ObjectFamily,
}
#[derive(Clone, Copy)]
enum Step {
ObjectOrTwoPoints,
SecondObject(ObjectPick),
FirstPoint,
SecondPoint(CoincidentPick),
ObjectAxis(ObjectPick, ObjectPick),
PointAxis(CoincidentPick, CoincidentPick),
}
pub struct SymmetricConstraintCommand {
step: Step,
picked_entity: Option<EntityType>,
}
impl SymmetricConstraintCommand {
pub fn new() -> Self {
Self {
step: Step::ObjectOrTwoPoints,
picked_entity: None,
}
}
fn point_pick(handle: Option<Handle>, point: DVec3) -> CoincidentPick {
CoincidentPick {
handle,
point,
whole_curve: false,
}
}
fn segment_count(entity: &EntityType) -> Option<usize> {
match entity {
EntityType::LwPolyline(polyline) => Some(if polyline.is_closed {
polyline.vertices.len()
} else {
polyline.vertices.len().saturating_sub(1)
}),
EntityType::Polyline2D(polyline) => Some(if polyline.is_closed() {
polyline.vertices.len()
} else {
polyline.vertices.len().saturating_sub(1)
}),
_ => None,
}
}
fn segment_family(entity: &EntityType, index: usize) -> Option<ObjectFamily> {
let bulge = match entity {
EntityType::LwPolyline(polyline) => polyline.vertices.get(index)?.bulge,
EntityType::Polyline2D(polyline) => polyline.vertices.get(index)?.bulge,
_ => return None,
};
Some(if bulge.abs() <= 1.0e-12 {
ObjectFamily::Line
} else {
ObjectFamily::Circular
})
}
fn picked_segment(entity: &EntityType, point: DVec3) -> Option<(usize, ObjectFamily)> {
let planar = crate::entities::curve::entity_curve(entity)?;
let local = planar.plane.project(point.to_array())?;
let segments = planar.curve.segments();
let (index, _) = cadkernel::geom2d::nearest_of(segments.iter(), local)?;
Some((index, Self::segment_family(entity, index)?))
}
fn picked_object(entity: &EntityType, handle: Handle, point: DVec3) -> Option<ObjectPick> {
let (reference, family) = match entity {
EntityType::Line(_) => (ParametricRef::whole(handle), ObjectFamily::Line),
EntityType::Circle(_) | EntityType::Arc(_) => {
(ParametricRef::whole(handle), ObjectFamily::Circular)
}
EntityType::Ellipse(_) => (ParametricRef::whole(handle), ObjectFamily::Ellipse),
EntityType::LwPolyline(_) | EntityType::Polyline2D(_) => {
let (index, family) = Self::picked_segment(entity, point)?;
(ParametricRef::segment(handle, index), family)
}
_ => return None,
};
Some(ObjectPick { reference, family })
}
fn preselected_object(entity: &EntityType, handle: Handle) -> Option<ObjectPick> {
match entity {
EntityType::Line(_) => Some(ObjectPick {
reference: ParametricRef::whole(handle),
family: ObjectFamily::Line,
}),
EntityType::Circle(_) | EntityType::Arc(_) => Some(ObjectPick {
reference: ParametricRef::whole(handle),
family: ObjectFamily::Circular,
}),
EntityType::Ellipse(_) => Some(ObjectPick {
reference: ParametricRef::whole(handle),
family: ObjectFamily::Ellipse,
}),
EntityType::LwPolyline(_) | EntityType::Polyline2D(_)
if Self::segment_count(entity) == Some(1) =>
{
Some(ObjectPick {
reference: ParametricRef::segment(handle, 0),
family: Self::segment_family(entity, 0)?,
})
}
_ => None,
}
}
fn picked_axis(entity: &EntityType, handle: Handle, point: DVec3) -> Option<ParametricRef> {
match entity {
EntityType::Line(_) => Some(ParametricRef::whole(handle)),
EntityType::LwPolyline(_) | EntityType::Polyline2D(_) => {
let (index, family) = Self::picked_segment(entity, point)?;
(family == ObjectFamily::Line).then_some(ParametricRef::segment(handle, index))
}
_ => None,
}
}
fn preselected_axis(entity: &EntityType, handle: Handle) -> Option<ParametricRef> {
match entity {
EntityType::Line(_) => Some(ParametricRef::whole(handle)),
EntityType::LwPolyline(_) | EntityType::Polyline2D(_)
if Self::segment_count(entity) == Some(1)
&& Self::segment_family(entity, 0) == Some(ObjectFamily::Line) =>
{
Some(ParametricRef::segment(handle, 0))
}
_ => None,
}
}
pub fn preselected_refs(
first: (&EntityType, Handle),
second: (&EntityType, Handle),
axis: (&EntityType, Handle),
) -> Option<[ParametricRef; 3]> {
let first = Self::preselected_object(first.0, first.1)?;
let second = Self::preselected_object(second.0, second.1)?;
let axis = Self::preselected_axis(axis.0, axis.1)?;
(first.family == second.family
&& first.reference != second.reference
&& axis.entity != first.reference.entity
&& axis.entity != second.reference.entity)
.then_some([first.reference, second.reference, axis])
}
fn invalid_object() -> CmdResult {
CmdResult::ReportError(
"Symmetric: select two compatible lines, circular curves, polyline segments, or ellipses."
.to_string(),
)
}
fn invalid_point() -> CmdResult {
CmdResult::ReportError(
"Symmetric: select an endpoint, center, midpoint, or polyline vertex.".to_string(),
)
}
fn invalid_axis() -> CmdResult {
CmdResult::ReportError("Symmetric: select a line as the symmetry axis.".to_string())
}
}
impl CadCommand for SymmetricConstraintCommand {
fn name(&self) -> &'static str {
"SYCONSTRAINT"
}
fn prompt(&self) -> String {
match self.step {
Step::ObjectOrTwoPoints => {
"Select first object or [2Points] <2Points>:".to_string()
}
Step::SecondObject(_) => "Select second object:".to_string(),
Step::FirstPoint => "Select first point:".to_string(),
Step::SecondPoint(_) => "Select second point:".to_string(),
Step::ObjectAxis(_, _) | Step::PointAxis(_, _) => {
"Select symmetry line:".to_string()
}
}
}
fn options(&self) -> Vec<CmdOption> {
matches!(self.step, Step::ObjectOrTwoPoints)
.then(|| vec![CmdOption::new("2Points", "2P")])
.unwrap_or_default()
}
fn wants_text_input(&self) -> bool {
true
}
fn point_step_accepts_keywords(&self) -> bool {
matches!(self.step, Step::ObjectOrTwoPoints)
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let keyword = text.trim().trim_start_matches('_').to_ascii_uppercase();
if matches!(self.step, Step::ObjectOrTwoPoints)
&& matches!(keyword.as_str(), "2" | "2P" | "2POINT" | "2POINTS")
{
self.step = Step::FirstPoint;
Some(CmdResult::NeedPoint)
} else {
None
}
}
fn needs_entity_pick(&self) -> bool {
true
}
fn entity_pick_accepts_points(&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 self.on_point(point);
}
let Some(entity) = self.picked_entity.take() else {
return CmdResult::NeedPoint;
};
match self.step {
Step::ObjectOrTwoPoints => {
let Some(first) = Self::picked_object(&entity, handle, point) else {
return Self::invalid_object();
};
self.step = Step::SecondObject(first);
CmdResult::NeedPoint
}
Step::SecondObject(first) => {
let Some(second) = Self::picked_object(&entity, handle, point) else {
return Self::invalid_object();
};
if first.family != second.family || first.reference == second.reference {
return Self::invalid_object();
}
self.step = Step::ObjectAxis(first, second);
CmdResult::NeedPoint
}
Step::FirstPoint | Step::SecondPoint(_) => {
let world = acadrust::types::Vector3::new(point.x, point.y, point.z);
if !is_parametric_point_near(&entity, world) {
return Self::invalid_point();
}
let picked = Self::point_pick(Some(handle), point);
match self.step {
Step::FirstPoint => {
self.step = Step::SecondPoint(picked);
CmdResult::NeedPoint
}
Step::SecondPoint(first) => {
self.step = Step::PointAxis(first, picked);
CmdResult::NeedPoint
}
_ => unreachable!(),
}
}
Step::ObjectAxis(first, second) => {
let Some(axis) = Self::picked_axis(&entity, handle, point) else {
return Self::invalid_axis();
};
if axis.entity == first.reference.entity || axis.entity == second.reference.entity {
return Self::invalid_axis();
}
CmdResult::AddSymmetricConstraint {
selection: SymmetricConstraintSelection::Objects(
first.reference,
second.reference,
),
axis,
label: "Symmetric constraint",
}
}
Step::PointAxis(first, second) => {
let Some(axis) = Self::picked_axis(&entity, handle, point) else {
return Self::invalid_axis();
};
if first.handle == Some(axis.entity) || second.handle == Some(axis.entity) {
return Self::invalid_axis();
}
CmdResult::AddSymmetricConstraint {
selection: SymmetricConstraintSelection::Points(first, second),
axis,
label: "Symmetric constraint",
}
}
}
}
fn on_point(&mut self, point: DVec3) -> CmdResult {
let picked = Self::point_pick(None, point);
match self.step {
Step::ObjectOrTwoPoints | Step::FirstPoint => {
self.step = Step::SecondPoint(picked);
CmdResult::NeedPoint
}
Step::SecondPoint(first) => {
self.step = Step::PointAxis(first, picked);
CmdResult::NeedPoint
}
_ => CmdResult::NeedPoint,
}
}
fn on_enter(&mut self) -> CmdResult {
if matches!(self.step, Step::ObjectOrTwoPoints) {
self.step = Step::FirstPoint;
CmdResult::NeedPoint
} else {
CmdResult::Cancel
}
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
inventory::submit!(crate::command::CommandRegistration {
names: &["SYCONSTRAINT"]
});
+42 -14
View File
@@ -231,13 +231,19 @@ pub(crate) fn grip_solve_anchor_refs(
}
acadrust::EntityType::Arc(_) => match grip_id {
0 => vec![ParametricRef::center(handle)],
1 => vec![ParametricRef::point(handle, 0)],
2 => vec![ParametricRef::point(handle, 1)],
1..=3 => vec![ParametricRef::whole(handle)],
_ => Vec::new(),
},
acadrust::EntityType::Circle(_) => match grip_id {
0 => vec![ParametricRef::center(handle)],
1..=4 => vec![ParametricRef::whole(handle)],
_ => Vec::new(),
},
acadrust::EntityType::Ellipse(_) => match grip_id {
0 => vec![ParametricRef::center(handle)],
1..=6 => vec![ParametricRef::whole(handle)],
_ => Vec::new(),
},
acadrust::EntityType::Circle(_) if grip_id == 0 => {
vec![ParametricRef::center(handle)]
}
acadrust::EntityType::Point(_)
| acadrust::EntityType::Insert(_)
| acadrust::EntityType::Text(_)
@@ -304,11 +310,11 @@ pub enum ConstraintKind {
/// For the supported entity types, equal curvature is already covered
/// by the circle/circle radius branch of `Equal`.
EqualDistance,
/// Two circles/arcs are mirror images of each other across a line —
/// `refs`: `[center(a), center(b), whole(mirror_line)]`. Scoped to the
/// circle-center-pair case (unambiguous with whole-entity selection);
/// point-symmetry about a point, and symmetry between two lines, aren't
/// modeled.
/// Two points or compatible curves are symmetric across a line.
/// Point mode stores two point refs plus the axis. Object mode stores two
/// whole/segment refs plus the axis and constrains the complete relevant
/// geometry: line direction, circular center/radius, or ellipse
/// center/axes.
Symmetric,
/// A circle/arc's diameter (twice `Radius`'s target) — `refs`:
/// `[whole(circle_or_arc)]`. Same DWG class as `Radius`
@@ -951,6 +957,10 @@ fn glyph_placement_for_reference(
let anchor = arc.midpoint_wcs();
Some((anchor, anchor - center))
}
(acadrust::EntityType::Ellipse(ellipse), None | Some(-3)) => {
let anchor = ellipse.center + ellipse.major_axis;
Some((anchor, ellipse.major_axis))
}
(acadrust::EntityType::Line(line), Some(marker)) => {
let anchor = resolve_point(entity, marker)?;
let direction = anchor - line_midpoint(line);
@@ -983,7 +993,10 @@ fn glyph_placements(
document: &acadrust::CadDocument,
constraint: &ParametricConstraint,
) -> Vec<(Vector3, Vector3)> {
if constraint.kind == ConstraintKind::Parallel {
if matches!(
constraint.kind,
ConstraintKind::Parallel | ConstraintKind::Symmetric
) {
return constraint
.refs
.iter()
@@ -1659,7 +1672,7 @@ impl super::Scene {
})
.flat_map(|c| {
let is_conflicting = set.conflicts.iter().any(|(id, _)| *id == c.id);
let label = if show_values {
let base_label = if show_values {
glyph_label(c)
} else {
c.kind.glyph_symbol().to_string()
@@ -1682,7 +1695,22 @@ impl super::Scene {
.collect();
glyph_placements(&self.document, c)
.into_iter()
.filter_map(|(anchor, outward)| {
.enumerate()
.filter_map(|(index, (anchor, outward))| {
let label = if c.kind == ConstraintKind::Symmetric {
if index == 2 {
"S│".to_string()
} else if c.refs.get(index).is_some_and(|reference| {
reference.marker.is_some()
&& reference.segment_index().is_none()
}) {
"S•".to_string()
} else {
"S◇".to_string()
}
} else {
base_label.clone()
};
let screen = crate::scene::pick::grip::project_rte(
glam::DVec3::new(anchor.x, anchor.y, anchor.z),
view_rot,
@@ -1706,7 +1734,7 @@ impl super::Scene {
c.id,
point,
direction.to_array(),
label.clone(),
label,
is_conflicting,
hover_points.clone(),
))
+127 -14
View File
@@ -17,7 +17,7 @@ use cadkernel_constraints::constraints::curve_generic::{BoundedArcValue, CurveVa
use cadkernel_constraints::constraints::point_line::{
CenterOfGravity, Difference, Equal, EqualLineLength, MidpointOnLine, P2PDistance,
Parallel as ParallelConstraint, Perpendicular as PerpendicularConstraint, PointOnLine,
ProjectedDistance, ProjectedDistanceAlongLine,
ProjectedDistance, ProjectedDistanceAlongLine, SymmetricLineDirections,
};
use cadkernel_constraints::constraints::Constraint;
use cadkernel_constraints::geo::{
@@ -1363,18 +1363,86 @@ fn build_constraint(
let [a, b, m] = c.refs.as_slice() else {
return Vec::new();
};
let (Some(pa), Some(pb), Some(mirror)) = (
point_ref(sys, cache, *a),
point_ref(sys, cache, *b),
whole_line(sys, cache, *m),
let Some(mirror) = whole_line(sys, cache, *m) else {
return Vec::new();
};
if let (Some(pa), Some(pb)) =
(point_ref(sys, cache, *a), point_ref(sys, cache, *b))
{
let pair = GLine { p1: pa, p2: pb };
return vec![
Rc::new(MidpointOnLine::new(pair, mirror)),
Rc::new(PerpendicularConstraint::new(sys.store(), pair, mirror)),
];
}
let (Some(first), Some(second)) = (
resolve_ref(document, sys, cache, *a),
resolve_ref(document, sys, cache, *b),
) else {
return Vec::new();
};
let pair = GLine { p1: pa, p2: pb };
vec![
Rc::new(MidpointOnLine::new(pair, mirror)),
Rc::new(PerpendicularConstraint::new(sys.store(), pair, mirror)),
]
match (
as_circle_or_line(first, *a),
as_circle_or_line(second, *b),
) {
(CircleOrLine::Line(first), CircleOrLine::Line(second)) => {
vec![Rc::new(SymmetricLineDirections::new(
sys.store(),
first,
second,
mirror,
))]
}
(CircleOrLine::Circle(first), CircleOrLine::Circle(second)) => {
let centers = GLine {
p1: first.center,
p2: second.center,
};
vec![
Rc::new(MidpointOnLine::new(centers, mirror)),
Rc::new(PerpendicularConstraint::new(
sys.store(),
centers,
mirror,
)),
Rc::new(Equal::new(second.rad, first.rad, 1.0)),
]
}
(CircleOrLine::Ellipse(first), CircleOrLine::Ellipse(second)) => {
let centers = GLine {
p1: first.center,
p2: second.center,
};
let first_axis = GLine {
p1: first.center,
p2: first.focus1,
};
let second_axis = GLine {
p1: second.center,
p2: second.focus1,
};
vec![
Rc::new(MidpointOnLine::new(centers, mirror)),
Rc::new(PerpendicularConstraint::new(
sys.store(),
centers,
mirror,
)),
Rc::new(SymmetricLineDirections::new(
sys.store(),
first_axis,
second_axis,
mirror,
)),
Rc::new(EqualMajorAxesConic::new(
GConic::Ellipse(first),
GConic::Ellipse(second),
)),
Rc::new(Equal::new(second.radmin, first.radmin, 1.0)),
]
}
_ => Vec::new(),
}
}
ConstraintKind::Fixed => {
let Some(r) = c.refs.first() else {
@@ -2332,10 +2400,28 @@ fn solve_scope(
}
// Ordered two-object constraints use the first pick as a reference for
// their initial placement. Build temporary Fixed equations into this
// solve only; they are deliberately omitted from `owner` and from the
// persistent constraint set/native graph.
for reference in initial_fixed_refs {
// their initial placement. Temporary Fixed equations stay out of `owner`
// and the persistent native graph. Symmetric references are handled as
// exact driven anchors below, so they are excluded from this approximate
// equality-equation path.
let symmetric_constraints = constraints
.iter()
.filter(|constraint| {
constraint.enabled
&& constraint.kind == ConstraintKind::Symmetric
&& constraint.refs.len() == 3
})
.collect::<Vec<_>>();
let temporary_fixed_refs = initial_fixed_refs
.iter()
.copied()
.filter(|reference| {
!symmetric_constraints
.iter()
.any(|constraint| constraint.refs.contains(reference))
})
.collect::<Vec<_>>();
for reference in &temporary_fixed_refs {
let temporary = ParametricConstraint {
id: ConstraintId::MAX,
kind: ConstraintKind::Fixed,
@@ -2375,6 +2461,18 @@ fn solve_scope(
}
let mut anchors = driven_refs.to_vec();
for constraint in &symmetric_constraints {
if let Some(axis) = constraint.refs.get(2).copied() {
if !anchors.contains(&axis) {
anchors.push(axis);
}
}
for reference in initial_fixed_refs {
if constraint.refs[..2].contains(reference) && !anchors.contains(reference) {
anchors.push(*reference);
}
}
}
if !retain_lengths {
let mut axis_lines: Vec<_> = line_axes.into_iter().filter_map(|(reference, vertical)| {
let geometry = cache.get(&reference.entity)?;
@@ -2501,6 +2599,9 @@ fn solve_scope(
if initial_fixed_refs
.iter()
.any(|reference| reference.entity == *handle && reference.marker.is_none())
|| symmetric_constraints.iter().any(|constraint| {
constraint.refs.get(2).is_some_and(|axis| axis.entity == *handle)
})
{
continue;
}
@@ -2659,6 +2760,18 @@ fn solve_scope(
let EntityGeom::Ellipse(el) = geom else {
continue;
};
let symmetric = set.constraints.iter().any(|constraint| {
constraint.enabled
&& constraint.kind == ConstraintKind::Symmetric
&& constraint.refs.get(..2).is_some_and(|references| {
references.iter().any(|reference| {
reference.entity == *handle && reference.marker.is_none()
})
})
});
if symmetric {
continue;
}
let (cx, cy, fx, fy) = {
let store = sys.store();
(