// Profile-based kernel solid commands stored as exact ACIS data. use std::sync::{Mutex, OnceLock}; use acadrust::{entities::Solid3D, EntityType, Handle}; use glam::DVec3; use crate::command::{ CadCommand, CmdOption, CmdResult, ExtrudeExtent, ExtrudeMode, SelectionEntity, }; use crate::scene::WireModel; use crate::t; // ── EXTRUDE command ──────────────────────────────────────────────────────── pub struct ExtrudeCommand { command_name: &'static str, step: ExtrudeStep, handles: Vec, preview_profiles: Vec<(Handle, EntityType)>, injected_profile: Option, anchor: DVec3, profile_direction: Option, direction_start: Option, mode: ExtrudeMode, taper_angle: f64, last_height: Option, taper_return: ExtrudeStep, color: [f32; 4], } #[derive(Clone, Copy)] struct ExtrudeDefaults { mode: ExtrudeMode, height: Option, taper_angle: f64, } fn extrude_defaults() -> &'static Mutex { static DEFAULTS: OnceLock> = OnceLock::new(); DEFAULTS.get_or_init(|| { Mutex::new(ExtrudeDefaults { mode: ExtrudeMode::Solid, height: None, taper_angle: 0.0, }) }) } #[derive(Clone, Copy, PartialEq, Eq)] enum ExtrudeStep { Pick, Mode, Height, DirectionStart, DirectionEnd, Path, Taper, Expression, TaperExpression, } impl ExtrudeCommand { pub fn new_named(name: &str, color: [f32; 4]) -> Self { let defaults = *extrude_defaults().lock().unwrap_or_else(|error| error.into_inner()); Self { command_name: match name { "THICKEN" => "THICKEN", _ => "EXTRUDE", }, step: ExtrudeStep::Pick, handles: Vec::new(), preview_profiles: Vec::new(), injected_profile: None, anchor: DVec3::ZERO, profile_direction: None, direction_start: None, mode: defaults.mode, taper_angle: defaults.taper_angle, last_height: defaults.height, taper_return: ExtrudeStep::Height, color, } } pub fn set_preselection( &mut self, profiles: Vec<(Handle, EntityType)>, anchor: DVec3, direction: Option, ) { self.handles = profiles.iter().map(|(handle, _)| *handle).collect(); self.preview_profiles = profiles; self.anchor = anchor; self.profile_direction = direction.and_then(DVec3::try_normalize); if !self.handles.is_empty() { self.step = ExtrudeStep::Height; } } fn finish(&self, extent: ExtrudeExtent) -> CmdResult { if let Some(height) = match extent { ExtrudeExtent::Height(height) => Some(height), ExtrudeExtent::Direction(direction) => Some(direction.length()), ExtrudeExtent::Path(_) => None, } { extrude_defaults() .lock() .unwrap_or_else(|error| error.into_inner()) .height = Some(height); } CmdResult::ExtrudeEntities { handles: self.handles.clone(), extent, mode: self.mode, taper_angle: self.taper_angle, color: self.color, } } fn preview_wires(&self, cursor: DVec3) -> Vec { if self.step != ExtrudeStep::Height { return Vec::new(); } let Some(axis) = self.profile_direction else { return Vec::new(); }; let height = (cursor - self.anchor).dot(axis); if !height.is_finite() || height.abs() <= 1e-6 { return Vec::new(); } self.preview_profiles .iter() .flat_map(|(_, entity)| { let Some((profile, closed)) = crate::scene::model::sweep_model::extrusion_profile_of(entity) else { return Vec::new(); }; let Some(normal) = profile.plane.normal() else { return Vec::new(); }; let direction = [normal[0] * height, normal[1] * height, normal[2] * height]; let creates_surface = self.mode == ExtrudeMode::Surface || !closed; let body = if creates_surface { crate::scene::model::sweep_model::extruded_surface( entity, direction, self.taper_angle, ) } else { crate::scene::model::sweep_model::extruded_direction( entity, direction, self.taper_angle, ) }; body.map(|body| preview_body_wires(&body, self.color)) .unwrap_or_default() }) .collect() } } fn preview_body_wires(body: &cadkernel::brep::Body, color: [f32; 4]) -> Vec { let mesh = cadkernel::brep::mesh::tessellate( body, cadkernel::brep::mesh::TessellationTolerance::new( cadkernel::tessellation::DEFAULT_ANGLE, 1e-9, ), ); mesh.edges .into_iter() .filter(|edge| edge.positions.len() >= 2) .map(|edge| { WireModel::solid_f64( "EXTRUDE-PREVIEW".to_owned(), edge.positions, color, false, ) }) .collect() } impl CadCommand for ExtrudeCommand { fn name(&self) -> &'static str { self.command_name } fn prompt(&self) -> String { match self.step { ExtrudeStep::Pick => t!("EXTRUDE Select objects to extrude or [Mode] (Enter to finish):").into_owned(), ExtrudeStep::Mode => format!( "{} <{}>:", t!("EXTRUDE Creation mode [Solid/Surface]"), if self.mode == ExtrudeMode::Solid { "Solid" } else { "Surface" } ), ExtrudeStep::Height => { let base = t!("EXTRUDE Specify height or [Direction/Path/Taper angle/Expression]"); self.last_height.map_or_else( || format!("{base}:"), |height| format!("{base} <{}>:", crate::entities::common::format_length(height)), ) } ExtrudeStep::DirectionStart => t!("EXTRUDE Start point of direction:").into_owned(), ExtrudeStep::DirectionEnd => t!("EXTRUDE End point of direction:").into_owned(), ExtrudeStep::Path => t!("EXTRUDE Select extrusion path or [Taper angle]:").into_owned(), ExtrudeStep::Taper => format!( "{} <{}>:", t!("EXTRUDE Specify taper angle or [Expression]"), crate::entities::common::format_angle(self.taper_angle) ), ExtrudeStep::Expression => t!("EXTRUDE Enter height expression:").into_owned(), ExtrudeStep::TaperExpression => t!("EXTRUDE Enter taper angle expression:").into_owned(), } } fn options(&self) -> Vec { match self.step { ExtrudeStep::Pick => vec![CmdOption::new("Mode", "MODE"), CmdOption::enter("Done")], ExtrudeStep::Mode => vec![ CmdOption::new("Solid", "SOLID"), CmdOption::new("Surface", "SURFACE"), ], ExtrudeStep::Height => vec![ CmdOption::new("Direction", "DIRECTION"), CmdOption::new("Path", "PATH"), CmdOption::new("Taper angle", "TAPER"), CmdOption::new("Expression", "EXPRESSION"), ], ExtrudeStep::Path => vec![CmdOption::new("Taper angle", "TAPER")], ExtrudeStep::Taper => vec![CmdOption::new("Expression", "EXPRESSION")], _ => Vec::new(), } } fn needs_entity_pick(&self) -> bool { self.step == ExtrudeStep::Path } fn entity_pick_uses_surface_point(&self) -> bool { true } fn set_entity_pick_direction(&mut self, direction: Option) { if self.step == ExtrudeStep::Pick { self.profile_direction = direction.and_then(DVec3::try_normalize); } } fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult { if handle.is_null() { return CmdResult::NeedPoint; } match self.step { ExtrudeStep::Pick => { if !self.handles.contains(&handle) { self.handles.push(handle); self.anchor = point; if let Some(entity) = self.injected_profile.take() { self.preview_profiles.push((handle, entity)); } } else { self.injected_profile = None; } CmdResult::NeedPoint } ExtrudeStep::Path if !self.handles.contains(&handle) => { self.finish(ExtrudeExtent::Path(handle)) } _ => CmdResult::NeedPoint, } } fn on_point(&mut self, pt: DVec3) -> CmdResult { match self.step { ExtrudeStep::Height => { let height = self .profile_direction .map(|direction| (pt - self.anchor).dot(direction)) .unwrap_or_else(|| pt.distance(self.anchor)); if height.is_finite() && height.abs() > 1e-6 { self.finish(ExtrudeExtent::Height(height)) } else { CmdResult::NeedPoint } } ExtrudeStep::DirectionStart => { self.direction_start = Some(pt); self.step = ExtrudeStep::DirectionEnd; CmdResult::NeedPoint } ExtrudeStep::DirectionEnd => { let direction = pt - self.direction_start.unwrap_or(pt); if direction.is_finite() && direction.length_squared() > 1e-12 { self.finish(ExtrudeExtent::Direction(direction)) } else { CmdResult::NeedPoint } } _ => CmdResult::NeedPoint, } } fn wants_text_input(&self) -> bool { matches!( self.step, ExtrudeStep::Pick | ExtrudeStep::Mode | ExtrudeStep::Height | ExtrudeStep::Path | ExtrudeStep::Taper | ExtrudeStep::Expression | ExtrudeStep::TaperExpression ) } fn point_step_accepts_keywords(&self) -> bool { matches!(self.step, ExtrudeStep::Height | ExtrudeStep::Path) } fn on_text_input(&mut self, text: &str) -> Option { let value = text.trim(); // A bare Enter finalizes the current source selection through // `on_enter`. Treating the empty string as an option prefix makes it // match every keyword and silently advances to the wrong branch. if value.is_empty() { return None; } match self.step { ExtrudeStep::Pick if "MODE".starts_with(&value.to_ascii_uppercase()) => { self.step = ExtrudeStep::Mode; Some(CmdResult::NeedPoint) } ExtrudeStep::Mode => { let upper = value.to_ascii_uppercase(); if "SOLID".starts_with(&upper) { self.mode = ExtrudeMode::Solid; } else if "SURFACE".starts_with(&upper) { self.mode = ExtrudeMode::Surface; } else { return Some(CmdResult::NeedPoint); } extrude_defaults() .lock() .unwrap_or_else(|error| error.into_inner()) .mode = self.mode; self.step = ExtrudeStep::Pick; Some(CmdResult::NeedPoint) } ExtrudeStep::Height => { let upper = value.to_ascii_uppercase(); if "DIRECTION".starts_with(&upper) { self.step = ExtrudeStep::DirectionStart; return Some(CmdResult::NeedPoint); } if "PATH".starts_with(&upper) { self.step = ExtrudeStep::Path; return Some(CmdResult::NeedPoint); } if "TAPER".starts_with(&upper) || "TAPER ANGLE".starts_with(&upper) { self.taper_return = ExtrudeStep::Height; self.step = ExtrudeStep::Taper; return Some(CmdResult::NeedPoint); } if "EXPRESSION".starts_with(&upper) { self.step = ExtrudeStep::Expression; return Some(CmdResult::NeedPoint); } crate::entities::common::parse_typed_length(value) .filter(|height| height.is_finite() && height.abs() > 1e-6) .map(|height| self.finish(ExtrudeExtent::Height(height))) } ExtrudeStep::Path => { let upper = value.to_ascii_uppercase(); if "TAPER".starts_with(&upper) || "TAPER ANGLE".starts_with(&upper) { self.taper_return = ExtrudeStep::Path; self.step = ExtrudeStep::Taper; } Some(CmdResult::NeedPoint) } ExtrudeStep::Taper => { if "EXPRESSION".starts_with(&value.to_ascii_uppercase()) { self.step = ExtrudeStep::TaperExpression; return Some(CmdResult::NeedPoint); } let angle = crate::entities::common::parse_angle(value)?; if !angle.is_finite() || angle.abs() >= std::f64::consts::FRAC_PI_2 { return Some(CmdResult::NeedPoint); } self.taper_angle = angle; extrude_defaults() .lock() .unwrap_or_else(|error| error.into_inner()) .taper_angle = angle; self.step = self.taper_return; Some(CmdResult::NeedPoint) } ExtrudeStep::Expression => crate::app::expr_eval::eval_number(value) .filter(|height| height.is_finite() && height.abs() > 1e-6) .map(|height| self.finish(ExtrudeExtent::Height(height))), ExtrudeStep::TaperExpression => { let angle = crate::app::expr_eval::eval_number(value) .and_then(|value| crate::entities::common::parse_angle(&value.to_string()))?; if !angle.is_finite() || angle.abs() >= std::f64::consts::FRAC_PI_2 { return Some(CmdResult::NeedPoint); } self.taper_angle = angle; extrude_defaults() .lock() .unwrap_or_else(|error| error.into_inner()) .taper_angle = angle; self.step = self.taper_return; Some(CmdResult::NeedPoint) } _ => None, } } fn on_enter(&mut self) -> CmdResult { match self.step { ExtrudeStep::Pick if !self.handles.is_empty() => { self.step = ExtrudeStep::Height; CmdResult::NeedPoint } ExtrudeStep::Mode => { self.step = ExtrudeStep::Pick; CmdResult::NeedPoint } ExtrudeStep::Height => self .last_height .filter(|height| height.is_finite() && height.abs() > 1e-6) .map(|height| self.finish(ExtrudeExtent::Height(height))) .unwrap_or(CmdResult::Cancel), ExtrudeStep::Taper => { self.step = self.taper_return; CmdResult::NeedPoint } _ => CmdResult::Cancel, } } fn cursor_axis(&self) -> Option<(DVec3, DVec3)> { (self.step == ExtrudeStep::Height) .then_some((self.anchor, self.profile_direction?)) } fn dyn_spec(&self) -> Option { (self.step == ExtrudeStep::Height).then_some(crate::command::DynSpec { anchor: crate::command::DynAnchor::Point(self.anchor), fields: vec![crate::command::DynFieldSpec::new( crate::command::DynRole::Distance, )], guide: crate::command::DynGuide::Radius, ref_point: None, }) } fn dyn_live_value(&self, cursor: DVec3) -> Option { Some((cursor - self.anchor).dot(self.profile_direction?)) } fn is_selection_gathering(&self) -> bool { self.step == ExtrudeStep::Pick } fn selection_forces_add(&self) -> bool { self.step == ExtrudeStep::Pick } fn inject_selection_entities(&mut self, entities: Vec) { if self.step != ExtrudeStep::Pick { return; } self.handles = entities.iter().map(|entry| entry.handle).collect(); self.preview_profiles = entities .iter() .map(|entry| (entry.handle, entry.entity.clone())) .collect(); if let Some(curve) = entities .iter() .find_map(|entry| crate::entities::curve::entity_curve(&entry.entity)) { self.anchor = DVec3::from_array(curve.plane.origin); self.profile_direction = curve.plane.normal().map(DVec3::from_array); } else { self.profile_direction = None; } } fn on_selection_complete(&mut self, _handles: Vec) -> CmdResult { CmdResult::NeedPoint } fn inject_before_entity_pick(&self) -> bool { self.step == ExtrudeStep::Pick } fn inject_picked_entity(&mut self, entity: EntityType) { if self.step == ExtrudeStep::Pick { self.injected_profile = Some(entity); } } fn on_preview_wires(&mut self, cursor: DVec3) -> Vec { self.preview_wires(cursor) } } // ── PRESSPULL command ───────────────────────────────────────────────────── pub struct PresspullCommand { picked: Option<(acadrust::Handle, DVec3)>, direction: Option, color: [f32; 4], } impl PresspullCommand { pub fn new(color: [f32; 4]) -> Self { Self { picked: None, direction: None, color, } } } impl CadCommand for PresspullCommand { fn name(&self) -> &'static str { "PRESSPULL" } fn prompt(&self) -> String { if self.picked.is_none() { t!("PRESSPULL Select a closed profile or planar solid face:").into_owned() } else { t!("PRESSPULL Signed distance:").into_owned() } } fn needs_entity_pick(&self) -> bool { self.picked.is_none() } fn entity_pick_includes_fills(&self) -> bool { true } fn entity_pick_uses_surface_point(&self) -> bool { true } fn set_entity_pick_direction(&mut self, direction: Option) { self.direction = direction.and_then(|value| value.try_normalize()); } fn entity_pick_highlights_hover(&self) -> bool { true } fn on_entity_pick(&mut self, handle: acadrust::Handle, point: DVec3) -> CmdResult { if handle.is_null() { return CmdResult::NeedPoint; } self.picked = Some((handle, point)); CmdResult::NeedPoint } fn on_point(&mut self, point: DVec3) -> CmdResult { let Some((handle, pick)) = self.picked else { return CmdResult::NeedPoint; }; let distance = self .direction .map(|direction| (point - pick).dot(direction)) .unwrap_or_else(|| point.distance(pick)); if !distance.is_finite() || distance.abs() <= 1e-6 { return CmdResult::NeedPoint; } CmdResult::PresspullEntity { handle, pick, distance, drag: Some(point), color: self.color, } } fn wants_text_input(&self) -> bool { self.picked.is_some() } fn on_text_input(&mut self, text: &str) -> Option { let (handle, pick) = self.picked?; crate::entities::common::parse_typed_length(text) .filter(|distance| distance.abs() > 1e-6) .map(|distance| CmdResult::PresspullEntity { handle, pick, distance, drag: None, color: self.color, }) } fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } fn cursor_axis(&self) -> Option<(DVec3, DVec3)> { Some((self.picked?.1, self.direction?)) } fn dyn_spec(&self) -> Option { let (_, anchor) = self.picked?; Some(crate::command::DynSpec { anchor: crate::command::DynAnchor::Point(anchor), fields: vec![crate::command::DynFieldSpec::new( crate::command::DynRole::Distance, )], guide: crate::command::DynGuide::Radius, ref_point: None, }) } fn dyn_live_value(&self, cursor: DVec3) -> Option { let (_, anchor) = self.picked?; Some((cursor - anchor).dot(self.direction?)) } } // ── REVOLVE command ──────────────────────────────────────────────────────── pub struct RevolveCommand { step: RevolveStep, target_handle: acadrust::Handle, axis_start: DVec3, axis_end: DVec3, color: [f32; 4], } #[derive(PartialEq)] enum RevolveStep { Pick, AxisStart, AxisEnd, Angle, } impl RevolveCommand { pub fn new(color: [f32; 4]) -> Self { Self { step: RevolveStep::Pick, target_handle: acadrust::Handle::NULL, axis_start: DVec3::ZERO, axis_end: DVec3::new(0.0, 0.0, 1.0), color, } } } impl CadCommand for RevolveCommand { fn name(&self) -> &'static str { "REVOLVE" } fn prompt(&self) -> String { match self.step { RevolveStep::Pick => t!("REVOLVE Select profile:").into_owned(), RevolveStep::AxisStart => t!("REVOLVE Axis start point:").into_owned(), RevolveStep::AxisEnd => t!("REVOLVE Axis end point:").into_owned(), RevolveStep::Angle => t!("REVOLVE Angle of revolution <360>:").into_owned(), } } fn needs_entity_pick(&self) -> bool { self.step == RevolveStep::Pick } fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: DVec3) -> CmdResult { if handle.is_null() { return CmdResult::NeedPoint; } self.target_handle = handle; self.step = RevolveStep::AxisStart; CmdResult::NeedPoint } fn on_point(&mut self, pt: DVec3) -> CmdResult { match self.step { RevolveStep::AxisStart => { self.axis_start = pt; self.step = RevolveStep::AxisEnd; CmdResult::NeedPoint } RevolveStep::AxisEnd => { self.axis_end = pt; self.step = RevolveStep::Angle; CmdResult::NeedPoint } RevolveStep::Angle => self.make_revolve(360.0), _ => CmdResult::NeedPoint, } } fn wants_text_input(&self) -> bool { self.step == RevolveStep::Angle } fn on_text_input(&mut self, text: &str) -> Option { let angle = if text.trim().is_empty() { 360.0f32 } else { text.trim() .parse::() .ok() .filter(|&a| a.abs() > 1e-3)? }; Some(self.make_revolve(angle)) } fn on_enter(&mut self) -> CmdResult { if self.step == RevolveStep::Angle { self.make_revolve(360.0) } else { CmdResult::Cancel } } } impl RevolveCommand { fn make_revolve(&self, angle_deg: f32) -> CmdResult { CmdResult::RevolveEntity { handle: self.target_handle, axis_start: self.axis_start, axis_end: self.axis_end, angle_deg, color: self.color, } } } // ── SWEEP command ────────────────────────────────────────────────────────── pub struct SweepCommand { step: SweepStep, profile_handle: acadrust::Handle, color: [f32; 4], } #[derive(PartialEq)] enum SweepStep { PickProfile, PickPath, } impl SweepCommand { pub fn new(color: [f32; 4]) -> Self { Self { step: SweepStep::PickProfile, profile_handle: acadrust::Handle::NULL, color, } } } impl CadCommand for SweepCommand { fn name(&self) -> &'static str { "SWEEP" } fn prompt(&self) -> String { match self.step { SweepStep::PickProfile => t!("SWEEP Select profile to sweep:").into_owned(), SweepStep::PickPath => { t!("SWEEP Select path (Line, Arc, LwPolyline):").into_owned() } } } fn needs_entity_pick(&self) -> bool { true } fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: DVec3) -> CmdResult { if handle.is_null() { return CmdResult::NeedPoint; } match self.step { SweepStep::PickProfile => { self.profile_handle = handle; self.step = SweepStep::PickPath; CmdResult::NeedPoint } SweepStep::PickPath => CmdResult::SweepEntity { profile_handle: self.profile_handle, path_handle: handle, color: self.color, }, } } fn on_point(&mut self, _pt: DVec3) -> CmdResult { CmdResult::NeedPoint } fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } } // ── LOFT command ─────────────────────────────────────────────────────────── pub struct LoftCommand { profiles: Vec, color: [f32; 4], } impl LoftCommand { pub fn new(color: [f32; 4]) -> Self { Self { profiles: Vec::new(), color, } } } impl CadCommand for LoftCommand { fn name(&self) -> &'static str { "LOFT" } fn prompt(&self) -> String { if self.profiles.is_empty() { t!("LOFT Select first cross-section:").into_owned() } else { t!( "LOFT Select next cross-section (%{count} selected, Enter to finish):", count = self.profiles.len() ) .into_owned() } } fn needs_entity_pick(&self) -> bool { true } fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: DVec3) -> CmdResult { if handle.is_null() { return CmdResult::NeedPoint; } // Avoid duplicate picks. if !self.profiles.contains(&handle) { self.profiles.push(handle); } CmdResult::NeedPoint } fn on_point(&mut self, _pt: DVec3) -> CmdResult { CmdResult::NeedPoint } fn wants_text_input(&self) -> bool { self.profiles.len() >= 2 } fn on_text_input(&mut self, _text: &str) -> Option { None } fn on_enter(&mut self) -> CmdResult { if self.profiles.len() < 2 { CmdResult::Cancel } else { CmdResult::LoftEntities { handles: self.profiles.clone(), color: self.color, } } } } // ── Solid3D entity construction ──────────────────────────────────────────── pub fn empty_solid3d() -> EntityType { EntityType::Solid3D(Solid3D::new()) } pub fn empty_extruded_surface(direction: DVec3, taper_angle: f64) -> EntityType { use acadrust::entities::{Surface, SurfaceData, SurfaceKind}; use acadrust::types::Vector3; let mut surface = Surface::new(SurfaceKind::Extruded); if let SurfaceData::Extruded { options, sweep_vector, .. } = &mut surface.surface_data { options.draft_angle = taper_angle; options.is_solid = false; *sweep_vector = Vector3::new(direction.x, direction.y, direction.z); } EntityType::Surface(surface) } // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { names: &["EXTRUDE", "THICKEN", "PRESSPULL"] }); inventory::submit!(crate::command::CommandRegistration { names: &["LOFT"] }); inventory::submit!(crate::command::CommandRegistration { names: &["REVOLVE"] }); inventory::submit!(crate::command::CommandRegistration { names: &["SWEEP"] });