diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 71bd8ff26..0c36076a6 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -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: diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 2f44e3700..8a85cf230 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -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: diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index 8f746a2a2..f4bbdc1b9 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -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::() + .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), diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 3a7fce6f2..0613eb00a 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -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 by typing):" => Some(("view", "draworder-front-back-above-under-handle-by-typ")), diff --git a/src/modules/draw/defaults.rs b/src/modules/draw/defaults.rs index a3598fa8b..f21a40f89 100644 --- a/src/modules/draw/defaults.rs +++ b/src/modules/draw/defaults.rs @@ -26,6 +26,8 @@ thread_local! { static RECT_CHAMFER1: Cell = Cell::new(0.0); static RECT_CHAMFER2: Cell = Cell::new(0.0); static RECT_FILLET: Cell = Cell::new(0.0); + static DONUT_INNER_DIAMETER: Cell = Cell::new(0.5); + static DONUT_OUTER_DIAMETER: Cell = 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 +); diff --git a/src/modules/draw/draw/donut.rs b/src/modules/draw/draw/donut.rs index c459492a6..c0a3611d6 100644 --- a/src/modules/draw/draw/donut.rs +++ b/src/modules/draw/draw/donut.rs @@ -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 { + 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)