From 00b3ef54ebd0e8796e456ef106c54a58c89c2957 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:29:44 +0300 Subject: [PATCH 1/3] fix(donut): complete diameter workflow --- locales/en-US/opencadstudio.ftl | 2 + locales/tr-TR/opencadstudio.ftl | 2 + src/app/commands/styleprops.rs | 47 ++++++++- src/locale_catalog.rs | 2 + src/modules/draw/defaults.rs | 12 +++ src/modules/draw/draw/donut.rs | 171 +++++++++++++++++++++++++++----- 6 files changed, 208 insertions(+), 28 deletions(-) diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 71bd8ff2..0c36076a 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 2f44e370..8a85cf23 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 8f746a2a..f4bbdc1b 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 3a7fce6f..0613eb00 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 a3598fa8..f21a40f8 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 c459492a..c0a3611d 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) From 4e80afb0da696863ee1a93e1fb38148a899d2535 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 24 Aug 2026 18:04:01 +0300 Subject: [PATCH 2/3] Show effective ring segment widths --- src/entities/lwpolyline.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index e51bc8bb..467d5cb7 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -852,8 +852,24 @@ fn properties(pline: &LwPolyline) -> Vec { let v = pline.vertices.get(vi); let vx = v.map_or(0.0, |v| v.location.x); let vy = v.map_or(0.0, |v| v.location.y); - let start_w = v.map_or(0.0, |v| v.start_width); - let end_w = v.map_or(0.0, |v| v.end_width); + // A constant width is the effective width of every segment whose + // per-vertex value is not overridden. Showing the raw zero stored on + // those vertices made wide polylines (notably rings) claim that their + // visible segment width was zero even though the global width was active. + let start_w = v.map_or(0.0, |v| { + if v.start_width.abs() > 1.0e-12 { + v.start_width + } else { + pline.constant_width + } + }); + let end_w = v.map_or(0.0, |v| { + if v.end_width.abs() > 1.0e-12 { + v.end_width + } else { + pline.constant_width + } + }); let mp = ::mass_props(pline); let cloud_arc_length = revision_cloud_arc_length(pline); let vertex_label = if n == 0 { From 8bb18562d21491a63e3b15907e94901c5d1185ef Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 24 Aug 2026 20:40:08 +0300 Subject: [PATCH 3/3] fix(donut): keep workflow state consistent --- src/entities/lwpolyline.rs | 54 +++++++++++---------------------- src/modules/draw/draw/donut.rs | 55 ++++++++++++++++++---------------- 2 files changed, 47 insertions(+), 62 deletions(-) diff --git a/src/entities/lwpolyline.rs b/src/entities/lwpolyline.rs index 467d5cb7..d40417c9 100644 --- a/src/entities/lwpolyline.rs +++ b/src/entities/lwpolyline.rs @@ -16,6 +16,15 @@ use crate::scene::model::wire_model::TangentGeom; const TAU: f64 = std::f64::consts::TAU; const REVCLOUD_BULGE: f64 = 0.5; const MAX_REVCLOUD_VERTICES: usize = 100_000; +const WIDTH_EPSILON: f64 = 1.0e-9; + +fn effective_width(width: f64, constant_width: f64) -> f64 { + if width > WIDTH_EPSILON { + width + } else { + constant_width + } +} /// Midpoint position on an arc segment defined by its bulge. fn arc_midpoint(p0: [f64; 2], p1: [f64; 2], bulge: f64) -> [f64; 2] { @@ -500,8 +509,8 @@ fn tapered_band_verts(pline: &LwPolyline) -> Option 1e-9 { v.start_width } else { c }; - let ew = if v.end_width > 1e-9 { v.end_width } else { c }; + let sw = effective_width(v.start_width, c); + let ew = effective_width(v.end_width, c); ([v.location.x, v.location.y], v.bulge, sw, ew) }) .collect(); @@ -852,23 +861,11 @@ fn properties(pline: &LwPolyline) -> Vec { let v = pline.vertices.get(vi); let vx = v.map_or(0.0, |v| v.location.x); let vy = v.map_or(0.0, |v| v.location.y); - // A constant width is the effective width of every segment whose - // per-vertex value is not overridden. Showing the raw zero stored on - // those vertices made wide polylines (notably rings) claim that their - // visible segment width was zero even though the global width was active. let start_w = v.map_or(0.0, |v| { - if v.start_width.abs() > 1.0e-12 { - v.start_width - } else { - pline.constant_width - } + effective_width(v.start_width, pline.constant_width) }); let end_w = v.map_or(0.0, |v| { - if v.end_width.abs() > 1.0e-12 { - v.end_width - } else { - pline.constant_width - } + effective_width(v.end_width, pline.constant_width) }); let mp = ::mass_props(pline); let cloud_arc_length = revision_cloud_arc_length(pline); @@ -1217,16 +1214,8 @@ impl crate::entities::traits::Grippable for LwPolyline { new_v.location.x = midpoint[0]; new_v.location.y = midpoint[1]; new_v.vertex_id = 0; - let effective_start = if v0.start_width > 1e-9 { - v0.start_width - } else { - self.constant_width - }; - let effective_end = if v0.end_width > 1e-9 { - v0.end_width - } else { - self.constant_width - }; + let effective_start = effective_width(v0.start_width, self.constant_width); + let effective_end = effective_width(v0.end_width, self.constant_width); let middle_width = (effective_start + effective_end) * 0.5; self.vertices[i0].end_width = middle_width; new_v.start_width = middle_width; @@ -1287,7 +1276,6 @@ pub(crate) fn wide_fills(pl: &acadrust::entities::LwPolyline) -> ([f64; 2], Vec< // Feeding the stored width in whole draws every wide polyline twice as wide // as the file asks for, which on a donut (a closed 2-vertex bulge-1 polyline) // shows up as a disc 1.5× its real radius. - let hw_const = pl.constant_width as f32 * 0.5; let verts = &pl.vertices; let n = verts.len(); if n < 2 { @@ -1299,16 +1287,8 @@ pub(crate) fn wide_fills(pl: &acadrust::entities::LwPolyline) -> ([f64; 2], Vec< for i in 0..seg_count { let v0 = &verts[i]; let v1 = &verts[(i + 1) % n]; - let hw0 = if v0.start_width > 1e-9 { - v0.start_width as f32 * 0.5 - } else { - hw_const - }; - let hw1 = if v0.end_width > 1e-9 { - v0.end_width as f32 * 0.5 - } else { - hw_const - }; + let hw0 = effective_width(v0.start_width, pl.constant_width) as f32 * 0.5; + let hw1 = effective_width(v0.end_width, pl.constant_width) as f32 * 0.5; if hw0 < 1e-6 && hw1 < 1e-6 { continue; } diff --git a/src/modules/draw/draw/donut.rs b/src/modules/draw/draw/donut.rs index c0a3611d..fe1fb841 100644 --- a/src/modules/draw/draw/donut.rs +++ b/src/modules/draw/draw/donut.rs @@ -1,19 +1,12 @@ -// 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) -// - constant_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 +// DONUT creates a ring as a closed, constant-width LwPolyline. use acadrust::entities::{LwPolyline, LwVertex}; use acadrust::EntityType; -use cadkernel::geom2d::{Circle as KernelCircle, Curve as KernelCurve}; +use cadkernel::geom2d::{ + Circle as KernelCircle, Curve as KernelCurve, Vec2 as KernelVec2, +}; use glam::DVec3; + use crate::t; use crate::command::{CadCommand, CmdResult, WorkingPlane}; @@ -44,8 +37,6 @@ impl DonutCommand { 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, @@ -71,8 +62,23 @@ impl DonutCommand { } fn point_distance(&self, first: DVec3, second: DVec3) -> f64 { - let delta = self.plane.vector_to_local(second - first); - delta.x.hypot(delta.y) + let first = self.plane.to_local(first); + let second = self.plane.to_local(second); + KernelVec2::new(first.x, first.y).distance(KernelVec2::new(second.x, second.y)) + } + + fn project_to_plane(&self, point: DVec3) -> DVec3 { + let local = self.plane.to_local(point); + self.plane.to_world(DVec3::new(local.x, local.y, 0.0)) + } +} + +fn prompt_with_default(prompt: &str, value: f64) -> String { + match prompt.char_indices().next_back() { + Some((index, suffix @ (':' | ':'))) => { + format!("{} <{value:.4}>{suffix}", &prompt[..index]) + } + _ => format!("{prompt} <{value:.4}>"), } } @@ -92,10 +98,9 @@ impl CadCommand for DonutCommand { 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::AskOuter => prompt_with_default( + t!("DONUT Specify outside diameter:").as_ref(), + self.outer_r * 2.0, ), DonutState::AskOuterSecond(_) => { t!("DONUT Specify second point for outside diameter:").into_owned() @@ -147,7 +152,7 @@ impl CadCommand for DonutCommand { fn on_point(&mut self, pt: DVec3) -> CmdResult { match self.state { DonutState::AskInner => { - self.state = DonutState::AskInnerSecond(pt); + self.state = DonutState::AskInnerSecond(self.project_to_plane(pt)); CmdResult::NeedPoint } DonutState::AskInnerSecond(first) => { @@ -159,7 +164,7 @@ impl CadCommand for DonutCommand { CmdResult::NeedPoint } DonutState::AskOuter => { - self.state = DonutState::AskOuterSecond(pt); + self.state = DonutState::AskOuterSecond(self.project_to_plane(pt)); CmdResult::NeedPoint } DonutState::AskOuterSecond(first) => { @@ -197,9 +202,10 @@ impl CadCommand for DonutCommand { fn on_mouse_move(&mut self, pt: DVec3) -> Option { match self.state { DonutState::AskInnerSecond(first) | DonutState::AskOuterSecond(first) => { + let point = self.project_to_plane(pt); Some(WireModel::solid_f64( "rubber_band".into(), - vec![first.to_array(), pt.to_array()], + vec![first.to_array(), point.to_array()], WireModel::CYAN, false, )) @@ -257,8 +263,7 @@ fn make_donut(cx: f64, cy: f64, elevation: f64, inner_r: f64, outer_r: f64) -> E p.constant_width = width; p.elevation = elevation; - // Constant width is stored once on the polyline. Per-segment width fields - // stay zero so a later Global width edit controls the whole ring. + // Segment widths inherit the polyline's constant width. let mut v0 = LwVertex::new(Vector2::new(cx - r_avg, cy)); v0.bulge = 1.0;