From 8bb18562d21491a63e3b15907e94901c5d1185ef Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 24 Aug 2026 20:40:08 +0300 Subject: [PATCH] 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;