diff --git a/Cargo.lock b/Cargo.lock index 5bd723c07..4544c3ff8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=ac2e69070278bf56ae44829f31a69f6a1006ac41#ac2e69070278bf56ae44829f31a69f6a1006ac41" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=284df21acdbc778e2f0fde7cbf47a35dae328920#284df21acdbc778e2f0fde7cbf47a35dae328920" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 443ed645d..2ba64c829 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ rfd = "0.17" clap = { version = "4", features = ["derive"] } env_logger = "0.11" acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "393d6c0857447f9ca678bed97a612707f0a842c6", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "ac2e69070278bf56ae44829f31a69f6a1006ac41", features = ["acis", "offset"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "284df21acdbc778e2f0fde7cbf47a35dae328920", features = ["acis", "offset"] } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index e04bdaa25..6572d4b5f 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -3822,66 +3822,133 @@ impl OpenCADStudio { } // ── REVOLVE ──────────────────────────────────────────────────── - CmdResult::RevolveEntity { - handle, + CmdResult::RevolveEntities { + handles, axis_start, axis_end, - angle_deg, + angle, + start_angle, + mode, color: _, } => { - if self.reject_locked_edit(i, handle) { + if handles.is_empty() + || handles + .iter() + .any(|handle| self.reject_locked_edit(i, *handle)) + { self.tabs[i].active_cmd = None; return Task::none(); } - use crate::modules::insert::solid3d_cmds::empty_solid3d; + let delete_sources = self.tabs[i].scene.document.header.delete_objects; + use crate::command::ExtrudeMode; + use crate::modules::insert::solid3d_cmds::{ + empty_revolved_surface, empty_solid3d, + }; use crate::scene::model::sweep_model; - - let entity_opt = self.tabs[i].scene.document.get_entity(handle).cloned(); - if let Some(entity) = entity_opt { - let from = [ - axis_start.x as f64, - axis_start.y as f64, - axis_start.z as f64, - ]; - let to = [axis_end.x as f64, axis_end.y as f64, axis_end.z as f64]; - let angle = (angle_deg as f64).to_radians(); - let result = sweep_model::revolve_history(&entity, from, to, angle) + let from = axis_start.to_array(); + let to = axis_end.to_array(); + let pending = self.begin_undo(i, "REVOLVE", handles.len(), true); + let mut created_handles = Vec::new(); + let mut consumed = Vec::new(); + let mut failed = 0usize; + for handle in &handles { + let Some(entity) = self.tabs[i].scene.document.get_entity(*handle).cloned() + else { + failed += 1; + continue; + }; + let Some((_, closed)) = sweep_model::extrusion_profile_of(&entity) else { + failed += 1; + continue; + }; + let creates_surface = mode == ExtrudeMode::Surface || !closed; + let created = if creates_surface { + let Some(body) = + sweep_model::revolved_surface(&entity, from, to, angle, start_angle) + else { + failed += 1; + continue; + }; + self.add_surface_model( + empty_revolved_surface( + &entity, + axis_start, + axis_end, + angle, + start_angle, + ), + body, + ) + } else { + let result = sweep_model::revolve_history( + &entity, + from, + to, + angle, + start_angle, + ) .and_then(|history| { cadkernel::acis::rebuild_body(&history) .ok() .map(|solid| (solid, history)) }) .or_else(|| { - sweep_model::revolved(&entity, from, to, angle).map(|solid| { - let history = - crate::scene::model::solid_history::brep_op(&solid); - (solid, history) - }) + sweep_model::revolved(&entity, from, to, angle, start_angle).map( + |solid| { + let history = + crate::scene::model::solid_history::brep_op(&solid); + (solid, history) + }, + ) }); - if let Some((solid, history)) = result { - let pending = self.begin_undo(i, "REVOLVE", 1, true); - let created = self.add_solid_model(empty_solid3d(), solid, history); - if !created.is_null() { - self.tabs[i].dirty = true; - self.command_line.push_output( - crate::tf!("REVOLVE: solid created ({:.0}°).", angle_deg).as_ref(), - ); - if let Some(pd) = pending { - self.commit_undo_delta(i, pd); - } - } + let Some((solid, history)) = result else { + failed += 1; + continue; + }; + self.add_solid_model(empty_solid3d(), solid, history) + }; + if created.is_null() { + failed += 1; } else { - self.command_line - .push_error(crate::t!("REVOLVE: could not revolve profile.").as_ref()); + created_handles.push(created); + if delete_sources { + consumed.push(*handle); + } } + } + if delete_sources && !created_handles.is_empty() { + consumed.sort_unstable_by_key(|handle| handle.value()); + consumed.dedup(); + self.tabs[i].scene.erase_entities(&consumed); + } + if !created_handles.is_empty() { + self.tabs[i].scene.deselect_all(); + for handle in &created_handles { + self.tabs[i].scene.select_entity(*handle, true); + } + self.tabs[i].dirty = true; + self.command_line.push_output( + crate::tf!( + "REVOLVE: created %{created} object(s); %{failed} source(s) could not be revolved.", + created = created_handles.len(), + failed = failed + ) + .as_ref(), + ); } else { - self.command_line - .push_error(crate::t!("REVOLVE: entity not found.").as_ref()); + self.command_line.push_error( + crate::t!("REVOLVE: no selected object could be revolved with the requested options.") + .as_ref(), + ); + } + if let Some(pending) = pending { + self.commit_undo_delta(i, pending); } self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.tabs[i].scene.clear_preview_wire(); self.restore_pre_cmd_tangent(); + self.refresh_properties(); } // ── SWEEP ───────────────────────────────────────────────────── CmdResult::SweepEntity { diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs index ed44e08af..645e6753b 100644 --- a/src/app/commands/display.rs +++ b/src/app/commands/display.rs @@ -794,8 +794,17 @@ impl OpenCADStudio { // ── REVOLVE ──────────────────────────────────────────────────── "REVOLVE" => { use crate::modules::insert::solid3d_cmds::RevolveCommand; + let selected: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect(); let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer); - let cmd = RevolveCommand::new(color); + let mut cmd = RevolveCommand::new(color); + if !selected.is_empty() { + cmd.set_preselection( + selected + .iter() + .map(|(handle, entity)| (*handle, (*entity).clone())) + .collect(), + ); + } self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } diff --git a/src/app/update/dynamic.rs b/src/app/update/dynamic.rs index 1e232cf7e..feaf6b016 100644 --- a/src/app/update/dynamic.rs +++ b/src/app/update/dynamic.rs @@ -660,7 +660,16 @@ impl OpenCADStudio { // the sign of the cursor's side relative to the reference, so the // committed direction matches the drag (the box shows magnitude // only). Commands receive an already-signed string. - let text = self.dyn_sign_angle_text(i, text); + let auto_sign = self.tabs[i] + .active_cmd + .as_ref() + .map(|command| command.dyn_auto_sign_angle()) + .unwrap_or(true); + let text = if auto_sign { + self.dyn_sign_angle_text(i, text) + } else { + text + }; let result = self.tabs[i] .active_cmd .as_mut() diff --git a/src/command.rs b/src/command.rs index 4aec1e3e3..25520107d 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1443,12 +1443,14 @@ pub enum CmdResult { drag: Option, color: [f32; 4], }, - /// Revolve the profile entity `handle` around the given axis by `angle_deg`. - RevolveEntity { - handle: Handle, + /// Revolve the profile entities around the given axis. + RevolveEntities { + handles: Vec, axis_start: glam::DVec3, axis_end: glam::DVec3, - angle_deg: f32, + angle: f64, + start_angle: f64, + mode: ExtrudeMode, color: [f32; 4], }, /// Sweep the profile entity `profile_handle` along `path_handle`. @@ -2161,6 +2163,12 @@ pub trait CadCommand: Send { false } + /// Whether a bare dynamic angle inherits its sign from the world-XY + /// cursor side. Commands with their own 3D axis semantics opt out. + fn dyn_auto_sign_angle(&self) -> bool { + true + } + /// Explicit per-step dynamic-input description. `Some(spec)` takes full /// control of the boxes, guide geometry and anchor for this step; `None` /// (the default) falls back to the legacy `dyn_field()` behaviour so diff --git a/src/modules/insert/solid3d_cmds.rs b/src/modules/insert/solid3d_cmds.rs index da478712e..be7bbf0f3 100644 --- a/src/modules/insert/solid3d_cmds.rs +++ b/src/modules/insert/solid3d_cmds.rs @@ -6,7 +6,7 @@ use acadrust::{entities::Solid3D, EntityType, Handle}; use glam::DVec3; use crate::command::{ - CadCommand, CmdOption, CmdResult, ExtrudeExtent, ExtrudeMode, SelectionEntity, + CadCommand, CmdOption, CmdResult, ExtrudeExtent, ExtrudeMode, SelectionEntity, WorkingPlane, }; use crate::scene::WireModel; use crate::t; @@ -615,30 +615,221 @@ impl CadCommand for PresspullCommand { pub struct RevolveCommand { step: RevolveStep, - target_handle: acadrust::Handle, + handles: Vec, + preview_profiles: Vec<(Handle, EntityType)>, + injected_axis: Option, axis_start: DVec3, axis_end: DVec3, + working_plane: WorkingPlane, + mode: ExtrudeMode, + angle: f64, + start_angle: f64, + reverse: bool, color: [f32; 4], } -#[derive(PartialEq)] +#[derive(Clone, Copy)] +struct RevolveDefaults { + mode: ExtrudeMode, + angle: f64, + start_angle: f64, +} + +fn revolve_defaults() -> &'static Mutex { + static DEFAULTS: OnceLock> = OnceLock::new(); + DEFAULTS.get_or_init(|| { + Mutex::new(RevolveDefaults { + mode: ExtrudeMode::Solid, + angle: std::f64::consts::TAU, + start_angle: 0.0, + }) + }) +} + +#[derive(Clone, Copy, PartialEq, Eq)] enum RevolveStep { Pick, + Mode, AxisStart, AxisEnd, + AxisObject, Angle, + StartAngle, + Expression, } impl RevolveCommand { pub fn new(color: [f32; 4]) -> Self { + let defaults = *revolve_defaults().lock().unwrap_or_else(|error| error.into_inner()); Self { step: RevolveStep::Pick, - target_handle: acadrust::Handle::NULL, + handles: Vec::new(), + preview_profiles: Vec::new(), + injected_axis: None, axis_start: DVec3::ZERO, axis_end: DVec3::new(0.0, 0.0, 1.0), + working_plane: WorkingPlane::default(), + mode: defaults.mode, + angle: defaults.angle, + start_angle: defaults.start_angle, + reverse: false, color, } } + + pub fn set_preselection(&mut self, profiles: Vec<(Handle, EntityType)>) { + self.handles = profiles.iter().map(|(handle, _)| *handle).collect(); + self.preview_profiles = profiles; + if !self.handles.is_empty() { + self.step = RevolveStep::AxisStart; + } + } + + fn profile_reference(&self) -> Option { + let axis = (self.axis_end - self.axis_start).try_normalize()?; + self.preview_profiles + .iter() + .filter_map(|(_, entity)| { + crate::scene::model::sweep_model::extrusion_profile_of(entity) + }) + .flat_map(|(profile, _)| { + profile.pieces.into_iter().flat_map(move |piece| { + [0.0, 0.5, 1.0].into_iter().map(move |parameter| { + DVec3::from_array(profile.plane.point_at(piece.point_at(parameter))) + }) + }) + }) + .filter(|point| point.is_finite()) + .max_by(|first, second| { + let radial_length = |point: DVec3| { + let delta = point - self.axis_start; + (delta - axis * delta.dot(axis)).length_squared() + }; + radial_length(*first).total_cmp(&radial_length(*second)) + }) + } + + fn angle_anchor(&self) -> Option { + let axis = (self.axis_end - self.axis_start).try_normalize()?; + let reference = self.profile_reference()?; + Some(self.axis_start + axis * (reference - self.axis_start).dot(axis)) + } + + fn cursor_angle(&self, cursor: DVec3, full_at_zero: bool) -> Option { + let axis = (self.axis_end - self.axis_start).try_normalize()?; + let anchor = self.angle_anchor()?; + let reference = (self.profile_reference()? - anchor).try_normalize()?; + let radial = (cursor - (self.axis_start + + axis * (cursor - self.axis_start).dot(axis))) + .try_normalize()?; + let mut angle = axis.dot(reference.cross(radial)).atan2(reference.dot(radial)); + if angle < 0.0 { + angle += std::f64::consts::TAU; + } + if full_at_zero && angle.abs() <= 1e-9 { + angle = std::f64::consts::TAU; + } + angle.is_finite().then_some(angle) + } + + fn signed_angle(&self, angle: f64) -> f64 { + angle * if self.reverse { -1.0 } else { 1.0 } + } + + fn preview_wires(&self, cursor: DVec3) -> Vec { + let (angle, start_angle) = match self.step { + RevolveStep::Angle => ( + self.cursor_angle(cursor, true) + .map(|angle| self.signed_angle(angle)), + Some(self.start_angle), + ), + RevolveStep::StartAngle => ( + Some(self.signed_angle(self.angle)), + self.cursor_angle(cursor, false), + ), + _ => return Vec::new(), + }; + let (Some(angle), Some(start_angle)) = (angle, start_angle) else { + return Vec::new(); + }; + let from = self.axis_start.to_array(); + let to = self.axis_end.to_array(); + self.preview_profiles + .iter() + .flat_map(|(_, entity)| { + let Some((_, closed)) = + crate::scene::model::sweep_model::extrusion_profile_of(entity) + else { + return Vec::new(); + }; + let creates_surface = self.mode == ExtrudeMode::Surface || !closed; + let body = if creates_surface { + crate::scene::model::sweep_model::revolved_surface( + entity, + from, + to, + angle, + start_angle, + ) + } else { + crate::scene::model::sweep_model::revolved( + entity, + from, + to, + angle, + start_angle, + ) + }; + body.map(|body| preview_body_wires(&body, self.color)) + .unwrap_or_default() + }) + .collect() + } + + fn set_axis(&mut self, direction: DVec3) -> CmdResult { + let Some(direction) = direction.try_normalize() else { + return CmdResult::NeedPoint; + }; + self.axis_start = self.working_plane.origin; + self.axis_end = self.axis_start + direction; + self.step = RevolveStep::Angle; + CmdResult::NeedPoint + } + + fn finish(&self, angle: f64) -> CmdResult { + let angle = self.signed_angle(angle); + if angle.is_finite() && angle.abs() > 1e-9 && angle.abs() <= std::f64::consts::TAU + 1e-9 { + let mut defaults = revolve_defaults().lock().unwrap_or_else(|error| error.into_inner()); + defaults.mode = self.mode; + defaults.angle = angle.abs(); + defaults.start_angle = self.start_angle; + } + CmdResult::RevolveEntities { + handles: self.handles.clone(), + axis_start: self.axis_start, + axis_end: self.axis_end, + angle, + start_angle: self.start_angle, + mode: self.mode, + color: self.color, + } + } +} + +fn revolve_axis(entity: &EntityType) -> Option<(DVec3, DVec3)> { + let point = |value: &acadrust::types::Vector3| DVec3::new(value.x, value.y, value.z); + let (start, direction) = match entity { + EntityType::Line(line) => (point(&line.start), point(&line.end) - point(&line.start)), + EntityType::Ray(ray) => (point(&ray.base_point), point(&ray.direction)), + EntityType::XLine(line) => (point(&line.base_point), point(&line.direction)), + EntityType::Polyline3D(line) if line.vertices.len() == 2 => { + let start = point(&line.vertices[0].position); + (start, point(&line.vertices[1].position) - start) + } + _ => return None, + }; + let direction = direction.try_normalize()?; + Some((start, start + direction)) } impl CadCommand for RevolveCommand { @@ -647,22 +838,69 @@ impl CadCommand for RevolveCommand { } 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::Pick => t!("REVOLVE Select objects to revolve or [Mode] (Enter to finish):").into_owned(), + RevolveStep::Mode => format!( + "{} <{}>:", + t!("REVOLVE Creation mode [Solid/Surface]"), + if self.mode == ExtrudeMode::Solid { "Solid" } else { "Surface" } + ), + RevolveStep::AxisStart => t!("REVOLVE Specify axis start point or [Object/X/Y/Z]:").into_owned(), RevolveStep::AxisEnd => t!("REVOLVE Axis end point:").into_owned(), - RevolveStep::Angle => t!("REVOLVE Angle of revolution <360>:").into_owned(), + RevolveStep::AxisObject => t!("REVOLVE Select line, ray, or construction line for axis:").into_owned(), + RevolveStep::Angle => format!( + "{} <{}>:", + t!("REVOLVE Specify angle of revolution or [Start angle/Reverse/Expression]"), + crate::entities::common::format_angle(self.angle) + ), + RevolveStep::StartAngle => format!( + "{} <{}>:", + t!("REVOLVE Specify start angle"), + crate::entities::common::format_angle(self.start_angle) + ), + RevolveStep::Expression => t!("REVOLVE Enter angle expression:").into_owned(), + } + } + fn options(&self) -> Vec { + match self.step { + RevolveStep::Pick => vec![CmdOption::new("Mode", "MODE"), CmdOption::enter("Done")], + RevolveStep::Mode => vec![ + CmdOption::new("Solid", "SOLID"), + CmdOption::new("Surface", "SURFACE"), + ], + RevolveStep::AxisStart => vec![ + CmdOption::new("Object", "OBJECT"), + CmdOption::new("X", "X"), + CmdOption::new("Y", "Y"), + CmdOption::new("Z", "Z"), + ], + RevolveStep::Angle => vec![ + CmdOption::new("Start angle", "START"), + CmdOption::new("Reverse", "REVERSE"), + CmdOption::new("Expression", "EXPRESSION"), + ], + _ => Vec::new(), } } fn needs_entity_pick(&self) -> bool { - self.step == RevolveStep::Pick + self.step == RevolveStep::AxisObject } 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 + match self.step { + RevolveStep::AxisObject => { + let Some((start, end)) = self.injected_axis.take().as_ref().and_then(revolve_axis) + else { + return CmdResult::NeedPoint; + }; + self.axis_start = start; + self.axis_end = end; + self.step = RevolveStep::Angle; + CmdResult::NeedPoint + } + _ => CmdResult::NeedPoint, + } } fn on_point(&mut self, pt: DVec3) -> CmdResult { match self.step { @@ -672,46 +910,213 @@ impl CadCommand for RevolveCommand { CmdResult::NeedPoint } RevolveStep::AxisEnd => { - self.axis_end = pt; + if (pt - self.axis_start).length_squared() <= 1e-12 { + CmdResult::NeedPoint + } else { + self.axis_end = pt; + self.step = RevolveStep::Angle; + CmdResult::NeedPoint + } + } + RevolveStep::Angle => self + .cursor_angle(pt, true) + .map(|angle| self.finish(angle)) + .unwrap_or(CmdResult::NeedPoint), + RevolveStep::StartAngle => { + let Some(angle) = self.cursor_angle(pt, false) else { + return CmdResult::NeedPoint; + }; + self.start_angle = angle; 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 + matches!( + self.step, + RevolveStep::Pick + | RevolveStep::Mode + | RevolveStep::AxisStart + | RevolveStep::Angle + | RevolveStep::StartAngle + | RevolveStep::Expression + ) + } + fn point_step_accepts_keywords(&self) -> bool { + matches!(self.step, RevolveStep::AxisStart | 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)) + let value = text.trim(); + if value.is_empty() { + return None; + } + let upper = value.to_ascii_uppercase(); + match self.step { + RevolveStep::Pick if "MODE".starts_with(&upper) => { + self.step = RevolveStep::Mode; + Some(CmdResult::NeedPoint) + } + RevolveStep::Mode => { + if "SOLID".starts_with(&upper) { + self.mode = ExtrudeMode::Solid; + } else if "SURFACE".starts_with(&upper) { + self.mode = ExtrudeMode::Surface; + } else { + return Some(CmdResult::NeedPoint); + } + revolve_defaults() + .lock() + .unwrap_or_else(|error| error.into_inner()) + .mode = self.mode; + self.step = RevolveStep::Pick; + Some(CmdResult::NeedPoint) + } + RevolveStep::AxisStart => { + if "OBJECT".starts_with(&upper) { + self.step = RevolveStep::AxisObject; + Some(CmdResult::NeedPoint) + } else if upper == "X" { + Some(self.set_axis(self.working_plane.x)) + } else if upper == "Y" { + Some(self.set_axis(self.working_plane.y)) + } else if upper == "Z" { + Some(self.set_axis(self.working_plane.z)) + } else { + None + } + } + RevolveStep::Angle => { + if "START ANGLE".starts_with(&upper) || "START".starts_with(&upper) { + self.step = RevolveStep::StartAngle; + return Some(CmdResult::NeedPoint); + } + if "REVERSE".starts_with(&upper) { + self.reverse = !self.reverse; + return Some(CmdResult::NeedPoint); + } + if "EXPRESSION".starts_with(&upper) { + self.step = RevolveStep::Expression; + return Some(CmdResult::NeedPoint); + } + crate::entities::common::parse_angle(value) + .filter(|angle| { + angle.is_finite() + && angle.abs() > 1e-9 + && angle.abs() <= std::f64::consts::TAU + 1e-9 + }) + .map(|angle| self.finish(angle)) + } + RevolveStep::StartAngle => { + let angle = crate::entities::common::parse_angle(value)?; + if !angle.is_finite() { + return Some(CmdResult::NeedPoint); + } + self.start_angle = angle; + self.step = RevolveStep::Angle; + Some(CmdResult::NeedPoint) + } + RevolveStep::Expression => { + 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() <= 1e-9 + || angle.abs() > std::f64::consts::TAU + 1e-9 + { + return Some(CmdResult::NeedPoint); + } + Some(self.finish(angle)) + } + _ => None, + } } fn on_enter(&mut self) -> CmdResult { - if self.step == RevolveStep::Angle { - self.make_revolve(360.0) - } else { - CmdResult::Cancel + match self.step { + RevolveStep::Pick if !self.handles.is_empty() => { + self.step = RevolveStep::AxisStart; + CmdResult::NeedPoint + } + RevolveStep::Mode => { + self.step = RevolveStep::Pick; + CmdResult::NeedPoint + } + RevolveStep::Angle => self.finish(self.angle), + RevolveStep::StartAngle => { + self.step = RevolveStep::Angle; + CmdResult::NeedPoint + } + _ => 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, + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.working_plane = plane; + } + fn is_selection_gathering(&self) -> bool { + self.step == RevolveStep::Pick + } + fn selection_forces_add(&self) -> bool { + self.step == RevolveStep::Pick + } + fn inject_selection_entities(&mut self, entities: Vec) { + if self.step != RevolveStep::Pick { + return; } + self.handles = entities.iter().map(|entry| entry.handle).collect(); + self.preview_profiles = entities + .into_iter() + .map(|entry| (entry.handle, entry.entity)) + .collect(); + } + fn on_selection_complete(&mut self, _handles: Vec) -> CmdResult { + CmdResult::NeedPoint + } + fn inject_before_entity_pick(&self) -> bool { + self.step == RevolveStep::AxisObject + } + fn inject_picked_entity(&mut self, entity: EntityType) { + if self.step == RevolveStep::AxisObject { + self.injected_axis = Some(entity); + } + } + fn on_preview_wires(&mut self, cursor: DVec3) -> Vec { + self.preview_wires(cursor) + } + fn dyn_field(&self) -> crate::command::DynField { + if matches!(self.step, RevolveStep::Angle | RevolveStep::StartAngle) { + crate::command::DynField::Angle + } else { + crate::command::DynField::Point + } + } + fn dyn_spec(&self) -> Option { + matches!(self.step, RevolveStep::Angle | RevolveStep::StartAngle).then(|| { + crate::command::DynSpec { + anchor: crate::command::DynAnchor::Point( + self.angle_anchor().unwrap_or(self.axis_start), + ), + fields: vec![crate::command::DynFieldSpec::new(crate::command::DynRole::Angle)], + guide: crate::command::DynGuide::None, + ref_point: self.profile_reference(), + } + }) + } + fn dyn_commit_as_text(&self) -> bool { + matches!(self.step, RevolveStep::Angle | RevolveStep::StartAngle) + } + fn dyn_auto_sign_angle(&self) -> bool { + false + } + fn dyn_live_value(&self, cursor: DVec3) -> Option { + let full_at_zero = self.step == RevolveStep::Angle; + self.cursor_angle(cursor, full_at_zero).map(|angle| { + let angle = if self.step == RevolveStep::Angle { + self.signed_angle(angle) + } else { + angle + }; + crate::command::dyn_display_angle_deg(angle as f32) as f64 + }) } } @@ -868,6 +1273,45 @@ pub fn empty_extruded_surface(direction: DVec3, taper_angle: f64) -> EntityType EntityType::Surface(surface) } +pub fn empty_revolved_surface( + profile: &EntityType, + axis_start: DVec3, + axis_end: DVec3, + angle: f64, + start_angle: f64, +) -> EntityType { + use acadrust::entities::{Surface, SurfaceData, SurfaceKind}; + use acadrust::types::Vector3; + + let mut surface = Surface::new(SurfaceKind::Revolved); + let axis = (axis_end - axis_start).normalize_or(DVec3::Z); + surface.point_of_reference = Vector3::new(axis_start.x, axis_start.y, axis_start.z); + if let SurfaceData::Revolved { + revolve_entity, + axis_point, + axis_vector, + revolve_angle, + start_angle: stored_start, + entity_transform, + solid, + .. + } = &mut surface.surface_data + { + if let Some((embedded, transform)) = + crate::scene::model::sweep_model::embedded_revolve_profile(profile) + { + *revolve_entity = Some(embedded); + *entity_transform = transform; + } + *axis_point = Vector3::new(axis_start.x, axis_start.y, axis_start.z); + *axis_vector = Vector3::new(axis.x, axis.y, axis.z); + *revolve_angle = angle; + *stored_start = start_angle; + *solid = false; + } + EntityType::Surface(surface) +} + // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { names: &["EXTRUDE", "THICKEN", "PRESSPULL"] diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index 31d75a372..a9ecad5f2 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -68,6 +68,13 @@ pub const PROP_SCALE_ALONG_PATH: &str = "solid_history_scale_along_path"; pub const PROP_SWEEP_LENGTH: &str = "solid_history_sweep_length"; pub const PROP_EXTRUSION_HEIGHT: &str = "solid_history_extrusion_height"; pub const PROP_TAPER_ANGLE: &str = "solid_history_taper_angle"; +pub const PROP_REVOLVE_ANGLE: &str = "solid_history_revolve_angle"; +pub const PROP_AXIS_POSITION_X: &str = "solid_history_axis_position_x"; +pub const PROP_AXIS_POSITION_Y: &str = "solid_history_axis_position_y"; +pub const PROP_AXIS_POSITION_Z: &str = "solid_history_axis_position_z"; +pub const PROP_AXIS_DIRECTION_X: &str = "solid_history_axis_direction_x"; +pub const PROP_AXIS_DIRECTION_Y: &str = "solid_history_axis_direction_y"; +pub const PROP_AXIS_DIRECTION_Z: &str = "solid_history_axis_direction_z"; pub const PROP_PYRAMID_TYPE: &str = "solid_history_pyramid_type"; pub const PROP_HISTORY: &str = "solid_history_record"; pub const PROP_SHOW_HISTORY: &str = "solid_history_show"; @@ -122,6 +129,7 @@ pub fn has_specialized_primitive_properties( | SolidHistoryOperation::Pyramid(_) | SolidHistoryOperation::Sweep(_) | SolidHistoryOperation::Extrusion(_) + | SolidHistoryOperation::Revolve(_) ) ) } @@ -147,10 +155,112 @@ pub fn reference_point(operation: &SolidHistoryOperation) -> Option ), SolidHistoryOperation::Torus(value) => world_point(value.base.transform, [0.0; 3]), SolidHistoryOperation::Pyramid(value) => world_point(value.base.transform, [0.0; 3]), + SolidHistoryOperation::Revolve(value) => world_point( + value.base.transform, + [value.axis_point.x, value.axis_point.y, value.axis_point.z], + ), _ => None, } } +fn revolve_properties( + document: &acadrust::CadDocument, + handle: acadrust::Handle, + value: &SolidHistoryRevolve, +) -> Vec { + let Some(axis_position) = world_point( + value.base.transform, + [value.axis_point.x, value.axis_point.y, value.axis_point.z], + ) else { + return Vec::new(); + }; + let Some(axis_direction) = world_vector( + value.base.transform, + [value.direction.x, value.direction.y, value.direction.z], + ) else { + return Vec::new(); + }; + let (record_history, object_show_history, show_history_mode) = + history_flags(document, handle).unwrap_or((false, false, 1)); + let (show_history, show_history_editable) = + displayed_history_state(object_show_history, show_history_mode); + let show_value = if show_history { "Yes" } else { "No" }; + vec![ + PropSection { + title: t!("Geometry").into_owned(), + props: vec![ + Property { + label: t!("Solid type").into_owned(), + field: "solid_history_type", + value: PropValue::ReadOnly(t!("Revolve").into_owned()), + }, + Property { + label: t!("Angle of revolution").into_owned(), + field: PROP_REVOLVE_ANGLE, + value: PropValue::EditText(crate::entities::common::format_angle( + value.revolve_angle, + )), + }, + crate::entities::common::edit_prop( + t!("Axis position X").as_ref(), + PROP_AXIS_POSITION_X, + axis_position.x, + ), + crate::entities::common::edit_prop( + t!("Axis position Y").as_ref(), + PROP_AXIS_POSITION_Y, + axis_position.y, + ), + crate::entities::common::edit_prop( + t!("Axis position Z").as_ref(), + PROP_AXIS_POSITION_Z, + axis_position.z, + ), + crate::entities::common::edit_scalar_prop( + t!("Axis direction X").as_ref(), + PROP_AXIS_DIRECTION_X, + axis_direction.x, + ), + crate::entities::common::edit_scalar_prop( + t!("Axis direction Y").as_ref(), + PROP_AXIS_DIRECTION_Y, + axis_direction.y, + ), + crate::entities::common::edit_scalar_prop( + t!("Axis direction Z").as_ref(), + PROP_AXIS_DIRECTION_Z, + axis_direction.z, + ), + ], + }, + PropSection { + title: t!("Solid History").into_owned(), + props: vec![ + Property { + label: t!("History").into_owned(), + field: PROP_HISTORY, + value: PropValue::Choice { + selected: if record_history { "Record" } else { "None" }.to_string(), + options: vec!["None".to_string(), "Record".to_string()], + }, + }, + Property { + label: t!("Show History").into_owned(), + field: PROP_SHOW_HISTORY, + value: if show_history_editable { + PropValue::Choice { + selected: show_value.to_string(), + options: vec!["No".to_string(), "Yes".to_string()], + } + } else { + PropValue::ReadOnly(show_value.to_string()) + }, + }, + ], + }, + ] +} + fn extrusion_properties( document: &acadrust::CadDocument, handle: acadrust::Handle, @@ -1021,6 +1131,7 @@ pub fn primitive_properties( SolidHistoryOperation::Extrusion(value) => { extrusion_properties(document, handle, value) } + SolidHistoryOperation::Revolve(value) => revolve_properties(document, handle, value), _ => Vec::new(), } } @@ -1053,6 +1164,13 @@ pub fn is_primitive_property(field: &str) -> bool { | PROP_SCALE_ALONG_PATH | PROP_EXTRUSION_HEIGHT | PROP_TAPER_ANGLE + | PROP_REVOLVE_ANGLE + | PROP_AXIS_POSITION_X + | PROP_AXIS_POSITION_Y + | PROP_AXIS_POSITION_Z + | PROP_AXIS_DIRECTION_X + | PROP_AXIS_DIRECTION_Y + | PROP_AXIS_DIRECTION_Z | PROP_PYRAMID_TYPE ) } @@ -1142,6 +1260,93 @@ fn apply_extrusion_geometry_property( } } +fn apply_revolve_geometry_property( + value: &mut SolidHistoryRevolve, + field: &str, + text: &str, +) -> Option { + if field == PROP_REVOLVE_ANGLE { + let angle = crate::entities::common::parse_angle(text)?; + if !angle.is_finite() + || angle.abs() <= 1e-12 + || angle.abs() > std::f64::consts::TAU + 1e-12 + || (angle - value.revolve_angle).abs() <= 1e-12 + { + return Some(false); + } + value.revolve_angle = angle; + return Some(true); + } + + if matches!( + field, + PROP_AXIS_POSITION_X | PROP_AXIS_POSITION_Y | PROP_AXIS_POSITION_Z + ) { + let axis = match field { + PROP_AXIS_POSITION_X => 0, + PROP_AXIS_POSITION_Y => 1, + _ => 2, + }; + let target = crate::entities::common::parse_length(text)?; + if !target.is_finite() { + return Some(false); + } + let mut world = world_point( + value.base.transform, + [value.axis_point.x, value.axis_point.y, value.axis_point.z], + )?; + if (world[axis] - target).abs() <= 1e-12 { + return Some(false); + } + world[axis] = target; + let local = local_point(value.base.transform, world)?; + if !local.is_finite() { + return Some(false); + } + value.axis_point = acadrust::types::Vector3::new(local.x, local.y, local.z); + return Some(true); + } + + if matches!( + field, + PROP_AXIS_DIRECTION_X | PROP_AXIS_DIRECTION_Y | PROP_AXIS_DIRECTION_Z + ) { + let axis = match field { + PROP_AXIS_DIRECTION_X => 0, + PROP_AXIS_DIRECTION_Y => 1, + _ => 2, + }; + let target = text.trim().replace(',', ".").parse::().ok()?; + if !target.is_finite() { + return Some(false); + } + let mut world = world_vector( + value.base.transform, + [value.direction.x, value.direction.y, value.direction.z], + )?; + world[axis] = target; + let Some(world) = world.try_normalize() else { + return Some(false); + }; + let Some(local) = matrix(value.base.transform)? + .inverse() + .transform_vector3(world) + .try_normalize() + else { + return Some(false); + }; + let current = glam::DVec3::new(value.direction.x, value.direction.y, value.direction.z) + .normalize_or_zero(); + if (local - current).length_squared() <= 1e-24 { + return Some(false); + } + value.direction = acadrust::types::Vector3::new(local.x, local.y, local.z); + return Some(true); + } + + None +} + pub fn apply_history_choice( document: &mut acadrust::CadDocument, handle: acadrust::Handle, @@ -1578,6 +1783,11 @@ pub fn apply_primitive_property( return applied; } } + if let SolidHistoryOperation::Revolve(revolve_value) = operation { + if let Some(applied) = apply_revolve_geometry_property(revolve_value, field, value) { + return applied; + } + } if let SolidHistoryOperation::Torus(torus_value) = operation { if let Some(applied) = apply_torus_position_property(torus_value, field, value) { return applied; diff --git a/src/scene/model/sweep_model.rs b/src/scene/model/sweep_model.rs index cbc223513..6979b9c2e 100644 --- a/src/scene/model/sweep_model.rs +++ b/src/scene/model/sweep_model.rs @@ -386,15 +386,65 @@ pub fn revolved( from: [f64; 3], to: [f64; 3], angle: f64, + start_angle: f64, ) -> Option { - let profile = profile_of(entity)?; - brep::revolve( - profile.plane, - &profile.pieces, - from, - [to[0] - from[0], to[1] - from[1], to[2] - from[2]], - angle, - ) + let axis = [to[0] - from[0], to[1] - from[1], to[2] - from[2]]; + let body = if let Some((plane, profiles)) = + region_profiles(entity).filter(|(_, profiles)| profiles.len() > 1) + { + brep::revolve_region(plane, &profiles, from, axis, angle)? + } else { + let profile = profile_of(entity)?; + brep::revolve(profile.plane, &profile.pieces, from, axis, angle)? + }; + turn_revolved_body(body, from, axis, start_angle) +} + +/// REVOLVE sheet mode for an open or closed planar profile. +pub fn revolved_surface( + entity: &EntityType, + from: [f64; 3], + to: [f64; 3], + angle: f64, + start_angle: f64, +) -> Option { + let axis = [to[0] - from[0], to[1] - from[1], to[2] - from[2]]; + let body = if let Some((plane, profiles)) = + region_profiles(entity).filter(|(_, profiles)| profiles.len() > 1) + { + brep::revolve_surface_region(plane, &profiles, from, axis, angle)? + } else { + let (profile, _) = extrusion_profile_of(entity)?; + brep::revolve_surface(profile.plane, &profile.pieces, from, axis, angle)? + }; + turn_revolved_body(body, from, axis, start_angle) +} + +fn turn_revolved_body( + body: Body, + pivot: [f64; 3], + axis: [f64; 3], + angle: f64, +) -> Option { + if !angle.is_finite() { + return None; + } + if angle.abs() <= 1e-12 { + return Some(body); + } + let axis = Vec3::from(axis).normalize()?; + let rotate = |vector: Vec3| { + let (sin, cos) = angle.sin_cos(); + vector * cos + axis.cross(vector) * sin + axis * axis.dot(vector) * (1.0 - cos) + }; + let pivot = Vec3::from(pivot); + let placement = brep::Placement { + x_axis: rotate(Vec3::X).to_array(), + y_axis: rotate(Vec3::Y).to_array(), + z_axis: rotate(Vec3::Z).to_array(), + origin: (pivot - rotate(pivot)).to_array(), + }; + brep::transform(&body, &placement) } fn profile_center(profile: &Profile) -> Option { @@ -413,8 +463,13 @@ pub fn revolve_history( from: [f64; 3], to: [f64; 3], angle: f64, + start_angle: f64, ) -> Option { - if !angle.is_finite() || angle.abs() <= 1e-12 { + if !angle.is_finite() + || angle.abs() <= 1e-12 + || !start_angle.is_finite() + || matches!(entity, EntityType::Region(region) if region.wires.len() > 1) + { return None; } let profile = profile_of(entity)?; @@ -422,16 +477,27 @@ pub fn revolve_history( let direction = (Vec3::from(to) - from).normalize()?; let center = profile_center(&profile)?; let axis_point = from + direction * (center - from).dot(direction); + let (sweep_entity, transform) = embedded_planar_entity(entity)?; + let transform = glam::DMat4::from_cols_array(&transform); + let inverse = transform.inverse(); + if !transform.is_finite() || !inverse.is_finite() || transform.determinant().abs() <= 1e-12 { + return None; + } + let axis_point = inverse.transform_point3(glam::DVec3::from_array(axis_point.to_array())); + let direction = inverse + .transform_vector3(glam::DVec3::from_array(direction.to_array())) + .try_normalize()?; let mut base = SolidHistoryNodeBase::new(1); - base.transform = glam::DMat4::IDENTITY.to_cols_array(); + base.transform = transform.to_cols_array(); Some(SolidHistoryOperation::Revolve(SolidHistoryRevolve { base, operation_major: 1, axis_point: Vector3::new(axis_point.x, axis_point.y, axis_point.z), direction: Vector3::new(direction.x, direction.y, direction.z), revolve_angle: angle, + start_angle, flag_290: true, - sweep_entity: Some(embedded_path(entity)?), + sweep_entity: Some(sweep_entity), ..SolidHistoryRevolve::default() })) } @@ -597,6 +663,10 @@ fn embedded_planar_entity(entity: &EntityType) -> Option<(EmbeddedEntity, [f64; Some((EmbeddedEntity::LwPolyline(polyline), transform)) } +pub fn embedded_revolve_profile(entity: &EntityType) -> Option<(EmbeddedEntity, [f64; 16])> { + embedded_planar_entity(entity) +} + pub fn extrusion_history( profile: &EntityType, path: Option<&EntityType>, @@ -850,6 +920,7 @@ mod tests { [0.0, 0.0, 0.0], [0.0, 10.0, 0.0], std::f64::consts::TAU, + 0.0, ); // The profile is in the XY plane and the axis lies in it, so this is // a revolution about the y axis: radius ten, height ten. @@ -875,7 +946,8 @@ mod tests { &square(10.0), [0.0, 0.0, 0.0], [0.0, 1.0, 1.0], - std::f64::consts::TAU + std::f64::consts::TAU, + 0.0, ) .is_none()); }