Merge pull request #856 from ramox81/codex/align-circle-behavior

This commit is contained in:
Hakan Seven committed 2026-08-21 23:29:57 +03:00
commit d9aead3fbc
12 files changed
+417 -172

No files matched your search

+7 -2
View File
@@ -5,7 +5,6 @@ use std::cell::Cell;
thread_local! {
static CIRCLE_RADIUS: Cell<f64> = Cell::new(1.0);
static CIRCLE_DIAM: Cell<f64> = Cell::new(2.0);
static ROTATE_ANGLE: Cell<f64> = Cell::new(0.0); // degrees
static SCALE_FACTOR: Cell<f64> = Cell::new(1.0);
static OFFSET_DIST: Cell<f64> = 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);
+210 -124
View File
@@ -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<WireModel> {
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<CmdResult> {
@@ -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<crate::command::DynSpec> {
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<WireModel> {
// 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<CmdResult> {
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<f64> {
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<CmdResult> {
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<WireModel> {
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<crate::command::DynSpec> {
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<WireModel> {
None
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
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 };
+10 -2
View File
@@ -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;
+2 -2
View File
@@ -105,8 +105,8 @@ fn offset_xline(x: &XLineEnt, dist: f64, side_pt: Vec3) -> Option<EntityType> {
// ── Circle offset ──────────────────────────────────────────────────────────
fn offset_circle(c: &CircleEnt, dist: f64, side_pt: Vec3) -> Option<EntityType> {
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 {