From ad47b17603a7239e9eb55fe97c9cc3375f0e0457 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:15:44 +0300 Subject: [PATCH] Complete SWEEP command options and kernel integration --- Cargo.lock | 6 +- Cargo.toml | 6 +- src/app/command_driver.rs | 125 +++++--- src/app/commands/display.rs | 11 +- src/app/update/viewport.rs | 9 + src/command.rs | 39 ++- src/entities/object_data.rs | 1 + src/entities/solid3d.rs | 1 + src/modules/insert/solid3d_cmds.rs | 411 +++++++++++++++++++++++-- src/scene/model/mod.rs | 1 + src/scene/model/solid_history.rs | 73 ++--- src/scene/model/sweep_command_model.rs | 113 +++++++ src/scene/model/sweep_model.rs | 29 +- 13 files changed, 709 insertions(+), 116 deletions(-) create mode 100644 src/scene/model/sweep_command_model.rs diff --git a/Cargo.lock b/Cargo.lock index 30020efbb..b647c57cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,8 +71,8 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" -version = "0.5.1" -source = "git+https://git@github.com/HakanSeven12/cadcodec.git?rev=393d6c0857447f9ca678bed97a612707f0a842c6#393d6c0857447f9ca678bed97a612707f0a842c6" +version = "0.5.3" +source = "git+https://git@github.com/HakanSeven12/cadcodec.git?rev=305e93d4cf8ff70ce8449015edb201c2765fcc04#305e93d4cf8ff70ce8449015edb201c2765fcc04" dependencies = [ "ahash 0.8.12", "anyhow", @@ -878,7 +878,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=1a1505c7bb8e26770f14b49859587660c7519a25#1a1505c7bb8e26770f14b49859587660c7519a25" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=209df414c9c0434af00edabc5529c6c774091f0e#209df414c9c0434af00edabc5529c6c774091f0e" dependencies = [ "acadrust", "cavalier_contours", diff --git a/Cargo.toml b/Cargo.toml index 5fc7e2fa3..0889b94f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,8 @@ glam = { version = "0.33", features = ["bytemuck"] } 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 = "1a1505c7bb8e26770f14b49859587660c7519a25", features = ["acis", "offset"] } +acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "305e93d4cf8ff70ce8449015edb201c2765fcc04", features = ["serde"] } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "209df414c9c0434af00edabc5529c6c774091f0e", features = ["acis", "offset"] } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } @@ -61,7 +61,7 @@ iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aa iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" } [patch."https://github.com/HakanSeven12/cadcodec.git"] -acadrust = { git = "https://git@github.com/HakanSeven12/cadcodec.git", rev = "393d6c0857447f9ca678bed97a612707f0a842c6" } +acadrust = { git = "https://git@github.com/HakanSeven12/cadcodec.git", rev = "305e93d4cf8ff70ce8449015edb201c2765fcc04" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index bda0e71b1..2c8d361f1 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -353,6 +353,17 @@ impl OpenCADStudio { self.reset_tracking_after_point(); self.push_ucs_to_cmd(i); } + if let StepInput::EntityPick(handle, _) = &input { + if self.tabs[i].active_cmd.as_ref() + .is_some_and(|command| command.inject_before_entity_pick()) + { + if let Some(entity) = self.tabs[i].scene.document.get_entity(*handle).cloned() { + if let Some(command) = self.tabs[i].active_cmd.as_mut() { + command.inject_picked_entity(entity); + } + } + } + } if let StepInput::SelectionComplete(handles) = &input { let entities = { let scene = &self.tabs[i].scene; @@ -3979,57 +3990,95 @@ impl OpenCADStudio { self.refresh_properties(); } // ── SWEEP ───────────────────────────────────────────────────── - CmdResult::SweepEntity { - profile_handle, + CmdResult::SweepEntities { + handles, path_handle, + mode, + options, color: _, } => { - if self.reject_locked_edit(i, profile_handle) - || self.reject_locked_edit(i, path_handle) - { - self.tabs[i].active_cmd = None; - return Task::none(); - } + use crate::command::ExtrudeMode; use crate::modules::insert::solid3d_cmds::empty_solid3d; - use crate::scene::model::{solid_history, sweep_model}; - - let profile_ent = self.tabs[i] - .scene - .document - .get_entity(profile_handle) - .cloned(); - let path_ent = self.tabs[i].scene.document.get_entity(path_handle).cloned(); - let result = profile_ent.zip(path_ent).and_then(|(profile, path)| { - let reference_point = sweep_model::profile_of(&profile)?.plane.origin; - let solid = sweep_model::swept(&profile, &path)?; - let history = sweep_model::sweep_history( - &profile, - &path, - 0.0, - reference_point, - ) - .unwrap_or_else(|| solid_history::brep_op(&solid)); - Some((solid, history)) - }); - - if let Some((solid, history)) = result { - let pending = self.begin_undo(i, "SWEEP", 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::t!("SWEEP: solid created.").as_ref()); - if let Some(pd) = pending { - self.commit_undo_delta(i, pd); + use crate::scene::model::sweep_model; + let delete_sources = self.tabs[i].scene.document.header.delete_objects; + let path = self.tabs[i].scene.document.get_entity(path_handle).cloned(); + let mut profiles = Vec::new(); + let mut failed = 0usize; + for handle in handles { + if handle == path_handle || self.reject_locked_edit(i, handle) { + failed += 1; + continue; + } + if let Some(entity) = self.tabs[i].scene.document.get_entity(handle).cloned() { + profiles.push((handle, entity)); + } else { + failed += 1; + } + } + let selection = profiles.iter().map(|(_, entity)| entity.clone()).collect::>(); + let options = sweep_model::sweep_selection_options(&selection, options); + let pending = if profiles.is_empty() { + None + } else { + self.begin_undo(i, "SWEEP", profiles.len(), true) + }; + let mut created_handles = Vec::new(); + let mut consumed = Vec::new(); + for (handle, profile) in profiles { + let result = path.as_ref().zip(options).and_then(|(path, options)| { + let record = sweep_model::sweep_record(&profile, path, options)?; + let (_, _, closed) = cadkernel::acis::sweep_profile_geometry( + record.sweep_entity.as_ref()?, record.sweep_entity_transform, + ).ok()?; + let surface = mode == ExtrudeMode::Surface || !closed; + let body = cadkernel::acis::rebuild_sweep_with_mode(&record, surface).ok()?; + Some((body, record, surface)) + }); + let Some((body, record, surface)) = result else { + failed += 1; + continue; + }; + let created = if surface { + self.add_surface_model(sweep_model::swept_surface_entity(&record), body) + } else { + self.add_solid_model( + empty_solid3d(), body, + acadrust::objects::SolidHistoryOperation::Sweep(record), + ) + }; + if created.is_null() { + failed += 1; + } else { + created_handles.push(created); + if delete_sources { + consumed.push(handle); } } + } + if !created_handles.is_empty() { + consumed.sort_unstable_by_key(|handle| handle.value()); + consumed.dedup(); + self.tabs[i].scene.erase_entities(&consumed); + 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!( + "SWEEP: created %{created} object(s); %{failed} source(s) could not be swept.", + created = created_handles.len(), failed = failed + ).as_ref()); } else { self.command_line.push_error(crate::t!("SWEEP: could not sweep the profile along the path.").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(); } // ── LOFT ────────────────────────────────────────────────────── diff --git a/src/app/commands/display.rs b/src/app/commands/display.rs index a27dfade3..1d8ab193f 100644 --- a/src/app/commands/display.rs +++ b/src/app/commands/display.rs @@ -814,7 +814,16 @@ impl OpenCADStudio { "SWEEP" => { use crate::modules::insert::solid3d_cmds::SweepCommand; let color = self.tabs[i].scene.layer_color(&self.tabs[i].active_layer); - let cmd = SweepCommand::new(color); + let isolines = self.tabs[i].scene.document.header.isolines.max(0) as usize; + let mut cmd = SweepCommand::new(color, isolines); + let selected = self.tabs[i].scene.selected_handles_in_order() + .into_iter() + .filter_map(|handle| self.tabs[i].scene.document.get_entity(handle) + .cloned().map(|entity| (handle, entity))) + .collect::>(); + if !selected.is_empty() { + cmd.set_preselection(selected); + } self.command_line.push_info(&cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(cmd)); } diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index f192d03c0..19141aa5b 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -2298,6 +2298,15 @@ impl OpenCADStudio { self.tabs[i].scene.set_hover_highlight(None); } let hover_handle = hovered.unwrap_or(acadrust::Handle::NULL); + let wants_entity = self.tabs[i].active_cmd.as_ref() + .is_some_and(|command| command.wants_hover_entity(hover_handle)); + if wants_entity { + if let Some(entity) = self.tabs[i].scene.document.get_entity(hover_handle).cloned() { + if let Some(command) = self.tabs[i].active_cmd.as_mut() { + command.inject_hover_entity(hover_handle, entity); + } + } + } let shift = self.shift_down; let mut p = self.tabs[i] .active_cmd diff --git a/src/command.rs b/src/command.rs index 518539fb6..3a21fd09a 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1142,6 +1142,29 @@ pub enum ExtrudeExtent { Path(Handle), } +/// Construction options shared by SWEEP creation and its live preview. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct SweepOptions { + pub align: bool, + pub bank: bool, + pub base_point: Option, + pub scale: f64, + /// Total twist along the path, in radians. + pub twist_angle: f64, +} + +impl Default for SweepOptions { + fn default() -> Self { + Self { + align: true, + bank: false, + base_point: None, + scale: 1.0, + twist_angle: 0.0, + } + } +} + /// Returned by every `CadCommand` method to tell main.rs what to do. #[allow(dead_code)] pub enum CmdResult { @@ -1453,10 +1476,12 @@ pub enum CmdResult { mode: ExtrudeMode, color: [f32; 4], }, - /// Sweep the profile entity `profile_handle` along `path_handle`. - SweepEntity { - profile_handle: Handle, + /// Sweep the selected profiles along one path in a single undo step. + SweepEntities { + handles: Vec, path_handle: Handle, + mode: ExtrudeMode, + options: SweepOptions, color: [f32; 4], }, /// Loft through a series of profile entities. @@ -1951,6 +1976,14 @@ pub trait CadCommand: Send { vec![] } + /// Request a source snapshot only when the hovered entity changes. + /// Commands can cache geometry across mouse moves without holding a scene. + fn wants_hover_entity(&self, _handle: Handle) -> bool { + false + } + + fn inject_hover_entity(&mut self, _handle: Handle, _entity: EntityType) {} + /// Called on every mouse-move in the viewport. /// Return `Some(WireModel)` to update the rubber-band preview, `None` to skip. fn on_mouse_move(&mut self, _pt: DVec3) -> Option { diff --git a/src/entities/object_data.rs b/src/entities/object_data.rs index bed43563d..24545b460 100644 --- a/src/entities/object_data.rs +++ b/src/entities/object_data.rs @@ -204,6 +204,7 @@ fn embedded_name(value: Option<&acadrust::entities::EmbeddedEntity>) -> &'static Some(acadrust::entities::EmbeddedEntity::Ellipse(_)) => "Ellipse", Some(acadrust::entities::EmbeddedEntity::Spline(_)) => "Spline", Some(acadrust::entities::EmbeddedEntity::LwPolyline(_)) => "Polyline", + Some(acadrust::entities::EmbeddedEntity::Region(_)) => "Region", Some(acadrust::entities::EmbeddedEntity::Ray(_)) => "Ray", Some(acadrust::entities::EmbeddedEntity::XLine(_)) => "XLine", Some(acadrust::entities::EmbeddedEntity::Unknown { .. }) => "Unknown", diff --git a/src/entities/solid3d.rs b/src/entities/solid3d.rs index e713a61b7..ce06f0b5a 100644 --- a/src/entities/solid3d.rs +++ b/src/entities/solid3d.rs @@ -371,6 +371,7 @@ fn embedded_name(entity: Option<&acadrust::entities::EmbeddedEntity>) -> &'stati Some(acadrust::entities::EmbeddedEntity::Ellipse(_)) => "Ellipse", Some(acadrust::entities::EmbeddedEntity::Spline(_)) => "Spline", Some(acadrust::entities::EmbeddedEntity::LwPolyline(_)) => "Polyline", + Some(acadrust::entities::EmbeddedEntity::Region(_)) => "Region", Some(acadrust::entities::EmbeddedEntity::Ray(_)) => "Ray", Some(acadrust::entities::EmbeddedEntity::XLine(_)) => "XLine", Some(acadrust::entities::EmbeddedEntity::Unknown { .. }) => "Unknown", diff --git a/src/modules/insert/solid3d_cmds.rs b/src/modules/insert/solid3d_cmds.rs index 4a1ddb76f..319ee8a13 100644 --- a/src/modules/insert/solid3d_cmds.rs +++ b/src/modules/insert/solid3d_cmds.rs @@ -6,7 +6,8 @@ use acadrust::{entities::Solid3D, EntityType, Handle}; use glam::DVec3; use crate::command::{ - CadCommand, CmdOption, CmdResult, ExtrudeExtent, ExtrudeMode, SelectionEntity, WorkingPlane, + CadCommand, CmdOption, CmdResult, ExtrudeExtent, ExtrudeMode, SelectionEntity, SweepOptions, + WorkingPlane, }; use crate::scene::WireModel; use crate::t; @@ -1184,24 +1185,118 @@ impl CadCommand for RevolveCommand { pub struct SweepCommand { step: SweepStep, - profile_handle: acadrust::Handle, + profiles: Vec<(Handle, EntityType)>, + injected_path: Option, + hover_path: Option<(Handle, EntityType)>, + preview_key: Option<(Handle, ExtrudeMode, SweepOptions)>, + preview_cache: Vec, + mode: ExtrudeMode, + options: SweepOptions, + reference_start: Option, + reference_length: f64, + new_length_start: Option, + isolines: usize, color: [f32; 4], } -#[derive(PartialEq)] +#[derive(Clone, Copy, PartialEq, Eq)] enum SweepStep { - PickProfile, + PickProfiles, + Mode, PickPath, + Alignment, + BasePoint, + Scale, + ReferenceLength, + ReferenceEnd, + NewLength, + NewLengthStart, + NewLengthEnd, + Twist, } impl SweepCommand { - pub fn new(color: [f32; 4]) -> Self { + pub fn new(color: [f32; 4], isolines: usize) -> Self { Self { - step: SweepStep::PickProfile, - profile_handle: acadrust::Handle::NULL, + step: SweepStep::PickProfiles, + profiles: Vec::new(), + injected_path: None, + hover_path: None, + preview_key: None, + preview_cache: Vec::new(), + mode: ExtrudeMode::Solid, + options: SweepOptions::default(), + reference_start: None, + reference_length: 1.0, + new_length_start: None, + isolines, color, } } + + pub fn set_preselection(&mut self, profiles: Vec<(Handle, EntityType)>) { + self.set_profiles(profiles); + if !self.profiles.is_empty() { + self.step = SweepStep::PickPath; + } + } + + fn set_profiles(&mut self, profiles: Vec<(Handle, EntityType)>) { + self.profiles.clear(); + for (handle, entity) in profiles { + if !handle.is_null() + && !self.profiles.iter().any(|(selected, _)| *selected == handle) + && crate::scene::model::sweep_model::is_sweep_profile(&entity) + { + self.profiles.push((handle, entity)); + } + } + self.preview_key = None; + self.preview_cache.clear(); + } + + fn contains_profile(&self, handle: Handle) -> bool { + self.profiles.iter().any(|(selected, _)| *selected == handle) + } + + fn selection_options(&self) -> Option { + let profiles = self.profiles.iter().map(|(_, entity)| entity.clone()).collect::>(); + crate::scene::model::sweep_model::sweep_selection_options(&profiles, self.options) + } + + fn finish(&self, path_handle: Handle) -> CmdResult { + CmdResult::SweepEntities { + handles: self.profiles.iter().map(|(handle, _)| *handle).collect(), + path_handle, + mode: self.mode, + options: self.options, + color: self.color, + } + } + + fn set_scale(&mut self, scale: f64) -> bool { + if !scale.is_finite() || scale <= 0.0 { + return false; + } + self.options.scale = scale; + self.step = SweepStep::PickPath; + true + } + + fn set_reference_length(&mut self, length: f64) -> bool { + if !length.is_finite() || length <= 1e-12 { + return false; + } + self.reference_length = length; + self.step = SweepStep::NewLength; + true + } + + fn length_anchor(&self) -> DVec3 { + self.reference_start + .or_else(|| self.selection_options().and_then(|options| options.base_point)) + .unwrap_or(DVec3::ZERO) + } } impl CadCommand for SweepCommand { @@ -1210,37 +1305,301 @@ impl CadCommand for SweepCommand { } 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() - } + SweepStep::PickProfiles => t!("SWEEP Select objects to sweep or [Mode] (Enter to finish):").into_owned(), + SweepStep::Mode => format!( + "{} <{}>:", + t!("SWEEP Creation mode [Solid/Surface]"), + if self.mode == ExtrudeMode::Solid { "Solid" } else { "Surface" }, + ), + SweepStep::PickPath => t!("SWEEP Select sweep path or [Alignment/Base point/Scale/Twist]:").into_owned(), + SweepStep::Alignment => format!( + "{} <{}>:", + t!("SWEEP Align sweep object perpendicular to path [Yes/No]"), + if self.options.align { "Yes" } else { "No" }, + ), + SweepStep::BasePoint => t!("SWEEP Specify base point:").into_owned(), + SweepStep::Scale => format!( + "{} <{}>:", t!("SWEEP Enter scale factor or [Reference]"), self.options.scale, + ), + SweepStep::ReferenceLength => format!( + "{} <{}>:", + t!("SWEEP Specify reference length or first point"), + crate::entities::common::format_length(self.reference_length), + ), + SweepStep::ReferenceEnd => t!("SWEEP Specify second reference point:").into_owned(), + SweepStep::NewLength => format!( + "{} <{}>:", + t!("SWEEP Specify new length or [Points]"), + crate::entities::common::format_length(self.reference_length * self.options.scale), + ), + SweepStep::NewLengthStart => t!("SWEEP Specify first point of new length:").into_owned(), + SweepStep::NewLengthEnd => t!("SWEEP Specify second point of new length:").into_owned(), + SweepStep::Twist => format!( + "{} <{}>:", + t!("SWEEP Specify twist angle or [Bank]"), + crate::entities::common::format_angle(self.options.twist_angle), + ), } } - fn needs_entity_pick(&self) -> bool { - true + + fn options(&self) -> Vec { + match self.step { + SweepStep::PickProfiles => vec![CmdOption::new("Mode", "MODE"), CmdOption::enter("Done")], + SweepStep::Mode => vec![CmdOption::new("Solid", "SOLID"), CmdOption::new("Surface", "SURFACE")], + SweepStep::PickPath => vec![ + CmdOption::new("Alignment", "ALIGNMENT"), + CmdOption::new("Base point", "BASE"), + CmdOption::new("Scale", "SCALE"), + CmdOption::new("Twist", "TWIST"), + ], + SweepStep::Alignment => vec![CmdOption::new("Yes", "YES"), CmdOption::new("No", "NO")], + SweepStep::Scale => vec![CmdOption::new("Reference", "REFERENCE")], + SweepStep::NewLength => vec![CmdOption::new("Points", "POINTS")], + SweepStep::Twist => vec![CmdOption::new("Bank", "BANK")], + _ => Vec::new(), + } } - fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: DVec3) -> CmdResult { - if handle.is_null() { + + fn needs_entity_pick(&self) -> bool { + self.step == SweepStep::PickPath + } + + fn entity_pick_highlights_hover(&self) -> bool { + self.step == SweepStep::PickPath + } + + fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult { + if self.step != SweepStep::PickPath || handle.is_null() || self.contains_profile(handle) { + self.injected_path = None; + return CmdResult::NeedPoint; + } + let path = self.injected_path.take().or_else(|| { + self.hover_path.as_ref() + .filter(|(hovered, _)| *hovered == handle) + .map(|(_, entity)| entity.clone()) + }); + if path.as_ref().is_some_and(crate::scene::model::sweep_model::is_sweep_path) { + self.finish(handle) + } else { + CmdResult::NeedPoint + } + } + + fn on_point(&mut self, point: DVec3) -> CmdResult { + if !point.is_finite() { return CmdResult::NeedPoint; } match self.step { - SweepStep::PickProfile => { - self.profile_handle = handle; + SweepStep::BasePoint => { + self.options.base_point = Some(point); self.step = SweepStep::PickPath; - CmdResult::NeedPoint } - SweepStep::PickPath => CmdResult::SweepEntity { - profile_handle: self.profile_handle, - path_handle: handle, - color: self.color, - }, + SweepStep::ReferenceLength => { + self.reference_start = Some(point); + self.step = SweepStep::ReferenceEnd; + } + SweepStep::ReferenceEnd => { + if let Some(start) = self.reference_start { + self.set_reference_length(point.distance(start)); + } + } + SweepStep::NewLength => { + self.set_scale(point.distance(self.length_anchor()) / self.reference_length); + } + SweepStep::NewLengthStart => { + self.new_length_start = Some(point); + self.step = SweepStep::NewLengthEnd; + } + SweepStep::NewLengthEnd => { + if let Some(start) = self.new_length_start { + self.set_scale(point.distance(start) / self.reference_length); + } + } + _ => {} } - } - fn on_point(&mut self, _pt: DVec3) -> CmdResult { CmdResult::NeedPoint } + + fn wants_text_input(&self) -> bool { + matches!(self.step, + SweepStep::PickProfiles | SweepStep::Mode | SweepStep::PickPath + | SweepStep::Alignment | SweepStep::Scale | SweepStep::ReferenceLength + | SweepStep::NewLength | SweepStep::Twist + ) + } + + fn point_step_accepts_keywords(&self) -> bool { + matches!(self.step, SweepStep::PickPath | SweepStep::ReferenceLength | SweepStep::NewLength) + } + + fn on_text_input(&mut self, text: &str) -> Option { + let value = text.trim(); + if value.is_empty() { + return None; + } + let keyword = value.to_ascii_uppercase(); + match self.step { + SweepStep::PickProfiles if "MODE".starts_with(&keyword) => { + self.step = SweepStep::Mode; + } + SweepStep::Mode => { + if "SOLID".starts_with(&keyword) { + self.mode = ExtrudeMode::Solid; + } else if "SURFACE".starts_with(&keyword) { + self.mode = ExtrudeMode::Surface; + } else { + return Some(CmdResult::NeedPoint); + } + self.step = SweepStep::PickProfiles; + } + SweepStep::PickPath => { + self.step = if "ALIGNMENT".starts_with(&keyword) { + SweepStep::Alignment + } else if "BASE".starts_with(&keyword) || "BASE POINT".starts_with(&keyword) { + SweepStep::BasePoint + } else if "SCALE".starts_with(&keyword) { + SweepStep::Scale + } else if "TWIST".starts_with(&keyword) { + SweepStep::Twist + } else { + // Unconsumed text can be an object handle from automation. + return None; + }; + } + SweepStep::Alignment => { + if "YES".starts_with(&keyword) { + self.options.align = true; + } else if "NO".starts_with(&keyword) { + self.options.align = false; + } else { + return Some(CmdResult::NeedPoint); + } + self.step = SweepStep::PickPath; + } + SweepStep::Scale => { + if "REFERENCE".starts_with(&keyword) { + self.reference_start = None; + self.new_length_start = None; + self.step = SweepStep::ReferenceLength; + } else if let Ok(scale) = value.parse::() { + self.set_scale(scale); + } + } + SweepStep::ReferenceLength => { + if let Some(length) = crate::entities::common::parse_typed_length(value) { + self.set_reference_length(length); + } else { + return None; + } + } + SweepStep::NewLength => { + if "POINTS".starts_with(&keyword) { + self.step = SweepStep::NewLengthStart; + } else if let Some(length) = crate::entities::common::parse_typed_length(value) { + self.set_scale(length / self.reference_length); + } else { + return None; + } + } + SweepStep::Twist => { + if "BANK".starts_with(&keyword) { + self.options.bank = true; + self.step = SweepStep::PickPath; + } else if let Some(angle) = crate::entities::common::parse_angle(value) { + if angle.is_finite() { + self.options.twist_angle = angle; + self.options.bank = false; + self.step = SweepStep::PickPath; + } + } + } + _ => return None, + } + Some(CmdResult::NeedPoint) + } + fn on_enter(&mut self) -> CmdResult { - CmdResult::Cancel + match self.step { + SweepStep::PickProfiles if !self.profiles.is_empty() => { + self.step = SweepStep::PickPath; + } + SweepStep::Mode => self.step = SweepStep::PickProfiles, + SweepStep::Alignment | SweepStep::Scale | SweepStep::Twist + | SweepStep::NewLength => self.step = SweepStep::PickPath, + SweepStep::ReferenceLength => self.step = SweepStep::NewLength, + _ => return CmdResult::Cancel, + } + CmdResult::NeedPoint + } + + fn is_selection_gathering(&self) -> bool { + self.step == SweepStep::PickProfiles + } + + fn selection_forces_add(&self) -> bool { + self.step == SweepStep::PickProfiles + } + + fn inject_selection_entities(&mut self, entities: Vec) { + if self.step == SweepStep::PickProfiles { + self.set_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 == SweepStep::PickPath + } + + fn inject_picked_entity(&mut self, entity: EntityType) { + if self.step == SweepStep::PickPath { + self.injected_path = Some(entity); + } + } + + fn wants_hover_entity(&self, handle: Handle) -> bool { + self.step == SweepStep::PickPath + && !handle.is_null() + && !self.contains_profile(handle) + && self.hover_path.as_ref().is_none_or(|(hovered, _)| *hovered != handle) + } + + fn inject_hover_entity(&mut self, handle: Handle, entity: EntityType) { + self.hover_path = Some((handle, entity)); + self.preview_key = None; + } + + fn on_hover_entity(&mut self, handle: Handle, _point: DVec3) -> Vec { + if self.step != SweepStep::PickPath || handle.is_null() || self.contains_profile(handle) { + return Vec::new(); + } + let key = (handle, self.mode, self.options); + if self.preview_key == Some(key) { + return self.preview_cache.clone(); + } + self.preview_cache.clear(); + let Some((hovered, path)) = self.hover_path.as_ref() else { + return Vec::new(); + }; + if *hovered != handle { + return Vec::new(); + } + self.preview_key = Some(key); + if !crate::scene::model::sweep_model::is_sweep_path(path) { + return Vec::new(); + } + let Some(options) = self.selection_options() else { + return Vec::new(); + }; + self.preview_cache = self.profiles.iter().flat_map(|(_, profile)| { + crate::scene::model::sweep_model::swept_with_options(profile, path, self.mode, options) + .map(|body| preview_body_wires(&body, self.color, self.isolines)) + .unwrap_or_default() + }).collect(); + self.preview_cache.clone() } } diff --git a/src/scene/model/mod.rs b/src/scene/model/mod.rs index 0120d272f..64ba59c73 100644 --- a/src/scene/model/mod.rs +++ b/src/scene/model/mod.rs @@ -11,5 +11,6 @@ pub mod mesh_model; pub mod solid_model; pub mod solid_history; pub mod sweep_model; +mod sweep_command_model; pub mod visual_style_model; pub mod object; diff --git a/src/scene/model/solid_history.rs b/src/scene/model/solid_history.rs index b631f02b6..177360a87 100644 --- a/src/scene/model/solid_history.rs +++ b/src/scene/model/solid_history.rs @@ -212,7 +212,9 @@ pub fn reference_point(operation: &SolidHistoryOperation) -> Option } SolidHistoryOperation::Cone(value) => world_point(value.base.transform, [0.0; 3]), SolidHistoryOperation::Cylinder(value) => world_point(value.base.transform, [0.0; 3]), - SolidHistoryOperation::Sweep(value) | SolidHistoryOperation::Extrusion(value) => world_point( + SolidHistoryOperation::Sweep(value) => cadkernel::acis::sweep_history_reference_point(value) + .ok().map(glam::DVec3::from_array), + SolidHistoryOperation::Extrusion(value) => world_point( value.base.transform, [ value.reference_point.x, @@ -444,7 +446,7 @@ fn sweep_properties( let (show_history, show_history_editable) = displayed_history_state(object_show_history, show_history_mode); let show_value = if show_history { "Yes" } else { "No" }; - let length = sweep_path_length(value) + let length = cadkernel::acis::sweep_history_path_length(value).ok() .map(crate::entities::common::format_length) .unwrap_or_default(); vec![ @@ -497,7 +499,7 @@ fn sweep_properties( field: PROP_HISTORY, value: PropValue::Choice { selected: if record_history { "Record" } else { "None" }.to_string(), - options: vec!["None".to_string(), "Record".to_string()], + options: vec!["Record".to_string(), "None".to_string()], }, }, Property { @@ -506,7 +508,7 @@ fn sweep_properties( value: if show_history_editable { PropValue::Choice { selected: show_value.to_string(), - options: vec!["No".to_string(), "Yes".to_string()], + options: vec!["Yes".to_string(), "No".to_string()], } } else { PropValue::ReadOnly(show_value.to_string()) @@ -2179,6 +2181,7 @@ fn embedded_entity(value: &EmbeddedEntity) -> Option { EmbeddedEntity::Ellipse(value) => EntityType::Ellipse(value.clone()), EmbeddedEntity::Spline(value) => EntityType::Spline(value.clone()), EmbeddedEntity::LwPolyline(value) => EntityType::LwPolyline(value.clone()), + EmbeddedEntity::Region(value) => EntityType::Region(value.clone()), EmbeddedEntity::Ray(value) => EntityType::Ray(value.clone()), EmbeddedEntity::XLine(value) => EntityType::XLine(value.clone()), EmbeddedEntity::Unknown { .. } => return None, @@ -2194,6 +2197,7 @@ fn into_embedded_entity(value: EntityType) -> Option { EntityType::Ellipse(value) => EmbeddedEntity::Ellipse(value), EntityType::Spline(value) => EmbeddedEntity::Spline(value), EntityType::LwPolyline(value) => EmbeddedEntity::LwPolyline(value), + EntityType::Region(value) => EmbeddedEntity::Region(value), EntityType::Ray(value) => EmbeddedEntity::Ray(value), EntityType::XLine(value) => EmbeddedEntity::XLine(value), _ => return None, @@ -2288,21 +2292,27 @@ fn apply_extrusion_profile_grip( true } +fn sweep_placement_matrices(value: &SolidHistorySweep) -> Option<(glam::DMat4, glam::DMat4)> { + let (profile, path) = cadkernel::acis::sweep_history_placements(value).ok()?; + let matrix = |place: cadkernel::brep::Placement| { + glam::DMat4::from_cols( + glam::DVec3::from_array(place.x_axis).extend(0.0), + glam::DVec3::from_array(place.y_axis).extend(0.0), + glam::DVec3::from_array(place.z_axis).extend(0.0), + glam::DVec3::from_array(place.origin).extend(1.0), + ) + }; + Some((matrix(profile), matrix(path))) +} + fn sweep_embedded_grips( value: Option<&EmbeddedEntity>, - base_transform: [f64; 16], - entity_transform: [f64; 16], + transform: glam::DMat4, first_id: usize, ) -> Vec { let Some(entity) = value.and_then(embedded_entity) else { return Vec::new(); }; - let (Some(base), Some(entity_placement)) = - (matrix(base_transform), matrix(entity_transform)) - else { - return Vec::new(); - }; - let transform = base * entity_placement; entity .grips() .into_iter() @@ -2330,8 +2340,7 @@ fn sweep_embedded_grips( fn apply_sweep_embedded_grip( value: &mut Option, - base_transform: [f64; 16], - entity_transform: [f64; 16], + transform: glam::DMat4, index: usize, apply: GripApply, ) -> bool { @@ -2341,12 +2350,7 @@ fn apply_sweep_embedded_grip( let Some(source_grip) = entity.grips().get(index).cloned() else { return false; }; - let (Some(base), Some(entity_placement)) = - (matrix(base_transform), matrix(entity_transform)) - else { - return false; - }; - let inverse = (base * entity_placement).inverse(); + let inverse = transform.inverse(); if !inverse.is_finite() { return false; } @@ -2891,18 +2895,14 @@ pub fn primitive_grips( ); } SolidHistoryOperation::Sweep(value) => { - grips.extend(sweep_embedded_grips( - value.sweep_entity.as_ref(), - value.base.transform, - value.sweep_entity_transform, - GRIP_SWEEP_PROFILE_FIRST, - )); - grips.extend(sweep_embedded_grips( - value.path_entity.as_ref(), - value.base.transform, - value.path_entity_transform, - GRIP_SWEEP_PATH_FIRST, - )); + if let Some((profile, path)) = sweep_placement_matrices(value) { + grips.extend(sweep_embedded_grips( + value.sweep_entity.as_ref(), profile, GRIP_SWEEP_PROFILE_FIRST, + )); + grips.extend(sweep_embedded_grips( + value.path_entity.as_ref(), path, GRIP_SWEEP_PATH_FIRST, + )); + } } SolidHistoryOperation::Loft(value) => grips.extend(loft_section_grips(value)), SolidHistoryOperation::Revolve(value) => { @@ -2962,11 +2962,13 @@ pub fn apply_primitive_grip( } } if let SolidHistoryOperation::Sweep(value) = operation { + let Some((profile, path)) = sweep_placement_matrices(value) else { + return false; + }; if let Some(index) = grip_id.checked_sub(GRIP_SWEEP_PATH_FIRST) { return apply_sweep_embedded_grip( &mut value.path_entity, - value.base.transform, - value.path_entity_transform, + path, index, apply, ); @@ -2977,8 +2979,7 @@ pub fn apply_primitive_grip( { return apply_sweep_embedded_grip( &mut value.sweep_entity, - value.base.transform, - value.sweep_entity_transform, + profile, index, apply, ); diff --git a/src/scene/model/sweep_command_model.rs b/src/scene/model/sweep_command_model.rs new file mode 100644 index 000000000..2361b7c3c --- /dev/null +++ b/src/scene/model/sweep_command_model.rs @@ -0,0 +1,113 @@ +//! SWEEP command data mapped to the shared kernel history reconstruction. + +use acadrust::entities::{Surface, SurfaceData, SurfaceKind, SurfaceSweepOptions}; +use acadrust::objects::{SolidHistoryNodeBase, SolidHistorySweep}; +use acadrust::types::Vector3; +use acadrust::EntityType; +use cadkernel::brep::Body; + +use crate::command::{ExtrudeMode, SweepOptions}; +use super::sweep_model::{embedded_path, embedded_revolve_profile}; + +fn embedded_sweep_profile(entity: &EntityType) -> Option<(acadrust::entities::EmbeddedEntity, [f64; 16])> { + if let EntityType::Region(region) = entity { + Some((acadrust::entities::EmbeddedEntity::Region(region.clone()), glam::DMat4::IDENTITY.to_cols_array())) + } else { + embedded_revolve_profile(entity) + } +} + +pub fn is_sweep_profile(entity: &EntityType) -> bool { + embedded_sweep_profile(entity).is_some_and(|(profile, transform)| { + cadkernel::acis::sweep_profile_geometry(&profile, transform).is_ok() + }) +} + +pub fn is_sweep_path(entity: &EntityType) -> bool { + match embedded_path(entity) { + Some(acadrust::entities::EmbeddedEntity::Spline(value)) => { + value.degree > 0 && (value.control_points.len() > value.degree as usize + || value.fit_points.len() >= 2) + } + Some(_) => crate::entities::curve::entity_curve(entity) + .is_some_and(|curve| curve.curve.length().is_finite() && curve.curve.length() > 1e-9), + None => false, + } +} + +/// All selected profiles use one base point, preserving their relative offsets. +pub fn sweep_selection_options(profiles: &[EntityType], mut options: SweepOptions) -> Option { + if options.base_point.is_some() { + return Some(options); + } + let geometry = profiles.iter().map(|profile| { + let (entity, transform) = embedded_sweep_profile(profile)?; + let (plane, wires, _) = cadkernel::acis::sweep_profile_geometry(&entity, transform).ok()?; + Some((plane, wires)) + }).collect::>>()?; + options.base_point = Some(glam::DVec3::from_array( + cadkernel::brep::sweep_profile_group_base(&geometry)?, + )); + Some(options) +} + +pub fn sweep_record(profile: &EntityType, path: &EntityType, options: SweepOptions) -> Option { + let (sweep_entity, sweep_entity_transform) = embedded_sweep_profile(profile)?; + let (plane, wires, _) = cadkernel::acis::sweep_profile_geometry(&sweep_entity, sweep_entity_transform).ok()?; + let base_point = match options.base_point { + Some(point) => point.to_array(), + None => cadkernel::brep::sweep_profile_base(plane, &wires)?, + }; + let mut base = SolidHistoryNodeBase::new(1); + base.transform = glam::DMat4::IDENTITY.to_cols_array(); + Some(SolidHistorySweep { + base, + operation_major: 1, + sweep_entity: Some(sweep_entity), + path_entity: Some(embedded_path(path)?), + scale_factor: options.scale, + twist_angle: options.twist_angle, + align_option: u8::from(options.align), + has_align_start: true, + bank: options.bank, + sweep_entity_transform, + path_entity_transform: glam::DMat4::IDENTITY.to_cols_array(), + reference_point: Vector3::new(base_point[0], base_point[1], base_point[2]), + ..SolidHistorySweep::default() + }) +} + +pub fn swept_with_options(profile: &EntityType, path: &EntityType, mode: ExtrudeMode, options: SweepOptions) -> Option { + let record = sweep_record(profile, path, options)?; + cadkernel::acis::rebuild_sweep_with_mode(&record, mode == ExtrudeMode::Surface).ok() +} + +/// Preserve native construction parameters alongside the sheet's saved B-rep. +pub fn swept_surface_entity(record: &SolidHistorySweep) -> EntityType { + let mut surface = Surface::new(SurfaceKind::Swept); + if let Ok(point) = cadkernel::acis::sweep_history_reference_point(record) { + surface.point_of_reference = Vector3::new(point[0], point[1], point[2]); + } + surface.surface_data = SurfaceData::Swept { + class_version: 0, + sweep_entity: record.sweep_entity.clone(), + path_entity: record.path_entity.clone(), + sweep_transform: glam::DMat4::IDENTITY.to_cols_array(), + path_transform: glam::DMat4::IDENTITY.to_cols_array(), + options: SurfaceSweepOptions { + draft_angle: record.draft_angle, + twist_angle: record.twist_angle, + scale_factor: record.scale_factor, + align_angle: record.align_angle, + sweep_entity_transform: record.sweep_entity_transform, + path_entity_transform: record.path_entity_transform, + sweep_alignment_flags: record.align_option as i16, + align_start: record.has_align_start, + bank: record.bank, + base_point_set: true, + reference_vector: record.reference_point, + ..SurfaceSweepOptions::default() + }, + }; + EntityType::Surface(surface) +} diff --git a/src/scene/model/sweep_model.rs b/src/scene/model/sweep_model.rs index 6979b9c2e..8e8a5682f 100644 --- a/src/scene/model/sweep_model.rs +++ b/src/scene/model/sweep_model.rs @@ -12,6 +12,9 @@ use acadrust::types::{Vector2, Vector3}; use acadrust::EntityType; use crate::entities::curve::entity_curve; +pub use super::sweep_command_model::{ + is_sweep_path, is_sweep_profile, sweep_record, sweep_selection_options, swept_surface_entity, swept_with_options, +}; /// A drawn profile, as the kernel wants it: the plane it lies in and the /// chain of pieces closing a loop in that plane. @@ -591,18 +594,32 @@ pub fn embedded_path(entity: &EntityType) -> Option { EntityType::Circle(value) => Some(EmbeddedEntity::Circle(value.clone())), EntityType::Ellipse(value) => Some(EmbeddedEntity::Ellipse(value.clone())), EntityType::LwPolyline(value) => Some(EmbeddedEntity::LwPolyline(value.clone())), + EntityType::Polyline2D(value) => { + let mut polyline = LwPolyline::new(); + polyline.elevation = value.elevation; + polyline.normal = value.normal; + polyline.is_closed = value.is_closed(); + polyline.vertices = value.vertices.iter().map(|vertex| { + let mut converted = LwVertex::new(Vector2::new(vertex.location.x, vertex.location.y)); + converted.bulge = vertex.bulge; + converted + }).collect(); + Some(EmbeddedEntity::LwPolyline(polyline)) + } EntityType::Spline(value) => Some(EmbeddedEntity::Spline(value.clone())), - EntityType::Polyline3D(value) if !value.is_closed() && value.vertices.len() >= 2 => { + EntityType::Helix(value) => Some(EmbeddedEntity::Spline(value.spline.clone())), + EntityType::Polyline3D(value) if value.vertices.len() >= 2 => { + let mut points = value.vertices.iter().map(|vertex| vertex.position).collect::>(); + if value.is_closed() && points.first() != points.last() { + points.push(*points.first()?); + } let mut spline = Spline::from_control_points( 1, - value - .vertices - .iter() - .map(|vertex| vertex.position) - .collect(), + points, ); spline.flags.linear = true; spline.flags.planar = false; + spline.flags.closed = value.is_closed(); Some(EmbeddedEntity::Spline(spline)) } _ => None,