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]]
|
[[package]]
|
||||||
name = "cadkernel"
|
name = "cadkernel"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"acadrust",
|
"acadrust",
|
||||||
"cavalier_contours",
|
"cavalier_contours",
|
||||||
@@ -933,7 +933,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cadkernel-constraints"
|
name = "cadkernel-constraints"
|
||||||
version = "0.1.0"
|
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 = [
|
dependencies = [
|
||||||
"nalgebra",
|
"nalgebra",
|
||||||
]
|
]
|
||||||
|
|||||||
+2
-2
@@ -34,8 +34,8 @@ clap = { version = "4", features = ["derive", "string"] }
|
|||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "8a28c21", features = ["serde"] }
|
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 = { git = "https://github.com/ramox81/cadkernel.git", rev = "0493b59", features = ["acis", "offset"] }
|
||||||
cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "d2a288b" }
|
cadkernel-constraints = { git = "https://github.com/ramox81/cadkernel.git", rev = "0493b59" }
|
||||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
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,
|
entity, target.handle, target.grip_id,
|
||||||
)).unwrap_or_default()
|
)).unwrap_or_default()
|
||||||
}).collect();
|
}).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(
|
let solved = self.tabs[i].scene.solve_parametric_constraints_preview(
|
||||||
&touched, &driven_refs,
|
&touched,
|
||||||
self.constraint_solve_mode && !driven_refs.is_empty(), &self.grip_originals,
|
&driven_refs,
|
||||||
|
retain_size,
|
||||||
|
&self.grip_originals,
|
||||||
);
|
);
|
||||||
for (handle, entity) in solved {
|
for (handle, entity) in solved {
|
||||||
if let Some(slot) = self.tabs[i].scene.document.get_entity_mut(handle) {
|
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);
|
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 {
|
CmdResult::AddPerpendicularConstraint {
|
||||||
first,
|
first,
|
||||||
second,
|
second,
|
||||||
|
|||||||
+33
-20
@@ -1618,27 +1618,40 @@ impl OpenCADStudio {
|
|||||||
"SYCONSTRAINT" => {
|
"SYCONSTRAINT" => {
|
||||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||||
if handles.is_empty() {
|
if handles.is_empty() {
|
||||||
use crate::modules::draw::select::SelectObjectsCommand;
|
use crate::modules::parametric::SymmetricConstraintCommand;
|
||||||
let sel = SelectObjectsCommand::new(cmd);
|
let command = SymmetricConstraintCommand::new();
|
||||||
self.command_line.push_info(&sel.prompt());
|
self.command_line.push_info(&command.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(sel));
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||||
} 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.",
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
use crate::command::CmdResult;
|
use crate::command::{
|
||||||
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef};
|
CmdResult, SymmetricConstraintSelection,
|
||||||
return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint {
|
};
|
||||||
kind: ConstraintKind::Symmetric,
|
use crate::modules::parametric::SymmetricConstraintCommand;
|
||||||
refs: vec![
|
|
||||||
ParametricRef::center(handles[0]),
|
let refs = (handles.len() == 3).then(|| {
|
||||||
ParametricRef::center(handles[1]),
|
let first = self.tabs[i].scene.document.get_entity(handles[0])?;
|
||||||
ParametricRef::whole(handles[2]),
|
let second = self.tabs[i].scene.document.get_entity(handles[1])?;
|
||||||
],
|
let axis = self.tabs[i].scene.document.get_entity(handles[2])?;
|
||||||
driving_param: None,
|
SymmetricConstraintCommand::preselected_refs(
|
||||||
label: "Symmetric constraint",
|
(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],
|
handles: &[Handle],
|
||||||
driven_refs: &[crate::scene::parametric_constraints::ParametricRef],
|
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(
|
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,
|
i: usize,
|
||||||
handles: &[Handle],
|
handles: &[Handle],
|
||||||
driven_refs: &[crate::scene::parametric_constraints::ParametricRef],
|
driven_refs: &[crate::scene::parametric_constraints::ParametricRef],
|
||||||
|
retain_size: bool,
|
||||||
retained_originals: &[(Handle, acadrust::EntityType)],
|
retained_originals: &[(Handle, acadrust::EntityType)],
|
||||||
) {
|
) {
|
||||||
let mut context_object_changed = false;
|
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(
|
.bump_entities_with_parametric_originals(
|
||||||
&changes,
|
&changes,
|
||||||
driven_refs,
|
driven_refs,
|
||||||
self.constraint_solve_mode && !driven_refs.is_empty(),
|
retain_size,
|
||||||
retained_originals,
|
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;
|
self.tabs[i].properties.active_field = None;
|
||||||
let handles = self.property_target_handles(i);
|
let handles = self.property_target_handles(i);
|
||||||
if !handles.is_empty() {
|
if !handles.is_empty() {
|
||||||
let driven_marker = match field {
|
let driven_reference = match field {
|
||||||
"start_x" | "start_y" | "start_z" => Some(0),
|
"start_x" | "start_y" | "start_z" => Some(Some(0)),
|
||||||
"end_x" | "end_y" | "end_z" => Some(1),
|
"end_x" | "end_y" | "end_z" => Some(Some(1)),
|
||||||
"center_x" | "center_y" | "center_z" => Some(-3),
|
"center_x" | "center_y" | "center_z" => Some(Some(-3)),
|
||||||
|
"radius" | "diameter" | "circumference" | "area"
|
||||||
|
| "major_r" | "minor_r" | "ratio" => Some(None),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let retained_originals: Vec<_> = if self.constraint_solve_mode
|
let retain_size = self.constraint_solve_mode
|
||||||
&& driven_marker.is_some()
|
&& driven_reference.is_some_and(|marker| marker.is_some());
|
||||||
{
|
let retained_originals: Vec<_> = if retain_size {
|
||||||
handles
|
handles
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|handle| {
|
.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()
|
.into_iter()
|
||||||
.flat_map(|marker| {
|
.flat_map(|marker| {
|
||||||
handles.iter().copied().map(move |handle| {
|
handles.iter().copied().map(move |handle| match marker {
|
||||||
crate::scene::parametric_constraints::ParametricRef::point(
|
Some(marker) => {
|
||||||
handle, marker,
|
crate::scene::parametric_constraints::ParametricRef::point(
|
||||||
)
|
handle, marker,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
crate::scene::parametric_constraints::ParametricRef::whole(
|
||||||
|
handle,
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -3616,6 +3625,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
i,
|
i,
|
||||||
&handles,
|
&handles,
|
||||||
&driven_refs,
|
&driven_refs,
|
||||||
|
retain_size,
|
||||||
&retained_originals,
|
&retained_originals,
|
||||||
);
|
);
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
|
|||||||
@@ -1324,6 +1324,18 @@ pub enum HorizontalConstraintSelection {
|
|||||||
Points(CoincidentPick, CoincidentPick),
|
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.
|
/// Construction options shared by SWEEP creation and its live preview.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct SweepOptions {
|
pub struct SweepOptions {
|
||||||
@@ -1578,6 +1590,13 @@ pub enum CmdResult {
|
|||||||
direction: acadrust::types::Vector3,
|
direction: acadrust::types::Vector3,
|
||||||
label: &'static str,
|
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
|
/// Adds an ordered perpendicular relation. The first picked direction and
|
||||||
/// the second direction's start point stay fixed during the initial solve;
|
/// the second direction's start point stay fixed during the initial solve;
|
||||||
/// those temporary anchors are not persisted as geometric constraints.
|
/// those temporary anchors are not persisted as geometric constraints.
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ mod horizontal_command;
|
|||||||
mod perpendicular_command;
|
mod perpendicular_command;
|
||||||
mod point_on_entity;
|
mod point_on_entity;
|
||||||
mod smooth;
|
mod smooth;
|
||||||
|
#[path = "symmetric.rs"]
|
||||||
|
mod symmetric_command;
|
||||||
#[path = "tangent.rs"]
|
#[path = "tangent.rs"]
|
||||||
mod tangent_command;
|
mod tangent_command;
|
||||||
mod tools;
|
mod tools;
|
||||||
@@ -28,6 +30,7 @@ pub use point_on_entity::{
|
|||||||
center_point_tool, midpoint_tool, point_on_curve_tool, PointOnEntityConstraintCommand,
|
center_point_tool, midpoint_tool, point_on_curve_tool, PointOnEntityConstraintCommand,
|
||||||
};
|
};
|
||||||
pub use smooth::SmoothConstraintCommand;
|
pub use smooth::SmoothConstraintCommand;
|
||||||
|
pub use symmetric_command::SymmetricConstraintCommand;
|
||||||
pub use tangent_command::TangentConstraintCommand;
|
pub use tangent_command::TangentConstraintCommand;
|
||||||
pub use tools::{
|
pub use tools::{
|
||||||
colinear, concentric as concentric_tool, equal, fixed, horizontal, normal, parallel,
|
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 {
|
acadrust::EntityType::Arc(_) => match grip_id {
|
||||||
0 => vec![ParametricRef::center(handle)],
|
0 => vec![ParametricRef::center(handle)],
|
||||||
1 => vec![ParametricRef::point(handle, 0)],
|
1..=3 => vec![ParametricRef::whole(handle)],
|
||||||
2 => vec![ParametricRef::point(handle, 1)],
|
_ => 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(),
|
_ => Vec::new(),
|
||||||
},
|
},
|
||||||
acadrust::EntityType::Circle(_) if grip_id == 0 => {
|
|
||||||
vec![ParametricRef::center(handle)]
|
|
||||||
}
|
|
||||||
acadrust::EntityType::Point(_)
|
acadrust::EntityType::Point(_)
|
||||||
| acadrust::EntityType::Insert(_)
|
| acadrust::EntityType::Insert(_)
|
||||||
| acadrust::EntityType::Text(_)
|
| acadrust::EntityType::Text(_)
|
||||||
@@ -304,11 +310,11 @@ pub enum ConstraintKind {
|
|||||||
/// For the supported entity types, equal curvature is already covered
|
/// For the supported entity types, equal curvature is already covered
|
||||||
/// by the circle/circle radius branch of `Equal`.
|
/// by the circle/circle radius branch of `Equal`.
|
||||||
EqualDistance,
|
EqualDistance,
|
||||||
/// Two circles/arcs are mirror images of each other across a line —
|
/// Two points or compatible curves are symmetric across a line.
|
||||||
/// `refs`: `[center(a), center(b), whole(mirror_line)]`. Scoped to the
|
/// Point mode stores two point refs plus the axis. Object mode stores two
|
||||||
/// circle-center-pair case (unambiguous with whole-entity selection);
|
/// whole/segment refs plus the axis and constrains the complete relevant
|
||||||
/// point-symmetry about a point, and symmetry between two lines, aren't
|
/// geometry: line direction, circular center/radius, or ellipse
|
||||||
/// modeled.
|
/// center/axes.
|
||||||
Symmetric,
|
Symmetric,
|
||||||
/// A circle/arc's diameter (twice `Radius`'s target) — `refs`:
|
/// A circle/arc's diameter (twice `Radius`'s target) — `refs`:
|
||||||
/// `[whole(circle_or_arc)]`. Same DWG class as `Radius`
|
/// `[whole(circle_or_arc)]`. Same DWG class as `Radius`
|
||||||
@@ -951,6 +957,10 @@ fn glyph_placement_for_reference(
|
|||||||
let anchor = arc.midpoint_wcs();
|
let anchor = arc.midpoint_wcs();
|
||||||
Some((anchor, anchor - center))
|
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)) => {
|
(acadrust::EntityType::Line(line), Some(marker)) => {
|
||||||
let anchor = resolve_point(entity, marker)?;
|
let anchor = resolve_point(entity, marker)?;
|
||||||
let direction = anchor - line_midpoint(line);
|
let direction = anchor - line_midpoint(line);
|
||||||
@@ -983,7 +993,10 @@ fn glyph_placements(
|
|||||||
document: &acadrust::CadDocument,
|
document: &acadrust::CadDocument,
|
||||||
constraint: &ParametricConstraint,
|
constraint: &ParametricConstraint,
|
||||||
) -> Vec<(Vector3, Vector3)> {
|
) -> Vec<(Vector3, Vector3)> {
|
||||||
if constraint.kind == ConstraintKind::Parallel {
|
if matches!(
|
||||||
|
constraint.kind,
|
||||||
|
ConstraintKind::Parallel | ConstraintKind::Symmetric
|
||||||
|
) {
|
||||||
return constraint
|
return constraint
|
||||||
.refs
|
.refs
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1659,7 +1672,7 @@ impl super::Scene {
|
|||||||
})
|
})
|
||||||
.flat_map(|c| {
|
.flat_map(|c| {
|
||||||
let is_conflicting = set.conflicts.iter().any(|(id, _)| *id == c.id);
|
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)
|
glyph_label(c)
|
||||||
} else {
|
} else {
|
||||||
c.kind.glyph_symbol().to_string()
|
c.kind.glyph_symbol().to_string()
|
||||||
@@ -1682,7 +1695,22 @@ impl super::Scene {
|
|||||||
.collect();
|
.collect();
|
||||||
glyph_placements(&self.document, c)
|
glyph_placements(&self.document, c)
|
||||||
.into_iter()
|
.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(
|
let screen = crate::scene::pick::grip::project_rte(
|
||||||
glam::DVec3::new(anchor.x, anchor.y, anchor.z),
|
glam::DVec3::new(anchor.x, anchor.y, anchor.z),
|
||||||
view_rot,
|
view_rot,
|
||||||
@@ -1706,7 +1734,7 @@ impl super::Scene {
|
|||||||
c.id,
|
c.id,
|
||||||
point,
|
point,
|
||||||
direction.to_array(),
|
direction.to_array(),
|
||||||
label.clone(),
|
label,
|
||||||
is_conflicting,
|
is_conflicting,
|
||||||
hover_points.clone(),
|
hover_points.clone(),
|
||||||
))
|
))
|
||||||
|
|||||||
+127
-14
@@ -17,7 +17,7 @@ use cadkernel_constraints::constraints::curve_generic::{BoundedArcValue, CurveVa
|
|||||||
use cadkernel_constraints::constraints::point_line::{
|
use cadkernel_constraints::constraints::point_line::{
|
||||||
CenterOfGravity, Difference, Equal, EqualLineLength, MidpointOnLine, P2PDistance,
|
CenterOfGravity, Difference, Equal, EqualLineLength, MidpointOnLine, P2PDistance,
|
||||||
Parallel as ParallelConstraint, Perpendicular as PerpendicularConstraint, PointOnLine,
|
Parallel as ParallelConstraint, Perpendicular as PerpendicularConstraint, PointOnLine,
|
||||||
ProjectedDistance, ProjectedDistanceAlongLine,
|
ProjectedDistance, ProjectedDistanceAlongLine, SymmetricLineDirections,
|
||||||
};
|
};
|
||||||
use cadkernel_constraints::constraints::Constraint;
|
use cadkernel_constraints::constraints::Constraint;
|
||||||
use cadkernel_constraints::geo::{
|
use cadkernel_constraints::geo::{
|
||||||
@@ -1363,18 +1363,86 @@ fn build_constraint(
|
|||||||
let [a, b, m] = c.refs.as_slice() else {
|
let [a, b, m] = c.refs.as_slice() else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let (Some(pa), Some(pb), Some(mirror)) = (
|
let Some(mirror) = whole_line(sys, cache, *m) else {
|
||||||
point_ref(sys, cache, *a),
|
return Vec::new();
|
||||||
point_ref(sys, cache, *b),
|
};
|
||||||
whole_line(sys, cache, *m),
|
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 {
|
) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let pair = GLine { p1: pa, p2: pb };
|
match (
|
||||||
vec![
|
as_circle_or_line(first, *a),
|
||||||
Rc::new(MidpointOnLine::new(pair, mirror)),
|
as_circle_or_line(second, *b),
|
||||||
Rc::new(PerpendicularConstraint::new(sys.store(), pair, mirror)),
|
) {
|
||||||
]
|
(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 => {
|
ConstraintKind::Fixed => {
|
||||||
let Some(r) = c.refs.first() else {
|
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
|
// Ordered two-object constraints use the first pick as a reference for
|
||||||
// their initial placement. Build temporary Fixed equations into this
|
// their initial placement. Temporary Fixed equations stay out of `owner`
|
||||||
// solve only; they are deliberately omitted from `owner` and from the
|
// and the persistent native graph. Symmetric references are handled as
|
||||||
// persistent constraint set/native graph.
|
// exact driven anchors below, so they are excluded from this approximate
|
||||||
for reference in initial_fixed_refs {
|
// 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 {
|
let temporary = ParametricConstraint {
|
||||||
id: ConstraintId::MAX,
|
id: ConstraintId::MAX,
|
||||||
kind: ConstraintKind::Fixed,
|
kind: ConstraintKind::Fixed,
|
||||||
@@ -2375,6 +2461,18 @@ fn solve_scope(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut anchors = driven_refs.to_vec();
|
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 {
|
if !retain_lengths {
|
||||||
let mut axis_lines: Vec<_> = line_axes.into_iter().filter_map(|(reference, vertical)| {
|
let mut axis_lines: Vec<_> = line_axes.into_iter().filter_map(|(reference, vertical)| {
|
||||||
let geometry = cache.get(&reference.entity)?;
|
let geometry = cache.get(&reference.entity)?;
|
||||||
@@ -2501,6 +2599,9 @@ fn solve_scope(
|
|||||||
if initial_fixed_refs
|
if initial_fixed_refs
|
||||||
.iter()
|
.iter()
|
||||||
.any(|reference| reference.entity == *handle && reference.marker.is_none())
|
.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;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -2659,6 +2760,18 @@ fn solve_scope(
|
|||||||
let EntityGeom::Ellipse(el) = geom else {
|
let EntityGeom::Ellipse(el) = geom else {
|
||||||
continue;
|
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 (cx, cy, fx, fy) = {
|
||||||
let store = sys.store();
|
let store = sys.store();
|
||||||
(
|
(
|
||||||
|
|||||||
Reference in new issue
Block a user