forked from 1b-agent/opencadstudio-upstream
Merge pull request #856 from ramox81/codex/align-circle-behavior
This commit is contained in:
commit
d9aead3fbc
12 files changed
+417
-172
No files matched your search
Generated
+2
-2
@@ -72,7 +72,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "acadrust"
|
name = "acadrust"
|
||||||
version = "0.4.1"
|
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 = [
|
dependencies = [
|
||||||
"ahash 0.8.12",
|
"ahash 0.8.12",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "cadkernel"
|
name = "cadkernel"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=35abd0f#35abd0fbabe224040bbe0e07055d6d172cc61c67"
|
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=10d1339#10d1339bba38be8ae9499176f0e64c737472c532"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"acadrust",
|
"acadrust",
|
||||||
"cavalier_contours",
|
"cavalier_contours",
|
||||||
|
|||||||
+2
-2
@@ -27,8 +27,8 @@ glam = { version = "0.33", features = ["bytemuck"] }
|
|||||||
rfd = "0.17"
|
rfd = "0.17"
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "dd4cd16", features = ["serde"] }
|
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7f54d68", features = ["serde"] }
|
||||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "35abd0f", features = ["acis", "offset"] }
|
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "10d1339", features = ["acis", "offset"] }
|
||||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
|
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
|
||||||
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
||||||
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
|
# 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).
|
# Runtime IPC and serialization (host feature only).
|
||||||
interprocess = { version = "2", optional = true }
|
interprocess = { version = "2", optional = true }
|
||||||
@@ -37,7 +37,7 @@ serde_json = "1"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
cargo-lock = "11"
|
cargo-lock = "11"
|
||||||
# acadrust is scanned at build time to generate the embedded type registry.
|
# 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]
|
[dev-dependencies]
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
@@ -8,7 +8,7 @@ publish = false
|
|||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[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"
|
bincode = "1.3"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
|
|||||||
+59
-34
@@ -20,14 +20,15 @@ fn to_render(circle: &Circle) -> RenderEntity {
|
|||||||
let (ax, ay) = crate::scene::view::transform::ocs_axes(normal);
|
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 (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
|
// Centre and quadrants come from the entity's own curve, so the same
|
||||||
// definition answers here, in the tessellation and in a trim.
|
// definition answers here, in the tessellation and in a trim.
|
||||||
let curve = crate::entities::curve::circle_curve(circle);
|
let curve = crate::entities::curve::circle_curve(circle);
|
||||||
let snap_pts = crate::entities::curve::snap_from(&curve).snap_pts;
|
let snap_pts = crate::entities::curve::snap_from(&curve).snap_pts;
|
||||||
let tangent = TangentGeom::Circle {
|
let tangent = TangentGeom::PlanarCircle {
|
||||||
center: [cwx as f32, cwy as f32, cwz as f32],
|
center: [cwx, cwy, cwz],
|
||||||
radius: rf,
|
axis_x: [ax.0, ax.1, ax.2],
|
||||||
|
axis_y: [ay.0, ay.1, ay.2],
|
||||||
|
radius: r,
|
||||||
};
|
};
|
||||||
|
|
||||||
if circle.thickness.abs() > 1e-10 {
|
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
|
// Keep tessellation on the entity curve for large-coordinate precision.
|
||||||
// `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.
|
|
||||||
let pts = crate::entities::curve::curve_points(&curve);
|
let pts = crate::entities::curve::curve_points(&curve);
|
||||||
|
|
||||||
RenderEntity {
|
RenderEntity {
|
||||||
@@ -95,26 +89,31 @@ fn to_render(circle: &Circle) -> RenderEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn grips(circle: &Circle) -> Vec<GripDef> {
|
fn grips(circle: &Circle) -> Vec<GripDef> {
|
||||||
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;
|
let r = circle.radius;
|
||||||
vec![
|
vec![
|
||||||
center_grip(0, ctr),
|
center_grip(0, ctr),
|
||||||
square_grip(1, ctr + glam::DVec3::new(r, 0.0, 0.0)),
|
square_grip(1, ctr + axis_x * r),
|
||||||
square_grip(2, ctr + glam::DVec3::new(0.0, r, 0.0)),
|
square_grip(2, ctr + axis_y * r),
|
||||||
square_grip(3, ctr - glam::DVec3::new(r, 0.0, 0.0)),
|
square_grip(3, ctr - axis_x * r),
|
||||||
square_grip(4, ctr - glam::DVec3::new(0.0, r, 0.0)),
|
square_grip(4, ctr - axis_y * r),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn properties(circle: &Circle) -> Vec<PropSection> {
|
fn properties(circle: &Circle) -> Vec<PropSection> {
|
||||||
use std::f64::consts::PI;
|
use std::f64::consts::PI;
|
||||||
let r = circle.radius;
|
let r = circle.radius;
|
||||||
|
let center = circle.center_wcs();
|
||||||
vec![PropSection {
|
vec![PropSection {
|
||||||
title: t!("Geometry").into_owned(),
|
title: t!("Geometry").into_owned(),
|
||||||
props: vec![
|
props: vec![
|
||||||
edit(t!("Center X").as_ref(), "center_x", circle.center.x),
|
edit(t!("Center X").as_ref(), "center_x", center.x),
|
||||||
edit(t!("Center Y").as_ref(), "center_y", circle.center.y),
|
edit(t!("Center Y").as_ref(), "center_y", center.y),
|
||||||
edit(t!("Center Z").as_ref(), "center_z", circle.center.z),
|
edit(t!("Center Z").as_ref(), "center_z", center.z),
|
||||||
edit(t!("Radius").as_ref(), "radius", r),
|
edit(t!("Radius").as_ref(), "radius", r),
|
||||||
edit(t!("Diameter").as_ref(), "diameter", r * 2.0),
|
edit(t!("Diameter").as_ref(), "diameter", r * 2.0),
|
||||||
edit(t!("Circumference").as_ref(), "circumference", 2.0 * PI * r),
|
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;
|
return;
|
||||||
};
|
};
|
||||||
match field {
|
match field {
|
||||||
"center_x" => circle.center.x = v,
|
"center_x" | "center_y" | "center_z" => {
|
||||||
"center_y" => circle.center.y = v,
|
let normal = (circle.normal.x, circle.normal.y, circle.normal.z);
|
||||||
"center_z" => circle.center.z = v,
|
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,
|
"radius" if v > 0.0 => circle.radius = v,
|
||||||
"diameter" if v > 0.0 => circle.radius = v / 2.0,
|
"diameter" if v > 0.0 => circle.radius = v / 2.0,
|
||||||
"circumference" if v > 0.0 => circle.radius = v / (2.0 * PI),
|
"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) {
|
fn apply_grip(circle: &mut Circle, grip_id: usize, apply: GripApply) {
|
||||||
match (grip_id, apply) {
|
match (grip_id, apply) {
|
||||||
(0, GripApply::Absolute(p)) => {
|
(0, GripApply::Absolute(p)) => {
|
||||||
circle.center.x = p.x as f64;
|
let (x, y, z) = crate::scene::view::transform::wcs_point_to_ocs(
|
||||||
circle.center.y = p.y as f64;
|
(p.x, p.y, p.z),
|
||||||
circle.center.z = p.z as f64;
|
(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)) => {
|
(0, GripApply::Translate(d)) => {
|
||||||
circle.center.x += d.x as f64;
|
let (x, y, z) = crate::scene::view::transform::wcs_point_to_ocs(
|
||||||
circle.center.y += d.y as f64;
|
(d.x, d.y, d.z),
|
||||||
circle.center.z += d.z as f64;
|
(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)) => {
|
(1..=4, GripApply::Absolute(p)) => {
|
||||||
let dx = p.x - circle.center.x;
|
let plane = crate::entities::curve::circle_curve(circle).plane;
|
||||||
let dy = p.y - circle.center.y;
|
if let Some(point) = plane.project([p.x, p.y, p.z]) {
|
||||||
circle.radius = (dx * dx + dy * dy).sqrt();
|
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 {
|
fn mass_props(&self) -> crate::entities::traits::MassProps {
|
||||||
use std::f64::consts::{PI, TAU};
|
use std::f64::consts::{PI, TAU};
|
||||||
let r = self.radius;
|
let r = self.radius;
|
||||||
|
let center = self.center_wcs();
|
||||||
crate::entities::traits::MassProps {
|
crate::entities::traits::MassProps {
|
||||||
area: PI * r * r,
|
area: PI * r * r,
|
||||||
perimeter: TAU * r,
|
perimeter: TAU * r,
|
||||||
cx: self.center.x,
|
cx: center.x,
|
||||||
cy: self.center.y,
|
cy: center.y,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,6 @@ use std::cell::Cell;
|
|||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static CIRCLE_RADIUS: Cell<f64> = Cell::new(1.0);
|
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 ROTATE_ANGLE: Cell<f64> = Cell::new(0.0); // degrees
|
||||||
static SCALE_FACTOR: Cell<f64> = Cell::new(1.0);
|
static SCALE_FACTOR: Cell<f64> = Cell::new(1.0);
|
||||||
static OFFSET_DIST: 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_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_rotate_angle, set_rotate_angle, ROTATE_ANGLE);
|
||||||
accessors!(get_scale_factor, set_scale_factor, SCALE_FACTOR);
|
accessors!(get_scale_factor, set_scale_factor, SCALE_FACTOR);
|
||||||
accessors!(get_offset_dist, set_offset_dist, OFFSET_DIST);
|
accessors!(get_offset_dist, set_offset_dist, OFFSET_DIST);
|
||||||
|
|||||||
+210
-124
@@ -1,12 +1,4 @@
|
|||||||
// Circle tool — ribbon dropdown + all OpenCADStudio circle creation methods.
|
// Circle creation commands.
|
||||||
//
|
|
||||||
// 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)
|
|
||||||
|
|
||||||
use acadrust::types::Vector3;
|
use acadrust::types::Vector3;
|
||||||
use acadrust::{Circle, EntityType};
|
use acadrust::{Circle, EntityType};
|
||||||
@@ -64,23 +56,16 @@ pub const ICON: IconKind = ICON_CR;
|
|||||||
// ── Shared geometry ────────────────────────────────────────────────────────
|
// ── Shared geometry ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn circle_wire(center: DVec3, radius: f64, plane: WorkingPlane) -> WireModel {
|
fn circle_wire(center: DVec3, radius: f64, plane: WorkingPlane) -> WireModel {
|
||||||
let segs = 64u32;
|
let center = plane.to_local(center);
|
||||||
let mut pts: Vec<[f64; 3]> = (0..=segs)
|
let points = KernelCurve::Circle(KernelCircle {
|
||||||
.map(|i| {
|
centre: [center.x, center.y],
|
||||||
let a = (i as f64) * TAU / segs as f64;
|
radius,
|
||||||
let point = center
|
})
|
||||||
+ plane.vector_to_world(DVec3::new(
|
.tessellate_angle(TAU / 64.0)
|
||||||
radius * a.cos(),
|
.into_iter()
|
||||||
radius * a.sin(),
|
.map(|point| plane.to_world(DVec3::new(point[0], point[1], center.z)).to_array())
|
||||||
0.0,
|
|
||||||
));
|
|
||||||
[point.x, point.y, point.z]
|
|
||||||
})
|
|
||||||
.collect();
|
.collect();
|
||||||
if let Some(first) = pts.first().cloned() {
|
WireModel::solid_f64("rubber_band".into(), points, WireModel::CYAN, false)
|
||||||
pts.push(first);
|
|
||||||
}
|
|
||||||
WireModel::solid_f64("rubber_band".into(), pts, WireModel::CYAN, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_circle(center: DVec3, radius: f64, plane: WorkingPlane) -> EntityType {
|
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(
|
fn circumcircle(
|
||||||
a: DVec3,
|
a: DVec3,
|
||||||
b: DVec3,
|
b: DVec3,
|
||||||
@@ -101,26 +84,13 @@ fn circumcircle(
|
|||||||
plane: WorkingPlane,
|
plane: WorkingPlane,
|
||||||
) -> Option<(DVec3, f64)> {
|
) -> Option<(DVec3, f64)> {
|
||||||
let (a, b, c) = (plane.to_local(a), plane.to_local(b), plane.to_local(c));
|
let (a, b, c) = (plane.to_local(a), plane.to_local(b), plane.to_local(c));
|
||||||
let ax = a.x;
|
let circle = cadkernel::geom2d::arc_through_points(
|
||||||
let ay = a.y;
|
[a.x, a.y],
|
||||||
let bx = b.x;
|
[b.x, b.y],
|
||||||
let by = b.y;
|
[c.x, c.y],
|
||||||
let cx = c.x;
|
)?;
|
||||||
let cy = c.y;
|
let center = plane.to_world(DVec3::new(circle.centre[0], circle.centre[1], a.z));
|
||||||
let d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
|
Some((center, circle.radius))
|
||||||
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)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn plane_distance(a: DVec3, b: DVec3, plane: WorkingPlane) -> f64 {
|
fn plane_distance(a: DVec3, b: DVec3, plane: WorkingPlane) -> f64 {
|
||||||
@@ -151,6 +121,7 @@ pub struct CircleCommand {
|
|||||||
enum StepCR {
|
enum StepCR {
|
||||||
Center,
|
Center,
|
||||||
Radius(DVec3),
|
Radius(DVec3),
|
||||||
|
Diameter(DVec3),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CircleCommand {
|
impl CircleCommand {
|
||||||
@@ -186,6 +157,11 @@ impl CadCommand for CircleCommand {
|
|||||||
)
|
)
|
||||||
.into_owned()
|
.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("2P", "2P"),
|
||||||
CmdOption::new("Ttr", "TTR"),
|
CmdOption::new("Ttr", "TTR"),
|
||||||
CmdOption::new("Ttt", "TTT"),
|
CmdOption::new("Ttt", "TTT"),
|
||||||
|
],
|
||||||
|
StepCR::Radius(_) => vec![
|
||||||
CmdOption::new(t!("Diameter").as_ref(), "D"),
|
CmdOption::new(t!("Diameter").as_ref(), "D"),
|
||||||
],
|
],
|
||||||
StepCR::Radius(_) => vec![],
|
StepCR::Diameter(_) => vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn point_step_accepts_keywords(&self) -> bool {
|
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 {
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||||
match &self.step {
|
match &self.step {
|
||||||
@@ -214,27 +192,49 @@ impl CadCommand for CircleCommand {
|
|||||||
}
|
}
|
||||||
StepCR::Radius(c) => {
|
StepCR::Radius(c) => {
|
||||||
let r = plane_distance(*c, pt, self.plane);
|
let r = plane_distance(*c, pt, self.plane);
|
||||||
|
if r <= 1.0e-9 {
|
||||||
|
return CmdResult::NeedPoint;
|
||||||
|
}
|
||||||
defaults::set_circle_radius(r);
|
defaults::set_circle_radius(r);
|
||||||
CmdResult::CommitAndExit(make_circle(*c, r, self.plane))
|
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 {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
if let StepCR::Radius(c) = &self.step {
|
match self.step {
|
||||||
let c = *c;
|
StepCR::Radius(center) => {
|
||||||
let r = self.default_r;
|
CmdResult::CommitAndExit(make_circle(center, self.default_r, self.plane))
|
||||||
return CmdResult::CommitAndExit(make_circle(c, 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 {
|
fn on_escape(&mut self) -> CmdResult {
|
||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||||
if let StepCR::Radius(c) = &self.step {
|
match self.step {
|
||||||
Some(circle_wire(*c, plane_distance(*c, pt, self.plane), self.plane))
|
StepCR::Radius(center) => Some(circle_wire(
|
||||||
} else {
|
center,
|
||||||
None
|
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> {
|
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())),
|
"2P" => Some(CmdResult::Dispatch("CIRCLE_2P".into())),
|
||||||
"T" | "TTR" => Some(CmdResult::Dispatch("CIRCLE_TTR".into())),
|
"T" | "TTR" => Some(CmdResult::Dispatch("CIRCLE_TTR".into())),
|
||||||
"TTT" => Some(CmdResult::Dispatch("CIRCLE_TTT".into())),
|
"TTT" => Some(CmdResult::Dispatch("CIRCLE_TTT".into())),
|
||||||
"D" | "DIAMETER" => Some(CmdResult::Dispatch("CIRCLE_CD".into())),
|
|
||||||
_ => None,
|
_ => 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 {
|
if let StepCR::Radius(c) = &self.step {
|
||||||
let r: f64 = text.trim().replace(',', ".").parse().ok()?;
|
let r: f64 = text.trim().replace(',', ".").parse().ok()?;
|
||||||
if r > 0.0 {
|
if r > 0.0 {
|
||||||
@@ -257,28 +262,42 @@ impl CadCommand for CircleCommand {
|
|||||||
return Some(CmdResult::CommitAndExit(make_circle(*c, r, self.plane)));
|
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
|
None
|
||||||
}
|
}
|
||||||
fn dyn_field(&self) -> DynField {
|
fn dyn_field(&self) -> DynField {
|
||||||
match self.step {
|
match self.step {
|
||||||
StepCR::Center => DynField::Point,
|
StepCR::Center => DynField::Point,
|
||||||
StepCR::Radius(_) => DynField::Distance,
|
StepCR::Radius(_) | StepCR::Diameter(_) => DynField::Distance,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
|
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
|
||||||
use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec};
|
use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec};
|
||||||
match self.step {
|
match self.step {
|
||||||
// Center is a normal first-point pick.
|
|
||||||
StepCR::Center => None,
|
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 {
|
StepCR::Radius(c) => Some(DynSpec {
|
||||||
anchor: DynAnchor::Point(c),
|
anchor: DynAnchor::Point(c),
|
||||||
fields: vec![DynFieldSpec::new(DynRole::Radius)],
|
fields: vec![DynFieldSpec::new(DynRole::Radius)],
|
||||||
guide: DynGuide::Radius,
|
guide: DynGuide::Radius,
|
||||||
ref_point: None,
|
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 {
|
fn prompt(&self) -> String {
|
||||||
match &self.step {
|
match &self.step {
|
||||||
StepCR::Center => t!("CIRCLE CD Specify center point:").into_owned(),
|
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})]:",
|
"CIRCLE CD Specify diameter or type value <{:.4}> [center ({:.3},{:.3})]:",
|
||||||
self.default_d, c.x, c.y
|
self.default_d, c.x, c.y
|
||||||
)
|
)
|
||||||
.into_owned(),
|
.into_owned(),
|
||||||
|
StepCR::Radius(_) => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||||
match &self.step {
|
match &self.step {
|
||||||
StepCR::Center => {
|
StepCR::Center => {
|
||||||
self.step = StepCR::Radius(pt);
|
self.step = StepCR::Diameter(pt);
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
StepCR::Radius(c) => {
|
StepCR::Diameter(c) => {
|
||||||
let d = plane_distance(*c, pt, self.plane) * 2.0;
|
let radius = plane_distance(*c, pt, self.plane);
|
||||||
defaults::set_circle_diam(d);
|
if radius <= 1.0e-9 {
|
||||||
CmdResult::CommitAndExit(make_circle(*c, d / 2.0, self.plane))
|
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 {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
if let StepCR::Radius(c) = &self.step {
|
if let StepCR::Diameter(c) = &self.step {
|
||||||
let c = *c;
|
let c = *c;
|
||||||
let d = self.default_d;
|
let d = self.default_d;
|
||||||
return CmdResult::CommitAndExit(make_circle(c, d / 2.0, self.plane));
|
return CmdResult::CommitAndExit(make_circle(c, d / 2.0, self.plane));
|
||||||
@@ -344,15 +368,18 @@ impl CadCommand for CircleCDCommand {
|
|||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
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::Diameter(c) = &self.step {
|
||||||
if let StepCR::Radius(c) = &self.step {
|
Some(circle_wire(
|
||||||
Some(circle_wire(*c, plane_distance(*c, pt, self.plane), self.plane))
|
*c,
|
||||||
|
plane_distance(*c, pt, self.plane),
|
||||||
|
self.plane,
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
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()?;
|
let d: f64 = text.trim().replace(',', ".").parse().ok()?;
|
||||||
if d > 0.0 {
|
if d > 0.0 {
|
||||||
defaults::set_circle_diam(d);
|
defaults::set_circle_diam(d);
|
||||||
@@ -366,14 +393,13 @@ impl CadCommand for CircleCDCommand {
|
|||||||
use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec};
|
use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec};
|
||||||
match self.step {
|
match self.step {
|
||||||
StepCR::Center => None,
|
StepCR::Center => None,
|
||||||
// Diameter: the box shows/accepts twice the cursor radius; resolved
|
StepCR::Diameter(c) => Some(DynSpec {
|
||||||
// back to a radius point by the host (role scaling).
|
|
||||||
StepCR::Radius(c) => Some(DynSpec {
|
|
||||||
anchor: DynAnchor::Point(c),
|
anchor: DynAnchor::Point(c),
|
||||||
fields: vec![DynFieldSpec::new(DynRole::Diameter)],
|
fields: vec![DynFieldSpec::new(DynRole::Diameter)],
|
||||||
guide: DynGuide::Radius,
|
guide: DynGuide::Radius,
|
||||||
ref_point: None,
|
ref_point: None,
|
||||||
}),
|
}),
|
||||||
|
StepCR::Radius(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,6 +444,10 @@ impl CadCommand for Circle2PCommand {
|
|||||||
Some(p1) => {
|
Some(p1) => {
|
||||||
let center = (p1 + pt) * 0.5;
|
let center = (p1 + pt) * 0.5;
|
||||||
let radius = plane_distance(p1, pt, self.plane) / 2.0;
|
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))
|
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]);
|
let (a, b, c) = (self.pts[0], self.pts[1], self.pts[2]);
|
||||||
match circumcircle(a, b, c, self.plane) {
|
match circumcircle(a, b, c, self.plane) {
|
||||||
Some((center, radius)) => {
|
Some((center, radius)) => {
|
||||||
|
defaults::set_circle_radius(radius);
|
||||||
CmdResult::CommitAndExit(make_circle(center, radius, self.plane))
|
CmdResult::CommitAndExit(make_circle(center, radius, self.plane))
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
@@ -693,12 +724,7 @@ fn ttt_solve_sign(objs: &[TangentObject; 3], eps: &[f64; 3]) -> Vec<(DVec3, f64)
|
|||||||
let e1 = &lin_eqs[1];
|
let e1 = &lin_eqs[1];
|
||||||
let det = e0.lx * e1.ly - e1.lx * e0.ly;
|
let det = e0.lx * e1.ly - e1.lx * e0.ly;
|
||||||
if det.abs() < 1e-9 {
|
if det.abs() < 1e-9 {
|
||||||
// Degenerate 2x2 — the CONCENTRIC circle pair case (#318): the
|
// Recover the concentric-circle case from its radius constraint.
|
||||||
// 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|.
|
|
||||||
let (pin, other) = if e0.lx.hypot(e0.ly) < 1e-9 {
|
let (pin, other) = if e0.lx.hypot(e0.ly) < 1e-9 {
|
||||||
(e0, e1)
|
(e0, e1)
|
||||||
} else {
|
} else {
|
||||||
@@ -794,6 +820,7 @@ fn ttt_solve_sign(objs: &[TangentObject; 3], eps: &[f64; 3]) -> Vec<(DVec3, f64)
|
|||||||
|
|
||||||
pub struct CircleTTRCommand {
|
pub struct CircleTTRCommand {
|
||||||
step: StepTTR,
|
step: StepTTR,
|
||||||
|
default_r: f64,
|
||||||
plane: WorkingPlane,
|
plane: WorkingPlane,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -815,9 +842,44 @@ impl CircleTTRCommand {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
step: StepTTR::First,
|
step: StepTTR::First,
|
||||||
|
default_r: defaults::get_circle_radius(),
|
||||||
plane: WorkingPlane::default(),
|
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 {
|
impl CadCommand for CircleTTRCommand {
|
||||||
@@ -849,12 +911,18 @@ impl CadCommand for CircleTTRCommand {
|
|||||||
match &self.step {
|
match &self.step {
|
||||||
StepTTR::First => crate::t!("CIRCLE TTR Select first tangent object:").into_owned(),
|
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::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 {
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||||
CmdResult::NeedPoint
|
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 {
|
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> {
|
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 {
|
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,
|
obj1,
|
||||||
obj2,
|
obj2,
|
||||||
hit1,
|
hit1,
|
||||||
hit2,
|
hit2,
|
||||||
} = &self.step
|
} = &self.step
|
||||||
{
|
else {
|
||||||
let hint = (*hit1 + *hit2) * 0.5;
|
return None;
|
||||||
let candidates = ttr_candidates(*obj1, *obj2, r);
|
};
|
||||||
if let Some(center) = best_of(&candidates, hint) {
|
let center = best_of(&ttr_candidates(*obj1, *obj2, radius), (*hit1 + *hit2) * 0.5)?;
|
||||||
Some(CmdResult::CommitAndExit(make_circle(
|
Some(circle_wire(
|
||||||
self.plane.to_world(center),
|
self.plane.to_world(center),
|
||||||
r,
|
radius,
|
||||||
self.plane,
|
self.plane,
|
||||||
)))
|
))
|
||||||
} else {
|
|
||||||
Some(CmdResult::Cancel)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
|
||||||
CmdResult::Cancel
|
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 dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
|
||||||
}
|
self.radius_from_point(cursor)
|
||||||
fn on_mouse_move(&mut self, _pt: DVec3) -> Option<WireModel> {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -973,11 +1057,14 @@ impl CadCommand for CircleTTTCommand {
|
|||||||
let hint = self.hits.iter().fold(DVec3::ZERO, |a, &b| a + b) / 3.0;
|
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]);
|
let candidates = ttt_candidates(self.objs[0], self.objs[1], self.objs[2]);
|
||||||
match best_circle_of(&candidates, hint) {
|
match best_circle_of(&candidates, hint) {
|
||||||
Some((center, r)) => CmdResult::CommitAndExit(make_circle(
|
Some((center, r)) => {
|
||||||
self.plane.to_world(center),
|
defaults::set_circle_radius(r);
|
||||||
r,
|
CmdResult::CommitAndExit(make_circle(
|
||||||
self.plane,
|
self.plane.to_world(center),
|
||||||
)),
|
r,
|
||||||
|
self.plane,
|
||||||
|
))
|
||||||
|
}
|
||||||
None => {
|
None => {
|
||||||
self.objs.pop();
|
self.objs.pop();
|
||||||
self.hits.pop();
|
self.hits.pop();
|
||||||
@@ -999,12 +1086,12 @@ impl CadCommand for CircleTTTCommand {
|
|||||||
|
|
||||||
|
|
||||||
// ── Autocomplete registry ─────────────────────────────────
|
// ── Autocomplete registry ─────────────────────────────────
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_2P"] }); // Circle2PCommand
|
inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_2P"] });
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_3P"] }); // Circle3PCommand
|
inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_3P"] });
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_CD"] }); // CircleCDCommand
|
inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_CD"] });
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE"] }); // CircleCommand
|
inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE"] });
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_TTR"] }); // CircleTTRCommand
|
inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_TTR"] });
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_TTT"] }); // CircleTTTCommand
|
inventory::submit!(crate::command::CommandRegistration { names: &["CIRCLE_TTT"] });
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@@ -1064,8 +1151,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// #318: concentric ring + a line through the center — the degenerate
|
/// #318: solve a concentric ring with a line through its center.
|
||||||
/// circle-pair equation must still yield the annulus circle.
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ttt_concentric_ring_with_line() {
|
fn ttt_concentric_ring_with_line() {
|
||||||
let inner = TangentObject::Circle { center: DVec3::ZERO, radius: 3.5 };
|
let inner = TangentObject::Circle { center: DVec3::ZERO, radius: 3.5 };
|
||||||
|
|||||||
@@ -131,8 +131,15 @@ fn break_circle(circle: &acadrust::entities::Circle, p1: DVec3, p2: DVec3) -> Ve
|
|||||||
let cx = circle.center.x;
|
let cx = circle.center.x;
|
||||||
let cy = circle.center.y;
|
let cy = circle.center.y;
|
||||||
|
|
||||||
let a1 = angle_on_arc(cx, cy, p1);
|
let plane = crate::entities::curve::circle_curve(circle).plane;
|
||||||
let a2 = angle_on_arc(cx, cy, p2);
|
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 {
|
if (a1 - a2).abs() < 0.01 {
|
||||||
return vec![EntityType::Circle(circle.clone())];
|
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.common.handle = Handle::NULL;
|
||||||
arc.center = circle.center.clone();
|
arc.center = circle.center.clone();
|
||||||
arc.radius = circle.radius;
|
arc.radius = circle.radius;
|
||||||
|
arc.thickness = circle.thickness;
|
||||||
arc.normal = circle.normal.clone();
|
arc.normal = circle.normal.clone();
|
||||||
arc.start_angle = a2;
|
arc.start_angle = a2;
|
||||||
arc.end_angle = a1;
|
arc.end_angle = a1;
|
||||||
|
|||||||
@@ -105,8 +105,8 @@ fn offset_xline(x: &XLineEnt, dist: f64, side_pt: Vec3) -> Option<EntityType> {
|
|||||||
// ── Circle offset ──────────────────────────────────────────────────────────
|
// ── Circle offset ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn offset_circle(c: &CircleEnt, dist: f64, side_pt: Vec3) -> Option<EntityType> {
|
fn offset_circle(c: &CircleEnt, dist: f64, side_pt: Vec3) -> Option<EntityType> {
|
||||||
let px = side_pt.x as f64;
|
let plane = crate::entities::curve::circle_curve(c).plane;
|
||||||
let py = side_pt.y as f64;
|
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 dc = ((px - c.center.x).powi(2) + (py - c.center.y).powi(2)).sqrt();
|
||||||
|
|
||||||
let new_r = if dc < c.radius {
|
let new_r = if dc < c.radius {
|
||||||
|
|||||||
Vendored
+32
-1
@@ -1268,7 +1268,8 @@ fn translated_prototype_wire(
|
|||||||
center[axis] += delta_f32[axis];
|
center[axis] += delta_f32[axis];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TangentGeom::Arc { center, .. } => {
|
TangentGeom::PlanarCircle { center, .. }
|
||||||
|
| TangentGeom::Arc { center, .. } => {
|
||||||
for axis in 0..3 {
|
for axis in 0..3 {
|
||||||
center[axis] += delta[axis];
|
center[axis] += delta[axis];
|
||||||
}
|
}
|
||||||
@@ -2371,6 +2372,36 @@ fn transform_tangent(
|
|||||||
end_angle: *end_angle,
|
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),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,13 @@ pub enum TangentGeom {
|
|||||||
Line { p1: [f32; 3], p2: [f32; 3] },
|
Line { p1: [f32; 3], p2: [f32; 3] },
|
||||||
/// Complete circle.
|
/// Complete circle.
|
||||||
Circle { center: [f32; 3], radius: f32 },
|
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.
|
/// Bounded circular arc in its world-space plane.
|
||||||
Arc {
|
Arc {
|
||||||
center: [f64; 3],
|
center: [f64; 3],
|
||||||
|
|||||||
+83
@@ -1692,6 +1692,62 @@ impl Snapper {
|
|||||||
.fold(f32::INFINITY, f32::min);
|
.fold(f32::INFINITY, f32::min);
|
||||||
(world, edge_d2)
|
(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) = (
|
let (tier, sub) = (
|
||||||
snap_tier(SnapType::Tangent),
|
snap_tier(SnapType::Tangent),
|
||||||
@@ -1722,6 +1778,12 @@ impl Snapper {
|
|||||||
center: glam::DVec3::from_array(*center),
|
center: glam::DVec3::from_array(*center),
|
||||||
radius: *radius,
|
radius: *radius,
|
||||||
},
|
},
|
||||||
|
TangentGeom::PlanarCircle { center, radius, .. } => {
|
||||||
|
TangentObject::Circle {
|
||||||
|
center: glam::DVec3::from_array(*center),
|
||||||
|
radius: *radius,
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
best = Some(SnapResult {
|
best = Some(SnapResult {
|
||||||
world: world_pt,
|
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<DVec3> {
|
||||||
|
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(
|
fn arc_tangent_points(
|
||||||
from: DVec3,
|
from: DVec3,
|
||||||
center: [f64; 3],
|
center: [f64; 3],
|
||||||
|
|||||||
Reference in new issue
Block a user