Feat: DONUT command (DO) — filled circular ring as LwPolyline

Creates LwPolyline with 2 vertices at (cx ± r_avg, cy), bulge=1.0,
start_width/end_width = (outer - inner) / 2, is_closed = true.

Workflow: inner diameter → outer diameter → click center points (Enter to exit).
Inner diameter 0 → filled solid circle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-04-02 23:50:27 +03:00
commit 8fca75fade
3 changed files with 134 additions and 1 deletions

View file

@ -332,6 +332,13 @@ impl H7CAD {
self.tabs[i].active_cmd = Some(Box::new(new_cmd)); self.tabs[i].active_cmd = Some(Box::new(new_cmd));
} }
"DONUT"|"DO" => {
use crate::modules::home::draw::donut::DonutCommand;
let cmd = DonutCommand::new();
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
"CIRCLE"|"C" => { "CIRCLE"|"C" => {
use crate::modules::home::draw::circle::CircleCommand; use crate::modules::home::draw::circle::CircleCommand;
let new_cmd = CircleCommand::new(); let new_cmd = CircleCommand::new();
@ -1037,7 +1044,7 @@ impl H7CAD {
"HELP"|"?" => { "HELP"|"?" => {
self.command_line.push_output( self.command_line.push_output(
"Draw: LINE CIRCLE ARC PLINE RECT POLY POINT ELLIPSE SPLINE RAY XLINE HATCH | \ "Draw: LINE CIRCLE ARC PLINE RECT POLY POINT ELLIPSE SPLINE RAY XLINE HATCH DONUT | \
Modify: MOVE COPY ROTATE SCALE MIRROR ERASE OFFSET EXTEND FILLET CHAMFER STRETCH EXPLODE TRIM BREAK JOIN LENGTHEN ALIGN | \ Modify: MOVE COPY ROTATE SCALE MIRROR ERASE OFFSET EXTEND FILLET CHAMFER STRETCH EXPLODE TRIM BREAK JOIN LENGTHEN ALIGN | \
Array: ARRAY ARRAYRECT ARRAYPOLAR ARRAYPATH | \ Array: ARRAY ARRAYRECT ARRAYPOLAR ARRAYPATH | \
Text: TEXT MTEXT LEADER MLEADER | \ Text: TEXT MTEXT LEADER MLEADER | \

View file

@ -0,0 +1,125 @@
// DONUT command — create a filled circular ring (thick LwPolyline).
//
// 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
// - is_closed = true
//
// Workflow:
// 1. Type inner diameter (or 0 for a filled circle)
// 2. Type outer diameter
// 3. Click center point(s); Enter to finish
use acadrust::entities::{LwPolyline, LwVertex};
use acadrust::EntityType;
use glam::Vec3;
use crate::command::{CadCommand, CmdResult};
pub struct DonutCommand {
state: DonutState,
inner_r: f64,
outer_r: f64,
}
enum DonutState {
AskInner,
AskOuter,
PlaceCenter,
}
impl DonutCommand {
pub fn new() -> Self {
Self {
state: DonutState::AskInner,
inner_r: 0.0,
outer_r: 1.0,
}
}
}
impl CadCommand for DonutCommand {
fn name(&self) -> &'static str { "DONUT" }
fn prompt(&self) -> String {
match &self.state {
DonutState::AskInner => "DONUT Specify inside diameter <0>:".into(),
DonutState::AskOuter => "DONUT Specify outside diameter:".into(),
DonutState::PlaceCenter => "DONUT Specify center of donut (Enter to exit):".into(),
}
}
fn wants_text_input(&self) -> bool {
matches!(self.state, DonutState::AskInner | DonutState::AskOuter)
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let val: f64 = text.trim().replace(',', ".").parse().ok().filter(|&v: &f64| v >= 0.0)?;
match &self.state {
DonutState::AskInner => {
self.inner_r = val / 2.0;
self.state = DonutState::AskOuter;
Some(CmdResult::NeedPoint)
}
DonutState::AskOuter => {
if val <= 0.0 { return None; }
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;
Some(CmdResult::NeedPoint)
}
_ => None,
}
}
fn on_point(&mut self, pt: Vec3) -> CmdResult {
match &self.state {
DonutState::PlaceCenter => {
let entity = make_donut(pt.x as f64, pt.z as f64, self.inner_r, self.outer_r);
// Keep command active so user can place more donuts.
CmdResult::CommitEntity(entity)
}
_ => CmdResult::NeedPoint,
}
}
fn on_enter(&mut self) -> CmdResult {
match &self.state {
DonutState::AskInner => {
// Accept default 0 for inner diameter
self.inner_r = 0.0;
self.state = DonutState::AskOuter;
CmdResult::NeedPoint
}
DonutState::PlaceCenter => CmdResult::Cancel,
_ => CmdResult::Cancel,
}
}
}
fn make_donut(cx: f64, cy: f64, inner_r: f64, outer_r: f64) -> EntityType {
use acadrust::types::Vector2;
let r_avg = (inner_r + outer_r) / 2.0;
let width = outer_r - inner_r;
let mut p = LwPolyline::new();
p.is_closed = true;
p.constant_width = width;
// Vertex at (cx - r, cy) with bulge 1.0 (180° CCW arc to next vertex)
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)
}

View file

@ -1,5 +1,6 @@
pub mod arc; pub mod arc;
pub mod circle; pub mod circle;
pub mod donut;
pub mod ellipse; pub mod ellipse;
pub mod hatch; pub mod hatch;
pub mod line; pub mod line;