Merge pull request #1332 from ramox81/codex/concentric-constraint-parity

# Conflicts:
#	src/entities/arc.rs
#	src/entities/ellipse.rs
#	src/modules/parametric/mod.rs
#	src/scene/parametric_solve.rs
#	src/ui/overlay.rs
This commit is contained in:
Hakan Seven committed 2026-09-17 17:24:14 +03:00
commit 16c417f81d
17 files changed
+692 -52

No files matched your search

Generated
+2 -2
View File
@@ -921,7 +921,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
[[package]]
name = "cadkernel"
version = "0.1.0"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=7e43598#7e435984d9ba32999d2471930c18449a2ab58d67"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=4815518#481551836494e581279fdbfbf8114921170ee75b"
dependencies = [
"acadrust",
"cavalier_contours",
@@ -932,7 +932,7 @@ dependencies = [
[[package]]
name = "cadkernel-constraints"
version = "0.1.0"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=7e43598#7e435984d9ba32999d2471930c18449a2ab58d67"
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=4815518#481551836494e581279fdbfbf8114921170ee75b"
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 = "7e43598", features = ["acis", "offset"] }
cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "7e43598" }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "4815518", features = ["acis", "offset"] }
cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "4815518" }
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
flate2 = "1"
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
+82
View File
@@ -3372,6 +3372,88 @@ impl OpenCADStudio {
self.commit_undo_delta(i, pd);
}
}
CmdResult::AddConcentricConstraint {
first,
second,
label,
} => {
use crate::scene::parametric_constraints::ConstraintKind;
let refs = vec![first, second];
if let Err(message) = self.tabs[i].scene.validate_parametric_constraint(
ConstraintKind::Concentric,
&refs,
None,
) {
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.command_line.push_error(message);
return Task::none();
}
let scope = self.tabs[i].current_parametric_scope();
let duplicate = self.tabs[i]
.scene
.parametric_constraint_set(scope)
.is_some_and(|set| {
set.constraints.iter().any(|constraint| {
constraint.enabled
&& constraint.kind == ConstraintKind::Concentric
&& matches!(constraint.refs.as_slice(), [a, b]
if (*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 Concentric 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 touched = if first.entity == second.entity {
vec![first.entity]
} else {
vec![first.entity, second.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::Concentric,
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],
true,
);
self.tabs[i].dirty = true;
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.command_line.push_output("Constraint applied.");
self.refresh_properties();
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
CmdResult::AddCoincidentConstraint {
first,
second,
+32 -20
View File
@@ -1496,29 +1496,41 @@ impl OpenCADStudio {
}
}
"NCONSTRAINT" => {
"NCONSTRAINT" | "GCCONCENTRIC" => {
use crate::command::CmdResult;
use crate::modules::parametric::ConcentricConstraintCommand;
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() != 2 {
self.command_line.push_output(
"Select exactly two circles/arcs, then run this constraint again.",
);
let command = ConcentricConstraintCommand::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::Concentric,
refs: vec![
ParametricRef::center(handles[0]),
ParametricRef::center(handles[1]),
],
driving_param: None,
label: "Concentric constraint",
}));
let refs = handles
.iter()
.filter_map(|handle| {
let entity = self.tabs[i].scene.document.get_entity(*handle)?;
ConcentricConstraintCommand::preselected_reference(entity, *handle)
})
.collect::<Vec<_>>();
if handles.len() == 2 && refs.len() == 2 && refs[0] != refs[1] {
return Some(self.apply_cmd_result(CmdResult::AddConcentricConstraint {
first: refs[0],
second: refs[1],
label: "Concentric constraint",
}));
}
self.tabs[i].scene.deselect_all();
self.command_line.push_error(
"Invalid selection for Concentric. Select a circle, arc, ellipse or polyline arc segment.",
);
let command = refs
.first()
.copied()
.map(ConcentricConstraintCommand::with_first)
.unwrap_or_else(ConcentricConstraintCommand::new);
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
}
+18
View File
@@ -834,6 +834,23 @@ bg={bg_ms:.1}ms n={view_count}"
.active_cmd
.as_ref()
.is_some_and(|cmd| !cmd.needs_entity_pick() && !cmd.is_selection_gathering());
let constraint_cursor_badge = if is_paper {
None
} else {
tab.scene.hover_highlight.and_then(|handle| {
tab.scene
.parametric_constraint_set(tab.current_parametric_scope())
.and_then(|set| {
set.constraints_touching(handle)
.find(|constraint| {
constraint.kind
== crate::scene::parametric_constraints::ConstraintKind::Concentric
})
.or_else(|| set.constraints_touching(handle).next())
})
.map(|constraint| constraint.kind.glyph_symbol().to_string())
})
};
let constraint_glyphs: Vec<(
iced::Point,
[f32; 2],
@@ -912,6 +929,7 @@ bg={bg_ms:.1}ms n={view_count}"
constraint_glyphs,
self.constraint_glyph_tooltip
.map(|kind| crate::t!(kind.label()).into_owned()),
constraint_cursor_badge,
)
};
+8
View File
@@ -1512,6 +1512,13 @@ pub enum CmdResult {
second: crate::scene::parametric_constraints::ParametricRef,
label: &'static str,
},
/// Adds an ordered concentric relation. The first picked center remains
/// fixed during the initial solve while the second curve moves rigidly.
AddConcentricConstraint {
first: crate::scene::parametric_constraints::ParametricRef,
second: crate::scene::parametric_constraints::ParametricRef,
label: &'static str,
},
/// Opens the Auto Constrain settings dialog from the selection prompt.
OpenAutoConstrainSettings,
/// Adds an ordered Coincident relation. Point/point selections create a
@@ -2778,6 +2785,7 @@ mod constraint_registry_tests {
"ECONSTRAINT",
"TCONSTRAINT",
"NCONSTRAINT",
"GCCONCENTRIC",
"NRCONSTRAINT",
"LCONSTRAINT",
"FXCONSTRAINT",
+21 -3
View File
@@ -212,9 +212,9 @@ fn properties(arc: &Arc) -> Vec<PropSection> {
ro(t!("Total angle").as_ref(), "total_angle", format_angle(total_angle.to_radians())),
ro(t!("Arc length").as_ref(), "arc_length", format_length(arc_length)),
ro(t!("Area").as_ref(), "area", format_area(area)),
ro(t!("Normal X").as_ref(), "normal_x", format!("{:.4}", arc.normal.x)),
ro(t!("Normal Y").as_ref(), "normal_y", format!("{:.4}", arc.normal.y)),
ro(t!("Normal Z").as_ref(), "normal_z", format!("{:.4}", arc.normal.z)),
edit(t!("Normal X").as_ref(), "normal_x", arc.normal.x),
edit(t!("Normal Y").as_ref(), "normal_y", arc.normal.y),
edit(t!("Normal Z").as_ref(), "normal_z", arc.normal.z),
],
}]
}
@@ -244,6 +244,24 @@ fn apply_geom_prop(arc: &mut Arc, field: &str, value: &str) {
"radius" if v > 0.0 => arc.radius = v,
"start_angle" => arc.start_angle = v.to_radians(),
"end_angle" => arc.end_angle = v.to_radians(),
"normal_x" | "normal_y" | "normal_z" => {
let center = arc.center_wcs();
let mut normal = glam::DVec3::new(arc.normal.x, arc.normal.y, arc.normal.z);
match field {
"normal_x" => normal.x = v,
"normal_y" => normal.y = v,
"normal_z" => normal.z = v,
_ => {}
}
if let Some(normal) = normal.try_normalize() {
arc.normal = acadrust::types::Vector3::new(normal.x, normal.y, normal.z);
let (x, y, z) = crate::scene::view::transform::wcs_point_to_ocs(
(center.x, center.y, center.z),
(normal.x, normal.y, normal.z),
);
arc.center = acadrust::types::Vector3::new(x, y, z);
}
}
_ => {}
}
}
+22 -4
View File
@@ -3,7 +3,7 @@ use crate::t;
use crate::command::EntityTransform;
use crate::entities::common::{
center_grip, edit_prop as edit, parse_f64, ro_prop as ro, square_grip,
center_grip, edit_prop as edit, parse_f64, square_grip,
};
use crate::entities::traits::RenderConvertible;
use crate::scene::convert::acad_to_render::{extrusion_wall_tris, RenderEntity, RenderObject};
@@ -169,9 +169,9 @@ fn properties(circle: &Circle) -> Vec<PropSection> {
edit(t!("Diameter").as_ref(), "diameter", r * 2.0),
edit(t!("Circumference").as_ref(), "circumference", 2.0 * PI * r),
edit(t!("Area").as_ref(), "area", PI * r * r),
ro(t!("Normal X").as_ref(), "normal_x", format!("{:.4}", circle.normal.x)),
ro(t!("Normal Y").as_ref(), "normal_y", format!("{:.4}", circle.normal.y)),
ro(t!("Normal Z").as_ref(), "normal_z", format!("{:.4}", circle.normal.z)),
edit(t!("Normal X").as_ref(), "normal_x", circle.normal.x),
edit(t!("Normal Y").as_ref(), "normal_y", circle.normal.y),
edit(t!("Normal Z").as_ref(), "normal_z", circle.normal.z),
],
}]
}
@@ -202,6 +202,24 @@ fn apply_geom_prop(circle: &mut Circle, field: &str, value: &str) {
"diameter" if v > 0.0 => circle.radius = v / 2.0,
"circumference" if v > 0.0 => circle.radius = v / (2.0 * PI),
"area" if v > 0.0 => circle.radius = (v / PI).sqrt(),
"normal_x" | "normal_y" | "normal_z" => {
let center = circle.center_wcs();
let mut normal = glam::DVec3::new(circle.normal.x, circle.normal.y, circle.normal.z);
match field {
"normal_x" => normal.x = v,
"normal_y" => normal.y = v,
"normal_z" => normal.z = v,
_ => {}
}
if let Some(normal) = normal.try_normalize() {
circle.normal = acadrust::types::Vector3::new(normal.x, normal.y, normal.z);
let (x, y, z) = crate::scene::view::transform::wcs_point_to_ocs(
(center.x, center.y, center.z),
(normal.x, normal.y, normal.z),
);
circle.center = acadrust::types::Vector3::new(x, y, z);
}
}
_ => {}
}
}
+35 -3
View File
@@ -187,9 +187,9 @@ fn properties(ell: &Ellipse) -> Vec<PropSection> {
ro(t!("Start parameter").as_ref(), "start_param", format!("{:.4}", ell.start_parameter)),
ro(t!("End parameter").as_ref(), "end_param", format!("{:.4}", ell.end_parameter)),
ro(t!("Length").as_ref(), "length", format_length(props.perimeter)),
ro(t!("Normal X").as_ref(), "normal_x", format!("{:.4}", ell.normal.x)),
ro(t!("Normal Y").as_ref(), "normal_y", format!("{:.4}", ell.normal.y)),
ro(t!("Normal Z").as_ref(), "normal_z", format!("{:.4}", ell.normal.z)),
edit(t!("Normal X").as_ref(), "normal_x", ell.normal.x),
edit(t!("Normal Y").as_ref(), "normal_y", ell.normal.y),
edit(t!("Normal Z").as_ref(), "normal_z", ell.normal.z),
],
}]
}
@@ -220,6 +220,38 @@ fn apply_geom_prop(ell: &mut Ellipse, field: &str, value: &str) {
"ratio" if v > 0.0 => ell.minor_axis_ratio = v,
"start_angle" => ell.start_parameter = v.to_radians(),
"end_angle" => ell.end_parameter = v.to_radians(),
"normal_x" | "normal_y" | "normal_z" => {
let mut normal = glam::DVec3::new(ell.normal.x, ell.normal.y, ell.normal.z);
match field {
"normal_x" => normal.x = v,
"normal_y" => normal.y = v,
"normal_z" => normal.z = v,
_ => {}
}
if let Some(normal) = normal.try_normalize() {
let current = glam::DVec3::new(
ell.major_axis.x,
ell.major_axis.y,
ell.major_axis.z,
);
let length = current.length();
let projected = current - normal * current.dot(normal);
let major = projected.try_normalize().or_else(|| {
let helper = if normal.x.abs() < 0.8 {
glam::DVec3::X
} else {
glam::DVec3::Y
};
normal.cross(helper).try_normalize()
});
if let Some(major) = major {
ell.normal = acadrust::types::Vector3::new(normal.x, normal.y, normal.z);
let major = major * length;
ell.major_axis =
acadrust::types::Vector3::new(major.x, major.y, major.z);
}
}
}
_ => {}
}
}
+158
View File
@@ -0,0 +1,158 @@
use acadrust::{EntityType, Handle};
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
use crate::scene::parametric_constraints::ParametricRef;
use crate::t;
pub struct ConcentricConstraintCommand {
first: Option<ParametricRef>,
picked_entity: Option<EntityType>,
}
impl ConcentricConstraintCommand {
pub fn new() -> Self {
Self {
first: None,
picked_entity: None,
}
}
pub fn with_first(first: ParametricRef) -> Self {
Self {
first: Some(first),
picked_entity: None,
}
}
fn invalid_selection() -> CmdResult {
CmdResult::ReportError(
t!("Invalid selection for Concentric. Select a circle, arc, ellipse or polyline arc segment.")
.into_owned(),
)
}
fn polyline_segment_is_arc(entity: &EntityType, index: usize) -> bool {
match entity {
EntityType::LwPolyline(polyline) => polyline
.vertices
.get(index)
.is_some_and(|vertex| vertex.bulge.abs() > 1.0e-12),
EntityType::Polyline2D(polyline) => polyline
.vertices
.get(index)
.is_some_and(|vertex| vertex.bulge.abs() > 1.0e-12),
_ => false,
}
}
fn picked_reference(entity: &EntityType, handle: Handle, point: DVec3) -> Option<ParametricRef> {
match entity {
EntityType::Circle(_) | EntityType::Arc(_) | EntityType::Ellipse(_) => {
Some(ParametricRef::center(handle))
}
EntityType::LwPolyline(_) | EntityType::Polyline2D(_) => {
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)?;
matches!(segments.get(index), Some(cadkernel::geom2d::Curve::Arc(_)))
.then(|| ParametricRef::segment_center(handle, index))
}
_ => None,
}
}
pub fn preselected_reference(entity: &EntityType, handle: Handle) -> Option<ParametricRef> {
match entity {
EntityType::Circle(_) | EntityType::Arc(_) | EntityType::Ellipse(_) => {
Some(ParametricRef::center(handle))
}
EntityType::LwPolyline(polyline) => {
let segment_count = if polyline.is_closed {
polyline.vertices.len()
} else {
polyline.vertices.len().saturating_sub(1)
};
(segment_count == 1 && Self::polyline_segment_is_arc(entity, 0))
.then(|| ParametricRef::segment_center(handle, 0))
}
EntityType::Polyline2D(polyline) => {
let segment_count = if polyline.is_closed() {
polyline.vertices.len()
} else {
polyline.vertices.len().saturating_sub(1)
};
(segment_count == 1 && Self::polyline_segment_is_arc(entity, 0))
.then(|| ParametricRef::segment_center(handle, 0))
}
_ => None,
}
}
}
impl CadCommand for ConcentricConstraintCommand {
fn name(&self) -> &'static str {
"GCCONCENTRIC"
}
fn prompt(&self) -> String {
if self.first.is_some() {
t!("GCCONCENTRIC Select second object:").into_owned()
} else {
t!("GCCONCENTRIC Select first object:").into_owned()
}
}
fn needs_entity_pick(&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 CmdResult::NeedPoint;
}
let Some(entity) = self.picked_entity.take() else {
return CmdResult::NeedPoint;
};
let Some(reference) = Self::picked_reference(&entity, handle, point) else {
return Self::invalid_selection();
};
let Some(first) = self.first else {
self.first = Some(reference);
return CmdResult::NeedPoint;
};
if first == reference {
return Self::invalid_selection();
}
CmdResult::AddConcentricConstraint {
first,
second: reference,
label: "Concentric constraint",
}
}
fn on_point(&mut self, _point: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
+1 -1
View File
@@ -21,7 +21,7 @@ impl GeomConstraintCommand {
"T" | "TANGENT" => "TCONSTRAINT",
"SM" | "SMOOTH" => "GCSMOOTH",
"C" | "COINCIDENT" => "GCCOINCIDENT",
"CON" | "CONCENTRIC" => "NCONSTRAINT",
"CON" | "CONCENTRIC" => "GCCONCENTRIC",
"COL" | "COLLINEAR" | "COLINEAR" => "LCONSTRAINT",
"SY" | "SYMMETRIC" => "SYCONSTRAINT",
"E" | "EQUAL" => "ECONSTRAINT",
+6 -4
View File
@@ -3,6 +3,7 @@
//! resulting constraints through the geometry kernel.
mod coincident;
mod concentric;
mod equal_distance;
mod geom_constraint;
#[path = "perpendicular.rs"]
@@ -14,6 +15,7 @@ mod tangent_command;
mod tools;
mod value;
pub use coincident::{coincident_tool, CoincidentConstraintCommand};
pub use concentric::ConcentricConstraintCommand;
pub use equal_distance::{equal_distance_tool, EqualDistanceConstraintCommand};
pub use geom_constraint::GeomConstraintCommand;
pub use perpendicular_command::{PerpendicularConstraintCommand, PerpendicularPick};
@@ -23,8 +25,8 @@ pub use point_on_entity::{
pub use smooth::SmoothConstraintCommand;
pub use tangent_command::TangentConstraintCommand;
pub use tools::{
colinear, concentric, equal, fixed, horizontal, normal, parallel, perpendicular, symmetric,
tangent, vertical,
colinear, concentric as concentric_tool, equal, fixed, horizontal, normal, parallel,
perpendicular, symmetric, tangent, vertical,
};
pub use value::{
angle_tool, dimensional_tools, distance_tool, AngleConstraintCommand,
@@ -67,7 +69,7 @@ impl CadModule for ParametricModule {
"GCSMOOTH", "Smooth",
include_bytes!("../../../assets/icons/constrain/smooth.svg"),
)),
RibbonItem::LargeTool(concentric::tool()),
RibbonItem::LargeTool(concentric_tool::tool()),
RibbonItem::LargeTool(horizontal::tool()),
RibbonItem::LargeTool(symmetric::tool()),
RibbonItem::LargeTool(fixed::tool()),
@@ -170,7 +172,7 @@ mod tests {
groups[0].tools.iter().map(item_id).collect::<Vec<_>>(),
[
"AUTOCONSTRAIN", "CCONSTRAINT", "PCONSTRAINT", "TCONSTRAINT",
"LCONSTRAINT", "QCONSTRAINT", "GCSMOOTH", "NCONSTRAINT",
"LCONSTRAINT", "QCONSTRAINT", "GCSMOOTH", "GCCONCENTRIC",
"HCONSTRAINT", "SYCONSTRAINT", "FXCONSTRAINT", "VCONSTRAINT",
"ECONSTRAINT", "GCVISIBILITY", "GCSHOWALL", "GCHIDEALL",
]
+3 -2
View File
@@ -88,12 +88,12 @@ pub mod concentric {
use super::*;
pub fn tool() -> ToolDef {
ToolDef {
id: "NCONSTRAINT",
id: "GCCONCENTRIC",
label: "Concentric",
icon: IconKind::Svg(include_bytes!(
"../../../assets/icons/constrain/concentric.svg"
)),
event: ModuleEvent::Command("NCONSTRAINT".to_string()),
event: ModuleEvent::Command("GCCONCENTRIC".to_string()),
}
}
}
@@ -168,6 +168,7 @@ inventory::submit!(crate::command::CommandRegistration {
"ECONSTRAINT",
"TCONSTRAINT",
"NCONSTRAINT",
"GCCONCENTRIC",
"NRCONSTRAINT",
"LCONSTRAINT",
"FXCONSTRAINT",
+13 -1
View File
@@ -454,11 +454,17 @@ impl<'a> GroupBuilder<'a> {
return Some(*existing);
}
let segment_midpoint = ParametricRef::point(handle, marker).segment_midpoint_index();
let segment_center = ParametricRef::point(handle, marker).segment_center_index();
let polyline = matches!(
self.document.get_entity(handle),
Some(EntityType::LwPolyline(_) | EntityType::Polyline2D(_))
);
let (curve_id, point_type) = if let Some(segment) = segment_midpoint {
let (curve_id, point_type) = if let Some(segment) = segment_center {
(
self.segment_node(handle, segment)?,
implicit_point_type::CENTER,
)
} else if let Some(segment) = segment_midpoint {
(
self.segment_node(handle, segment)?,
implicit_point_type::MID,
@@ -551,6 +557,9 @@ impl<'a> GroupBuilder<'a> {
if r.segment_midpoint_index().is_some() {
return self.point_node(r.entity, r.marker?);
}
if r.segment_center_index().is_some() {
return self.point_node(r.entity, r.marker?);
}
if let Some(segment) = r.segment_index() {
return self.segment_node(r.entity, segment);
}
@@ -2079,6 +2088,9 @@ pub(super) fn native_constraint_set(
implicit_point_type::MID => {
ParametricRef::segment_midpoint(curve.entity, segment)
}
implicit_point_type::CENTER => {
ParametricRef::segment_center(curve.entity, segment)
}
_ => continue,
}
} else {
+43 -7
View File
@@ -14,8 +14,8 @@ use acadrust::types::{Handle, Vector3};
/// ordered per-entity-type point list when non-negative (0/1 = a line's
/// start/end, ...), or names a special case when negative (-3 = a
/// circle/arc's center; -2 = a bounded curve's midpoint). Polyline segment
/// and segment-midpoint references use private negative ranges so they stay
/// distinct from vertex markers.
/// segment-midpoint, and curved-segment-center references use private
/// negative ranges so they stay distinct from vertex markers.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ParametricRef {
pub entity: Handle,
@@ -28,6 +28,7 @@ pub struct ParametricRef {
const POLYLINE_SEGMENT_MARKER_BASE: i32 = -1_000_000;
const POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE: i32 = -2_000_000;
const POLYLINE_SEGMENT_CENTER_MARKER_BASE: i32 = -3_000_000;
const ELLIPSE_MAJOR_AXIS_MARKER: i32 = -4;
const ELLIPSE_MINOR_AXIS_MARKER: i32 = -5;
const TEXT_BASELINE_MARKER: i32 = -6;
@@ -88,10 +89,25 @@ impl ParametricRef {
pub fn segment_midpoint_index(self) -> Option<usize> {
let marker = self.marker?;
(marker <= POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE)
(marker <= POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE
&& marker > POLYLINE_SEGMENT_CENTER_MARKER_BASE)
.then(|| (POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE - marker) as usize)
}
/// Select the center of one curved polyline segment.
pub fn segment_center(entity: Handle, index: usize) -> Self {
Self {
entity,
marker: Some(POLYLINE_SEGMENT_CENTER_MARKER_BASE - index as i32),
}
}
pub fn segment_center_index(self) -> Option<usize> {
let marker = self.marker?;
(marker <= POLYLINE_SEGMENT_CENTER_MARKER_BASE)
.then(|| (POLYLINE_SEGMENT_CENTER_MARKER_BASE - marker) as usize)
}
/// Select the displayed baseline of a Text or MText entity.
pub fn text_baseline(entity: Handle) -> Self {
Self {
@@ -503,7 +519,7 @@ impl ParametricConstraintSet {
/// endpoint's equivalent of `dimension_assoc::resolve_reference`, restricted
/// to the marker conventions constraint endpoints actually use (whole-entity
/// `None`, an ordinary `source_points()` index, the `-3` center case, or a
/// bounded curve/segment midpoint).
/// bounded curve/segment midpoint, or a curved polyline-segment center).
///
/// Solver-side registration reads raw entity fields directly. This helper is
/// for UI-side consumers that need the current world-space position.
@@ -524,12 +540,20 @@ pub(crate) fn resolve_point(entity: &acadrust::EntityType, marker: i32) -> Optio
let point = curve.point_at(0.5);
return Some(Vector3::new(point[0], point[1], point[2]));
}
let segment = ParametricRef {
let reference = ParametricRef {
entity: Handle::NULL,
marker: Some(marker),
};
if let Some(segment) = reference.segment_center_index() {
let planar = crate::entities::curve::entity_curve(entity)?;
let curve = planar.curve.segments().into_iter().nth(segment)?;
let cadkernel::geom2d::Curve::Arc(arc) = curve else {
return None;
};
let point = planar.plane.point_at(arc.centre);
return Some(Vector3::new(point[0], point[1], point[2]));
}
.segment_midpoint_index();
if let Some(segment) = segment {
if let Some(segment) = reference.segment_midpoint_index() {
let planar = crate::entities::curve::entity_curve(entity)?;
let curve = planar.curve.segments().into_iter().nth(segment)?;
let point = planar.plane.point_at(curve.point_at(0.5));
@@ -831,6 +855,18 @@ fn glyph_placement_for_reference(
let anchor = (start + end) * 0.5;
return Some((anchor, segment_normal(start, end)));
}
if let Some(segment) = r.segment_center_index() {
let planar = crate::entities::curve::entity_curve(entity)?;
let curve = planar.curve.segments().into_iter().nth(segment)?;
let on_arc = planar.plane.point_at(curve.point_at(0.5));
let cadkernel::geom2d::Curve::Arc(arc) = curve else {
return None;
};
let center = planar.plane.point_at(arc.centre);
let center = Vector3::new(center[0], center[1], center[2]);
let on_arc = Vector3::new(on_arc[0], on_arc[1], on_arc[2]);
return Some((on_arc, on_arc - center));
}
if let Some(segment) = r.segment_index() {
let anchor = resolve_point(
entity,
+205 -1
View File
@@ -513,6 +513,12 @@ fn resolve_constraint_point(
if let Some(point) = geometry.point_for_marker(marker) {
return Some(point);
}
if let Some(segment) = reference
.segment_center_index()
.and_then(|index| geometry.arc_segment(index))
{
return Some(segment.arc.circle.center);
}
let midpoint_line = reference
.segment_midpoint_index()
@@ -794,6 +800,175 @@ fn refs_share_supported_plane(document: &acadrust::CadDocument, refs: &[Parametr
plane_z.is_some()
}
fn concentric_reference_geometry(
document: &acadrust::CadDocument,
reference: ParametricRef,
) -> Option<(Vector3, cadkernel::space::Plane)> {
let marker = reference.marker?;
if marker != -3 && reference.segment_center_index().is_none() {
return None;
}
let entity = document.get_entity(reference.entity)?;
let center = super::parametric_constraints::resolve_point(entity, marker)?;
let plane = crate::entities::curve::entity_curve(entity)?.plane;
Some((center, plane))
}
fn concentric_refs_share_plane(
document: &acadrust::CadDocument,
refs: &[ParametricRef],
) -> bool {
let [first, second] = refs else {
return false;
};
if first == second {
return false;
}
let Some((first_center, first_plane)) = concentric_reference_geometry(document, *first) else {
return false;
};
let Some((second_center, second_plane)) = concentric_reference_geometry(document, *second)
else {
return false;
};
let (Some(first_normal), Some(second_normal)) = (first_plane.normal(), second_plane.normal())
else {
return false;
};
let parallel = (first_normal[0] * second_normal[0]
+ first_normal[1] * second_normal[1]
+ first_normal[2] * second_normal[2])
.abs()
>= 1.0 - 1.0e-9;
let points = [
first_plane.origin,
second_plane.origin,
[first_center.x, first_center.y, first_center.z],
[second_center.x, second_center.y, second_center.z],
];
let tolerance = cadkernel::space::coplanarity_tolerance(&points);
parallel
&& first_plane
.distance_to(second_plane.origin)
.is_some_and(|distance| distance.abs() <= tolerance)
&& first_plane
.distance_to([second_center.x, second_center.y, second_center.z])
.is_some_and(|distance| distance.abs() <= tolerance)
}
fn ref_is_driven(driven: &[ParametricRef], reference: ParametricRef) -> bool {
driven.iter().any(|candidate| {
candidate.entity == reference.entity
&& (candidate.marker == reference.marker || candidate.marker.is_none())
})
}
fn apply_spatial_concentric_constraints(
document: &acadrust::CadDocument,
set: &ParametricConstraintSet,
driven_refs: &[ParametricRef],
initial_fixed_refs: &[ParametricRef],
results: &mut Vec<(Handle, EntityType)>,
) {
let current_entity = |handle: Handle, results: &[(Handle, EntityType)]| {
results
.iter()
.rev()
.find(|(candidate, _)| *candidate == handle)
.map(|(_, entity)| entity.clone())
.or_else(|| document.get_entity(handle).cloned())
};
let mut handles = Vec::<Handle>::new();
let mut relations = Vec::new();
let mut fixed = Vec::new();
let mut points = Vec::new();
for constraint in set.constraints.iter().filter(|constraint| {
constraint.enabled
&& constraint.kind == ConstraintKind::Concentric
&& !refs_share_supported_plane(document, &constraint.refs)
&& concentric_refs_share_plane(document, &constraint.refs)
}) {
let [first, second] = constraint.refs.as_slice() else {
continue;
};
if first.entity == second.entity {
continue;
}
let (Some(first_entity), Some(second_entity)) = (
current_entity(first.entity, results),
current_entity(second.entity, results),
) else {
continue;
};
let (Some(first_marker), Some(second_marker)) = (first.marker, second.marker) else {
continue;
};
let (Some(first_center), Some(second_center)) = (
super::parametric_constraints::resolve_point(&first_entity, first_marker),
super::parametric_constraints::resolve_point(&second_entity, second_marker),
) else {
continue;
};
let first_index = handles
.iter()
.position(|handle| *handle == first.entity)
.unwrap_or_else(|| {
handles.push(first.entity);
handles.len() - 1
});
let second_index = handles
.iter()
.position(|handle| *handle == second.entity)
.unwrap_or_else(|| {
handles.push(second.entity);
handles.len() - 1
});
let first_point = [first_center.x, first_center.y, first_center.z];
let second_point = [second_center.x, second_center.y, second_center.z];
relations.push((first_index, first_point, second_index, second_point));
points.extend([first_point, second_point]);
if ref_is_driven(driven_refs, *first) || ref_is_driven(initial_fixed_refs, *first) {
fixed.push(first_index);
}
if ref_is_driven(driven_refs, *second) || ref_is_driven(initial_fixed_refs, *second) {
fixed.push(second_index);
}
}
if relations.is_empty() {
return;
}
fixed.sort_unstable();
fixed.dedup();
let tolerance = cadkernel::space::coplanarity_tolerance(&points).max(MOVE_EPS);
let Some(translations) = cadkernel::space::solve_rigid_point_coincidence(
handles.len(),
&relations,
&fixed,
tolerance,
) else {
return;
};
for (handle, translation) in handles.into_iter().zip(translations) {
let translation = glam::DVec3::from_array(translation);
if translation.length() <= tolerance {
continue;
}
let Some(mut entity) = current_entity(handle, results) else {
continue;
};
crate::scene::view::dispatch::apply_transform(
&mut entity,
&crate::command::EntityTransform::Translate(translation),
);
if let Some((_, current)) = results.iter_mut().find(|(candidate, _)| *candidate == handle) {
*current = entity;
} else {
results.push((handle, entity));
}
}
}
fn resolved_target(params: &ParameterTable, constraint: &ParametricConstraint) -> Option<f64> {
let value = constraint.driving_param.as_ref()?.resolve(params).ok()?;
if !value.is_finite() {
@@ -1982,6 +2157,7 @@ fn polyline_ref_order(
}
let position = if let Some(index) = reference.segment_index()
.or_else(|| reference.segment_midpoint_index())
.or_else(|| reference.segment_center_index())
{
2 * index + 1
} else {
@@ -2133,7 +2309,13 @@ fn solve_scope(
let has_smooth = constraints.iter()
.any(|constraint| constraint.enabled && constraint.kind == ConstraintKind::Smooth);
if cache.is_empty() && !has_smooth {
let has_spatial_concentric = constraints.iter().any(|constraint| {
constraint.enabled
&& constraint.kind == ConstraintKind::Concentric
&& !refs_share_supported_plane(document, &constraint.refs)
&& concentric_refs_share_plane(document, &constraint.refs)
});
if cache.is_empty() && !has_smooth && !has_spatial_concentric {
return None;
}
@@ -3013,6 +3195,13 @@ fn solve_scope(
}
}
apply_smooth_constraints(document, set, &mut results);
apply_spatial_concentric_constraints(
document,
set,
driven_refs,
initial_fixed_refs,
&mut results,
);
Some((results, dof, conflicts))
}
@@ -3064,6 +3253,21 @@ impl Scene {
}
return Ok(());
}
if kind == ConstraintKind::Concentric {
if !concentric_refs_share_plane(&self.document, refs) {
return Err(
"Concentric requires two supported circular curves on the same plane.",
);
}
if !refs_share_supported_plane(&self.document, refs) {
if refs[0].entity == refs[1].entity {
return Err(
"Concentric cannot move two curved segments of the same object on this plane.",
);
}
return Ok(());
}
}
if !refs_share_supported_plane(&self.document, refs) {
return Err(
"Constraint references must be supported entities on the same world-XY plane.",
+41 -2
View File
@@ -84,6 +84,16 @@ fn draw_tangent_constraint_glyph(
frame.stroke(&canvas::Path::line(contact, tangent_end), stroke);
}
fn draw_concentric_constraint_glyph(
frame: &mut canvas::Frame,
center: Point,
color: Color,
) {
let stroke = canvas::Stroke::default().with_color(color).with_width(1.25);
frame.stroke(&canvas::Path::circle(center, 4.7), stroke.clone());
frame.stroke(&canvas::Path::circle(center, 2.15), stroke);
}
fn constraint_glyph_box(
anchor: Point,
outward: [f32; 2],
@@ -855,6 +865,7 @@ pub fn selection_overlay<'a>(
selection_visual: SelectionVisualOptions,
constraint_glyphs: Vec<(Point, [f32; 2], String, bool, bool, Vec<Point>)>,
constraint_glyph_tooltip: Option<String>,
constraint_cursor_badge: Option<String>,
) -> Element<'a, Message> {
canvas(SelectionCanvas {
selection,
@@ -880,6 +891,7 @@ pub fn selection_overlay<'a>(
selection_visual,
constraint_glyphs,
constraint_glyph_tooltip,
constraint_cursor_badge,
})
.width(Length::Fill)
.height(Length::Fill)
@@ -944,6 +956,9 @@ struct SelectionCanvas {
constraint_glyphs: Vec<(Point, [f32; 2], String, bool, bool, Vec<Point>)>,
/// Localized kind name made visible after the app-level hover dwell.
constraint_glyph_tooltip: Option<String>,
/// Symbol shown beside the cursor while it targets an entity that already
/// participates in an enabled geometric constraint.
constraint_cursor_badge: Option<String>,
}
fn draw_grip_marker(
@@ -1842,6 +1857,28 @@ impl canvas::Program<Message> for SelectionCanvas {
let hole = canvas::Path::circle(Point::new(bx + 6.0, by + 10.5), 1.4);
frame.fill(&hole, dark);
}
if let Some(label) = &self.constraint_cursor_badge {
let center = Point::new(cp.x + sq + if self.hover_locked { 30.0 } else { 12.0 }, cp.y - sq - 7.0);
let blue = Color::from_rgb8(35, 145, 230);
let badge = canvas::Path::circle(center, 8.5);
frame.fill(&badge, blue);
if label == "◎" {
draw_concentric_constraint_glyph(&mut frame, center, Color::WHITE);
} else if label == "T" {
draw_tangent_constraint_glyph(&mut frame, center, Color::WHITE);
} else {
frame.fill_text(canvas::Text {
content: label.clone(),
position: center,
color: Color::WHITE,
size: iced::Pixels(11.0),
align_x: iced::alignment::Horizontal::Center.into(),
align_y: iced::alignment::Vertical::Center,
shaping: iced::advanced::text::Shaping::Advanced,
..Default::default()
});
}
}
}
} // end !over_viewcube
@@ -1929,8 +1966,8 @@ impl canvas::Program<Message> for SelectionCanvas {
let hovered = cursor
.position_in(bounds)
.and_then(|point| constraint_glyph_hit_test(&glyphs_for_offsets, point));
let normal_bg = theme.palette().primary.base.color;
let normal_fg = theme.palette().primary.base.text;
let normal_bg = Color::from_rgb8(103, 109, 118);
let normal_fg = Color::WHITE;
// A redundant or conflicting constraint gets the danger palette
// instead of the
// ordinary primary one — same information a resolver panel
@@ -1981,6 +2018,8 @@ impl canvas::Program<Message> for SelectionCanvas {
draw_tangent_constraint_glyph(&mut frame, glyph_center, fg);
} else if label == "G²" {
draw_smooth_constraint_glyph(&mut frame, glyph_center, fg);
} else if label == "◎" {
draw_concentric_constraint_glyph(&mut frame, glyph_center, fg);
} else {
frame.fill_text(canvas::Text {
content: label.clone(),