fix(donut): complete diameter workflow

This commit is contained in:
ramox81 committed 2026-08-24 09:29:44 +03:00
1 parent 7215faae44
commit 00b3ef54eb
6 files changed
+208 -28

No files matched your search

+12
View File
@@ -26,6 +26,8 @@ thread_local! {
static RECT_CHAMFER1: Cell<f64> = Cell::new(0.0);
static RECT_CHAMFER2: Cell<f64> = Cell::new(0.0);
static RECT_FILLET: Cell<f64> = Cell::new(0.0);
static DONUT_INNER_DIAMETER: Cell<f64> = Cell::new(0.5);
static DONUT_OUTER_DIAMETER: Cell<f64> = Cell::new(1.0);
}
macro_rules! accessors {
@@ -68,3 +70,13 @@ accessors!(get_rect_rotation, set_rect_rotation, RECT_ROTATION);
accessors!(get_rect_chamfer1, set_rect_chamfer1, RECT_CHAMFER1);
accessors!(get_rect_chamfer2, set_rect_chamfer2, RECT_CHAMFER2);
accessors!(get_rect_fillet, set_rect_fillet, RECT_FILLET);
accessors!(
get_donut_inner_diameter,
set_donut_inner_diameter,
DONUT_INNER_DIAMETER
);
accessors!(
get_donut_outer_diameter,
set_donut_outer_diameter,
DONUT_OUTER_DIAMETER
);
+144 -27
View File
@@ -2,7 +2,7 @@
//
// A donut is an LwPolyline with:
// - 2 vertices at (cx ± r_avg, 0), both with bulge = 1.0 (two 180° CCW arcs)
// - start_width = end_width = (outer - inner) / 2
// - constant_width = (outer - inner) / 2
// - is_closed = true
//
// Workflow:
@@ -12,10 +12,15 @@
use acadrust::entities::{LwPolyline, LwVertex};
use acadrust::EntityType;
use cadkernel::geom2d::{Circle as KernelCircle, Curve as KernelCurve};
use glam::DVec3;
use crate::t;
use crate::command::{CadCommand, CmdResult, WorkingPlane};
use crate::modules::draw::defaults;
use crate::scene::model::wire_model::WireModel;
const TAU: f64 = std::f64::consts::TAU;
pub struct DonutCommand {
state: DonutState,
@@ -24,21 +29,51 @@ pub struct DonutCommand {
plane: WorkingPlane,
}
#[derive(Clone, Copy)]
enum DonutState {
AskInner,
AskInnerSecond(DVec3),
AskOuter,
AskOuterSecond(DVec3),
PlaceCenter,
}
impl DonutCommand {
pub fn new() -> Self {
let mut inner_diameter = defaults::get_donut_inner_diameter();
let mut outer_diameter = defaults::get_donut_outer_diameter();
if inner_diameter > outer_diameter {
std::mem::swap(&mut inner_diameter, &mut outer_diameter);
defaults::set_donut_inner_diameter(inner_diameter);
defaults::set_donut_outer_diameter(outer_diameter);
}
Self {
state: DonutState::AskInner,
inner_r: 0.0,
outer_r: 1.0,
inner_r: inner_diameter * 0.5,
outer_r: outer_diameter * 0.5,
plane: WorkingPlane::default(),
}
}
fn set_inner_diameter(&mut self, diameter: f64) {
self.inner_r = diameter * 0.5;
self.state = DonutState::AskOuter;
}
fn set_outer_diameter(&mut self, diameter: f64) {
self.outer_r = diameter * 0.5;
if self.inner_r > self.outer_r {
std::mem::swap(&mut self.inner_r, &mut self.outer_r);
}
defaults::set_donut_inner_diameter(self.inner_r * 2.0);
defaults::set_donut_outer_diameter(self.outer_r * 2.0);
self.state = DonutState::PlaceCenter;
}
fn point_distance(&self, first: DVec3, second: DVec3) -> f64 {
let delta = self.plane.vector_to_local(second - first);
delta.x.hypot(delta.y)
}
}
impl CadCommand for DonutCommand {
@@ -51,9 +86,20 @@ impl CadCommand for DonutCommand {
}
fn prompt(&self) -> String {
match &self.state {
DonutState::AskInner => t!("DONUT Specify inside diameter <0>:").into_owned(),
DonutState::AskOuter => t!("DONUT Specify outside diameter:").into_owned(),
match self.state {
DonutState::AskInner => t!("DONUT Specify inside diameter <0>:")
.replace("<0>", &format!("<{:.4}>", self.inner_r * 2.0)),
DonutState::AskInnerSecond(_) => {
t!("DONUT Specify second point for inside diameter:").into_owned()
}
DonutState::AskOuter => format!(
"{} <{:.4}>:",
t!("DONUT Specify outside diameter:").trim_end_matches(':'),
self.outer_r * 2.0
),
DonutState::AskOuterSecond(_) => {
t!("DONUT Specify second point for outside diameter:").into_owned()
}
DonutState::PlaceCenter => {
t!("DONUT Specify center of donut (Enter to exit):").into_owned()
}
@@ -78,22 +124,20 @@ impl CadCommand for DonutCommand {
.replace(',', ".")
.parse()
.ok()
.filter(|&v: &f64| v >= 0.0)?;
match &self.state {
.filter(|v: &f64| v.is_finite())?;
match self.state {
DonutState::AskInner => {
self.inner_r = val / 2.0;
self.state = DonutState::AskOuter;
if val < 0.0 {
return Some(CmdResult::NeedPoint);
}
self.set_inner_diameter(val);
Some(CmdResult::NeedPoint)
}
DonutState::AskOuter => {
if val <= 0.0 {
return None;
return Some(CmdResult::NeedPoint);
}
self.outer_r = val / 2.0;
if self.inner_r > self.outer_r {
std::mem::swap(&mut self.inner_r, &mut self.outer_r);
}
self.state = DonutState::PlaceCenter;
self.set_outer_diameter(val);
Some(CmdResult::NeedPoint)
}
_ => None,
@@ -101,29 +145,106 @@ impl CadCommand for DonutCommand {
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
match &self.state {
match self.state {
DonutState::AskInner => {
self.state = DonutState::AskInnerSecond(pt);
CmdResult::NeedPoint
}
DonutState::AskInnerSecond(first) => {
let diameter = self.point_distance(first, pt);
if diameter <= 1.0e-9 {
return CmdResult::NeedPoint;
}
self.set_inner_diameter(diameter);
CmdResult::NeedPoint
}
DonutState::AskOuter => {
self.state = DonutState::AskOuterSecond(pt);
CmdResult::NeedPoint
}
DonutState::AskOuterSecond(first) => {
let diameter = self.point_distance(first, pt);
if diameter <= 1.0e-9 {
return CmdResult::NeedPoint;
}
self.set_outer_diameter(diameter);
CmdResult::NeedPoint
}
DonutState::PlaceCenter => {
let center = self.plane.to_local(pt);
let entity = make_donut(center.x, center.y, center.z, self.inner_r, self.outer_r);
// Keep command active so user can place more donuts.
CmdResult::CommitEntity(self.plane.place_entity(entity))
}
_ => CmdResult::NeedPoint,
}
}
fn on_enter(&mut self) -> CmdResult {
match &self.state {
match self.state {
DonutState::AskInner => {
// Accept default 0 for inner diameter
self.inner_r = 0.0;
self.state = DonutState::AskOuter;
CmdResult::NeedPoint
}
DonutState::AskOuter => {
self.set_outer_diameter(self.outer_r * 2.0);
CmdResult::NeedPoint
}
DonutState::PlaceCenter => CmdResult::Cancel,
_ => CmdResult::Cancel,
}
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
match self.state {
DonutState::AskInnerSecond(first) | DonutState::AskOuterSecond(first) => {
Some(WireModel::solid_f64(
"rubber_band".into(),
vec![first.to_array(), pt.to_array()],
WireModel::CYAN,
false,
))
}
DonutState::PlaceCenter => Some(donut_wire(
pt,
self.inner_r,
self.outer_r,
self.plane,
)),
_ => None,
}
}
}
fn donut_wire(
center: DVec3,
inner_r: f64,
outer_r: f64,
plane: WorkingPlane,
) -> WireModel {
let local_center = plane.to_local(center);
let mut points = Vec::new();
for radius in [outer_r, inner_r] {
if radius <= 1.0e-9 {
continue;
}
if !points.is_empty() {
points.push([f64::NAN; 3]);
}
points.extend(
KernelCurve::Circle(KernelCircle {
centre: [local_center.x, local_center.y],
radius,
})
.tessellate_angle(TAU / 64.0)
.into_iter()
.map(|point| {
plane
.to_world(DVec3::new(point[0], point[1], local_center.z))
.to_array()
}),
);
}
WireModel::solid_f64("rubber_band".into(), points, WireModel::CYAN, false)
}
fn make_donut(cx: f64, cy: f64, elevation: f64, inner_r: f64, outer_r: f64) -> EntityType {
@@ -136,17 +257,13 @@ fn make_donut(cx: f64, cy: f64, elevation: f64, inner_r: f64, outer_r: f64) -> E
p.constant_width = width;
p.elevation = elevation;
// Vertex at (cx - r, cy) with bulge 1.0 (180° CCW arc to next vertex)
// Constant width is stored once on the polyline. Per-segment width fields
// stay zero so a later Global width edit controls the whole ring.
let mut v0 = LwVertex::new(Vector2::new(cx - r_avg, cy));
v0.bulge = 1.0;
v0.start_width = width;
v0.end_width = width;
// Vertex at (cx + r, cy) with bulge 1.0 (second 180° arc back to v0)
let mut v1 = LwVertex::new(Vector2::new(cx + r_avg, cy));
v1.bulge = 1.0;
v1.start_width = width;
v1.end_width = width;
p.vertices = vec![v0, v1];
EntityType::LwPolyline(p)