diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index fe427276b..c44add7b9 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -645,46 +645,96 @@ impl OpenCADStudio { // ── SETBYLAYER — clear color/linetype/lineweight overrides ──── // Resets the selected entities' direct property overrides back to // ByLayer so they follow their layer again. + "SETBYLAYERMODE" => { + let command = crate::modules::draw::modify::setbylayer::ModeCommand; + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } + value if value.starts_with("SETBYLAYERMODE ") => { + if let Ok(mode) = value.trim_start_matches("SETBYLAYERMODE ").trim().parse::() { + crate::modules::draw::modify::setbylayer::set_mode(mode); + } else { self.command_line.push_error("SETBYLAYERMODE requires an integer from 0 to 255."); } + } "SETBYLAYER" => { - if self.tabs[i].scene.selected.is_empty() { - use crate::modules::draw::select::SelectObjectsCommand; - let selection = SelectObjectsCommand::new("SETBYLAYER"); - self.command_line.push_info(&selection.prompt()); - self.tabs[i].active_cmd = Some(Box::new(selection)); - return Some(self.finish_dispatch(cmd)); + let command = crate::modules::draw::modify::setbylayer::SetByLayerCommand::new( + self.tabs[i].scene.selected.iter().copied().collect(), + ); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } + cmd if cmd.starts_with("SETBYLAYER_APPLY ") => { + let flags: Vec<_> = cmd.split_whitespace().skip(1).collect(); + if flags.len() != 2 || flags.iter().any(|flag| !matches!(*flag, "0" | "1")) { + return Some(Task::none()); } - let handles: Vec<_> = self.tabs[i] + let mask = crate::modules::draw::modify::setbylayer::mode(); + let change_byblock = flags[0] == "1"; + let include_blocks = flags[1] == "1"; + let mut handles: Vec<_> = self.tabs[i] .scene .selected_entities() .into_iter() .map(|(h, _)| h) .filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle)) .collect(); + if include_blocks { + let mut visited: std::collections::HashSet<_> = handles.iter().copied().collect(); + let mut index = 0; + while index < handles.len() { + let children = match self.tabs[i].scene.document.get_entity(handles[index]) { + Some(acadrust::EntityType::Insert(insert)) => self.tabs[i].scene.document + .block_records.get(&insert.block_name) + .map(|block| block.entity_handles.clone()).unwrap_or_default(), + _ => Vec::new(), + }; + for child in children { + if visited.insert(child) && !self.tabs[i].scene.is_layer_locked(child) { + handles.push(child); + } + } + index += 1; + } + } if handles.is_empty() { self.command_line .push_error(crate::t!("SETBYLAYER: select entities first.").as_ref()); } else { + let has_changes = handles.iter().any(|handle| { + self.tabs[i].scene.document.get_entity(*handle).is_some_and(|entity| { + let mut common = entity.common().clone(); + crate::modules::draw::modify::setbylayer::apply_mask( + &mut common, mask, change_byblock, + ) + }) + }); + if !has_changes { + self.command_line.push_output( + crate::t!("SETBYLAYER: no properties required changes.").as_ref(), + ); + return Some(Task::none()); + } self.push_undo_snapshot(i, "SETBYLAYER"); - let mut changed = 0usize; + let mut changed = Vec::new(); for handle in &handles { if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(*handle) { - let common = entity.common_mut(); - common.color = acadrust::types::Color::ByLayer; - common.color_name = None; - common.color_book_handle = None; - common.linetype = "ByLayer".to_string(); - common.line_weight = acadrust::types::LineWeight::ByLayer; - changed += 1; + if crate::modules::draw::modify::setbylayer::apply_mask( + entity.common_mut(), mask, change_byblock, + ) { + changed.push(*handle); + } } } self.tabs[i].dirty = true; - let changes: Vec<_> = handles - .into_iter() + let changes: Vec<_> = changed + .iter() + .copied() .map(|handle| (handle, crate::scene::ChangeKind::Modified)) .collect(); self.tabs[i].scene.bump_entities(&changes); + self.refresh_properties(); self.command_line.push_output(crate::tf!( - "SETBYLAYER: reset {changed} entity/entities to ByLayer." + "SETBYLAYER: reset {} entity/entities to ByLayer.", + changed.len() ).as_ref()); } } diff --git a/src/modules/draw/modify/mod.rs b/src/modules/draw/modify/mod.rs index 4964db414..651e4df12 100644 --- a/src/modules/draw/modify/mod.rs +++ b/src/modules/draw/modify/mod.rs @@ -21,6 +21,7 @@ pub mod refedit; pub mod reverse; pub mod rotate; pub mod scale; +pub mod setbylayer; pub mod spline_ops; pub mod splinedit; pub mod stretch; diff --git a/src/modules/draw/modify/setbylayer.rs b/src/modules/draw/modify/setbylayer.rs new file mode 100644 index 000000000..d266ab542 --- /dev/null +++ b/src/modules/draw/modify/setbylayer.rs @@ -0,0 +1,153 @@ +use acadrust::Handle; +use glam::DVec3; +use crate::command::{CadCommand, CmdOption, CmdResult}; + +static MODE: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(255); +pub fn mode() -> u8 { MODE.load(std::sync::atomic::Ordering::Relaxed) } +pub fn set_mode(value: u8) { MODE.store(value, std::sync::atomic::Ordering::Relaxed); } + +pub fn apply_mask(common: &mut acadrust::entities::EntityCommon, mask: u8, change_byblock: bool) -> bool { + let before = common.clone(); + if mask & 1 != 0 && (change_byblock || common.color != acadrust::types::Color::ByBlock) { + common.color = acadrust::types::Color::ByLayer; + common.color_name = None; + common.color_book_handle = None; + } + if mask & 2 != 0 && (change_byblock || !common.linetype.eq_ignore_ascii_case("ByBlock")) { + common.linetype = "ByLayer".to_string(); + common.linetype_handle = None; + } + if mask & 4 != 0 && (change_byblock || common.line_weight != acadrust::types::LineWeight::ByBlock) { + common.line_weight = acadrust::types::LineWeight::ByLayer; + } + if mask & 8 != 0 && (change_byblock || common.material_flags != 1) { + common.material_flags = 0; + common.material_handle = None; + } + if mask & 16 != 0 && (change_byblock || common.plotstyle_flags != 1) { + common.plotstyle_flags = 0; + common.plotstyle_handle = None; + } + if mask & 128 != 0 && (change_byblock || common.transparency != acadrust::types::Transparency::BY_BLOCK) { + common.transparency = acadrust::types::Transparency::BY_LAYER; + } + *common != before +} + +enum Step { Selection, ByBlock, Blocks } + +pub struct SetByLayerCommand { + selected: Vec, + step: Step, + change_byblock: bool, + include_blocks: bool, +} + +impl SetByLayerCommand { + pub fn new(selected: Vec) -> Self { + let step = if selected.is_empty() { Step::Selection } else { Step::ByBlock }; + Self { selected, step, change_byblock: mode() & 32 != 0, include_blocks: mode() & 64 != 0 } + } + + fn answer(&mut self, yes: bool) -> CmdResult { + match self.step { + Step::ByBlock => { + self.change_byblock = yes; + self.step = Step::Blocks; + CmdResult::NeedPoint + } + Step::Blocks => { + let flags = (mode() & !(32 | 64)) | if self.change_byblock { 32 } else { 0 } | if yes { 64 } else { 0 }; + set_mode(flags); + CmdResult::Relaunch( + format!("SETBYLAYER_APPLY {} {}", u8::from(self.change_byblock), u8::from(yes)), + self.selected.clone(), + ) }, + Step::Selection => CmdResult::NeedPoint, + } + } +} + +impl CadCommand for SetByLayerCommand { + fn name(&self) -> &'static str { "SETBYLAYER" } + fn prompt(&self) -> String { + match self.step { + Step::Selection => "Select objects:".into(), + Step::ByBlock => format!("Change ByBlock to ByLayer? [Yes/No] <{}>:", if self.change_byblock { "Yes" } else { "No" }), + Step::Blocks => format!("Include blocks? [Yes/No] <{}>:", if self.include_blocks { "Yes" } else { "No" }), + } + } + fn is_selection_gathering(&self) -> bool { matches!(self.step, Step::Selection) } + fn on_selection_complete(&mut self, handles: Vec) -> CmdResult { + self.selected = handles; + CmdResult::NeedPoint + } + fn options(&self) -> Vec { + if self.is_selection_gathering() { Vec::new() } + else { vec![CmdOption::new("Yes", "Y"), CmdOption::new("No", "N")] } + } + fn wants_text_input(&self) -> bool { !self.is_selection_gathering() } + fn on_point(&mut self, _: DVec3) -> CmdResult { CmdResult::NeedPoint } + fn on_enter(&mut self) -> CmdResult { + if self.is_selection_gathering() { + if self.selected.is_empty() { CmdResult::Cancel } + else { self.step = Step::ByBlock; CmdResult::NeedPoint } + } else { self.answer(if matches!(self.step, Step::ByBlock) { self.change_byblock } else { self.include_blocks }) } + } + fn on_text_input(&mut self, text: &str) -> Option { + Some(match text.trim().to_uppercase().as_str() { + "Y" | "YES" => self.answer(true), + "N" | "NO" => self.answer(false), + _ => CmdResult::NeedPoint, + }) + } +} + +pub struct ModeCommand; +impl CadCommand for ModeCommand { + fn name(&self) -> &'static str { "SETBYLAYERMODE" } + fn prompt(&self) -> String { format!("Enter new value for SETBYLAYERMODE <{}>:", mode()) } + fn wants_text_input(&self) -> bool { true } + fn on_point(&mut self, _: DVec3) -> CmdResult { CmdResult::NeedPoint } + fn on_enter(&mut self) -> CmdResult { CmdResult::Cancel } + fn on_text_input(&mut self, text: &str) -> Option { + Some(if let Ok(value) = text.trim().parse::() { + set_mode(value); CmdResult::Cancel + } else { CmdResult::NeedPoint }) + } +} +inventory::submit!(crate::command::CommandRegistration { names: &["SETBYLAYERMODE"] }); + +#[cfg(test)] +mod tests { + use super::*; + use acadrust::entities::EntityCommon; + use acadrust::types::{Color, Transparency}; + + #[test] + fn mask_changes_only_requested_properties() { + let mut common = EntityCommon::default(); + common.color = Color::from_index(3); + common.linetype = "DASHED".into(); + common.transparency = Transparency::from_percent(40.0); + + assert!(apply_mask(&mut common, 1 | 128, false)); + assert_eq!(common.color, Color::ByLayer); + assert_eq!(common.linetype, "DASHED"); + assert_eq!(common.transparency, Transparency::BY_LAYER); + } + + #[test] + fn byblock_values_are_kept_unless_requested() { + let mut common = EntityCommon::default(); + common.color = Color::ByBlock; + common.linetype = "ByBlock".into(); + common.transparency = Transparency::BY_BLOCK; + + assert!(!apply_mask(&mut common, 1 | 2 | 128, false)); + assert!(apply_mask(&mut common, 1 | 2 | 128, true)); + assert_eq!(common.color, Color::ByLayer); + assert_eq!(common.linetype, "ByLayer"); + assert_eq!(common.transparency, Transparency::BY_LAYER); + } +}