From a2a64dcfb74e3d6322d0657a9d16aadaaa548c9d Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:58:18 +0300 Subject: [PATCH] Complete CONE creation workflow --- src/modules/draw/draw/circle.rs | 6 +- src/modules/model/primitive_cmd.rs | 772 ++++++++++++++++++++++++++++- src/scene/model/solid_model.rs | 17 + 3 files changed, 771 insertions(+), 24 deletions(-) diff --git a/src/modules/draw/draw/circle.rs b/src/modules/draw/draw/circle.rs index a5bb9b943..12c684bf5 100644 --- a/src/modules/draw/draw/circle.rs +++ b/src/modules/draw/draw/circle.rs @@ -98,7 +98,7 @@ fn plane_distance(a: DVec3, b: DVec3, plane: WorkingPlane) -> f64 { d.x.hypot(d.y) } -fn tangent_object_local(object: TangentObject, plane: WorkingPlane) -> TangentObject { +pub(crate) fn tangent_object_local(object: TangentObject, plane: WorkingPlane) -> TangentObject { match object { TangentObject::Line { p1, p2 } => TangentObject::Line { p1: plane.to_local(p1), @@ -594,7 +594,7 @@ fn solve_quadratic(a: f64, b: f64, c: f64) -> Vec { vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)] } -fn best_of(candidates: &[DVec3], hint: DVec3) -> Option { +pub(crate) fn best_of(candidates: &[DVec3], hint: DVec3) -> Option { candidates.iter().copied().min_by(|a, b| { a.distance(hint) .partial_cmp(&b.distance(hint)) @@ -627,7 +627,7 @@ fn tangent_curve(object: TangentObject) -> KernelCurve { } } -fn ttr_candidates(first: TangentObject, second: TangentObject, radius: f64) -> Vec { +pub(crate) fn ttr_candidates(first: TangentObject, second: TangentObject, radius: f64) -> Vec { let first = tangent_curve(first); let second = tangent_curve(second); fillets_between(&first, &second, radius, Tolerance::default()) diff --git a/src/modules/model/primitive_cmd.rs b/src/modules/model/primitive_cmd.rs index cfaa65ca2..a3bf6f544 100644 --- a/src/modules/model/primitive_cmd.rs +++ b/src/modules/model/primitive_cmd.rs @@ -6,7 +6,7 @@ use acadrust::EntityType; use cadkernel::brep::Body; use glam::DVec3; -use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane}; +use crate::command::{CadCommand, CmdOption, CmdResult, TangentObject, WorkingPlane}; use crate::scene::model::solid_model; use crate::scene::model::wire_model::WireModel; use crate::t; @@ -36,6 +36,59 @@ enum BoxStep { HeightSecondPoint, } +#[derive(Clone, Copy)] +enum ConeStep { + BaseCenter, + BaseRadius, + BaseDiameter, + ThreePointFirst, + ThreePointSecond(DVec3), + ThreePointThird(DVec3, DVec3), + TwoPointFirst, + TwoPointSecond(DVec3), + EllipseFirst, + EllipseSecond(DVec3), + EllipseThird(DVec3, DVec3), + TtrFirst, + TtrSecond { object: TangentObject, hit: DVec3 }, + TtrRadius { + first: TangentObject, + second: TangentObject, + first_hit: DVec3, + second_hit: DVec3, + }, + Height, + HeightFirstPoint, + HeightSecondPoint(DVec3), + AxisEndpoint, + TopRadius, +} + +#[derive(Clone, Copy)] +struct ConeDefaults { + base_x_radius: f64, + base_y_radius: f64, + top_radius: f64, + height: f64, +} + +impl Default for ConeDefaults { + fn default() -> Self { + Self { + base_x_radius: 1.0, + base_y_radius: 1.0, + top_radius: 0.0, + height: 1.0, + } + } +} + +fn cone_defaults() -> &'static std::sync::Mutex { + static DEFAULTS: std::sync::OnceLock> = + std::sync::OnceLock::new(); + DEFAULTS.get_or_init(|| std::sync::Mutex::new(ConeDefaults::default())) +} + impl Shape { fn from_id(id: &str) -> Option { Some(match id { @@ -85,11 +138,21 @@ pub struct PrimitiveCommand { box_angle: f64, box_width_sign: f64, box_height_anchor: Option, + cone_step: ConeStep, + cone_frame: Option, + cone_base_x_radius: f64, + cone_base_y_radius: f64, + cone_top_radius: f64, + cone_defaults: ConeDefaults, plane: WorkingPlane, } impl PrimitiveCommand { pub fn new(id: &str) -> Self { + let remembered = cone_defaults() + .lock() + .map(|value| *value) + .unwrap_or_default(); Self { shape: Shape::from_id(id).unwrap_or(Shape::Box), pts: Vec::new(), @@ -102,10 +165,545 @@ impl PrimitiveCommand { box_angle: 0.0, box_width_sign: 1.0, box_height_anchor: None, + cone_step: ConeStep::BaseCenter, + cone_frame: None, + cone_base_x_radius: remembered.base_x_radius, + cone_base_y_radius: remembered.base_y_radius, + cone_top_radius: remembered.top_radius, + cone_defaults: remembered, plane: WorkingPlane::default(), } } + fn cone_frame_at(&self, center: DVec3) -> WorkingPlane { + WorkingPlane { + origin: center, + x: self.plane.x, + y: self.plane.y, + z: self.plane.z, + } + } + + fn set_cone_base( + &mut self, + frame: WorkingPlane, + base_x_radius: f64, + base_y_radius: f64, + ) -> bool { + if !frame.origin.is_finite() + || !frame.x.is_finite() + || !frame.y.is_finite() + || !frame.z.is_finite() + || !base_x_radius.is_finite() + || !base_y_radius.is_finite() + || base_x_radius < 1e-6 + || base_y_radius < 1e-6 + { + return false; + } + self.cone_frame = Some(frame); + self.cone_base_x_radius = base_x_radius; + self.cone_base_y_radius = base_y_radius; + self.cone_step = ConeStep::Height; + true + } + + fn cone_three_point_frame(a: DVec3, b: DVec3, c: DVec3) -> Option<(WorkingPlane, f64)> { + let normal = (b - a).cross(c - a).try_normalize()?; + let first_axis = (b - a).try_normalize()?; + let second_axis = normal.cross(first_axis).try_normalize()?; + let plane = WorkingPlane::new(a, first_axis, second_axis); + let local_b = plane.to_local(b); + let local_c = plane.to_local(c); + let circle = cadkernel::geom2d::arc_through_points( + [0.0, 0.0], + [local_b.x, local_b.y], + [local_c.x, local_c.y], + )?; + let center = plane.to_world(DVec3::new(circle.centre[0], circle.centre[1], 0.0)); + let x = (a - center).try_normalize()?; + let y = normal.cross(x).try_normalize()?; + Some((WorkingPlane::new(center, x, y), circle.radius)) + } + + fn cone_axis_frame(&self, axis: DVec3) -> Option<(WorkingPlane, f64)> { + let current = self.cone_frame?; + let height = axis.length(); + if !height.is_finite() || height < 1e-6 { + return None; + } + let z = axis / height; + let mut x = current.x - z * current.x.dot(z); + if x.length_squared() < 1e-12 { + x = current.y - z * current.y.dot(z); + } + let x = x.try_normalize()?; + let y = z.cross(x).try_normalize()?; + Some((WorkingPlane::new(current.origin, x, y), height)) + } + + fn cone_history_transform(frame: WorkingPlane) -> [f64; 16] { + glam::DMat4::from_cols( + frame.x.extend(0.0), + frame.y.extend(0.0), + frame.z.extend(0.0), + frame.origin.extend(1.0), + ) + .to_cols_array() + } + + fn build_cone(&self, axis: DVec3) -> Option<(Body, SolidHistoryOperation, f64)> { + let (frame, height) = self.cone_axis_frame(axis)?; + let local = solid_model::cone_frustum_solid( + [0.0; 3], + self.cone_base_x_radius, + self.cone_base_y_radius, + self.cone_top_radius, + height, + )?; + let placed = solid_model::placed( + &local, + frame.x.to_array(), + frame.y.to_array(), + frame.z.to_array(), + frame.origin.to_array(), + )?; + Some(( + placed, + crate::scene::model::solid_history::cone_op( + Self::cone_history_transform(frame), + self.cone_base_x_radius, + self.cone_base_y_radius, + self.cone_top_radius, + height, + ), + height, + )) + } + + fn commit_cone_axis(&self, axis: DVec3) -> CmdResult { + let Some((solid, history, height)) = self.build_cone(axis) else { + return CmdResult::NeedPoint; + }; + if let Ok(mut defaults) = cone_defaults().lock() { + *defaults = ConeDefaults { + base_x_radius: self.cone_base_x_radius, + base_y_radius: self.cone_base_y_radius, + top_radius: self.cone_top_radius, + height, + }; + } + let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&solid) else { + return CmdResult::Cancel; + }; + let mut entity = Solid3D::new(); + entity.set_sat_document(&document); + entity.wires = solid_model::edge_wires(&solid); + CmdResult::CommitSolid { + entity: EntityType::Solid3D(entity), + solid: Box::new(solid), + history, + } + } + + fn commit_cone_height(&self, height: f64) -> CmdResult { + let Some(frame) = self.cone_frame else { + return CmdResult::NeedPoint; + }; + if !height.is_finite() || height.abs() < 1e-6 { + return CmdResult::NeedPoint; + } + self.commit_cone_axis(frame.z * height) + } + + fn cone_height_at(&self, point: DVec3) -> Option { + let frame = self.cone_frame?; + Some((point - frame.origin).dot(frame.z)) + } + + fn cone_ttr_center(&self, radius: f64) -> Option { + let ConeStep::TtrRadius { + first, + second, + first_hit, + second_hit, + } = self.cone_step + else { + return None; + }; + let candidates = crate::modules::draw::draw::circle::ttr_candidates(first, second, radius); + let local = crate::modules::draw::draw::circle::best_of( + &candidates, + (first_hit + second_hit) * 0.5, + )?; + Some(self.plane.to_world(local)) + } + + fn cone_preview(&self, axis: DVec3) -> Option { + let (frame, height) = self.cone_axis_frame(axis)?; + let top = frame.origin + frame.z * height; + let top_y = if self.cone_top_radius > 0.0 { + self.cone_top_radius * self.cone_base_y_radius / self.cone_base_x_radius + } else { + 0.0 + }; + let mut points = Vec::new(); + push_ellipse_world( + &mut points, + frame.origin, + frame.x, + frame.y, + self.cone_base_x_radius, + self.cone_base_y_radius, + ); + if self.cone_top_radius > 1e-9 { + push_ellipse_world( + &mut points, + top, + frame.x, + frame.y, + self.cone_top_radius, + top_y, + ); + } + for index in 0..4 { + let angle = index as f64 * std::f64::consts::FRAC_PI_2; + let base = frame.origin + + frame.x * (self.cone_base_x_radius * angle.cos()) + + frame.y * (self.cone_base_y_radius * angle.sin()); + let upper = if self.cone_top_radius > 1e-9 { + top + frame.x * (self.cone_top_radius * angle.cos()) + + frame.y * (top_y * angle.sin()) + } else { + top + }; + push_segment(&mut points, base, upper); + } + Some(wire("primitive_height_preview", points)) + } + + fn cone_base_preview(&self, frame: WorkingPlane, x_radius: f64, y_radius: f64) -> WireModel { + let mut points = Vec::new(); + push_ellipse_world( + &mut points, + frame.origin, + frame.x, + frame.y, + x_radius.max(1e-9), + y_radius.max(1e-9), + ); + wire("primitive_preview", points) + } + + fn cone_prompt(&self) -> String { + match self.cone_step { + ConeStep::BaseCenter => + t!("CONE Specify center point of base or [3P/2P/Ttr/Elliptical]:").into_owned(), + ConeStep::BaseRadius => crate::tf!( + "CONE Specify base radius or [Diameter] <{:.4}>:", + self.cone_defaults.base_x_radius + ).into_owned(), + ConeStep::BaseDiameter => crate::tf!( + "CONE Specify base diameter <{:.4}>:", + self.cone_defaults.base_x_radius * 2.0 + ).into_owned(), + ConeStep::ThreePointFirst => t!("CONE Specify first point on base:").into_owned(), + ConeStep::ThreePointSecond(_) => t!("CONE Specify second point on base:").into_owned(), + ConeStep::ThreePointThird(_, _) => t!("CONE Specify third point on base:").into_owned(), + ConeStep::TwoPointFirst => t!("CONE Specify first endpoint of base diameter:").into_owned(), + ConeStep::TwoPointSecond(_) => t!("CONE Specify second endpoint of base diameter:").into_owned(), + ConeStep::EllipseFirst => t!("CONE Specify first endpoint of ellipse axis:").into_owned(), + ConeStep::EllipseSecond(_) => t!("CONE Specify second endpoint of ellipse axis:").into_owned(), + ConeStep::EllipseThird(_, _) => t!("CONE Specify distance to other ellipse axis:").into_owned(), + ConeStep::TtrFirst => t!("CONE Select first tangent object:").into_owned(), + ConeStep::TtrSecond { .. } => t!("CONE Select second tangent object:").into_owned(), + ConeStep::TtrRadius { .. } => crate::tf!( + "CONE Specify base radius <{:.4}>:", + self.cone_defaults.base_x_radius + ).into_owned(), + ConeStep::Height => crate::tf!( + "CONE Specify height or [2Point/Axis endpoint/Top radius] <{:.4}>:", + self.cone_defaults.height + ).into_owned(), + ConeStep::HeightFirstPoint => t!("CONE Specify first point for height:").into_owned(), + ConeStep::HeightSecondPoint(_) => t!("CONE Specify second point for height:").into_owned(), + ConeStep::AxisEndpoint => t!("CONE Specify axis endpoint:").into_owned(), + ConeStep::TopRadius => crate::tf!( + "CONE Specify top radius <{:.4}>:", + self.cone_defaults.top_radius + ).into_owned(), + } + } + + fn cone_options(&self) -> Vec { + match self.cone_step { + ConeStep::BaseCenter => vec![ + CmdOption::new("3P", "3P"), + CmdOption::new("2P", "2P"), + CmdOption::new("Ttr", "TTR"), + CmdOption::new(t!("Elliptical").as_ref(), "E"), + ], + ConeStep::BaseRadius => vec![CmdOption::new(t!("Diameter").as_ref(), "D")], + ConeStep::Height => vec![ + CmdOption::new("2Point", "2P"), + CmdOption::new(t!("Axis endpoint").as_ref(), "A"), + CmdOption::new(t!("Top radius").as_ref(), "T"), + ], + _ => Vec::new(), + } + } + + fn on_cone_point(&mut self, point: DVec3) -> CmdResult { + match self.cone_step { + ConeStep::BaseCenter => { + self.cone_frame = Some(self.cone_frame_at(point)); + self.cone_step = ConeStep::BaseRadius; + CmdResult::NeedPoint + } + ConeStep::BaseRadius | ConeStep::BaseDiameter => { + let Some(frame) = self.cone_frame else { + return CmdResult::NeedPoint; + }; + let local = frame.to_local(point); + let radius = local.x.hypot(local.y); + if self.set_cone_base(frame, radius, radius) { + CmdResult::NeedPoint + } else { + CmdResult::NeedPoint + } + } + ConeStep::ThreePointFirst => { + self.cone_step = ConeStep::ThreePointSecond(point); + CmdResult::NeedPoint + } + ConeStep::ThreePointSecond(first) => { + if point.distance_squared(first) > 1e-12 { + self.cone_step = ConeStep::ThreePointThird(first, point); + } + CmdResult::NeedPoint + } + ConeStep::ThreePointThird(first, second) => { + if let Some((frame, radius)) = Self::cone_three_point_frame(first, second, point) { + self.set_cone_base(frame, radius, radius); + } + CmdResult::NeedPoint + } + ConeStep::TwoPointFirst => { + self.cone_step = ConeStep::TwoPointSecond(point); + CmdResult::NeedPoint + } + ConeStep::TwoPointSecond(first) => { + let a = self.plane.to_local(first); + let b = self.plane.to_local(point); + let radius = (b - a).truncate().length() * 0.5; + let center = self.plane.to_world((a + b) * 0.5); + self.set_cone_base(self.cone_frame_at(center), radius, radius); + CmdResult::NeedPoint + } + ConeStep::EllipseFirst => { + self.cone_step = ConeStep::EllipseSecond(point); + CmdResult::NeedPoint + } + ConeStep::EllipseSecond(first) => { + if point.distance_squared(first) > 1e-12 { + self.cone_step = ConeStep::EllipseThird(first, point); + } + CmdResult::NeedPoint + } + ConeStep::EllipseThird(first, second) => { + let a = self.plane.to_local(first); + let b = self.plane.to_local(second); + let center_local = (a + b) * 0.5; + let axis = (b - a).truncate(); + let x_radius = axis.length() * 0.5; + if let Some(x2) = axis.try_normalize() { + let x = self.plane.vector_to_world(DVec3::new(x2.x, x2.y, 0.0)); + let y = self.plane.z.cross(x).normalize_or_zero(); + let frame = WorkingPlane::new(self.plane.to_world(center_local), x, y); + let local = frame.to_local(point); + self.set_cone_base(frame, x_radius, local.y.abs()); + } + CmdResult::NeedPoint + } + ConeStep::TtrRadius { second_hit, .. } => { + let local = self.plane.to_local(point); + let radius = (local - second_hit).truncate().length(); + if let Some(center) = self.cone_ttr_center(radius) { + self.set_cone_base(self.cone_frame_at(center), radius, radius); + } + CmdResult::NeedPoint + } + ConeStep::Height => self + .cone_height_at(point) + .map(|height| self.commit_cone_height(height)) + .unwrap_or(CmdResult::NeedPoint), + ConeStep::HeightFirstPoint => { + self.cone_step = ConeStep::HeightSecondPoint(point); + CmdResult::NeedPoint + } + ConeStep::HeightSecondPoint(first) => self.commit_cone_height(first.distance(point)), + ConeStep::AxisEndpoint => { + let Some(frame) = self.cone_frame else { + return CmdResult::NeedPoint; + }; + self.commit_cone_axis(point - frame.origin) + } + ConeStep::TopRadius => { + let Some(frame) = self.cone_frame else { + return CmdResult::NeedPoint; + }; + let local = frame.to_local(point); + let radius = local.x.hypot(local.y); + if radius.is_finite() { + self.cone_top_radius = radius; + self.cone_step = ConeStep::Height; + } + CmdResult::NeedPoint + } + ConeStep::TtrFirst | ConeStep::TtrSecond { .. } => CmdResult::NeedPoint, + } + } + + fn on_cone_tangent(&mut self, object: TangentObject, hit: DVec3) -> CmdResult { + let object = crate::modules::draw::draw::circle::tangent_object_local(object, self.plane); + let hit = self.plane.to_local(hit); + match self.cone_step { + ConeStep::TtrFirst => { + self.cone_step = ConeStep::TtrSecond { object, hit }; + } + ConeStep::TtrSecond { + object: first, + hit: first_hit, + } => { + self.cone_step = ConeStep::TtrRadius { + first, + second: object, + first_hit, + second_hit: hit, + }; + } + _ => {} + } + CmdResult::NeedPoint + } + + fn on_cone_text(&mut self, raw: &str) -> Option { + let token = raw.trim(); + let upper = token.to_uppercase(); + match self.cone_step { + ConeStep::BaseCenter => { + self.cone_step = match upper.as_str() { + "3P" => ConeStep::ThreePointFirst, + "2P" => ConeStep::TwoPointFirst, + "T" | "TTR" => ConeStep::TtrFirst, + "E" | "ELLIPTICAL" => ConeStep::EllipseFirst, + _ => return None, + }; + return Some(CmdResult::NeedPoint); + } + ConeStep::BaseRadius if matches!(upper.as_str(), "D" | "DIAMETER") => { + self.cone_step = ConeStep::BaseDiameter; + return Some(CmdResult::NeedPoint); + } + ConeStep::Height => { + self.cone_step = match upper.as_str() { + "2P" | "2POINT" => ConeStep::HeightFirstPoint, + "A" | "AXIS" | "AXIS ENDPOINT" => ConeStep::AxisEndpoint, + "T" | "TOP" | "TOP RADIUS" => ConeStep::TopRadius, + _ => { + let height = crate::entities::common::parse_typed_length(token)?; + return Some(self.commit_cone_height(height)); + } + }; + return Some(CmdResult::NeedPoint); + } + _ => {} + } + let number = crate::entities::common::parse_typed_length(token)?; + match self.cone_step { + ConeStep::BaseRadius => { + let frame = self.cone_frame?; + (number > 0.0).then(|| { + self.set_cone_base(frame, number, number); + CmdResult::NeedPoint + }) + } + ConeStep::BaseDiameter => { + let frame = self.cone_frame?; + (number > 0.0).then(|| { + self.set_cone_base(frame, number * 0.5, number * 0.5); + CmdResult::NeedPoint + }) + } + ConeStep::TtrRadius { .. } => { + let center = self.cone_ttr_center(number)?; + (number > 0.0).then(|| { + self.set_cone_base(self.cone_frame_at(center), number, number); + CmdResult::NeedPoint + }) + } + ConeStep::TopRadius if number >= 0.0 => { + self.cone_top_radius = number; + self.cone_step = ConeStep::Height; + Some(CmdResult::NeedPoint) + } + _ => None, + } + } + + fn cone_mouse_move(&self, point: DVec3) -> Option { + match self.cone_step { + ConeStep::BaseRadius | ConeStep::BaseDiameter => { + let frame = self.cone_frame?; + let local = frame.to_local(point); + let radius = local.x.hypot(local.y); + Some(self.cone_base_preview(frame, radius, radius)) + } + ConeStep::ThreePointSecond(first) | ConeStep::TwoPointSecond(first) => { + let a = self.plane.to_local(first); + let b = self.plane.to_local(point); + let center = self.plane.to_world((a + b) * 0.5); + let radius = (b - a).truncate().length() * 0.5; + Some(self.cone_base_preview(self.cone_frame_at(center), radius, radius)) + } + ConeStep::ThreePointThird(first, second) => { + let (frame, radius) = Self::cone_three_point_frame(first, second, point)?; + Some(self.cone_base_preview(frame, radius, radius)) + } + ConeStep::EllipseThird(first, second) => { + let a = self.plane.to_local(first); + let b = self.plane.to_local(second); + let center_local = (a + b) * 0.5; + let axis = (b - a).truncate(); + let x2 = axis.try_normalize()?; + let x = self.plane.vector_to_world(DVec3::new(x2.x, x2.y, 0.0)); + let y = self.plane.z.cross(x).normalize_or_zero(); + let frame = WorkingPlane::new(self.plane.to_world(center_local), x, y); + let local = frame.to_local(point); + Some(self.cone_base_preview(frame, axis.length() * 0.5, local.y.abs())) + } + ConeStep::TtrRadius { second_hit, .. } => { + let local = self.plane.to_local(point); + let radius = (local - second_hit).truncate().length(); + let center = self.cone_ttr_center(radius)?; + Some(self.cone_base_preview(self.cone_frame_at(center), radius, radius)) + } + ConeStep::Height => { + let frame = self.cone_frame?; + self.cone_preview(frame.z * self.cone_height_at(point)?) + } + ConeStep::HeightSecondPoint(first) => { + let frame = self.cone_frame?; + self.cone_preview(frame.z * first.distance(point)) + } + ConeStep::AxisEndpoint => { + let frame = self.cone_frame?; + self.cone_preview(point - frame.origin) + } + _ => None, + } + } + /// Number of footprint points the shape needs before the height step. fn footprint_pts(&self) -> usize { match self.shape { @@ -361,33 +959,19 @@ impl PrimitiveCommand { ), ) } - Shape::Cylinder | Shape::Cone => { + Shape::Cylinder => { let c = self.pts[0]; let r = (self.pts[1] - c).length(); if r < 1e-6 || height < 1e-6 { return None; } let center = [c.x, c.y, c.z]; - if self.shape == Shape::Cylinder { - ( - solid_model::cylinder_solid(center, r, height), - solid_history::cylinder_op( - self.history_transform(c), - r, - height, - ), - ) - } else { - ( - solid_model::cone_solid(center, r, height), - solid_history::cone_op( - self.history_transform(c), - r, - height, - ), - ) - } + ( + solid_model::cylinder_solid(center, r, height), + solid_history::cylinder_op(self.history_transform(c), r, height), + ) } + Shape::Cone => return None, Shape::Sphere => { let c = self.pts[0]; let r = (self.pts[1] - c).length(); @@ -482,6 +1066,12 @@ impl CadCommand for PrimitiveCommand { } fn cursor_axis(&self) -> Option<(DVec3, DVec3)> { + if self.shape == Shape::Cone { + return matches!(self.cone_step, ConeStep::Height).then(|| { + let frame = self.cone_frame.unwrap_or(self.plane); + (frame.origin, frame.z.normalize_or_zero()) + }); + } let constrained = if self.shape == Shape::Box { self.box_step == BoxStep::Height } else { @@ -501,6 +1091,9 @@ impl CadCommand for PrimitiveCommand { fn prompt(&self) -> String { let n = self.shape.name(); + if self.shape == Shape::Cone { + return self.cone_prompt(); + } if self.shape == Shape::Box { return match self.box_step { BoxStep::FirstCorner => t!("%{n} Specify first corner or [Center]:", n = n), @@ -540,6 +1133,9 @@ impl CadCommand for PrimitiveCommand { } fn options(&self) -> Vec { + if self.shape == Shape::Cone { + return self.cone_options(); + } if self.shape != Shape::Box { return Vec::new(); } @@ -555,6 +1151,9 @@ impl CadCommand for PrimitiveCommand { } fn on_point(&mut self, pt: DVec3) -> CmdResult { + if self.shape == Shape::Cone { + return self.on_cone_point(pt); + } if self.shape == Shape::Box { let local = self.plane.to_local(pt); if !local.is_finite() { @@ -632,6 +1231,36 @@ impl CadCommand for PrimitiveCommand { } fn on_enter(&mut self) -> CmdResult { + if self.shape == Shape::Cone { + return match self.cone_step { + ConeStep::BaseRadius | ConeStep::BaseDiameter => { + let Some(frame) = self.cone_frame else { + return CmdResult::Cancel; + }; + self.set_cone_base( + frame, + self.cone_defaults.base_x_radius, + self.cone_defaults.base_y_radius, + ); + CmdResult::NeedPoint + } + ConeStep::TtrRadius { .. } => { + let radius = self.cone_defaults.base_x_radius; + let Some(center) = self.cone_ttr_center(radius) else { + return CmdResult::NeedPoint; + }; + self.set_cone_base(self.cone_frame_at(center), radius, radius); + CmdResult::NeedPoint + } + ConeStep::Height => self.commit_cone_height(self.cone_defaults.height), + ConeStep::TopRadius => { + self.cone_top_radius = self.cone_defaults.top_radius; + self.cone_step = ConeStep::Height; + CmdResult::NeedPoint + } + _ => CmdResult::Cancel, + }; + } if self.shape == Shape::Box { return CmdResult::Cancel; } @@ -646,7 +1275,31 @@ impl CadCommand for PrimitiveCommand { CmdResult::Cancel } + fn needs_tangent_pick(&self) -> bool { + self.shape == Shape::Cone + && matches!(self.cone_step, ConeStep::TtrFirst | ConeStep::TtrSecond { .. }) + } + + fn on_tangent_point(&mut self, object: TangentObject, hit: DVec3) -> CmdResult { + if self.shape == Shape::Cone { + self.on_cone_tangent(object, hit) + } else { + self.on_point(hit) + } + } + fn wants_text_input(&self) -> bool { + if self.shape == Shape::Cone { + return matches!( + self.cone_step, + ConeStep::BaseCenter + | ConeStep::BaseRadius + | ConeStep::BaseDiameter + | ConeStep::TtrRadius { .. } + | ConeStep::Height + | ConeStep::TopRadius + ); + } if self.shape == Shape::Box { matches!( self.box_step, @@ -663,6 +1316,12 @@ impl CadCommand for PrimitiveCommand { } fn point_step_accepts_keywords(&self) -> bool { + if self.shape == Shape::Cone { + return matches!( + self.cone_step, + ConeStep::BaseCenter | ConeStep::BaseRadius | ConeStep::Height + ); + } self.shape == Shape::Box && matches!( self.box_step, @@ -671,6 +1330,9 @@ impl CadCommand for PrimitiveCommand { } fn on_text_input(&mut self, raw: &str) -> Option { + if self.shape == Shape::Cone { + return self.on_cone_text(raw); + } if self.shape == Shape::Box { let token = raw.trim(); let upper = token.to_uppercase(); @@ -719,6 +1381,9 @@ impl CadCommand for PrimitiveCommand { } fn on_mouse_move(&mut self, pt: DVec3) -> Option { + if self.shape == Shape::Cone { + return self.cone_mouse_move(pt); + } if self.pts.is_empty() { return None; } @@ -806,6 +1471,30 @@ impl CadCommand for PrimitiveCommand { fn dyn_spec(&self) -> Option { use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec}; + if self.shape == Shape::Cone { + let frame = self.cone_frame.unwrap_or(self.plane); + let role = match self.cone_step { + ConeStep::BaseRadius | ConeStep::TtrRadius { .. } | ConeStep::TopRadius => { + DynRole::Radius + } + ConeStep::BaseDiameter => DynRole::Diameter, + ConeStep::Height | ConeStep::HeightSecondPoint(_) | ConeStep::AxisEndpoint => { + DynRole::Height + } + _ => return None, + }; + return Some(DynSpec { + anchor: DynAnchor::Point(frame.origin), + fields: vec![DynFieldSpec::new(role)], + guide: if matches!(role, DynRole::Radius | DynRole::Diameter) { + DynGuide::Radius + } else { + DynGuide::None + }, + ref_point: None, + }); + } + let role = if self.shape == Shape::Box { match self.box_step { BoxStep::CubeSize | BoxStep::Length => DynRole::Distance, @@ -827,6 +1516,28 @@ impl CadCommand for PrimitiveCommand { } fn dyn_live_value(&self, cursor: DVec3) -> Option { + if self.shape == Shape::Cone { + return match self.cone_step { + ConeStep::BaseRadius | ConeStep::TopRadius => { + let frame = self.cone_frame?; + let local = frame.to_local(cursor); + Some(local.x.hypot(local.y)) + } + ConeStep::BaseDiameter => { + let frame = self.cone_frame?; + let local = frame.to_local(cursor); + Some(local.x.hypot(local.y) * 2.0) + } + ConeStep::TtrRadius { second_hit, .. } => { + let local = self.plane.to_local(cursor); + Some((local - second_hit).truncate().length()) + } + ConeStep::Height => self.cone_height_at(cursor).map(f64::abs), + ConeStep::HeightSecondPoint(first) => Some(first.distance(cursor)), + ConeStep::AxisEndpoint => self.cone_frame.map(|frame| frame.origin.distance(cursor)), + _ => None, + }; + } if self.shape == Shape::Box { let local = self.plane.to_local(cursor); if !local.is_finite() { @@ -972,6 +1683,25 @@ fn push_circle(points: &mut Vec<[f32; 3]>, center: DVec3, radius: f64) { circle_points(points, center, radius); } +fn push_ellipse_world( + points: &mut Vec<[f32; 3]>, + center: DVec3, + x_axis: DVec3, + y_axis: DVec3, + x_radius: f64, + y_radius: f64, +) { + push_break(points); + const SEGMENTS: usize = 64; + for index in 0..=SEGMENTS { + let angle = index as f64 / SEGMENTS as f64 * std::f64::consts::TAU; + let point = center + + x_axis * (x_radius * angle.cos()) + + y_axis * (y_radius * angle.sin()); + points.push(point.as_vec3().to_array()); + } +} + fn circle_points(out: &mut Vec<[f32; 3]>, c: DVec3, r: f64) { const SEG: usize = 48; for i in 0..=SEG { diff --git a/src/scene/model/solid_model.rs b/src/scene/model/solid_model.rs index b9d7f5dbf..403983bc9 100644 --- a/src/scene/model/solid_model.rs +++ b/src/scene/model/solid_model.rs @@ -45,6 +45,23 @@ pub fn cone_solid(center: [f64; 3], radius: f64, height: f64) -> Option { brep::make::cone(center, radius, height) } +/// Circular or elliptical cone/frustum standing on the z = base plane. +pub fn cone_frustum_solid( + center: [f64; 3], + base_x_radius: f64, + base_y_radius: f64, + top_radius: f64, + height: f64, +) -> Option { + brep::make::frustum( + center, + base_x_radius, + base_y_radius, + top_radius, + height, + ) +} + /// Solid sphere about `center`. pub fn sphere_solid(center: [f64; 3], radius: f64) -> Option { brep::make::sphere(center, radius)