forked from 1b-agent/opencadstudio-upstream
Merge pull request #1352 from ramox81/codex/symmetric-constraint-parity
Complete Symmetric constraint behavior
This commit is contained in:
commit
da02e38208
11 files changed
+755
-68
No files matched your search
Generated
+2
-2
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
});
|
||||
@@ -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
@@ -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();
|
||||
(
|
||||
|
||||
Reference in new issue
Block a user