From 80f2d5fd72208fefb0b23d70e72c680a15becff9 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 17 Sep 2026 17:39:01 +0300 Subject: [PATCH] fix: solve concentric geometry in kernel --- Cargo.lock | 4 +- Cargo.toml | 4 +- src/app/commands/draw.rs | 2 +- src/app/control/mod.rs | 2 +- src/command.rs | 1 - src/entities/arc.rs | 4 +- src/entities/circle.rs | 21 ++++++- src/entities/ellipse.rs | 55 +++++++++--------- src/modules/parametric/concentric.rs | 27 +++++++++ src/modules/parametric/tools.rs | 1 - src/scene/parametric_solve.rs | 83 ++++++++++++++++++++++++++++ 11 files changed, 165 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a7d869b8c..3b1aa45d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -921,7 +921,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=4815518#481551836494e581279fdbfbf8114921170ee75b" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=24fd86a#24fd86aafba83ee1d9a80dc6fd4f6d510d49eb30" 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=4815518#481551836494e581279fdbfbf8114921170ee75b" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=24fd86a#24fd86aafba83ee1d9a80dc6fd4f6d510d49eb30" dependencies = [ "nalgebra", ] diff --git a/Cargo.toml b/Cargo.toml index 67daaa70e..dbd55db8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 = "4815518", features = ["acis", "offset"] } -cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "4815518" } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "24fd86a", features = ["acis", "offset"] } +cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "24fd86a" } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index b82148f14..2806f6467 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -1496,7 +1496,7 @@ impl OpenCADStudio { } } - "NCONSTRAINT" | "GCCONCENTRIC" => { + "GCCONCENTRIC" => { use crate::command::CmdResult; use crate::modules::parametric::ConcentricConstraintCommand; diff --git a/src/app/control/mod.rs b/src/app/control/mod.rs index e846c849a..1f624ad96 100644 --- a/src/app/control/mod.rs +++ b/src/app/control/mod.rs @@ -231,7 +231,7 @@ fn command_category(name: &str) -> &'static str { "VCONSTRAINT", "ECONSTRAINT", "TCONSTRAINT", - "NCONSTRAINT", + "GCCONCENTRIC", "NRCONSTRAINT", "LCONSTRAINT", "DELCONSTRAINT", diff --git a/src/command.rs b/src/command.rs index bbba73fc1..16e75761f 100644 --- a/src/command.rs +++ b/src/command.rs @@ -2784,7 +2784,6 @@ mod constraint_registry_tests { "GCPERPENDICULAR", "ECONSTRAINT", "TCONSTRAINT", - "NCONSTRAINT", "GCCONCENTRIC", "NRCONSTRAINT", "LCONSTRAINT", diff --git a/src/entities/arc.rs b/src/entities/arc.rs index 9d30c7e29..c8315524a 100644 --- a/src/entities/arc.rs +++ b/src/entities/arc.rs @@ -246,14 +246,14 @@ fn apply_geom_prop(arc: &mut Arc, field: &str, value: &str) { "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); + let mut normal = cadkernel::space::Vec3::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() { + if let Some(normal) = normal.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), diff --git a/src/entities/circle.rs b/src/entities/circle.rs index cb3d4268e..5cef35386 100644 --- a/src/entities/circle.rs +++ b/src/entities/circle.rs @@ -204,14 +204,18 @@ fn apply_geom_prop(circle: &mut Circle, field: &str, value: &str) { "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); + let mut normal = cadkernel::space::Vec3::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() { + if let Some(normal) = normal.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), @@ -376,6 +380,19 @@ impl crate::entities::traits::MassPropsCalc for Circle { mod tests { use super::*; + #[test] + fn editing_normal_preserves_world_center() { + let mut circle = Circle::from_coords(2.0, 3.0, 4.0, 5.0); + let center = circle.center_wcs(); + + apply_geom_prop(&mut circle, "normal_x", "1"); + + assert!((circle.center_wcs() - center).length() < 1.0e-9); + let normal_length = + (circle.normal.x.powi(2) + circle.normal.y.powi(2) + circle.normal.z.powi(2)).sqrt(); + assert!((normal_length - 1.0).abs() < 1.0e-12); + } + #[test] fn quadrant_grips_offer_interactive_radius() { use crate::entities::traits::Grippable; diff --git a/src/entities/ellipse.rs b/src/entities/ellipse.rs index 71ef1d6bc..09560fd5c 100644 --- a/src/entities/ellipse.rs +++ b/src/entities/ellipse.rs @@ -221,36 +221,24 @@ fn apply_geom_prop(ell: &mut Ellipse, field: &str, value: &str) { "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); + let mut normal = [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, + "normal_x" => normal[0] = v, + "normal_y" => normal[1] = v, + "normal_z" => normal[2] = 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); - } - } + let Some(normal) = cadkernel::space::Vec3::from(normal).normalize() else { + return; + }; + let Some(major) = cadkernel::space::reorient_axis_to_plane( + [ell.major_axis.x, ell.major_axis.y, ell.major_axis.z], + normal.to_array(), + ) else { + return; + }; + ell.normal = acadrust::types::Vector3::new(normal.x, normal.y, normal.z); + ell.major_axis = acadrust::types::Vector3::new(major[0], major[1], major[2]); } _ => {} } @@ -456,6 +444,19 @@ mod grip_tests { (v.major_axis.x * v.major_axis.x + v.major_axis.y * v.major_axis.y).sqrt() } + #[test] + fn editing_normal_reorients_major_axis_in_kernel() { + let mut e = ell(10.0, 0.5); + + apply_geom_prop(&mut e, "normal_x", "1"); + + let normal = DVec3::new(e.normal.x, e.normal.y, e.normal.z); + let major = DVec3::new(e.major_axis.x, e.major_axis.y, e.major_axis.z); + assert!((normal.length() - 1.0).abs() < 1.0e-12); + assert!((major.length() - 10.0).abs() < 1.0e-9); + assert!(normal.dot(major).abs() < 1.0e-9); + } + #[test] fn minor_grip_below_major_updates_ratio_only() { let mut e = ell(10.0, 0.5); diff --git a/src/modules/parametric/concentric.rs b/src/modules/parametric/concentric.rs index 68bbb508f..448c3227c 100644 --- a/src/modules/parametric/concentric.rs +++ b/src/modules/parametric/concentric.rs @@ -156,3 +156,30 @@ impl CadCommand for ConcentricConstraintCommand { CmdResult::Cancel } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn polyline_pick_keeps_the_selected_arc_center() { + let handle = Handle::new(13); + let mut polyline = acadrust::entities::LwPolyline::new(); + polyline.vertices = vec![ + acadrust::entities::LwVertex::from_coords(0.0, 0.0), + acadrust::entities::LwVertex::with_bulge( + acadrust::types::Vector2::new(5.0, 0.0), + 1.0, + ), + acadrust::entities::LwVertex::from_coords(10.0, 0.0), + ]; + + let reference = ConcentricConstraintCommand::picked_reference( + &EntityType::LwPolyline(polyline), + handle, + DVec3::new(8.0, 2.0, 0.0), + ) + .expect("arc segment"); + assert_eq!(reference, ParametricRef::segment_center(handle, 1)); + } +} diff --git a/src/modules/parametric/tools.rs b/src/modules/parametric/tools.rs index d0a257a97..8e14b914f 100644 --- a/src/modules/parametric/tools.rs +++ b/src/modules/parametric/tools.rs @@ -167,7 +167,6 @@ inventory::submit!(crate::command::CommandRegistration { "GCPERPENDICULAR", "ECONSTRAINT", "TCONSTRAINT", - "NCONSTRAINT", "GCCONCENTRIC", "NRCONSTRAINT", "LCONSTRAINT", diff --git a/src/scene/parametric_solve.rs b/src/scene/parametric_solve.rs index 553af54ae..f6d2a1b75 100644 --- a/src/scene/parametric_solve.rs +++ b/src/scene/parametric_solve.rs @@ -3520,6 +3520,17 @@ mod tests { use acadrust::entities::EntityType; use acadrust::types::Vector3; + fn spatial_circle(center: Vector3, radius: f64) -> acadrust::entities::Circle { + let normal = Vector3::UNIT_Y; + let (x, y, z) = crate::scene::view::transform::wcs_point_to_ocs( + (center.x, center.y, center.z), + (normal.x, normal.y, normal.z), + ); + let mut circle = acadrust::entities::Circle::from_coords(x, y, z, radius); + circle.normal = normal; + circle + } + #[test] fn polyline_constraint_order_follows_stored_vertices_for_open_and_closed_polylines() { use acadrust::entities::{LwPolyline, LwVertex, Polyline2D, Vertex2D}; @@ -4278,4 +4289,76 @@ mod tests { < 1.0e-9); assert!(jet.tangent[1].abs() < 1.0e-9 && jet.tangent[2].abs() < 1.0e-9); } + + #[test] + fn spatial_concentric_initial_solve_keeps_the_first_circle_fixed() { + let mut scene = Scene::new(); + let first = scene.add_entity(EntityType::Circle(spatial_circle(Vector3::ZERO, 2.0))); + let second = scene.add_entity(EntityType::Circle(spatial_circle( + Vector3::new(5.0, 0.0, 3.0), + 4.0, + ))); + let first_ref = ParametricRef::center(first); + let second_ref = ParametricRef::center(second); + scene + .parametric_constraint_set_mut(ParametricScope::ModelSpace) + .add( + ConstraintKind::Concentric, + vec![first_ref, second_ref], + None, + ); + + scene.bump_entities_with_initial_parametric_policy( + &[ + (first, super::ChangeKind::Modified), + (second, super::ChangeKind::Modified), + ], + &[first_ref], + true, + ); + + let EntityType::Circle(first_circle) = scene.document.get_entity(first).unwrap() else { + panic!("expected circle"); + }; + let EntityType::Circle(second_circle) = scene.document.get_entity(second).unwrap() else { + panic!("expected circle"); + }; + assert!(first_circle.center_wcs().length() < 1.0e-9); + assert!(second_circle.center_wcs().length() < 1.0e-9); + assert!((second_circle.radius - 4.0).abs() < 1.0e-12); + } + + #[test] + fn spatial_concentric_chain_follows_the_driven_end() { + let mut scene = Scene::new(); + let handles = [1.0, 2.0, 3.0].map(|radius| { + scene.add_entity(EntityType::Circle(spatial_circle(Vector3::ZERO, radius))) + }); + let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace); + set.add( + ConstraintKind::Concentric, + vec![ParametricRef::center(handles[0]), ParametricRef::center(handles[1])], + None, + ); + set.add( + ConstraintKind::Concentric, + vec![ParametricRef::center(handles[1]), ParametricRef::center(handles[2])], + None, + ); + let driven_center = Vector3::new(8.0, 0.0, -3.0); + *scene.document.get_entity_mut(handles[2]).unwrap() = + EntityType::Circle(spatial_circle(driven_center, 3.0)); + + scene.bump_entities_with_parametric_driven( + &[(handles[2], super::ChangeKind::Modified)], + &[ParametricRef::whole(handles[2])], + ); + + for handle in handles { + let EntityType::Circle(circle) = scene.document.get_entity(handle).unwrap() else { + panic!("expected circle"); + }; + assert!((circle.center_wcs() - driven_center).length() < 1.0e-9); + } + } }