Merge pull request #867 from ramox81/codex/complete-donut-workflow

Complete donut diameter workflow
This commit is contained in:
Hakan Seven 2026-08-24 20:40:32 +03:00 committed by GitHub
commit 20fb52d6bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 244 additions and 63 deletions

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-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-inside-diameter-0 = DONUT Specify inside diameter <0>:
.donut-specify-outside-diameter = DONUT Specify outside diameter: .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 .draw-manually = Draw manually
.ellipse-specify-center = ELLIPSE Specify center: .ellipse-specify-center = ELLIPSE Specify center:
.ellipse-specify-major-axis-endpoint = ELLIPSE Specify major axis endpoint: .ellipse-specify-major-axis-endpoint = ELLIPSE Specify major axis endpoint:

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-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-inside-diameter-0 = DONUT İç çapı belirtin <0>:
.donut-specify-outside-diameter = DONUT Dış çapı belirtin: .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 .draw-manually = Elle çiz
.ellipse-specify-center = ELLIPSE Merkezi belirtin: .ellipse-specify-center = ELLIPSE Merkezi belirtin:
.ellipse-specify-major-axis-endpoint = ELLIPSE Büyük eksen uç noktasını belirtin: .ellipse-specify-major-axis-endpoint = ELLIPSE Büyük eksen uç noktasını belirtin:

View file

@ -935,6 +935,8 @@ impl OpenCADStudio {
| "SKETCHINC" | "SKETCHINC"
| "SKPOLY" | "SKPOLY"
| "SKTOLERANCE" | "SKTOLERANCE"
| "DONUTID"
| "DONUTOD"
| "CENTEREXE" | "CENTEREXE"
| "CENTERLAYER" | "CENTERLAYER"
| "CENTERLTYPE" | "CENTERLTYPE"
@ -965,7 +967,7 @@ impl OpenCADStudio {
let value = it.next().map(|s| s.trim().to_string()); let value = it.next().map(|s| s.trim().to_string());
if name.is_empty() || name == "?" { if name.is_empty() || name == "?" {
self.command_line.push_info( 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 { } else {
let frame_kind = crate::scene::frame::kind_for_name(&name); let frame_kind = crate::scene::frame::kind_for_name(&name);
@ -1081,6 +1083,49 @@ impl OpenCADStudio {
} }
return Some(self.finish_dispatch(cmd)); 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. // Parse a boolean given as 0/1 or ON/OFF.
let parse_bool = |s: &str| match s.to_uppercase().as_str() { let parse_bool = |s: &str| match s.to_uppercase().as_str() {
"1" | "ON" | "TRUE" => Some(true), "1" | "ON" | "TRUE" => Some(true),

View file

@ -16,6 +16,15 @@ use crate::scene::model::wire_model::TangentGeom;
const TAU: f64 = std::f64::consts::TAU; const TAU: f64 = std::f64::consts::TAU;
const REVCLOUD_BULGE: f64 = 0.5; const REVCLOUD_BULGE: f64 = 0.5;
const MAX_REVCLOUD_VERTICES: usize = 100_000; 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. /// Midpoint position on an arc segment defined by its bulge.
fn arc_midpoint(p0: [f64; 2], p1: [f64; 2], bulge: f64) -> [f64; 2] { fn arc_midpoint(p0: [f64; 2], p1: [f64; 2], bulge: f64) -> [f64; 2] {
@ -500,8 +509,8 @@ fn tapered_band_verts(pline: &LwPolyline) -> Option<Vec<([f64; 2], f64, f64, f64
.vertices .vertices
.iter() .iter()
.map(|v| { .map(|v| {
let sw = if v.start_width > 1e-9 { v.start_width } else { c }; let sw = effective_width(v.start_width, c);
let ew = if v.end_width > 1e-9 { v.end_width } else { c }; let ew = effective_width(v.end_width, c);
([v.location.x, v.location.y], v.bulge, sw, ew) ([v.location.x, v.location.y], v.bulge, sw, ew)
}) })
.collect(); .collect();
@ -852,8 +861,12 @@ fn properties(pline: &LwPolyline) -> Vec<PropSection> {
let v = pline.vertices.get(vi); let v = pline.vertices.get(vi);
let vx = v.map_or(0.0, |v| v.location.x); let vx = v.map_or(0.0, |v| v.location.x);
let vy = v.map_or(0.0, |v| v.location.y); let vy = v.map_or(0.0, |v| v.location.y);
let start_w = v.map_or(0.0, |v| v.start_width); let start_w = v.map_or(0.0, |v| {
let end_w = v.map_or(0.0, |v| v.end_width); effective_width(v.start_width, pline.constant_width)
});
let end_w = v.map_or(0.0, |v| {
effective_width(v.end_width, pline.constant_width)
});
let mp = <LwPolyline as crate::entities::traits::MassPropsCalc>::mass_props(pline); let mp = <LwPolyline as crate::entities::traits::MassPropsCalc>::mass_props(pline);
let cloud_arc_length = revision_cloud_arc_length(pline); let cloud_arc_length = revision_cloud_arc_length(pline);
let vertex_label = if n == 0 { let vertex_label = if n == 0 {
@ -1201,16 +1214,8 @@ impl crate::entities::traits::Grippable for LwPolyline {
new_v.location.x = midpoint[0]; new_v.location.x = midpoint[0];
new_v.location.y = midpoint[1]; new_v.location.y = midpoint[1];
new_v.vertex_id = 0; new_v.vertex_id = 0;
let effective_start = if v0.start_width > 1e-9 { let effective_start = effective_width(v0.start_width, self.constant_width);
v0.start_width let effective_end = effective_width(v0.end_width, self.constant_width);
} else {
self.constant_width
};
let effective_end = if v0.end_width > 1e-9 {
v0.end_width
} else {
self.constant_width
};
let middle_width = (effective_start + effective_end) * 0.5; let middle_width = (effective_start + effective_end) * 0.5;
self.vertices[i0].end_width = middle_width; self.vertices[i0].end_width = middle_width;
new_v.start_width = middle_width; new_v.start_width = middle_width;
@ -1271,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 // 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) // 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. // 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 verts = &pl.vertices;
let n = verts.len(); let n = verts.len();
if n < 2 { if n < 2 {
@ -1283,16 +1287,8 @@ pub(crate) fn wide_fills(pl: &acadrust::entities::LwPolyline) -> ([f64; 2], Vec<
for i in 0..seg_count { for i in 0..seg_count {
let v0 = &verts[i]; let v0 = &verts[i];
let v1 = &verts[(i + 1) % n]; let v1 = &verts[(i + 1) % n];
let hw0 = if v0.start_width > 1e-9 { let hw0 = effective_width(v0.start_width, pl.constant_width) as f32 * 0.5;
v0.start_width as f32 * 0.5 let hw1 = effective_width(v0.end_width, pl.constant_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
};
if hw0 < 1e-6 && hw1 < 1e-6 { if hw0 < 1e-6 && hw1 < 1e-6 {
continue; continue;
} }

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 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 inside diameter <0>:" => Some(("draw", "donut-specify-inside-diameter-0")),
"DONUT Specify outside diameter:" => Some(("draw", "donut-specify-outside-diameter")), "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 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 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")), "DRAWORDER [Front / Back] (Above/Under <handle> by typing):" => Some(("view", "draworder-front-back-above-under-handle-by-typ")),

View file

@ -26,6 +26,8 @@ thread_local! {
static RECT_CHAMFER1: Cell<f64> = Cell::new(0.0); static RECT_CHAMFER1: Cell<f64> = Cell::new(0.0);
static RECT_CHAMFER2: 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 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 { 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_chamfer1, set_rect_chamfer1, RECT_CHAMFER1);
accessors!(get_rect_chamfer2, set_rect_chamfer2, RECT_CHAMFER2); accessors!(get_rect_chamfer2, set_rect_chamfer2, RECT_CHAMFER2);
accessors!(get_rect_fillet, set_rect_fillet, RECT_FILLET); 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
);

View file

@ -1,21 +1,19 @@
// DONUT command — create a filled circular ring (thick LwPolyline). // DONUT creates a ring as a closed, constant-width 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::entities::{LwPolyline, LwVertex};
use acadrust::EntityType; use acadrust::EntityType;
use cadkernel::geom2d::{
Circle as KernelCircle, Curve as KernelCurve, Vec2 as KernelVec2,
};
use glam::DVec3; use glam::DVec3;
use crate::t; use crate::t;
use crate::command::{CadCommand, CmdResult, WorkingPlane}; 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 { pub struct DonutCommand {
state: DonutState, state: DonutState,
@ -24,21 +22,64 @@ pub struct DonutCommand {
plane: WorkingPlane, plane: WorkingPlane,
} }
#[derive(Clone, Copy)]
enum DonutState { enum DonutState {
AskInner, AskInner,
AskInnerSecond(DVec3),
AskOuter, AskOuter,
AskOuterSecond(DVec3),
PlaceCenter, PlaceCenter,
} }
impl DonutCommand { impl DonutCommand {
pub fn new() -> Self { 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);
}
Self { Self {
state: DonutState::AskInner, state: DonutState::AskInner,
inner_r: 0.0, inner_r: inner_diameter * 0.5,
outer_r: 1.0, outer_r: outer_diameter * 0.5,
plane: WorkingPlane::default(), 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 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}>"),
}
} }
impl CadCommand for DonutCommand { impl CadCommand for DonutCommand {
@ -51,9 +92,19 @@ impl CadCommand for DonutCommand {
} }
fn prompt(&self) -> String { fn prompt(&self) -> String {
match &self.state { match self.state {
DonutState::AskInner => t!("DONUT Specify inside diameter <0>:").into_owned(), DonutState::AskInner => t!("DONUT Specify inside diameter <0>:")
DonutState::AskOuter => t!("DONUT Specify outside diameter:").into_owned(), .replace("<0>", &format!("<{:.4}>", self.inner_r * 2.0)),
DonutState::AskInnerSecond(_) => {
t!("DONUT Specify second point for inside diameter:").into_owned()
}
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()
}
DonutState::PlaceCenter => { DonutState::PlaceCenter => {
t!("DONUT Specify center of donut (Enter to exit):").into_owned() t!("DONUT Specify center of donut (Enter to exit):").into_owned()
} }
@ -78,22 +129,20 @@ impl CadCommand for DonutCommand {
.replace(',', ".") .replace(',', ".")
.parse() .parse()
.ok() .ok()
.filter(|&v: &f64| v >= 0.0)?; .filter(|v: &f64| v.is_finite())?;
match &self.state { match self.state {
DonutState::AskInner => { DonutState::AskInner => {
self.inner_r = val / 2.0; if val < 0.0 {
self.state = DonutState::AskOuter; return Some(CmdResult::NeedPoint);
}
self.set_inner_diameter(val);
Some(CmdResult::NeedPoint) Some(CmdResult::NeedPoint)
} }
DonutState::AskOuter => { DonutState::AskOuter => {
if val <= 0.0 { if val <= 0.0 {
return None; return Some(CmdResult::NeedPoint);
} }
self.outer_r = val / 2.0; self.set_outer_diameter(val);
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) Some(CmdResult::NeedPoint)
} }
_ => None, _ => None,
@ -101,29 +150,107 @@ impl CadCommand for DonutCommand {
} }
fn on_point(&mut self, pt: DVec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match &self.state { match self.state {
DonutState::AskInner => {
self.state = DonutState::AskInnerSecond(self.project_to_plane(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(self.project_to_plane(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 => { DonutState::PlaceCenter => {
let center = self.plane.to_local(pt); let center = self.plane.to_local(pt);
let entity = make_donut(center.x, center.y, center.z, self.inner_r, self.outer_r); 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. // Keep command active so user can place more donuts.
CmdResult::CommitEntity(self.plane.place_entity(entity)) CmdResult::CommitEntity(self.plane.place_entity(entity))
} }
_ => CmdResult::NeedPoint,
} }
} }
fn on_enter(&mut self) -> CmdResult { fn on_enter(&mut self) -> CmdResult {
match &self.state { match self.state {
DonutState::AskInner => { DonutState::AskInner => {
// Accept default 0 for inner diameter
self.inner_r = 0.0;
self.state = DonutState::AskOuter; self.state = DonutState::AskOuter;
CmdResult::NeedPoint CmdResult::NeedPoint
} }
DonutState::AskOuter => {
self.set_outer_diameter(self.outer_r * 2.0);
CmdResult::NeedPoint
}
DonutState::PlaceCenter => CmdResult::Cancel, DonutState::PlaceCenter => CmdResult::Cancel,
_ => CmdResult::Cancel, _ => CmdResult::Cancel,
} }
} }
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
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(), point.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 { fn make_donut(cx: f64, cy: f64, elevation: f64, inner_r: f64, outer_r: f64) -> EntityType {
@ -136,17 +263,12 @@ fn make_donut(cx: f64, cy: f64, elevation: f64, inner_r: f64, outer_r: f64) -> E
p.constant_width = width; p.constant_width = width;
p.elevation = elevation; p.elevation = elevation;
// Vertex at (cx - r, cy) with bulge 1.0 (180° CCW arc to next vertex) // Segment widths inherit the polyline's constant width.
let mut v0 = LwVertex::new(Vector2::new(cx - r_avg, cy)); let mut v0 = LwVertex::new(Vector2::new(cx - r_avg, cy));
v0.bulge = 1.0; 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)); let mut v1 = LwVertex::new(Vector2::new(cx + r_avg, cy));
v1.bulge = 1.0; v1.bulge = 1.0;
v1.start_width = width;
v1.end_width = width;
p.vertices = vec![v0, v1]; p.vertices = vec![v0, v1];
EntityType::LwPolyline(p) EntityType::LwPolyline(p)