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

+2
View File
@@ -1271,6 +1271,8 @@ draw =
.donut-specify-center-of-donut-enter-to-exit = DONUT Specify center of donut (Enter to exit):
.donut-specify-inside-diameter-0 = DONUT Specify inside diameter <0>:
.donut-specify-outside-diameter = DONUT Specify outside diameter:
.donut-specify-second-point-for-inside-diameter = DONUT Specify second point for inside diameter:
.donut-specify-second-point-for-outside-diameter = DONUT Specify second point for outside diameter:
.draw-manually = Draw manually
.ellipse-specify-center = ELLIPSE Specify center:
.ellipse-specify-major-axis-endpoint = ELLIPSE Specify major axis endpoint:
+2
View File
@@ -1253,6 +1253,8 @@ draw =
.donut-specify-center-of-donut-enter-to-exit = DONUT Halkanın merkezini belirtin (çıkmak için Enter):
.donut-specify-inside-diameter-0 = DONUT İç çapı belirtin <0>:
.donut-specify-outside-diameter = DONUT Dış çapı belirtin:
.donut-specify-second-point-for-inside-diameter = DONUT İç çap için ikinci noktayı belirtin:
.donut-specify-second-point-for-outside-diameter = DONUT Dış çap için ikinci noktayı belirtin:
.draw-manually = Elle çiz
.ellipse-specify-center = ELLIPSE Merkezi belirtin:
.ellipse-specify-major-axis-endpoint = ELLIPSE Büyük eksen uç noktasını belirtin:
+46 -1
View File
@@ -935,6 +935,8 @@ impl OpenCADStudio {
| "SKETCHINC"
| "SKPOLY"
| "SKTOLERANCE"
| "DONUTID"
| "DONUTOD"
| "CENTEREXE"
| "CENTERLAYER"
| "CENTERLTYPE"
@@ -965,7 +967,7 @@ impl OpenCADStudio {
let value = it.next().map(|s| s.trim().to_string());
if name.is_empty() || name == "?" {
self.command_line.push_info(
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE | CLAYER CELTYPE TEXTSTYLE (read-only)",
"SETVAR: LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG ATTREQ ATTDIA DIMASSOC ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE | CLAYER CELTYPE TEXTSTYLE (read-only)",
);
} else {
let frame_kind = crate::scene::frame::kind_for_name(&name);
@@ -1081,6 +1083,49 @@ impl OpenCADStudio {
}
return Some(self.finish_dispatch(cmd));
}
if matches!(name.as_str(), "DONUTID" | "DONUTOD") {
let current = if name == "DONUTID" {
crate::modules::draw::defaults::get_donut_inner_diameter()
} else {
crate::modules::draw::defaults::get_donut_outer_diameter()
};
if let Some(value) = &value {
let parsed = value
.trim()
.replace(',', ".")
.parse::<f64>()
.ok()
.filter(|number| number.is_finite());
let valid = parsed.filter(|number| {
if name == "DONUTID" {
*number >= 0.0
} else {
*number > 0.0
}
});
if let Some(number) = valid {
if name == "DONUTID" {
crate::modules::draw::defaults::set_donut_inner_diameter(number);
} else {
crate::modules::draw::defaults::set_donut_outer_diameter(number);
}
self.command_line
.push_output(&format!("{name} = {number}"));
} else {
self.command_line.push_error(if name == "DONUTID" {
"SETVAR: DONUTID requires a finite value greater than or equal to zero."
} else {
"SETVAR: DONUTOD requires a finite value greater than zero."
});
}
} else {
self.command_line.push_output(crate::tf!(
"Enter new value for {name} <{current}>:"
).as_ref());
self.pending_setvar = Some(name.clone());
}
return Some(self.finish_dispatch(cmd));
}
// Parse a boolean given as 0/1 or ON/OFF.
let parse_bool = |s: &str| match s.to_uppercase().as_str() {
"1" | "ON" | "TRUE" => Some(true),
+2
View File
@@ -786,6 +786,8 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"DONUT Specify center of donut (Enter to exit):" => Some(("draw", "donut-specify-center-of-donut-enter-to-exit")),
"DONUT Specify inside diameter <0>:" => Some(("draw", "donut-specify-inside-diameter-0")),
"DONUT Specify outside diameter:" => Some(("draw", "donut-specify-outside-diameter")),
"DONUT Specify second point for inside diameter:" => Some(("draw", "donut-specify-second-point-for-inside-diameter")),
"DONUT Specify second point for outside diameter:" => Some(("draw", "donut-specify-second-point-for-outside-diameter")),
"DRAWORDER Select reference object (move selection above):" => Some(("view", "draworder-select-reference-object-move-selection-above")),
"DRAWORDER Select reference object (move selection under):" => Some(("view", "draworder-select-reference-object-move-selection-under")),
"DRAWORDER [Front / Back] (Above/Under <handle> by typing):" => Some(("view", "draworder-front-back-above-under-handle-by-typ")),
+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)