diff --git a/Cargo.lock b/Cargo.lock index 720ac707d..74f91e03a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,7 +72,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.4.1" -source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=dd4cd16#dd4cd16be0e0aec0e23b66d3d27311316883662a" +source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=7f54d68#7f54d688c958708dc3e01b09c72fd11d41462d57" dependencies = [ "ahash 0.8.12", "anyhow", @@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=35abd0f#35abd0fbabe224040bbe0e07055d6d172cc61c67" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=10d1339#10d1339bba38be8ae9499176f0e64c737472c532" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 1a2af18b0..1f2a74b71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,8 @@ glam = { version = "0.33", features = ["bytemuck"] } rfd = "0.17" clap = { version = "4", features = ["derive"] } env_logger = "0.11" -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "dd4cd16", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "35abd0f", features = ["acis", "offset"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7f54d68", features = ["serde"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "10d1339", features = ["acis", "offset"] } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } diff --git a/crates/ocs_plugin_api/Cargo.toml b/crates/ocs_plugin_api/Cargo.toml index eb719dbe8..e709a5b16 100644 --- a/crates/ocs_plugin_api/Cargo.toml +++ b/crates/ocs_plugin_api/Cargo.toml @@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] } # Pulled in only by the `host` feature, which adds the `acadrust`-typed # `HostApi` runtime surface. The default crate stays dependency-free so engine # crates and external tooling can depend on the manifest/ribbon contract cheaply. -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "dd4cd16", optional = true, features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7f54d68", optional = true, features = ["serde"] } # Runtime IPC and serialization (host feature only). interprocess = { version = "2", optional = true } @@ -37,7 +37,7 @@ serde_json = "1" serde = { version = "1", features = ["derive"] } cargo-lock = "11" # acadrust is scanned at build time to generate the embedded type registry. -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "dd4cd16", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7f54d68", features = ["serde"] } [dev-dependencies] serde_json = "1" diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml index df67d1ec9..6601d6c04 100644 --- a/crates/ocs_web_worker/Cargo.toml +++ b/crates/ocs_web_worker/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib"] [dependencies] -acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "dd4cd16", features = ["serde"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7f54d68", features = ["serde"] } bincode = "1.3" serde = { version = "1", features = ["derive"] } console_error_panic_hook = "0.1" diff --git a/src/entities/circle.rs b/src/entities/circle.rs index 6e292770b..34d579453 100644 --- a/src/entities/circle.rs +++ b/src/entities/circle.rs @@ -20,14 +20,15 @@ fn to_render(circle: &Circle) -> RenderEntity { let (ax, ay) = crate::scene::view::transform::ocs_axes(normal); let (cwx, cwy, cwz) = crate::scene::view::transform::ocs_point_to_wcs((cx, cy, cz), normal); - let rf = r as f32; // Centre and quadrants come from the entity's own curve, so the same // definition answers here, in the tessellation and in a trim. let curve = crate::entities::curve::circle_curve(circle); let snap_pts = crate::entities::curve::snap_from(&curve).snap_pts; - let tangent = TangentGeom::Circle { - center: [cwx as f32, cwy as f32, cwz as f32], - radius: rf, + let tangent = TangentGeom::PlanarCircle { + center: [cwx, cwy, cwz], + axis_x: [ax.0, ax.1, ax.2], + axis_y: [ay.0, ay.1, ay.2], + radius: r, }; if circle.thickness.abs() > 1e-10 { @@ -74,14 +75,7 @@ fn to_render(circle: &Circle) -> RenderEntity { }; } - // Sampled from the entity's own curve rather than as a - // `circle_arc` (arc-through-three-points). At large WCS coordinates - // (e.g. −1.2M UTM) the three-point fit cancels catastrophically — the - // circle comes back with a ~3% radius wobble and uneven segment lengths, - // which then throws off a dashed linetype's dash spacing. Direct - // evaluation only adds a small ±r term to the centre, so it stays - // precise; the `Lines` path RTE-splits the absolute-f64 points into the - // double-single the shader reconstructs. + // Keep tessellation on the entity curve for large-coordinate precision. let pts = crate::entities::curve::curve_points(&curve); RenderEntity { @@ -95,26 +89,31 @@ fn to_render(circle: &Circle) -> RenderEntity { } fn grips(circle: &Circle) -> Vec { - let ctr = glam::DVec3::new(circle.center.x, circle.center.y, circle.center.z); + let center = circle.center_wcs(); + let (axis_x, axis_y) = circle.axes_wcs(); + let ctr = glam::DVec3::new(center.x, center.y, center.z); + let axis_x = glam::DVec3::new(axis_x.x, axis_x.y, axis_x.z); + let axis_y = glam::DVec3::new(axis_y.x, axis_y.y, axis_y.z); let r = circle.radius; vec![ center_grip(0, ctr), - square_grip(1, ctr + glam::DVec3::new(r, 0.0, 0.0)), - square_grip(2, ctr + glam::DVec3::new(0.0, r, 0.0)), - square_grip(3, ctr - glam::DVec3::new(r, 0.0, 0.0)), - square_grip(4, ctr - glam::DVec3::new(0.0, r, 0.0)), + square_grip(1, ctr + axis_x * r), + square_grip(2, ctr + axis_y * r), + square_grip(3, ctr - axis_x * r), + square_grip(4, ctr - axis_y * r), ] } fn properties(circle: &Circle) -> Vec { use std::f64::consts::PI; let r = circle.radius; + let center = circle.center_wcs(); vec![PropSection { title: t!("Geometry").into_owned(), props: vec![ - edit(t!("Center X").as_ref(), "center_x", circle.center.x), - edit(t!("Center Y").as_ref(), "center_y", circle.center.y), - edit(t!("Center Z").as_ref(), "center_z", circle.center.z), + edit(t!("Center X").as_ref(), "center_x", center.x), + edit(t!("Center Y").as_ref(), "center_y", center.y), + edit(t!("Center Z").as_ref(), "center_z", center.z), edit(t!("Radius").as_ref(), "radius", r), edit(t!("Diameter").as_ref(), "diameter", r * 2.0), edit(t!("Circumference").as_ref(), "circumference", 2.0 * PI * r), @@ -132,9 +131,22 @@ fn apply_geom_prop(circle: &mut Circle, field: &str, value: &str) { return; }; match field { - "center_x" => circle.center.x = v, - "center_y" => circle.center.y = v, - "center_z" => circle.center.z = v, + "center_x" | "center_y" | "center_z" => { + let normal = (circle.normal.x, circle.normal.y, circle.normal.z); + let center = circle.center_wcs(); + let (mut x, mut y, mut z) = (center.x, center.y, center.z); + match field { + "center_x" => x = v, + "center_y" => y = v, + "center_z" => z = v, + _ => {} + } + let (ox, oy, oz) = + crate::scene::view::transform::wcs_point_to_ocs((x, y, z), normal); + circle.center.x = ox; + circle.center.y = oy; + circle.center.z = oz; + } "radius" if v > 0.0 => circle.radius = v, "diameter" if v > 0.0 => circle.radius = v / 2.0, "circumference" if v > 0.0 => circle.radius = v / (2.0 * PI), @@ -146,19 +158,31 @@ fn apply_geom_prop(circle: &mut Circle, field: &str, value: &str) { fn apply_grip(circle: &mut Circle, grip_id: usize, apply: GripApply) { match (grip_id, apply) { (0, GripApply::Absolute(p)) => { - circle.center.x = p.x as f64; - circle.center.y = p.y as f64; - circle.center.z = p.z as f64; + let (x, y, z) = crate::scene::view::transform::wcs_point_to_ocs( + (p.x, p.y, p.z), + (circle.normal.x, circle.normal.y, circle.normal.z), + ); + circle.center.x = x; + circle.center.y = y; + circle.center.z = z; } (0, GripApply::Translate(d)) => { - circle.center.x += d.x as f64; - circle.center.y += d.y as f64; - circle.center.z += d.z as f64; + let (x, y, z) = crate::scene::view::transform::wcs_point_to_ocs( + (d.x, d.y, d.z), + (circle.normal.x, circle.normal.y, circle.normal.z), + ); + circle.center.x += x; + circle.center.y += y; + circle.center.z += z; } (1..=4, GripApply::Absolute(p)) => { - let dx = p.x - circle.center.x; - let dy = p.y - circle.center.y; - circle.radius = (dx * dx + dy * dy).sqrt(); + let plane = crate::entities::curve::circle_curve(circle).plane; + if let Some(point) = plane.project([p.x, p.y, p.z]) { + let radius = (point[0] - circle.center.x).hypot(point[1] - circle.center.y); + if radius > 1.0e-9 { + circle.radius = radius; + } + } } _ => {} } @@ -187,11 +211,12 @@ impl crate::entities::traits::MassPropsCalc for Circle { fn mass_props(&self) -> crate::entities::traits::MassProps { use std::f64::consts::{PI, TAU}; let r = self.radius; + let center = self.center_wcs(); crate::entities::traits::MassProps { area: PI * r * r, perimeter: TAU * r, - cx: self.center.x, - cy: self.center.y, + cx: center.x, + cy: center.y, } } } diff --git a/src/modules/draw/defaults.rs b/src/modules/draw/defaults.rs index 71b22a2a9..d5f165473 100644 --- a/src/modules/draw/defaults.rs +++ b/src/modules/draw/defaults.rs @@ -5,7 +5,6 @@ use std::cell::Cell; thread_local! { static CIRCLE_RADIUS: Cell = Cell::new(1.0); - static CIRCLE_DIAM: Cell = Cell::new(2.0); static ROTATE_ANGLE: Cell = Cell::new(0.0); // degrees static SCALE_FACTOR: Cell = Cell::new(1.0); static OFFSET_DIST: Cell = Cell::new(1.0); @@ -34,7 +33,13 @@ macro_rules! accessors { } accessors!(get_circle_radius, set_circle_radius, CIRCLE_RADIUS); -accessors!(get_circle_diam, set_circle_diam, CIRCLE_DIAM); +pub fn get_circle_diam() -> f64 { + get_circle_radius() * 2.0 +} + +pub fn set_circle_diam(value: f64) { + set_circle_radius(value * 0.5); +} accessors!(get_rotate_angle, set_rotate_angle, ROTATE_ANGLE); accessors!(get_scale_factor, set_scale_factor, SCALE_FACTOR); accessors!(get_offset_dist, set_offset_dist, OFFSET_DIST); diff --git a/src/modules/draw/draw/circle.rs b/src/modules/draw/draw/circle.rs index ae4621d26..a5bb9b943 100644 --- a/src/modules/draw/draw/circle.rs +++ b/src/modules/draw/draw/circle.rs @@ -1,12 +1,4 @@ -// Circle tool — ribbon dropdown + all OpenCADStudio circle creation methods. -// -// Methods: -// CIRCLE — Center, Radius (default) -// CIRCLE_CD — Center, Diameter -// CIRCLE_2P — 2-Point (two endpoints of diameter) -// CIRCLE_3P — 3-Point (circumscribed circle through 3 points) -// CIRCLE_TTR — Tan, Tan, Radius (two tangent objects + radius) -// CIRCLE_TTT — Tan, Tan, Tan (inscribed circle tangent to three objects) +// Circle creation commands. use acadrust::types::Vector3; use acadrust::{Circle, EntityType}; @@ -64,23 +56,16 @@ pub const ICON: IconKind = ICON_CR; // ── Shared geometry ──────────────────────────────────────────────────────── fn circle_wire(center: DVec3, radius: f64, plane: WorkingPlane) -> WireModel { - let segs = 64u32; - let mut pts: Vec<[f64; 3]> = (0..=segs) - .map(|i| { - let a = (i as f64) * TAU / segs as f64; - let point = center - + plane.vector_to_world(DVec3::new( - radius * a.cos(), - radius * a.sin(), - 0.0, - )); - [point.x, point.y, point.z] - }) + let center = plane.to_local(center); + let points = KernelCurve::Circle(KernelCircle { + centre: [center.x, center.y], + radius, + }) + .tessellate_angle(TAU / 64.0) + .into_iter() + .map(|point| plane.to_world(DVec3::new(point[0], point[1], center.z)).to_array()) .collect(); - if let Some(first) = pts.first().cloned() { - pts.push(first); - } - WireModel::solid_f64("rubber_band".into(), pts, WireModel::CYAN, false) + WireModel::solid_f64("rubber_band".into(), points, WireModel::CYAN, false) } fn make_circle(center: DVec3, radius: f64, plane: WorkingPlane) -> EntityType { @@ -92,8 +77,6 @@ fn make_circle(center: DVec3, radius: f64, plane: WorkingPlane) -> EntityType { })) } -/// Circumscribed circle through three points. -/// Returns `None` if the points are collinear. fn circumcircle( a: DVec3, b: DVec3, @@ -101,26 +84,13 @@ fn circumcircle( plane: WorkingPlane, ) -> Option<(DVec3, f64)> { let (a, b, c) = (plane.to_local(a), plane.to_local(b), plane.to_local(c)); - let ax = a.x; - let ay = a.y; - let bx = b.x; - let by = b.y; - let cx = c.x; - let cy = c.y; - let d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by)); - if d.abs() < 1e-9 { - return None; - } - let ux = ((ax * ax + ay * ay) * (by - cy) - + (bx * bx + by * by) * (cy - ay) - + (cx * cx + cy * cy) * (ay - by)) - / d; - let uy = ((ax * ax + ay * ay) * (cx - bx) - + (bx * bx + by * by) * (ax - cx) - + (cx * cx + cy * cy) * (bx - ax)) - / d; - let center = DVec3::new(ux, uy, a.z); - Some((plane.to_world(center), center.distance(a))) + let circle = cadkernel::geom2d::arc_through_points( + [a.x, a.y], + [b.x, b.y], + [c.x, c.y], + )?; + let center = plane.to_world(DVec3::new(circle.centre[0], circle.centre[1], a.z)); + Some((center, circle.radius)) } fn plane_distance(a: DVec3, b: DVec3, plane: WorkingPlane) -> f64 { @@ -151,6 +121,7 @@ pub struct CircleCommand { enum StepCR { Center, Radius(DVec3), + Diameter(DVec3), } impl CircleCommand { @@ -186,6 +157,11 @@ impl CadCommand for CircleCommand { ) .into_owned() } + StepCR::Diameter(_) => crate::tf!( + "CIRCLE Specify diameter <{:.4}>:", + self.default_r * 2.0 + ) + .into_owned(), } } @@ -197,14 +173,16 @@ impl CadCommand for CircleCommand { CmdOption::new("2P", "2P"), CmdOption::new("Ttr", "TTR"), CmdOption::new("Ttt", "TTT"), + ], + StepCR::Radius(_) => vec![ CmdOption::new(t!("Diameter").as_ref(), "D"), ], - StepCR::Radius(_) => vec![], + StepCR::Diameter(_) => vec![], } } fn point_step_accepts_keywords(&self) -> bool { - matches!(self.step, StepCR::Center) + !matches!(self.step, StepCR::Diameter(_)) } fn on_point(&mut self, pt: DVec3) -> CmdResult { match &self.step { @@ -214,27 +192,49 @@ impl CadCommand for CircleCommand { } StepCR::Radius(c) => { let r = plane_distance(*c, pt, self.plane); + if r <= 1.0e-9 { + return CmdResult::NeedPoint; + } defaults::set_circle_radius(r); CmdResult::CommitAndExit(make_circle(*c, r, self.plane)) } + StepCR::Diameter(c) => { + let radius = plane_distance(*c, pt, self.plane); + if radius <= 1.0e-9 { + return CmdResult::NeedPoint; + } + defaults::set_circle_radius(radius); + CmdResult::CommitAndExit(make_circle(*c, radius, self.plane)) + } } } fn on_enter(&mut self) -> CmdResult { - if let StepCR::Radius(c) = &self.step { - let c = *c; - let r = self.default_r; - return CmdResult::CommitAndExit(make_circle(c, r, self.plane)); + match self.step { + StepCR::Radius(center) => { + CmdResult::CommitAndExit(make_circle(center, self.default_r, self.plane)) + } + StepCR::Diameter(center) => { + CmdResult::CommitAndExit(make_circle(center, self.default_r, self.plane)) + } + StepCR::Center => CmdResult::Cancel, } - CmdResult::Cancel } fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } fn on_mouse_move(&mut self, pt: DVec3) -> Option { - if let StepCR::Radius(c) = &self.step { - Some(circle_wire(*c, plane_distance(*c, pt, self.plane), self.plane)) - } else { - None + match self.step { + StepCR::Radius(center) => Some(circle_wire( + center, + plane_distance(center, pt, self.plane), + self.plane, + )), + StepCR::Diameter(center) => Some(circle_wire( + center, + plane_distance(center, pt, self.plane), + self.plane, + )), + StepCR::Center => None, } } fn on_text_input(&mut self, text: &str) -> Option { @@ -246,10 +246,15 @@ impl CadCommand for CircleCommand { "2P" => Some(CmdResult::Dispatch("CIRCLE_2P".into())), "T" | "TTR" => Some(CmdResult::Dispatch("CIRCLE_TTR".into())), "TTT" => Some(CmdResult::Dispatch("CIRCLE_TTT".into())), - "D" | "DIAMETER" => Some(CmdResult::Dispatch("CIRCLE_CD".into())), _ => None, }; } + if let StepCR::Radius(center) = &self.step { + if matches!(text.trim().to_uppercase().as_str(), "D" | "DIAMETER") { + self.step = StepCR::Diameter(*center); + return Some(CmdResult::NeedPoint); + } + } if let StepCR::Radius(c) = &self.step { let r: f64 = text.trim().replace(',', ".").parse().ok()?; if r > 0.0 { @@ -257,28 +262,42 @@ impl CadCommand for CircleCommand { return Some(CmdResult::CommitAndExit(make_circle(*c, r, self.plane))); } } + if let StepCR::Diameter(c) = &self.step { + let diameter: f64 = text.trim().replace(',', ".").parse().ok()?; + if diameter > 0.0 { + defaults::set_circle_diam(diameter); + return Some(CmdResult::CommitAndExit(make_circle( + *c, + diameter * 0.5, + self.plane, + ))); + } + } None } fn dyn_field(&self) -> DynField { match self.step { StepCR::Center => DynField::Point, - StepCR::Radius(_) => DynField::Distance, + StepCR::Radius(_) | StepCR::Diameter(_) => DynField::Distance, } } fn dyn_spec(&self) -> Option { use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec}; match self.step { - // Center is a normal first-point pick. StepCR::Center => None, - // Radius: a single R value with one dotted line from the centre to - // the cursor (no angle, no axis legs). StepCR::Radius(c) => Some(DynSpec { anchor: DynAnchor::Point(c), fields: vec![DynFieldSpec::new(DynRole::Radius)], guide: DynGuide::Radius, ref_point: None, }), + StepCR::Diameter(c) => Some(DynSpec { + anchor: DynAnchor::Point(c), + fields: vec![DynFieldSpec::new(DynRole::Diameter)], + guide: DynGuide::Radius, + ref_point: None, + }), } } } @@ -312,28 +331,33 @@ impl CadCommand for CircleCDCommand { fn prompt(&self) -> String { match &self.step { StepCR::Center => t!("CIRCLE CD Specify center point:").into_owned(), - StepCR::Radius(c) => crate::tf!( + StepCR::Diameter(c) => crate::tf!( "CIRCLE CD Specify diameter or type value <{:.4}> [center ({:.3},{:.3})]:", self.default_d, c.x, c.y ) .into_owned(), + StepCR::Radius(_) => unreachable!(), } } fn on_point(&mut self, pt: DVec3) -> CmdResult { match &self.step { StepCR::Center => { - self.step = StepCR::Radius(pt); + self.step = StepCR::Diameter(pt); CmdResult::NeedPoint } - StepCR::Radius(c) => { - let d = plane_distance(*c, pt, self.plane) * 2.0; - defaults::set_circle_diam(d); - CmdResult::CommitAndExit(make_circle(*c, d / 2.0, self.plane)) + StepCR::Diameter(c) => { + let radius = plane_distance(*c, pt, self.plane); + if radius <= 1.0e-9 { + return CmdResult::NeedPoint; + } + defaults::set_circle_radius(radius); + CmdResult::CommitAndExit(make_circle(*c, radius, self.plane)) } + StepCR::Radius(_) => unreachable!(), } } fn on_enter(&mut self) -> CmdResult { - if let StepCR::Radius(c) = &self.step { + if let StepCR::Diameter(c) = &self.step { let c = *c; let d = self.default_d; return CmdResult::CommitAndExit(make_circle(c, d / 2.0, self.plane)); @@ -344,15 +368,18 @@ impl CadCommand for CircleCDCommand { CmdResult::Cancel } fn on_mouse_move(&mut self, pt: DVec3) -> Option { - // Preview radius = distance to cursor; on commit that distance becomes the radius (diameter = 2x). - if let StepCR::Radius(c) = &self.step { - Some(circle_wire(*c, plane_distance(*c, pt, self.plane), self.plane)) + if let StepCR::Diameter(c) = &self.step { + Some(circle_wire( + *c, + plane_distance(*c, pt, self.plane), + self.plane, + )) } else { None } } fn on_text_input(&mut self, text: &str) -> Option { - if let StepCR::Radius(c) = &self.step { + if let StepCR::Diameter(c) = &self.step { let d: f64 = text.trim().replace(',', ".").parse().ok()?; if d > 0.0 { defaults::set_circle_diam(d); @@ -366,14 +393,13 @@ impl CadCommand for CircleCDCommand { use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec}; match self.step { StepCR::Center => None, - // Diameter: the box shows/accepts twice the cursor radius; resolved - // back to a radius point by the host (role scaling). - StepCR::Radius(c) => Some(DynSpec { + StepCR::Diameter(c) => Some(DynSpec { anchor: DynAnchor::Point(c), fields: vec![DynFieldSpec::new(DynRole::Diameter)], guide: DynGuide::Radius, ref_point: None, }), + StepCR::Radius(_) => None, } } } @@ -418,6 +444,10 @@ impl CadCommand for Circle2PCommand { Some(p1) => { let center = (p1 + pt) * 0.5; let radius = plane_distance(p1, pt, self.plane) / 2.0; + if radius <= 1.0e-9 { + return CmdResult::NeedPoint; + } + defaults::set_circle_radius(radius); CmdResult::CommitAndExit(make_circle(center, radius, self.plane)) } } @@ -475,6 +505,7 @@ impl CadCommand for Circle3PCommand { let (a, b, c) = (self.pts[0], self.pts[1], self.pts[2]); match circumcircle(a, b, c, self.plane) { Some((center, radius)) => { + defaults::set_circle_radius(radius); CmdResult::CommitAndExit(make_circle(center, radius, self.plane)) } None => { @@ -693,12 +724,7 @@ fn ttt_solve_sign(objs: &[TangentObject; 3], eps: &[f64; 3]) -> Vec<(DVec3, f64) let e1 = &lin_eqs[1]; let det = e0.lx * e1.ly - e1.lx * e0.ly; if det.abs() < 1e-9 { - // Degenerate 2x2 — the CONCENTRIC circle pair case (#318): the - // pair-difference equation loses its cx/cy terms (lx = ly = 0) and - // pins r directly, so the solve above has nothing to invert. Recover - // the centers instead: r from the pinning equation, then intersect - // the other equation (a line in cx/cy once r is known) with the - // first circle's distance constraint |C - P| = |r + eps·cr|. + // Recover the concentric-circle case from its radius constraint. let (pin, other) = if e0.lx.hypot(e0.ly) < 1e-9 { (e0, e1) } else { @@ -794,6 +820,7 @@ fn ttt_solve_sign(objs: &[TangentObject; 3], eps: &[f64; 3]) -> Vec<(DVec3, f64) pub struct CircleTTRCommand { step: StepTTR, + default_r: f64, plane: WorkingPlane, } @@ -815,9 +842,44 @@ impl CircleTTRCommand { pub fn new() -> Self { Self { step: StepTTR::First, + default_r: defaults::get_circle_radius(), plane: WorkingPlane::default(), } } + + fn result_for_radius(&self, radius: f64) -> CmdResult { + if radius <= 1.0e-9 { + return CmdResult::NeedPoint; + } + let StepTTR::Radius { + obj1, + obj2, + hit1, + hit2, + } = &self.step + else { + return CmdResult::NeedPoint; + }; + let hint = (*hit1 + *hit2) * 0.5; + let candidates = ttr_candidates(*obj1, *obj2, radius); + let Some(center) = best_of(&candidates, hint) else { + return CmdResult::NeedPoint; + }; + defaults::set_circle_radius(radius); + CmdResult::CommitAndExit(make_circle( + self.plane.to_world(center), + radius, + self.plane, + )) + } + + fn radius_from_point(&self, point: DVec3) -> Option { + let StepTTR::Radius { hit2, .. } = &self.step else { + return None; + }; + let point = self.plane.to_local(point); + Some((point.x - hit2.x).hypot(point.y - hit2.y)) + } } impl CadCommand for CircleTTRCommand { @@ -849,12 +911,18 @@ impl CadCommand for CircleTTRCommand { match &self.step { StepTTR::First => crate::t!("CIRCLE TTR Select first tangent object:").into_owned(), StepTTR::Second { .. } => crate::t!("CIRCLE TTR Select second tangent object:").into_owned(), - StepTTR::Radius { .. } => crate::t!("CIRCLE TTR Specify radius:").into_owned(), + StepTTR::Radius { .. } => format!( + "{} <{:.4}>", + crate::t!("CIRCLE TTR Specify radius:"), + self.default_r + ), } } - fn on_point(&mut self, _pt: DVec3) -> CmdResult { - CmdResult::NeedPoint + fn on_point(&mut self, pt: DVec3) -> CmdResult { + self.radius_from_point(pt) + .map(|radius| self.result_for_radius(radius)) + .unwrap_or(CmdResult::NeedPoint) } fn on_tangent_point(&mut self, obj: TangentObject, hit: DVec3) -> CmdResult { @@ -883,41 +951,57 @@ impl CadCommand for CircleTTRCommand { } fn on_text_input(&mut self, text: &str) -> Option { - let r: f64 = text.trim().parse().ok()?; + let r: f64 = text.trim().replace(',', ".").parse().ok()?; if r <= 0.0 { - return Some(CmdResult::Cancel); + return Some(CmdResult::NeedPoint); } - if let StepTTR::Radius { + matches!(self.step, StepTTR::Radius { .. }).then(|| self.result_for_radius(r)) + } + + fn on_enter(&mut self) -> CmdResult { + if matches!(self.step, StepTTR::Radius { .. }) { + self.result_for_radius(self.default_r) + } else { + CmdResult::Cancel + } + } + fn on_escape(&mut self) -> CmdResult { + CmdResult::Cancel + } + fn on_mouse_move(&mut self, pt: DVec3) -> Option { + let radius = self.radius_from_point(pt)?; + let StepTTR::Radius { obj1, obj2, hit1, hit2, } = &self.step - { - let hint = (*hit1 + *hit2) * 0.5; - let candidates = ttr_candidates(*obj1, *obj2, r); - if let Some(center) = best_of(&candidates, hint) { - Some(CmdResult::CommitAndExit(make_circle( - self.plane.to_world(center), - r, - self.plane, - ))) - } else { - Some(CmdResult::Cancel) - } - } else { - None - } + else { + return None; + }; + let center = best_of(&ttr_candidates(*obj1, *obj2, radius), (*hit1 + *hit2) * 0.5)?; + Some(circle_wire( + self.plane.to_world(center), + radius, + self.plane, + )) } - fn on_enter(&mut self) -> CmdResult { - CmdResult::Cancel + fn dyn_spec(&self) -> Option { + use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec}; + let StepTTR::Radius { hit2, .. } = &self.step else { + return None; + }; + Some(DynSpec { + anchor: DynAnchor::Point(self.plane.to_world(*hit2)), + fields: vec![DynFieldSpec::new(DynRole::Radius)], + guide: DynGuide::Radius, + ref_point: None, + }) } - fn on_escape(&mut self) -> CmdResult { - CmdResult::Cancel - } - fn on_mouse_move(&mut self, _pt: DVec3) -> Option { - None + + fn dyn_live_value(&self, cursor: DVec3) -> Option { + self.radius_from_point(cursor) } } @@ -973,11 +1057,14 @@ impl CadCommand for CircleTTTCommand { let hint = self.hits.iter().fold(DVec3::ZERO, |a, &b| a + b) / 3.0; let candidates = ttt_candidates(self.objs[0], self.objs[1], self.objs[2]); match best_circle_of(&candidates, hint) { - Some((center, r)) => CmdResult::CommitAndExit(make_circle( - self.plane.to_world(center), - r, - self.plane, - )), + Some((center, r)) => { + defaults::set_circle_radius(r); + CmdResult::CommitAndExit(make_circle( + self.plane.to_world(center), + r, + self.plane, + )) + } None => { self.objs.pop(); self.hits.pop(); @@ -999,12 +1086,12 @@ impl CadCommand for CircleTTTCommand { // ── Autocomplete registry ───────────────────────────────── -inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_2P"] }); // Circle2PCommand -inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_3P"] }); // Circle3PCommand -inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_CD"] }); // CircleCDCommand -inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE"] }); // CircleCommand -inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_TTR"] }); // CircleTTRCommand -inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_TTT"] }); // CircleTTTCommand +inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_2P"] }); +inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_3P"] }); +inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_CD"] }); +inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE"] }); +inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_TTR"] }); +inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_TTT"] }); #[cfg(test)] mod tests { @@ -1064,8 +1151,7 @@ mod tests { ); } - /// #318: concentric ring + a line through the center — the degenerate - /// circle-pair equation must still yield the annulus circle. + /// #318: solve a concentric ring with a line through its center. #[test] fn ttt_concentric_ring_with_line() { let inner = TangentObject::Circle { center: DVec3::ZERO, radius: 3.5 }; diff --git a/src/modules/draw/modify/break_cmd.rs b/src/modules/draw/modify/break_cmd.rs index 9309908f8..2ce6e8460 100644 --- a/src/modules/draw/modify/break_cmd.rs +++ b/src/modules/draw/modify/break_cmd.rs @@ -131,8 +131,15 @@ fn break_circle(circle: &acadrust::entities::Circle, p1: DVec3, p2: DVec3) -> Ve let cx = circle.center.x; let cy = circle.center.y; - let a1 = angle_on_arc(cx, cy, p1); - let a2 = angle_on_arc(cx, cy, p2); + let plane = crate::entities::curve::circle_curve(circle).plane; + let Some(q1) = plane.project(p1.to_array()) else { + return vec![EntityType::Circle(circle.clone())]; + }; + let Some(q2) = plane.project(p2.to_array()) else { + return vec![EntityType::Circle(circle.clone())]; + }; + let a1 = angle_on_arc(cx, cy, DVec3::new(q1[0], q1[1], 0.0)); + let a2 = angle_on_arc(cx, cy, DVec3::new(q2[0], q2[1], 0.0)); if (a1 - a2).abs() < 0.01 { return vec![EntityType::Circle(circle.clone())]; @@ -144,6 +151,7 @@ fn break_circle(circle: &acadrust::entities::Circle, p1: DVec3, p2: DVec3) -> Ve arc.common.handle = Handle::NULL; arc.center = circle.center.clone(); arc.radius = circle.radius; + arc.thickness = circle.thickness; arc.normal = circle.normal.clone(); arc.start_angle = a2; arc.end_angle = a1; diff --git a/src/modules/draw/modify/offset.rs b/src/modules/draw/modify/offset.rs index 76e390da0..d3b5cc0a9 100644 --- a/src/modules/draw/modify/offset.rs +++ b/src/modules/draw/modify/offset.rs @@ -105,8 +105,8 @@ fn offset_xline(x: &XLineEnt, dist: f64, side_pt: Vec3) -> Option { // ── Circle offset ────────────────────────────────────────────────────────── fn offset_circle(c: &CircleEnt, dist: f64, side_pt: Vec3) -> Option { - let px = side_pt.x as f64; - let py = side_pt.y as f64; + let plane = crate::entities::curve::circle_curve(c).plane; + let [px, py] = plane.project(side_pt.as_dvec3().to_array())?; let dc = ((px - c.center.x).powi(2) + (py - c.center.y).powi(2)).sqrt(); let new_r = if dc < c.radius { diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index bab8229dc..c63bd1d31 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -1268,7 +1268,8 @@ fn translated_prototype_wire( center[axis] += delta_f32[axis]; } } - TangentGeom::Arc { center, .. } => { + TangentGeom::PlanarCircle { center, .. } + | TangentGeom::Arc { center, .. } => { for axis in 0..3 { center[axis] += delta[axis]; } @@ -2371,6 +2372,36 @@ fn transform_tangent( end_angle: *end_angle, }) } + TangentGeom::PlanarCircle { + center, + axis_x, + axis_y, + radius, + } => { + let c = t.apply(Vector3::new(center[0], center[1], center[2])); + let x = t.apply_rotation(Vector3::new(axis_x[0], axis_x[1], axis_x[2])); + let y = t.apply_rotation(Vector3::new(axis_y[0], axis_y[1], axis_y[2])); + let sx = x.length(); + let sy = y.length(); + let scale = sx.max(sy); + if !scale.is_finite() + || scale <= 1.0e-12 + || (sx - sy).abs() > scale * 1.0e-9 + { + return None; + } + let x = x / sx; + let y = y / sy; + if x.dot(&y).abs() > 1.0e-9 { + return None; + } + Some(TangentGeom::PlanarCircle { + center: [c.x, c.y, c.z], + axis_x: [x.x, x.y, x.z], + axis_y: [y.x, y.y, y.z], + radius: radius * ((sx + sy) * 0.5), + }) + } } } diff --git a/src/scene/model/wire_model.rs b/src/scene/model/wire_model.rs index 703db12a4..87f8a894c 100644 --- a/src/scene/model/wire_model.rs +++ b/src/scene/model/wire_model.rs @@ -31,6 +31,13 @@ pub enum TangentGeom { Line { p1: [f32; 3], p2: [f32; 3] }, /// Complete circle. Circle { center: [f32; 3], radius: f32 }, + /// Complete circle in an arbitrary world-space plane. + PlanarCircle { + center: [f64; 3], + axis_x: [f64; 3], + axis_y: [f64; 3], + radius: f64, + }, /// Bounded circular arc in its world-space plane. Arc { center: [f64; 3], diff --git a/src/snap.rs b/src/snap.rs index 43da8b477..2077e5af1 100644 --- a/src/snap.rs +++ b/src/snap.rs @@ -1692,6 +1692,62 @@ impl Snapper { .fold(f32::INFINITY, f32::min); (world, edge_d2) } + TangentGeom::PlanarCircle { + center, + axis_x, + axis_y, + radius, + } => { + let cv = DVec3::from_array(*center); + let candidate = self.from_point.and_then(|from| { + planar_circle_tangent_points( + from.as_dvec3(), + *center, + *axis_x, + *axis_y, + *radius, + ) + .into_iter() + .min_by(|a, b| { + let sa = world_to_screen(*a, view_rot, eye, bounds); + let sb = world_to_screen(*b, view_rot, eye, bounds); + dist2(sa, cursor_screen).total_cmp(&dist2(sb, cursor_screen)) + }) + }); + if self.from_point.is_some() && candidate.is_none() { + continue; + } + let fallback = || { + (0..wire.points.len()) + .map(|index| wp_f64(wire, index)) + .filter(|point| point.is_finite()) + .min_by(|a, b| { + let sa = world_to_screen(*a, view_rot, eye, bounds); + let sb = world_to_screen(*b, view_rot, eye, bounds); + dist2(sa, cursor_screen) + .total_cmp(&dist2(sb, cursor_screen)) + }) + .unwrap_or(cv) + }; + let world = candidate.unwrap_or_else(fallback); + let edge_d2 = wire + .points + .windows(2) + .enumerate() + .filter_map(|(index, _)| { + let a = wp_f64(wire, index); + let b = wp_f64(wire, index + 1); + (a.is_finite() && b.is_finite()).then(|| { + dist2_to_segment( + cursor_screen, + world_to_screen(a, view_rot, eye, bounds), + world_to_screen(b, view_rot, eye, bounds), + ) + }) + }) + .fold(f32::INFINITY, f32::min); + (world, edge_d2) + } }; let (tier, sub) = ( snap_tier(SnapType::Tangent), @@ -1722,6 +1778,12 @@ impl Snapper { center: glam::DVec3::from_array(*center), radius: *radius, }, + TangentGeom::PlanarCircle { center, radius, .. } => { + TangentObject::Circle { + center: glam::DVec3::from_array(*center), + radius: *radius, + } + } }; best = Some(SnapResult { world: world_pt, @@ -2300,6 +2362,27 @@ fn circle_tangent_points(p: Vec3, center: Vec3, radius: f32) -> Option<(Vec3, Ve )) } +fn planar_circle_tangent_points( + from: DVec3, + center: [f64; 3], + axis_x: [f64; 3], + axis_y: [f64; 3], + radius: f64, +) -> Vec { + let plane = cadkernel::space::Plane::from_axes(center, axis_x, axis_y); + let Some(from) = plane.project(from.to_array()) else { + return Vec::new(); + }; + let curve = cadkernel::geom2d::Curve::Circle(cadkernel::geom2d::Circle { + centre: [0.0, 0.0], + radius, + }); + cadkernel::geom2d::tangent_from(&curve, from) + .into_iter() + .map(|point| DVec3::from_array(plane.point_at(point.point))) + .collect() +} + fn arc_tangent_points( from: DVec3, center: [f64; 3],