// Interactive kernel primitives stored as ACIS Solid3D entities. use acadrust::entities::Solid3D; use acadrust::objects::SolidHistoryOperation; use acadrust::EntityType; use cadkernel::brep::Body; use cadkernel::geom2d::{ fillets_between, Circle as KernelCircle, Curve as KernelCurve, Line as KernelLine, Tolerance, }; use glam::DVec3; use std::sync::{Mutex, OnceLock}; use std::sync::atomic::{AtomicU64, Ordering}; use crate::command::{CadCommand, CmdOption, CmdResult, TangentObject, WorkingPlane}; use crate::scene::model::solid_model; use crate::scene::model::wire_model::WireModel; use crate::t; static LAST_WEDGE_HEIGHT: AtomicU64 = AtomicU64::new(0); /// Which primitive a `PrimitiveCommand` builds. #[derive(Clone, Copy, PartialEq, Eq)] pub enum Shape { Box, Wedge, Cylinder, Cone, Sphere, Pyramid, Torus, } #[derive(Clone, Copy, PartialEq, Eq)] enum BoxStep { FirstCorner, CenterPoint, OppositeCorner, CubeSize, Length, Width, Height, HeightFirstPoint, HeightSecondPoint, } #[derive(Clone, Copy)] enum SphereStep { Center, Radius(DVec3), Diameter(DVec3), TwoPointFirst, TwoPointSecond(DVec3), ThreePointFirst, ThreePointSecond(DVec3), ThreePointThird(DVec3, DVec3), TtrFirst, TtrSecond { object: TangentObject, hit: DVec3, }, TtrRadius { first: TangentObject, second: TangentObject, first_hit: DVec3, second_hit: DVec3, }, } #[derive(Clone, Copy)] enum ConeStep { BaseCenter, BaseRadius, BaseDiameter, ThreePointFirst, ThreePointSecond(DVec3), ThreePointThird(DVec3, DVec3), TwoPointFirst, TwoPointSecond(DVec3), EllipseFirst, EllipseCenter, EllipseCenterFirstAxis(DVec3), EllipseCenterSecondAxis(DVec3, DVec3), EllipseSecond(DVec3), EllipseThird(DVec3, DVec3), TtrFirst, TtrSecond { object: TangentObject, hit: DVec3 }, TtrRadius { first: TangentObject, second: TangentObject, first_hit: DVec3, second_hit: DVec3, }, Height, HeightAfterTopRadius, HeightFirstPoint, HeightSecondPoint(DVec3), AxisEndpoint, TopRadius, } #[derive(Clone, Copy, PartialEq, Eq)] enum PyramidType { Circumscribed, Inscribed, } #[derive(Clone, Copy, PartialEq, Eq)] enum PyramidStep { BaseCenter, Sides, EdgeFirst, EdgeSecond, BaseRadius, Height, HeightAfterTopRadius, HeightFirstPoint, HeightSecondPoint, AxisEndpoint, TopRadius, } #[derive(Clone, Copy)] struct PyramidDefaults { displayed_base_radius: f64, displayed_top_radius: f64, height: f64, sides: usize, kind: PyramidType, } impl Default for PyramidDefaults { fn default() -> Self { Self { displayed_base_radius: 1.0, displayed_top_radius: 0.0, height: 1.0, sides: 4, kind: PyramidType::Circumscribed, } } } #[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 sphere_radius_store() -> &'static Mutex { static VALUE: OnceLock> = OnceLock::new(); VALUE.get_or_init(|| Mutex::new(1.0)) } fn sphere_radius_default() -> f64 { sphere_radius_store() .lock() .map(|value| *value) .unwrap_or(1.0) } fn remember_sphere_radius(radius: f64) { if radius.is_finite() && radius > 1e-6 { if let Ok(mut value) = sphere_radius_store().lock() { *value = radius; } } } fn sphere_tangent_local(object: TangentObject, plane: WorkingPlane) -> TangentObject { match object { TangentObject::Line { p1, p2 } => TangentObject::Line { p1: plane.to_local(p1), p2: plane.to_local(p2), }, TangentObject::Circle { center, radius } => TangentObject::Circle { center: plane.to_local(center), radius, }, } } fn sphere_tangent_curve(object: TangentObject) -> KernelCurve { match object { TangentObject::Line { p1, p2 } => KernelCurve::Line(KernelLine { start: [p1.x, p1.y], end: [p2.x, p2.y], }), TangentObject::Circle { center, radius } => KernelCurve::Circle(KernelCircle { centre: [center.x, center.y], radius, }), } } fn sphere_ttr_centers( first: TangentObject, second: TangentObject, radius: f64, ) -> Vec { fillets_between( &sphere_tangent_curve(first), &sphere_tangent_curve(second), radius, Tolerance::default(), ) .into_iter() .map(|fillet| DVec3::new(fillet.centre[0], fillet.centre[1], 0.0)) .collect() } fn closest_sphere_center(candidates: &[DVec3], hint: DVec3) -> Option { candidates.iter().copied().min_by(|first, second| { first .distance(hint) .partial_cmp(&second.distance(hint)) .unwrap_or(std::cmp::Ordering::Equal) }) } fn sphere_through_three_points( first: DVec3, second: DVec3, third: DVec3, ) -> Option<(DVec3, f64)> { let circle = cadkernel::geom2d::arc_through_points( [first.x, first.y], [second.x, second.y], [third.x, third.y], )?; Some(( DVec3::new(circle.centre[0], circle.centre[1], first.z), circle.radius, )) } 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())) } fn pyramid_defaults() -> &'static Mutex { static DEFAULTS: OnceLock> = OnceLock::new(); DEFAULTS.get_or_init(|| Mutex::new(PyramidDefaults::default())) } fn pyramid_apothem_factor(sides: usize) -> f64 { (std::f64::consts::PI / sides.max(3) as f64).cos() } fn pyramid_display_to_circumradius(value: f64, kind: PyramidType, sides: usize) -> f64 { match kind { PyramidType::Circumscribed => value / pyramid_apothem_factor(sides), PyramidType::Inscribed => value, } } fn pyramid_circumradius_to_display(value: f64, kind: PyramidType, sides: usize) -> f64 { match kind { PyramidType::Circumscribed => value * pyramid_apothem_factor(sides), PyramidType::Inscribed => value, } } impl Shape { fn from_id(id: &str) -> Option { Some(match id { "BOX" => Shape::Box, "WEDGE" => Shape::Wedge, "CYLINDER" => Shape::Cylinder, "CONE" => Shape::Cone, "SPHERE" => Shape::Sphere, "PYRAMID" | "PYR" => Shape::Pyramid, "TORUS" => Shape::Torus, _ => return None, }) } fn name(self) -> &'static str { match self { Shape::Box => "BOX", Shape::Wedge => "WEDGE", Shape::Cylinder => "CYLINDER", Shape::Cone => "CONE", Shape::Sphere => "SPHERE", Shape::Pyramid => "PYRAMID", Shape::Torus => "TORUS", } } /// True for footprints picked as a centre + radius (round shapes); false /// for corner-to-corner footprints (box/wedge). fn radial(self) -> bool { !self.rectangular() } fn rectangular(self) -> bool { matches!(self, Shape::Box | Shape::Wedge) } /// Whether a height value is collected after the footprint. fn needs_height(self) -> bool { !matches!(self, Shape::Sphere | Shape::Torus) } } pub struct PrimitiveCommand { shape: Shape, /// Footprint points collected so far (local/world XY, z = 0). pts: Vec, /// True once the footprint is set and we are collecting the height. height_step: bool, box_step: BoxStep, box_centered: bool, box_origin: Option, box_length: Option, box_width: Option, box_angle: f64, box_width_sign: f64, box_height_anchor: Option, sphere_step: SphereStep, sphere_default_radius: f64, cone_step: ConeStep, cone_frame: Option, cone_base_x_radius: f64, cone_base_y_radius: f64, cone_top_radius: f64, cone_defaults: ConeDefaults, pyramid_step: PyramidStep, pyramid_frame: Option, pyramid_edge_first: Option, pyramid_height_first: Option, pyramid_base_radius: f64, pyramid_top_radius: f64, pyramid_sides: usize, pyramid_type: PyramidType, pyramid_defaults: PyramidDefaults, plane: WorkingPlane, } impl PrimitiveCommand { pub fn new(id: &str) -> Self { let remembered = cone_defaults() .lock() .map(|value| *value) .unwrap_or_default(); let pyramid_remembered = pyramid_defaults() .lock() .map(|value| *value) .unwrap_or_default(); Self { shape: Shape::from_id(id).unwrap_or(Shape::Box), pts: Vec::new(), height_step: false, box_step: BoxStep::FirstCorner, box_centered: false, box_origin: None, box_length: None, box_width: None, box_angle: 0.0, box_width_sign: 1.0, box_height_anchor: None, sphere_step: SphereStep::Center, sphere_default_radius: sphere_radius_default(), 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, pyramid_step: PyramidStep::BaseCenter, pyramid_frame: None, pyramid_edge_first: None, pyramid_height_first: None, pyramid_base_radius: pyramid_display_to_circumradius( pyramid_remembered.displayed_base_radius, pyramid_remembered.kind, pyramid_remembered.sides, ), pyramid_top_radius: pyramid_display_to_circumradius( pyramid_remembered.displayed_top_radius, pyramid_remembered.kind, pyramid_remembered.sides, ), pyramid_sides: pyramid_remembered.sides, pyramid_type: pyramid_remembered.kind, pyramid_defaults: pyramid_remembered, plane: WorkingPlane::default(), } } fn commit_sphere(&mut self, center: DVec3, radius: f64) -> CmdResult { if !center.is_finite() || !radius.is_finite() || radius <= 1e-6 { return CmdResult::NeedPoint; } self.pts = vec![center, center + DVec3::X * radius]; self.sphere_default_radius = radius; remember_sphere_radius(radius); self.commit(0.0) } fn sphere_ttr_result(&mut self, radius: f64) -> CmdResult { let SphereStep::TtrRadius { first, second, first_hit, second_hit, } = self.sphere_step else { return CmdResult::NeedPoint; }; let hint = (first_hit + second_hit) * 0.5; let Some(center) = closest_sphere_center( &sphere_ttr_centers(first, second, radius), hint, ) else { return CmdResult::NeedPoint; }; self.commit_sphere(center, radius) } fn sphere_preview(&self, center: DVec3, radius: f64) -> Option { if !center.is_finite() || !radius.is_finite() || radius <= 1e-6 { return None; } Some(self.place_preview(footprint_wire( Shape::Sphere, &[center, center + DVec3::X * radius], ))) } 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 endpoint of first axis or [Center]:").into_owned(), ConeStep::EllipseCenter => t!("CONE Specify center point:").into_owned(), ConeStep::EllipseCenterFirstAxis(_) => crate::tf!( "CONE Specify distance to first axis <{:.4}>:", self.cone_defaults.base_x_radius ) .into_owned(), ConeStep::EllipseCenterSecondAxis(_, _) => t!("CONE Specify endpoint of second 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::HeightAfterTopRadius => crate::tf!( "CONE Specify height or [2Point/Axis endpoint] <{:.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::EllipseFirst => vec![CmdOption::new(t!("Center").as_ref(), "C")], ConeStep::Height => vec![ CmdOption::new("2Point", "2P"), CmdOption::new(t!("Axis endpoint").as_ref(), "A"), CmdOption::new(t!("Top radius").as_ref(), "T"), ], ConeStep::HeightAfterTopRadius => vec![ CmdOption::new("2Point", "2P"), CmdOption::new(t!("Axis endpoint").as_ref(), "A"), ], _ => 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 => { let Some(frame) = self.cone_frame else { return CmdResult::NeedPoint; }; let local = frame.to_local(point); let radius = local.x.hypot(local.y); self.set_cone_base(frame, radius, radius); CmdResult::NeedPoint } 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) * 0.5; self.set_cone_base(frame, radius, radius); 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::EllipseCenter => { self.cone_step = ConeStep::EllipseCenterFirstAxis(point); CmdResult::NeedPoint } ConeStep::EllipseCenterFirstAxis(center) => { if point.distance_squared(center) > 1e-12 { self.cone_step = ConeStep::EllipseCenterSecondAxis(center, point); } CmdResult::NeedPoint } ConeStep::EllipseCenterSecondAxis(center, first_axis_end) => { let axis = first_axis_end - center; if let Some(x) = axis.try_normalize() { let y = self.plane.z.cross(x).normalize_or_zero(); let frame = WorkingPlane::new(center, x, y); let local = frame.to_local(point); self.set_cone_base(frame, axis.length(), local.y.abs()); } 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 | ConeStep::HeightAfterTopRadius => 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::HeightAfterTopRadius; } 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::EllipseFirst if matches!(upper.as_str(), "C" | "CENTER") => { self.cone_step = ConeStep::EllipseCenter; 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); } ConeStep::HeightAfterTopRadius => { self.cone_step = match upper.as_str() { "2P" | "2POINT" => ConeStep::HeightFirstPoint, "A" | "AXIS" | "AXIS ENDPOINT" => ConeStep::AxisEndpoint, _ => { 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::EllipseCenterFirstAxis(center) => (number > 0.0).then(|| { self.cone_step = ConeStep::EllipseCenterSecondAxis( center, center + self.plane.x * number, ); 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::HeightAfterTopRadius; Some(CmdResult::NeedPoint) } _ => None, } } fn cone_mouse_move(&self, point: DVec3) -> Option { match self.cone_step { ConeStep::BaseRadius => { 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::BaseDiameter => { let frame = self.cone_frame?; let local = frame.to_local(point); let radius = local.x.hypot(local.y) * 0.5; 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::EllipseCenterSecondAxis(center, first_axis_end) => { let axis = first_axis_end - center; let x = axis.try_normalize()?; let y = self.plane.z.cross(x).normalize_or_zero(); let frame = WorkingPlane::new(center, x, y); let local = frame.to_local(point); Some(self.cone_base_preview(frame, axis.length(), 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 | ConeStep::HeightAfterTopRadius => { 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, } } fn pyramid_frame_at(&self, center: DVec3) -> WorkingPlane { WorkingPlane { origin: center, x: self.plane.x, y: self.plane.y, z: self.plane.z, } } fn set_pyramid_type(&mut self, kind: PyramidType) { let displayed_top = pyramid_circumradius_to_display( self.pyramid_top_radius, self.pyramid_type, self.pyramid_sides, ); self.pyramid_type = kind; self.pyramid_top_radius = pyramid_display_to_circumradius(displayed_top, kind, self.pyramid_sides); } fn pyramid_set_base_from_point(&mut self, point: DVec3) -> bool { let Some(frame) = self.pyramid_frame else { return false; }; let delta = point - frame.origin; let planar = delta - frame.z * delta.dot(frame.z); let displayed_radius = planar.length(); let Some(direction) = planar.try_normalize() else { return false; }; if !displayed_radius.is_finite() || displayed_radius < 1e-6 { return false; } let x = match self.pyramid_type { PyramidType::Inscribed => direction, PyramidType::Circumscribed => { let half = std::f64::consts::PI / self.pyramid_sides as f64; direction * half.cos() - frame.z.cross(direction) * half.sin() } }; let Some(x) = x.try_normalize() else { return false; }; let Some(y) = frame.z.cross(x).try_normalize() else { return false; }; self.pyramid_frame = Some(WorkingPlane::new(frame.origin, x, y)); self.pyramid_base_radius = pyramid_display_to_circumradius( displayed_radius, self.pyramid_type, self.pyramid_sides, ); self.pyramid_step = PyramidStep::Height; true } fn pyramid_set_base_from_edge(&mut self, first: DVec3, second: DVec3) -> bool { let delta = second - first; let planar = delta - self.plane.z * delta.dot(self.plane.z); let length = planar.length(); let Some(edge) = planar.try_normalize() else { return false; }; if !length.is_finite() || length < 1e-6 { return false; } let radius = length / (2.0 * (std::f64::consts::PI / self.pyramid_sides as f64).sin()); let apothem = radius * pyramid_apothem_factor(self.pyramid_sides); let center = (first + second) * 0.5 + self.plane.z.cross(edge) * apothem; let Some(x) = (first - center).try_normalize() else { return false; }; let Some(y) = self.plane.z.cross(x).try_normalize() else { return false; }; self.pyramid_frame = Some(WorkingPlane::new(center, x, y)); self.pyramid_base_radius = radius; self.pyramid_step = PyramidStep::Height; true } fn pyramid_axis_frame(&self, axis: DVec3) -> Option<(WorkingPlane, f64)> { let current = self.pyramid_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 pyramid_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_pyramid(&self, axis: DVec3) -> Option<(Body, SolidHistoryOperation, f64)> { let (frame, height) = self.pyramid_axis_frame(axis)?; let local = solid_model::pyramid_frustum_solid( [0.0; 3], self.pyramid_base_radius, self.pyramid_top_radius, height, self.pyramid_sides, )?; 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::pyramid_op( Self::pyramid_history_transform(frame), self.pyramid_base_radius, self.pyramid_top_radius, height, self.pyramid_sides, self.pyramid_type == PyramidType::Inscribed, ), height, )) } fn commit_pyramid_axis(&self, axis: DVec3) -> CmdResult { let Some((solid, history, height)) = self.build_pyramid(axis) else { return CmdResult::NeedPoint; }; if let Ok(mut defaults) = pyramid_defaults().lock() { *defaults = PyramidDefaults { displayed_base_radius: pyramid_circumradius_to_display( self.pyramid_base_radius, self.pyramid_type, self.pyramid_sides, ), displayed_top_radius: pyramid_circumradius_to_display( self.pyramid_top_radius, self.pyramid_type, self.pyramid_sides, ), height, sides: self.pyramid_sides, kind: self.pyramid_type, }; } 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_pyramid_height(&self, height: f64) -> CmdResult { let Some(frame) = self.pyramid_frame else { return CmdResult::NeedPoint; }; if !height.is_finite() || height.abs() < 1e-6 { return CmdResult::NeedPoint; } self.commit_pyramid_axis(frame.z * height) } fn pyramid_height_at(&self, point: DVec3) -> Option { let frame = self.pyramid_frame?; Some((point - frame.origin).dot(frame.z)) } fn pyramid_polygon(frame: WorkingPlane, radius: f64, sides: usize, z: f64) -> Vec { (0..sides) .map(|index| { let angle = std::f64::consts::TAU * index as f64 / sides as f64; frame.origin + frame.x * (radius * angle.cos()) + frame.y * (radius * angle.sin()) + frame.z * z }) .collect() } fn pyramid_preview(&self, axis: DVec3) -> Option { let (frame, height) = self.pyramid_axis_frame(axis)?; let base = Self::pyramid_polygon(frame, self.pyramid_base_radius, self.pyramid_sides, 0.0); let top = Self::pyramid_polygon(frame, self.pyramid_top_radius, self.pyramid_sides, height); let apex = frame.origin + frame.z * height; let mut points = Vec::new(); push_path_loop(&mut points, &base); if self.pyramid_top_radius > 1e-9 { push_path_loop(&mut points, &top); } for index in 0..self.pyramid_sides { push_segment( &mut points, base[index], if self.pyramid_top_radius > 1e-9 { top[index] } else { apex }, ); } Some(wire("primitive_height_preview", points)) } fn pyramid_base_preview(&self, frame: WorkingPlane, displayed_radius: f64) -> Option { if !displayed_radius.is_finite() || displayed_radius < 1e-9 { return None; } let radius = pyramid_display_to_circumradius( displayed_radius, self.pyramid_type, self.pyramid_sides, ); let points = Self::pyramid_polygon(frame, radius, self.pyramid_sides, 0.0); let mut wire_points = Vec::new(); push_path_loop(&mut wire_points, &points); Some(wire("primitive_preview", wire_points)) } fn pyramid_prompt(&self) -> String { let base_default = self.pyramid_defaults.displayed_base_radius; let top_default = self.pyramid_defaults.displayed_top_radius; match self.pyramid_step { PyramidStep::BaseCenter => crate::tf!( "PYRAMID Specify center point of base or [Edge/Sides]:" ).into_owned(), PyramidStep::Sides => crate::tf!( "PYRAMID Enter number of sides <{}>:", self.pyramid_sides ).into_owned(), PyramidStep::EdgeFirst => t!("PYRAMID Specify first endpoint of edge:").into_owned(), PyramidStep::EdgeSecond => t!("PYRAMID Specify second endpoint of edge:").into_owned(), PyramidStep::BaseRadius => match self.pyramid_type { PyramidType::Circumscribed => crate::tf!( "PYRAMID Specify base radius or [Inscribed] <{:.4}>:", base_default ).into_owned(), PyramidType::Inscribed => crate::tf!( "PYRAMID Specify base radius or [Circumscribed] <{:.4}>:", base_default ).into_owned(), }, PyramidStep::Height => crate::tf!( "PYRAMID Specify height or [2Point/Axis endpoint/Top radius] <{:.4}>:", self.pyramid_defaults.height ).into_owned(), PyramidStep::HeightAfterTopRadius => crate::tf!( "PYRAMID Specify height or [2Point/Axis endpoint] <{:.4}>:", self.pyramid_defaults.height ).into_owned(), PyramidStep::HeightFirstPoint => t!("PYRAMID Specify first point for height:").into_owned(), PyramidStep::HeightSecondPoint => t!("PYRAMID Specify second point for height:").into_owned(), PyramidStep::AxisEndpoint => t!("PYRAMID Specify axis endpoint:").into_owned(), PyramidStep::TopRadius => crate::tf!( "PYRAMID Specify top radius <{:.4}>:", top_default ).into_owned(), } } fn pyramid_options(&self) -> Vec { match self.pyramid_step { PyramidStep::BaseCenter => vec![ CmdOption::new(t!("Edge").as_ref(), "E"), CmdOption::new(t!("Sides").as_ref(), "S"), ], PyramidStep::BaseRadius => vec![match self.pyramid_type { PyramidType::Circumscribed => CmdOption::new(t!("Inscribed").as_ref(), "I"), PyramidType::Inscribed => CmdOption::new(t!("Circumscribed").as_ref(), "C"), }], PyramidStep::Height => vec![ CmdOption::new("2Point", "2P"), CmdOption::new(t!("Axis endpoint").as_ref(), "A"), CmdOption::new(t!("Top radius").as_ref(), "T"), ], PyramidStep::HeightAfterTopRadius => vec![ CmdOption::new("2Point", "2P"), CmdOption::new(t!("Axis endpoint").as_ref(), "A"), ], _ => Vec::new(), } } fn on_pyramid_point(&mut self, point: DVec3) -> CmdResult { match self.pyramid_step { PyramidStep::BaseCenter => { self.pyramid_frame = Some(self.pyramid_frame_at(point)); self.pyramid_step = PyramidStep::BaseRadius; CmdResult::NeedPoint } PyramidStep::EdgeFirst => { self.pyramid_edge_first = Some(point); self.pyramid_step = PyramidStep::EdgeSecond; CmdResult::NeedPoint } PyramidStep::EdgeSecond => { if let Some(first) = self.pyramid_edge_first { self.pyramid_set_base_from_edge(first, point); } CmdResult::NeedPoint } PyramidStep::BaseRadius => { self.pyramid_set_base_from_point(point); CmdResult::NeedPoint } PyramidStep::Height | PyramidStep::HeightAfterTopRadius => self .pyramid_height_at(point) .map(|height| self.commit_pyramid_height(height)) .unwrap_or(CmdResult::NeedPoint), PyramidStep::HeightFirstPoint => { self.pyramid_height_first = Some(point); self.pyramid_step = PyramidStep::HeightSecondPoint; CmdResult::NeedPoint } PyramidStep::HeightSecondPoint => self .pyramid_height_first .map(|first| self.commit_pyramid_height(first.distance(point))) .unwrap_or(CmdResult::NeedPoint), PyramidStep::AxisEndpoint => { let Some(frame) = self.pyramid_frame else { return CmdResult::NeedPoint; }; self.commit_pyramid_axis(point - frame.origin) } PyramidStep::TopRadius => { let Some(frame) = self.pyramid_frame else { return CmdResult::NeedPoint; }; let delta = point - frame.origin; let displayed = (delta - frame.z * delta.dot(frame.z)).length(); if displayed.is_finite() { self.pyramid_top_radius = pyramid_display_to_circumradius( displayed, self.pyramid_type, self.pyramid_sides, ); self.pyramid_step = PyramidStep::HeightAfterTopRadius; } CmdResult::NeedPoint } PyramidStep::Sides => CmdResult::NeedPoint, } } fn on_pyramid_text(&mut self, raw: &str) -> Option { let token = raw.trim(); let upper = token.to_uppercase(); match self.pyramid_step { PyramidStep::BaseCenter => { self.pyramid_step = match upper.as_str() { "E" | "EDGE" => PyramidStep::EdgeFirst, "S" | "SIDES" => PyramidStep::Sides, _ => return None, }; return Some(CmdResult::NeedPoint); } PyramidStep::BaseRadius if matches!(upper.as_str(), "I" | "INSCRIBED") => { self.set_pyramid_type(PyramidType::Inscribed); return Some(CmdResult::NeedPoint); } PyramidStep::BaseRadius if matches!(upper.as_str(), "C" | "CIRCUMSCRIBED") => { self.set_pyramid_type(PyramidType::Circumscribed); return Some(CmdResult::NeedPoint); } PyramidStep::Height => { self.pyramid_step = match upper.as_str() { "2P" | "2POINT" => PyramidStep::HeightFirstPoint, "A" | "AXIS" | "AXIS ENDPOINT" => PyramidStep::AxisEndpoint, "T" | "TOP" | "TOP RADIUS" => PyramidStep::TopRadius, _ => { let height = crate::entities::common::parse_typed_length(token)?; return Some(self.commit_pyramid_height(height)); } }; return Some(CmdResult::NeedPoint); } PyramidStep::HeightAfterTopRadius => { self.pyramid_step = match upper.as_str() { "2P" | "2POINT" => PyramidStep::HeightFirstPoint, "A" | "AXIS" | "AXIS ENDPOINT" => PyramidStep::AxisEndpoint, _ => { let height = crate::entities::common::parse_typed_length(token)?; return Some(self.commit_pyramid_height(height)); } }; return Some(CmdResult::NeedPoint); } PyramidStep::Sides => { let sides = token.parse::().ok()?; if !(3..=32).contains(&sides) { return None; } let displayed_top = pyramid_circumradius_to_display( self.pyramid_top_radius, self.pyramid_type, self.pyramid_sides, ); self.pyramid_sides = sides; self.pyramid_top_radius = pyramid_display_to_circumradius( displayed_top, self.pyramid_type, self.pyramid_sides, ); self.pyramid_defaults.sides = sides; self.pyramid_step = PyramidStep::BaseCenter; return Some(CmdResult::NeedPoint); } _ => {} } let number = crate::entities::common::parse_typed_length(token)?; match self.pyramid_step { PyramidStep::BaseRadius if number > 0.0 => { let frame = self.pyramid_frame?; self.pyramid_set_base_from_point(frame.origin + frame.x * number) .then_some(CmdResult::NeedPoint) } PyramidStep::TopRadius if number >= 0.0 => { self.pyramid_top_radius = pyramid_display_to_circumradius( number, self.pyramid_type, self.pyramid_sides, ); self.pyramid_step = PyramidStep::HeightAfterTopRadius; Some(CmdResult::NeedPoint) } _ => None, } } fn pyramid_mouse_move(&self, point: DVec3) -> Option { match self.pyramid_step { PyramidStep::EdgeSecond => { let first = self.pyramid_edge_first?; let mut preview = PrimitiveCommand::new("PYRAMID"); preview.plane = self.plane; preview.pyramid_sides = self.pyramid_sides; preview.pyramid_type = self.pyramid_type; preview.pyramid_set_base_from_edge(first, point); let frame = preview.pyramid_frame?; preview.pyramid_base_preview( frame, pyramid_circumradius_to_display( preview.pyramid_base_radius, preview.pyramid_type, preview.pyramid_sides, ), ) } PyramidStep::BaseRadius => { let frame = self.pyramid_frame?; let delta = point - frame.origin; let planar = delta - frame.z * delta.dot(frame.z); let displayed = planar.length(); let direction = planar.try_normalize()?; let x = match self.pyramid_type { PyramidType::Inscribed => direction, PyramidType::Circumscribed => { let half = std::f64::consts::PI / self.pyramid_sides as f64; direction * half.cos() - frame.z.cross(direction) * half.sin() } } .try_normalize()?; let y = frame.z.cross(x).try_normalize()?; self.pyramid_base_preview(WorkingPlane::new(frame.origin, x, y), displayed) } PyramidStep::Height | PyramidStep::HeightAfterTopRadius => { let frame = self.pyramid_frame?; self.pyramid_preview(frame.z * self.pyramid_height_at(point)?) } PyramidStep::HeightSecondPoint => { let frame = self.pyramid_frame?; self.pyramid_preview(frame.z * self.pyramid_height_first?.distance(point)) } PyramidStep::AxisEndpoint => { let frame = self.pyramid_frame?; self.pyramid_preview(point - frame.origin) } _ => None, } } /// Number of footprint points the shape needs before the height step. fn footprint_pts(&self) -> usize { match self.shape { Shape::Torus => 3, // centre, major-radius, minor-radius _ => 2, // corner/corner or centre/radius } } /// A reasonable default height when the user just presses Enter. fn default_height(&self) -> f64 { if self.shape == Shape::Wedge { let bits = LAST_WEDGE_HEIGHT.load(Ordering::Relaxed); if bits != 0 { let height = f64::from_bits(bits); if height.is_finite() && height >= 1e-6 { return height; } } } let height = match self.shape { Shape::Box | Shape::Wedge => match (self.box_length, self.box_width) { (Some(length), Some(width)) => length.max(width), _ if self.pts.len() >= 2 => { let d = self.pts[1] - self.pts[0]; d.x.abs().max(d.y.abs()) } _ => 1.0, }, _ if self.pts.len() >= 2 => (self.pts[1] - self.pts[0]).length(), _ => 1.0, }; height.max(1.0) } fn cursor_height(&self, point: DVec3) -> f64 { let height = self.plane.to_local(point).z - self.pts[0].z; if self.shape.rectangular() { height } else { height.max(1e-6) } } fn place_preview(&self, mut preview: WireModel) -> WireModel { for point in &mut preview.points { if !point[0].is_nan() { *point = self .plane .to_world(glam::Vec3::from_array(*point).as_dvec3()) .as_vec3() .to_array(); } } preview } fn history_transform(&self, origin: DVec3) -> [f64; 16] { glam::DMat4::from_cols( self.plane.x.extend(0.0), self.plane.y.extend(0.0), self.plane.z.extend(0.0), self.plane.to_world(origin).extend(1.0), ) .to_cols_array() } fn history_transform_axes( &self, origin: DVec3, x_axis: DVec3, y_axis: DVec3, ) -> [f64; 16] { let to_world_vector = |value: DVec3| { self.plane.x * value.x + self.plane.y * value.y + self.plane.z * value.z }; glam::DMat4::from_cols( to_world_vector(x_axis).extend(0.0), to_world_vector(y_axis).extend(0.0), self.plane.z.extend(0.0), self.plane.to_world(origin).extend(1.0), ) .to_cols_array() } fn box_spec(&self, height: f64) -> Option<(DVec3, DVec3, DVec3, f64, f64, f64)> { let (mut origin, x_axis, y_axis, length, width) = if let (Some(origin), Some(length), Some(width)) = (self.box_origin, self.box_length, self.box_width) { let (sin, cos) = self.box_angle.sin_cos(); ( origin, DVec3::new(cos, sin, 0.0), DVec3::new(-sin, cos, 0.0) * self.box_width_sign, length, width, ) } else { let (a, b) = (*self.pts.first()?, *self.pts.get(1)?); ( DVec3::new(a.x.min(b.x), a.y.min(b.y), a.z), DVec3::X, DVec3::Y, (b.x - a.x).abs(), (b.y - a.y).abs(), ) }; let height_abs = height.abs(); if !origin.is_finite() || !x_axis.is_finite() || !y_axis.is_finite() || !length.is_finite() || !width.is_finite() || !height_abs.is_finite() || length < 1e-6 || width < 1e-6 || height_abs < 1e-6 { return None; } if height < 0.0 { origin.z += height; } if !origin.is_finite() { return None; } Some((origin, x_axis, y_axis, length, width, height_abs)) } fn build_box(&self, height: f64) -> Option<(Body, SolidHistoryOperation)> { use crate::scene::model::solid_history; let (origin, x_axis, y_axis, length, width, height) = self.box_spec(height)?; let base = solid_model::box_solid( [length * 0.5, width * 0.5, height * 0.5], length, width, height, )?; let placed = solid_model::placed( &base, x_axis.to_array(), y_axis.to_array(), DVec3::Z.to_array(), origin.to_array(), )?; Some(( placed, solid_history::box_op( self.history_transform_axes(origin, x_axis, y_axis), length, width, height, ), )) } fn set_box_corner_footprint(&mut self, point: DVec3) -> bool { let first = self.pts[0]; let delta = point - first; if delta.x.abs() < 1e-6 || delta.y.abs() < 1e-6 { return false; } if self.box_centered { self.pts = vec![ DVec3::new(first.x - delta.x.abs(), first.y - delta.y.abs(), first.z), DVec3::new(first.x + delta.x.abs(), first.y + delta.y.abs(), first.z), ]; } else { self.pts.push(point); } self.box_step = BoxStep::Height; self.height_step = true; true } fn set_box_cube(&mut self, size: f64, angle: f64) -> Option { let size = size.abs(); if !size.is_finite() || !angle.is_finite() || size < 1e-6 { return None; } let (sin, cos) = angle.sin_cos(); let x_axis = DVec3::new(cos, sin, 0.0); let y_axis = DVec3::new(-sin, cos, 0.0); let origin = if self.box_centered { self.pts[0] - x_axis * size * 0.5 - y_axis * size * 0.5 } else { self.pts[0] }; self.box_origin = Some(origin); self.box_length = Some(size); self.box_width = Some(size); self.box_angle = angle; self.box_width_sign = 1.0; Some(self.commit(size)) } fn set_box_length(&mut self, length: f64, angle: f64) -> bool { let length = length.abs(); if !length.is_finite() || !angle.is_finite() || length < 1e-6 { return false; } self.box_origin = self.pts.first().copied(); self.box_length = Some(length); self.box_angle = angle; self.box_step = BoxStep::Width; true } fn set_box_width(&mut self, width: f64, sign: f64) -> bool { let width = width.abs(); if !width.is_finite() || !sign.is_finite() || width < 1e-6 { return false; } self.box_width = Some(width); self.box_width_sign = if sign < 0.0 { -1.0 } else { 1.0 }; if self.box_centered { let center = self.pts[0]; let (sin, cos) = self.box_angle.sin_cos(); let x_axis = DVec3::new(cos, sin, 0.0); let y_axis = DVec3::new(-sin, cos, 0.0) * self.box_width_sign; self.box_origin = Some( center - x_axis * self.box_length.unwrap_or_default() * 0.5 - y_axis * width * 0.5, ); } self.box_step = BoxStep::Height; self.height_step = true; true } fn rectangular_preview(&self, height: f64) -> Option { let (origin, x_axis, y_axis, length, width, height) = self.box_spec(height)?; let mut points = Vec::new(); if self.shape == Shape::Wedge { let near = [ origin, origin + x_axis * length, origin + DVec3::Z * height, ]; let far = near.map(|point| point + y_axis * width); push_loop(&mut points, &near); push_loop(&mut points, &far); for index in 0..3 { push_segment(&mut points, near[index], far[index]); } } else { let base = [ origin, origin + x_axis * length, origin + x_axis * length + y_axis * width, origin + y_axis * width, ]; let top = base.map(|point| point + DVec3::Z * height); push_loop(&mut points, &base); push_loop(&mut points, &top); for index in 0..4 { push_segment(&mut points, base[index], top[index]); } } Some(self.place_preview(wire("primitive_height_preview", points))) } fn build_wedge(&self, height: f64) -> Option<(Body, SolidHistoryOperation)> { use crate::scene::model::solid_history; let (origin, x_axis, y_axis, length, width, height) = self.box_spec(height)?; let base = solid_model::wedge_solid([0.0; 3], length, width, height)?; let placed = solid_model::placed( &base, x_axis.to_array(), y_axis.to_array(), DVec3::Z.to_array(), origin.to_array(), )?; Some(( placed, solid_history::wedge_op( self.history_transform_axes(origin, x_axis, y_axis), length, width, height, ), )) } /// Build the persistent kernel body and its history node. fn build(&self, height: f64) -> Option<(Body, SolidHistoryOperation)> { use crate::scene::model::solid_history; let (solid, history) = match self.shape { Shape::Box => return self.build_box(height), Shape::Wedge => return self.build_wedge(height), 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]; ( 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(); if r < 1e-6 { return None; } let center = [c.x, c.y, c.z]; ( solid_model::sphere_solid(center, r), solid_history::sphere_op(self.history_transform(c), r), ) } Shape::Pyramid => return None, Shape::Torus => { let c = self.pts[0]; let first = (self.pts[1] - c).length(); let second = (self.pts[2] - c).length(); let outer = first.max(second); let inner = first.min(second); if inner < 1e-6 || outer - inner < 1e-6 { return None; } let major = (outer + inner) * 0.5; let minor = (outer - inner) * 0.5; let center = [c.x, c.y, c.z]; ( solid_model::torus_solid(center, major, minor), solid_history::torus_op( self.history_transform(c), major, minor, ), ) } }; let solid = solid?; Some((solid, history)) } fn commit(&self, height: f64) -> CmdResult { match self.build(height) { Some((solid, history)) => { if self.shape == Shape::Wedge { LAST_WEDGE_HEIGHT.store(height.abs().to_bits(), Ordering::Relaxed); } // Place the local body on the working plane. let placed = solid_model::placed( &solid, [self.plane.x.x, self.plane.x.y, self.plane.x.z], [self.plane.y.x, self.plane.y.y, self.plane.y.z], [self.plane.z.x, self.plane.z.y, self.plane.z.z], [ self.plane.origin.x, self.plane.origin.y, self.plane.origin.z, ], ); match placed { Some(placed) => { let Some(document) = crate::scene::convert::acis_export::solid_to_sat(&placed) else { return CmdResult::Cancel; }; let mut entity = Solid3D::new(); entity.set_sat_document(&document); entity.wires = solid_model::edge_wires(&placed); CmdResult::CommitSolid { entity: EntityType::Solid3D(entity), solid: Box::new(placed), history, } } None => CmdResult::Cancel, } } None => CmdResult::Cancel, } } } impl CadCommand for PrimitiveCommand { fn set_working_plane(&mut self, plane: WorkingPlane) { self.plane = plane; } fn cursor_axis(&self) -> Option<(DVec3, DVec3)> { if self.shape == Shape::Cone { return matches!(self.cone_step, ConeStep::Height | ConeStep::HeightAfterTopRadius) .then(|| { let frame = self.cone_frame.unwrap_or(self.plane); (frame.origin, frame.z.normalize_or_zero()) }); } if self.shape == Shape::Pyramid { return matches!( self.pyramid_step, PyramidStep::Height | PyramidStep::HeightAfterTopRadius ) .then(|| { let frame = self.pyramid_frame.unwrap_or(self.plane); (frame.origin, frame.z.normalize_or_zero()) }); } let constrained = if self.shape.rectangular() { self.box_step == BoxStep::Height } else { self.height_step }; constrained.then(|| { ( self.plane.to_world(self.pts[0]), self.plane.z.normalize_or_zero(), ) }) } fn name(&self) -> &'static str { self.shape.name() } fn needs_tangent_pick(&self) -> bool { (self.shape == Shape::Sphere && matches!( self.sphere_step, SphereStep::TtrFirst | SphereStep::TtrSecond { .. } )) || (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 { return self.on_cone_tangent(object, hit); } if self.shape != Shape::Sphere { return self.on_point(hit); } let object = sphere_tangent_local(object, self.plane); let hit = self.plane.to_local(hit); match self.sphere_step { SphereStep::TtrFirst => { self.sphere_step = SphereStep::TtrSecond { object, hit }; CmdResult::NeedPoint } SphereStep::TtrSecond { object: first, hit: first_hit, } => { self.sphere_step = SphereStep::TtrRadius { first, second: object, first_hit, second_hit: hit, }; CmdResult::NeedPoint } _ => CmdResult::NeedPoint, } } fn prompt(&self) -> String { let n = self.shape.name(); if self.shape == Shape::Cone { return self.cone_prompt(); } if self.shape == Shape::Pyramid { return self.pyramid_prompt(); } if self.shape.rectangular() { return match self.box_step { BoxStep::FirstCorner => t!("%{n} Specify first corner or [Center]:", n = n), BoxStep::CenterPoint => t!("%{n} Specify center point:", n = n), BoxStep::OppositeCorner => { t!("%{n} Specify other corner or [Cube/Length]:", n = n) } BoxStep::CubeSize => t!("%{n} Specify cube length:", n = n), BoxStep::Length => t!("%{n} Specify length:", n = n), BoxStep::Width => t!("%{n} Specify width:", n = n), BoxStep::Height if self.shape == Shape::Wedge => t!( "%{n} Specify height or [2Point] <%{height}>:", n = n, height = format!("{:.4}", self.default_height()) ), BoxStep::Height => t!("%{n} Specify height or [2Point]:", n = n), BoxStep::HeightFirstPoint => { t!("%{n} Specify first point for height:", n = n) } BoxStep::HeightSecondPoint => { t!("%{n} Specify second point for height:", n = n) } } .into_owned(); } if self.shape == Shape::Sphere { return match self.sphere_step { SphereStep::Center => { t!("SPHERE Specify center point or [3P/2P/Ttr]:").into_owned() } SphereStep::Radius(_) => crate::tf!( "SPHERE Specify radius or [Diameter] <{:.4}>:", self.sphere_default_radius ) .into_owned(), SphereStep::Diameter(_) => crate::tf!( "SPHERE Specify diameter <{:.4}>:", self.sphere_default_radius * 2.0 ) .into_owned(), SphereStep::TwoPointFirst => { t!("SPHERE Specify first end point of diameter:").into_owned() } SphereStep::TwoPointSecond(_) => { t!("SPHERE Specify second end point of diameter:").into_owned() } SphereStep::ThreePointFirst => { t!("SPHERE Specify first point on sphere:").into_owned() } SphereStep::ThreePointSecond(_) => { t!("SPHERE Specify second point on sphere:").into_owned() } SphereStep::ThreePointThird(_, _) => { t!("SPHERE Specify third point on sphere:").into_owned() } SphereStep::TtrFirst => { t!("SPHERE Select first tangent object:").into_owned() } SphereStep::TtrSecond { .. } => { t!("SPHERE Select second tangent object:").into_owned() } SphereStep::TtrRadius { .. } => crate::tf!( "SPHERE Specify radius <{:.4}>:", self.sphere_default_radius ) .into_owned(), }; } if self.height_step { return t!("%{n} Specify height :", n = n).into_owned(); } match (self.shape, self.pts.len()) { (Shape::Torus, 0) => t!("%{n} Specify center point:", n = n).into_owned(), (Shape::Torus, 1) => t!("%{n} Specify outer radius:", n = n).into_owned(), (Shape::Torus, _) => t!("%{n} Specify inner radius:", n = n).into_owned(), (shape, 0) if shape.radial() => { t!("%{n} Specify center point:", n = n).into_owned() } (shape, _) if shape.radial() => { t!("%{n} Specify radius:", n = n).into_owned() } (_, 0) => t!("%{n} Specify first corner:", n = n).into_owned(), (_, _) => t!("%{n} Specify opposite corner:", n = n).into_owned(), } } fn options(&self) -> Vec { if self.shape == Shape::Sphere { return match self.sphere_step { SphereStep::Center => vec![ CmdOption::new("3P", "3P"), CmdOption::new("2P", "2P"), CmdOption::new("Ttr", "T"), ], SphereStep::Radius(_) => vec![CmdOption::new(t!("Diameter").as_ref(), "D")], _ => Vec::new(), }; } if self.shape == Shape::Cone { return self.cone_options(); } if self.shape == Shape::Pyramid { return self.pyramid_options(); } if !self.shape.rectangular() { return Vec::new(); } match self.box_step { BoxStep::FirstCorner => vec![CmdOption::new(t!("Center").as_ref(), "C")], BoxStep::OppositeCorner => vec![ CmdOption::new(t!("Cube").as_ref(), "C"), CmdOption::new(t!("Length").as_ref(), "L"), ], BoxStep::Height => vec![CmdOption::new(t!("2Point").as_ref(), "2P")], _ => Vec::new(), } } fn on_point(&mut self, pt: DVec3) -> CmdResult { if self.shape == Shape::Sphere { let local = self.plane.to_local(pt); if !local.is_finite() { return CmdResult::NeedPoint; } return match self.sphere_step { SphereStep::Center => { self.sphere_step = SphereStep::Radius(local); CmdResult::NeedPoint } SphereStep::Radius(center) => self.commit_sphere(center, center.distance(local)), SphereStep::Diameter(center) => { self.commit_sphere(center, center.distance(local) * 0.5) } SphereStep::TwoPointFirst => { self.sphere_step = SphereStep::TwoPointSecond(local); CmdResult::NeedPoint } SphereStep::TwoPointSecond(first) => { self.commit_sphere((first + local) * 0.5, first.distance(local) * 0.5) } SphereStep::ThreePointFirst => { self.sphere_step = SphereStep::ThreePointSecond(local); CmdResult::NeedPoint } SphereStep::ThreePointSecond(first) => { self.sphere_step = SphereStep::ThreePointThird(first, local); CmdResult::NeedPoint } SphereStep::ThreePointThird(first, second) => { let Some((center, radius)) = sphere_through_three_points(first, second, local) else { return CmdResult::NeedPoint; }; self.commit_sphere(center, radius) } SphereStep::TtrRadius { second_hit, .. } => { self.sphere_ttr_result(second_hit.distance(local)) } SphereStep::TtrFirst | SphereStep::TtrSecond { .. } => CmdResult::NeedPoint, }; } if self.shape == Shape::Cone { return self.on_cone_point(pt); } if self.shape == Shape::Pyramid { return self.on_pyramid_point(pt); } if self.shape.rectangular() { let local = self.plane.to_local(pt); if !local.is_finite() { return CmdResult::NeedPoint; } return match self.box_step { BoxStep::FirstCorner | BoxStep::CenterPoint => { self.pts = vec![local]; self.box_step = BoxStep::OppositeCorner; CmdResult::NeedPoint } BoxStep::OppositeCorner => { let height = local.z - self.pts[0].z; if !self.set_box_corner_footprint(local) { CmdResult::NeedPoint } else if self.shape == Shape::Wedge && height.abs() >= 1e-6 { self.commit(height) } else { CmdResult::NeedPoint } } BoxStep::CubeSize => { let delta = local - self.pts[0]; let (size, angle) = if self.shape == Shape::Wedge { (delta.x.hypot(delta.y), delta.y.atan2(delta.x)) } else { (delta.length(), 0.0) }; self.set_box_cube(size, angle) .unwrap_or(CmdResult::NeedPoint) } BoxStep::Length => { let delta = local - self.pts[0]; if !self.set_box_length(delta.x.hypot(delta.y), delta.y.atan2(delta.x)) { return CmdResult::NeedPoint; } CmdResult::NeedPoint } BoxStep::Width => { let delta = local - self.pts[0]; let normal = DVec3::new(-self.box_angle.sin(), self.box_angle.cos(), 0.0); let signed = delta.dot(normal); if !self.set_box_width(signed, signed) { return CmdResult::NeedPoint; } CmdResult::NeedPoint } BoxStep::Height => { let height = self.cursor_height(pt); if height.abs() < 1e-6 { CmdResult::NeedPoint } else { self.commit(height) } } BoxStep::HeightFirstPoint => { self.box_height_anchor = Some(local); self.box_step = BoxStep::HeightSecondPoint; CmdResult::NeedPoint } BoxStep::HeightSecondPoint => { let Some(first) = self.box_height_anchor else { return CmdResult::NeedPoint; }; let height = (local - first).length(); if height < 1e-6 { CmdResult::NeedPoint } else { self.commit(height) } } }; } if self.height_step { return self.commit(self.cursor_height(pt)); } self.pts.push(self.plane.to_local(pt)); if self.pts.len() < self.footprint_pts() { return CmdResult::NeedPoint; } // Footprint complete. if self.shape.needs_height() { self.height_step = true; CmdResult::NeedPoint } else { self.commit(0.0) } } fn on_enter(&mut self) -> CmdResult { if self.shape == Shape::Sphere { return match self.sphere_step { SphereStep::Radius(center) => { self.commit_sphere(center, self.sphere_default_radius) } SphereStep::Diameter(center) => { self.commit_sphere(center, self.sphere_default_radius) } SphereStep::TtrRadius { .. } => { self.sphere_ttr_result(self.sphere_default_radius) } _ => CmdResult::Cancel, }; } 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::EllipseCenterFirstAxis(center) => { self.cone_step = ConeStep::EllipseCenterSecondAxis( center, center + self.plane.x * self.cone_defaults.base_x_radius, ); CmdResult::NeedPoint } ConeStep::Height | ConeStep::HeightAfterTopRadius => { self.commit_cone_height(self.cone_defaults.height) } ConeStep::TopRadius => { self.cone_top_radius = self.cone_defaults.top_radius; self.cone_step = ConeStep::HeightAfterTopRadius; CmdResult::NeedPoint } _ => CmdResult::Cancel, }; } if self.shape == Shape::Pyramid { return match self.pyramid_step { PyramidStep::Sides => { self.pyramid_step = PyramidStep::BaseCenter; CmdResult::NeedPoint } PyramidStep::BaseRadius => { let displayed = self.pyramid_defaults.displayed_base_radius; let Some(frame) = self.pyramid_frame else { return CmdResult::Cancel; }; self.pyramid_set_base_from_point(frame.origin + frame.x * displayed); CmdResult::NeedPoint } PyramidStep::Height | PyramidStep::HeightAfterTopRadius => { self.commit_pyramid_height(self.pyramid_defaults.height) } PyramidStep::TopRadius => { self.pyramid_top_radius = pyramid_display_to_circumradius( self.pyramid_defaults.displayed_top_radius, self.pyramid_type, self.pyramid_sides, ); self.pyramid_step = PyramidStep::HeightAfterTopRadius; CmdResult::NeedPoint } _ => CmdResult::Cancel, }; } if self.shape.rectangular() { return if self.shape == Shape::Wedge && self.box_step == BoxStep::Height { self.commit(self.default_height()) } else { CmdResult::Cancel }; } if self.height_step { let h = self.default_height(); return self.commit(h); } CmdResult::Cancel } fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel } fn wants_text_input(&self) -> bool { if self.shape == Shape::Sphere { return matches!( self.sphere_step, SphereStep::Center | SphereStep::Radius(_) | SphereStep::Diameter(_) | SphereStep::TtrRadius { .. } ); } if self.shape == Shape::Cone { return matches!( self.cone_step, ConeStep::BaseCenter | ConeStep::BaseRadius | ConeStep::BaseDiameter | ConeStep::EllipseFirst | ConeStep::EllipseCenterFirstAxis(_) | ConeStep::TtrRadius { .. } | ConeStep::Height | ConeStep::HeightAfterTopRadius | ConeStep::TopRadius ); } if self.shape == Shape::Pyramid { return matches!( self.pyramid_step, PyramidStep::BaseCenter | PyramidStep::Sides | PyramidStep::BaseRadius | PyramidStep::Height | PyramidStep::HeightAfterTopRadius | PyramidStep::TopRadius ); } if self.shape.rectangular() { matches!( self.box_step, BoxStep::FirstCorner | BoxStep::OppositeCorner | BoxStep::CubeSize | BoxStep::Length | BoxStep::Width | BoxStep::Height ) } else { self.height_step } } fn point_step_accepts_keywords(&self) -> bool { if self.shape == Shape::Sphere { return matches!(self.sphere_step, SphereStep::Center | SphereStep::Radius(_)); } if self.shape == Shape::Cone { return matches!( self.cone_step, ConeStep::BaseCenter | ConeStep::BaseRadius | ConeStep::EllipseFirst | ConeStep::Height | ConeStep::HeightAfterTopRadius ); } if self.shape == Shape::Pyramid { return matches!( self.pyramid_step, PyramidStep::BaseCenter | PyramidStep::BaseRadius | PyramidStep::Height | PyramidStep::HeightAfterTopRadius ); } self.shape.rectangular() && matches!( self.box_step, BoxStep::FirstCorner | BoxStep::OppositeCorner | BoxStep::Height ) } fn on_text_input(&mut self, raw: &str) -> Option { if self.shape == Shape::Sphere { let token = raw.trim(); let upper = token.to_uppercase(); return match self.sphere_step { SphereStep::Center if upper == "3P" => { self.sphere_step = SphereStep::ThreePointFirst; Some(CmdResult::NeedPoint) } SphereStep::Center if upper == "2P" => { self.sphere_step = SphereStep::TwoPointFirst; Some(CmdResult::NeedPoint) } SphereStep::Center if matches!(upper.as_str(), "T" | "TTR") => { self.sphere_step = SphereStep::TtrFirst; Some(CmdResult::NeedPoint) } SphereStep::Radius(_) if matches!(upper.as_str(), "D" | "DIAMETER") => { let SphereStep::Radius(center) = self.sphere_step else { unreachable!(); }; self.sphere_step = SphereStep::Diameter(center); Some(CmdResult::NeedPoint) } SphereStep::Radius(center) => { let radius = crate::entities::common::parse_typed_length(token)?; Some(self.commit_sphere(center, radius)) } SphereStep::Diameter(center) => { let diameter = crate::entities::common::parse_typed_length(token)?; Some(self.commit_sphere(center, diameter * 0.5)) } SphereStep::TtrRadius { .. } => { let radius = crate::entities::common::parse_typed_length(token)?; Some(self.sphere_ttr_result(radius)) } _ => None, }; } if self.shape == Shape::Cone { return self.on_cone_text(raw); } if self.shape == Shape::Pyramid { return self.on_pyramid_text(raw); } if self.shape.rectangular() { let token = raw.trim(); let upper = token.to_uppercase(); return match self.box_step { BoxStep::FirstCorner if matches!(upper.as_str(), "C" | "CENTER") => { self.box_centered = true; self.box_step = BoxStep::CenterPoint; Some(CmdResult::NeedPoint) } BoxStep::OppositeCorner if matches!(upper.as_str(), "C" | "CUBE") => { self.box_step = BoxStep::CubeSize; Some(CmdResult::NeedPoint) } BoxStep::OppositeCorner if matches!(upper.as_str(), "L" | "LENGTH") => { self.box_step = BoxStep::Length; Some(CmdResult::NeedPoint) } BoxStep::Height if matches!(upper.as_str(), "2P" | "2POINT") => { self.box_step = BoxStep::HeightFirstPoint; Some(CmdResult::NeedPoint) } BoxStep::CubeSize => crate::entities::common::parse_typed_length(token) .and_then(|value| self.set_box_cube(value, 0.0)), BoxStep::Length => { let value = crate::entities::common::parse_typed_length(token)?; self.set_box_length(value, 0.0) .then_some(CmdResult::NeedPoint) } BoxStep::Width => { let value = crate::entities::common::parse_typed_length(token)?; self.set_box_width(value, value) .then_some(CmdResult::NeedPoint) } BoxStep::Height => { let value = crate::entities::common::parse_typed_length(token)?; (value.is_finite() && value.abs() >= 1e-6).then(|| self.commit(value)) } _ => None, }; } if !self.height_step { return None; } let h: f64 = raw.trim().parse().ok().filter(|v| *v > 0.0)?; Some(self.commit(h)) } fn on_mouse_move(&mut self, pt: DVec3) -> Option { if self.shape == Shape::Sphere { let local = self.plane.to_local(pt); if !local.is_finite() { return None; } return match self.sphere_step { SphereStep::Radius(center) => self.sphere_preview(center, center.distance(local)), SphereStep::Diameter(center) => { self.sphere_preview(center, center.distance(local) * 0.5) } SphereStep::TwoPointSecond(first) => { self.sphere_preview((first + local) * 0.5, first.distance(local) * 0.5) } SphereStep::ThreePointSecond(first) => { self.sphere_preview((first + local) * 0.5, first.distance(local) * 0.5) } SphereStep::ThreePointThird(first, second) => { let (center, radius) = sphere_through_three_points(first, second, local)?; self.sphere_preview(center, radius) } SphereStep::TtrRadius { first, second, first_hit, second_hit, } => { let radius = second_hit.distance(local); let hint = (first_hit + second_hit) * 0.5; let center = closest_sphere_center( &sphere_ttr_centers(first, second, radius), hint, )?; self.sphere_preview(center, radius) } _ => None, }; } if self.shape == Shape::Cone { return self.cone_mouse_move(pt); } if self.shape == Shape::Pyramid { return self.pyramid_mouse_move(pt); } if self.pts.is_empty() { return None; } if self.shape.rectangular() { let local = self.plane.to_local(pt); if !local.is_finite() { return None; } return match self.box_step { BoxStep::OppositeCorner => { let first = self.pts[0]; let points = if self.box_centered { let delta = local - first; vec![ DVec3::new( first.x - delta.x.abs(), first.y - delta.y.abs(), first.z, ), DVec3::new( first.x + delta.x.abs(), first.y + delta.y.abs(), first.z, ), ] } else { vec![first, local] }; Some(self.place_preview(footprint_wire(Shape::Box, &points))) } BoxStep::CubeSize => { let delta = local - self.pts[0]; let size = if self.shape == Shape::Wedge { delta.x.hypot(delta.y) } else { delta.length() } .max(1e-6); let mut preview = PrimitiveCommand::new(self.shape.name()); preview.plane = self.plane; preview.pts = self.pts.clone(); preview.box_length = Some(size); preview.box_width = Some(size); preview.box_width_sign = 1.0; if self.shape == Shape::Wedge { preview.box_origin = Some(self.pts[0]); preview.box_angle = delta.y.atan2(delta.x); } else { preview.box_origin = Some(if self.box_centered { self.pts[0] - DVec3::new(size * 0.5, size * 0.5, 0.0) } else { self.pts[0] }); } if self.shape == Shape::Wedge && self.box_centered { let (sin, cos) = preview.box_angle.sin_cos(); let x_axis = DVec3::new(cos, sin, 0.0); let y_axis = DVec3::new(-sin, cos, 0.0); preview.box_origin = Some( self.pts[0] - x_axis * size * 0.5 - y_axis * size * 0.5, ); } preview.rectangular_preview(size) } BoxStep::Length => Some(self.place_preview(wire( "primitive_preview", vec![self.pts[0].as_vec3().to_array(), local.as_vec3().to_array()], ))), BoxStep::Width => { let delta = local - self.pts[0]; let normal = DVec3::new(-self.box_angle.sin(), self.box_angle.cos(), 0.0); let signed = delta.dot(normal); let mut preview = PrimitiveCommand::new(self.shape.name()); preview.plane = self.plane; preview.pts = self.pts.clone(); preview.box_origin = self.box_origin; preview.box_length = self.box_length; preview.box_width = Some(signed.abs().max(1e-6)); preview.box_angle = self.box_angle; preview.box_width_sign = if signed < 0.0 { -1.0 } else { 1.0 }; preview.rectangular_preview(1e-6) } BoxStep::Height => self.rectangular_preview(self.cursor_height(pt)), BoxStep::HeightSecondPoint => self.box_height_anchor.map(|first| { self.place_preview(wire( "primitive_preview", vec![first.as_vec3().to_array(), local.as_vec3().to_array()], )) }), _ => None, }; } if self.height_step { return Some(self.place_preview(height_wire( self.shape, &self.pts, self.cursor_height(pt), ))); } let mut foot = self.pts.clone(); foot.push(self.plane.to_local(pt)); Some(self.place_preview(footprint_wire(self.shape, &foot))) } fn dyn_spec(&self) -> Option { use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec}; if self.shape == Shape::Sphere { let (anchor, role) = match self.sphere_step { SphereStep::Radius(center) => (center, DynRole::Radius), SphereStep::Diameter(center) => (center, DynRole::Diameter), SphereStep::TtrRadius { second_hit, .. } => { (second_hit, DynRole::Radius) } _ => return None, }; return Some(DynSpec { anchor: DynAnchor::Point(self.plane.to_world(anchor)), fields: vec![DynFieldSpec::new(role)], guide: DynGuide::Radius, ref_point: None, }); } if self.shape == Shape::Cone { let frame = self.cone_frame.unwrap_or(self.plane); let (anchor, role) = match self.cone_step { ConeStep::BaseRadius | ConeStep::TopRadius => { (frame.origin, DynRole::Radius) } ConeStep::BaseDiameter => (frame.origin, DynRole::Diameter), ConeStep::TtrRadius { second_hit, .. } => { (self.plane.to_world(second_hit), DynRole::Radius) } ConeStep::EllipseCenterFirstAxis(center) => (center, DynRole::Distance), ConeStep::Height | ConeStep::HeightAfterTopRadius | ConeStep::AxisEndpoint => { (frame.origin, DynRole::Height) } ConeStep::HeightSecondPoint(first) => (first, DynRole::Height), _ => return None, }; return Some(DynSpec { anchor: DynAnchor::Point(anchor), fields: vec![DynFieldSpec::new(role)], guide: if matches!(role, DynRole::Radius | DynRole::Diameter) { DynGuide::Radius } else { DynGuide::None }, ref_point: None, }); } if self.shape == Shape::Pyramid { let frame = self.pyramid_frame.unwrap_or(self.plane); let (anchor, role) = match self.pyramid_step { PyramidStep::BaseRadius | PyramidStep::TopRadius => { (frame.origin, DynRole::Radius) } PyramidStep::Height | PyramidStep::HeightAfterTopRadius | PyramidStep::AxisEndpoint => (frame.origin, DynRole::Height), PyramidStep::HeightSecondPoint => { (self.pyramid_height_first?, DynRole::Height) } _ => return None, }; return Some(DynSpec { anchor: DynAnchor::Point(anchor), fields: vec![DynFieldSpec::new(role)], guide: if role == DynRole::Radius { DynGuide::Radius } else { DynGuide::None }, ref_point: None, }); } let role = if self.shape.rectangular() { match self.box_step { BoxStep::CubeSize | BoxStep::Length => DynRole::Distance, BoxStep::Width => DynRole::Width, BoxStep::Height => DynRole::Height, _ => return None, } } else if self.height_step { DynRole::Height } else { return None; }; Some(DynSpec { anchor: DynAnchor::Point(self.plane.to_world(self.pts[0])), fields: vec![DynFieldSpec::new(role)], guide: DynGuide::None, ref_point: None, }) } fn dyn_live_value(&self, cursor: DVec3) -> Option { if self.shape == Shape::Sphere { let local = self.plane.to_local(cursor); return match self.sphere_step { SphereStep::Radius(center) => Some(center.distance(local)), SphereStep::Diameter(center) => Some(center.distance(local)), SphereStep::TtrRadius { second_hit, .. } => { Some(second_hit.distance(local)) } _ => None, }; } 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)) } ConeStep::TtrRadius { second_hit, .. } => { let local = self.plane.to_local(cursor); Some((local - second_hit).truncate().length()) } ConeStep::EllipseCenterFirstAxis(center) => Some(center.distance(cursor)), ConeStep::Height | ConeStep::HeightAfterTopRadius => { 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::Pyramid { return match self.pyramid_step { PyramidStep::BaseRadius | PyramidStep::TopRadius => { let frame = self.pyramid_frame?; let delta = cursor - frame.origin; Some((delta - frame.z * delta.dot(frame.z)).length()) } PyramidStep::Height | PyramidStep::HeightAfterTopRadius => { self.pyramid_height_at(cursor).map(f64::abs) } PyramidStep::HeightSecondPoint => { Some(self.pyramid_height_first?.distance(cursor)) } PyramidStep::AxisEndpoint => { self.pyramid_frame.map(|frame| frame.origin.distance(cursor)) } _ => None, }; } if self.shape.rectangular() { let local = self.plane.to_local(cursor); if !local.is_finite() { return None; } return match self.box_step { BoxStep::CubeSize | BoxStep::Length => { let delta = local - self.pts[0]; Some(if self.shape == Shape::Wedge { delta.x.hypot(delta.y) } else { delta.length() }) } BoxStep::Width => { let normal = DVec3::new(-self.box_angle.sin(), self.box_angle.cos(), 0.0); Some((local - self.pts[0]).dot(normal).abs()) } BoxStep::Height => Some(self.cursor_height(cursor)), _ => None, }; } self.height_step.then(|| self.cursor_height(cursor)) } } // ── Footprint preview ─────────────────────────────────────────────────────── fn footprint_wire(shape: Shape, pts: &[DVec3]) -> WireModel { let mut points: Vec<[f32; 3]> = Vec::new(); if shape == Shape::Pyramid { let center = pts[0]; let radius = (pts[1] - center).length(); let corners: [DVec3; 4] = std::array::from_fn(|index| { let angle = index as f64 * std::f64::consts::FRAC_PI_2; center + DVec3::new(radius * angle.cos(), radius * angle.sin(), 0.0) }); push_loop(&mut points, &corners); } else if shape.radial() { let c = pts[0]; let r = (pts[1] - c).length(); circle_points(&mut points, c, r); if shape == Shape::Torus && pts.len() >= 3 { let inner = (pts[2] - c).length(); points.push([f32::NAN; 3]); circle_points(&mut points, c, inner); } } else { let (a, b) = (pts[0], pts[1]); points.extend_from_slice(&[ [a.x as f32, a.y as f32, a.z as f32], [b.x as f32, a.y as f32, a.z as f32], [b.x as f32, b.y as f32, a.z as f32], [a.x as f32, b.y as f32, a.z as f32], [a.x as f32, a.y as f32, a.z as f32], ]); } wire("primitive_preview", points) } fn height_wire(shape: Shape, pts: &[DVec3], height: f64) -> WireModel { let mut points = Vec::new(); match shape { Shape::Box => { let (a, b) = (pts[0], pts[1]); let base = [ DVec3::new(a.x, a.y, a.z), DVec3::new(b.x, a.y, a.z), DVec3::new(b.x, b.y, a.z), DVec3::new(a.x, b.y, a.z), ]; let top = base.map(|point| point + DVec3::Z * height); push_loop(&mut points, &base); push_loop(&mut points, &top); for i in 0..4 { push_segment(&mut points, base[i], top[i]); } } Shape::Wedge => { let (a, b) = (pts[0], pts[1]); let (x0, x1) = (a.x.min(b.x), a.x.max(b.x)); let (y0, y1) = (a.y.min(b.y), a.y.max(b.y)); let low = [ DVec3::new(x0, y0, a.z), DVec3::new(x1, y0, a.z), DVec3::new(x0, y0, a.z + height), ]; let high = low.map(|point| DVec3::new(point.x, y1, point.z)); push_loop(&mut points, &low); push_loop(&mut points, &high); for i in 0..3 { push_segment(&mut points, low[i], high[i]); } } Shape::Cylinder | Shape::Cone | Shape::Pyramid => { let center = pts[0]; let radius = (pts[1] - center).length(); if shape == Shape::Pyramid { let base: [DVec3; 4] = std::array::from_fn(|index| { let angle = index as f64 * std::f64::consts::FRAC_PI_2; center + DVec3::new(radius * angle.cos(), radius * angle.sin(), 0.0) }); push_loop(&mut points, &base); let apex = center + DVec3::Z * height; for corner in base { push_segment(&mut points, corner, apex); } return wire("primitive_height_preview", points); } push_circle(&mut points, center, radius); if shape == Shape::Cylinder { push_circle(&mut points, center + DVec3::Z * height, radius); for i in 0..4 { let angle = i as f64 * std::f64::consts::FRAC_PI_2; let base = center + DVec3::new(angle.cos() * radius, angle.sin() * radius, 0.0); push_segment(&mut points, base, base + DVec3::Z * height); } } else { let apex = center + DVec3::Z * height; for i in 0..4 { let angle = i as f64 * std::f64::consts::FRAC_PI_2; let base = center + DVec3::new(angle.cos() * radius, angle.sin() * radius, 0.0); push_segment(&mut points, base, apex); } } } Shape::Sphere | Shape::Torus => {} } wire("primitive_height_preview", points) } fn push_break(points: &mut Vec<[f32; 3]>) { if !points.is_empty() { points.push([f32::NAN; 3]); } } fn push_loop(points: &mut Vec<[f32; 3]>, path: &[DVec3; N]) { push_break(points); points.extend(path.iter().chain(path.first()).map(|point| point.as_vec3().to_array())); } fn push_path_loop(points: &mut Vec<[f32; 3]>, path: &[DVec3]) { if path.is_empty() { return; } push_break(points); points.extend(path.iter().chain(path.first()).map(|point| point.as_vec3().to_array())); } fn push_segment(points: &mut Vec<[f32; 3]>, a: DVec3, b: DVec3) { push_break(points); points.extend([a.as_vec3().to_array(), b.as_vec3().to_array()]); } fn push_circle(points: &mut Vec<[f32; 3]>, center: DVec3, radius: f64) { push_break(points); 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 { let t = i as f64 / SEG as f64 * std::f64::consts::TAU; out.push([ (c.x + r * t.cos()) as f32, (c.y + r * t.sin()) as f32, c.z as f32, ]); } } // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { names: &["BOX", "WEDGE", "CYLINDER", "CONE", "SPHERE", "PYRAMID", "PYR", "TORUS"] }); fn wire(name: &str, points: Vec<[f32; 3]>) -> WireModel { WireModel { point_marker: None, taper_widths: Vec::new(), pattern_stations: Vec::new(), world_width: 0.0, depth_override: None, display_visible: true, plot_visible: true, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), name: name.into(), points, points_low: Vec::new(), color: WireModel::CYAN, selected: false, pattern_length: 0.0, pattern: [0.0; 8], line_weight_px: 1.0, snap_pts: vec![], tangent_geoms: vec![], aci: 0, key_vertices: vec![], aabb: WireModel::UNBOUNDED_AABB, plinegen: true, fill_tris: vec![], fill_tris_low: Vec::new(), } }