diff --git a/src/app/automation.rs b/src/app/automation.rs index 876fba186..1e4989cfb 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -200,6 +200,42 @@ fn serve_socket_with_idle( } } +/// The entities inside block definitions: owned by (or listed in) a block +/// record that is not a layout's model or paper space. +fn definition_members(document: &codec::CadDocument) -> std::collections::HashSet { + let mut layouts: std::collections::HashSet = document + .objects + .values() + .filter_map(|object| match object { + codec::objects::ObjectType::Layout(layout) if !layout.block_record.is_null() => Some(layout.block_record), + _ => None, + }) + .collect(); + layouts.extend(document.block_records.iter().filter_map(|record| { + let name = record.name.to_ascii_uppercase(); + (name.starts_with("*MODEL_SPACE") || name.starts_with("*PAPER_SPACE")).then_some(record.handle) + })); + let definitions: std::collections::HashSet = document + .block_records + .iter() + .filter(|record| !layouts.contains(&record.handle)) + .map(|record| record.handle) + .collect(); + let mut members: std::collections::HashSet = document + .block_records + .iter() + .filter(|record| definitions.contains(&record.handle)) + .flat_map(|record| record.entity_handles.iter().copied()) + .collect(); + members.extend( + document + .entities() + .filter(|e| definitions.contains(&e.common().owner_handle)) + .map(|e| e.common().handle), + ); + members +} + fn err(msg: impl std::fmt::Display) -> Value { json!({ "ok": false, "error": msg.to_string() }) } @@ -794,30 +830,58 @@ impl OpenCADStudio { } "select" => { let i = self.active_tab; + // By explicit handles (hex, as returned by `query`). An entity + // inside a block definition is not selectable on its own. + let explicit: Vec = req["handles"] + .as_array() + .map(|arr| { + arr.iter() + .filter_map(|h| h.as_str()) + .filter_map(|h| { + let h = h.strip_prefix("0x").or_else(|| h.strip_prefix("0X")).unwrap_or(h); + u64::from_str_radix(h, 16).ok().map(codec::Handle::new) + }) + .collect() + }) + .unwrap_or_default(); + if req["clear"].as_bool() != Some(true) { + let inside = definition_members(&self.tabs[i].scene.document); + let refused: Vec = explicit + .iter() + .filter(|h| inside.contains(h)) + .map(|h| format!("{:X}", h.value())) + .collect(); + if !refused.is_empty() { + return err(format!( + "select: inside a block definition, not selectable: {}", + refused.join(", ") + )); + } + } self.tabs[i].scene.deselect_all(); if req["clear"].as_bool() != Some(true) { - // By explicit handles (hex, as returned by `query`). - if let Some(arr) = req["handles"].as_array() { - for h in arr.iter().filter_map(|h| h.as_str()) { - let h = h - .strip_prefix("0x") - .or_else(|| h.strip_prefix("0X")) - .unwrap_or(h); - if let Ok(v) = u64::from_str_radix(h, 16) { - self.tabs[i] - .scene - .select_entity(codec::Handle::new(v), false); - } - } + for h in explicit { + self.tabs[i].scene.select_entity(h, false); } // Or by type / layer. let type_filter = req["type"].as_str(); let layer_filter = req["layer"].as_str(); if type_filter.is_some() || layer_filter.is_some() { - let handles: Vec = self.tabs[i] - .scene + // Only what the current space draws: an entity inside a + // block definition is not selectable on its own. + let scene = &self.tabs[i].scene; + let space = scene.current_layout_block_handle_pub(); + let listed: std::collections::HashSet = scene + .document + .block_records + .iter() + .find(|record| record.handle == space) + .map(|record| record.entity_handles.iter().copied().collect()) + .unwrap_or_default(); + let handles: Vec = scene .document .entities() + .filter(|e| e.common().owner_handle == space || listed.contains(&e.common().handle)) .filter(|e| type_filter.is_none_or(|t| entity_type_matches(e, t))) .filter(|e| layer_filter.is_none_or(|l| e.common().layer == l)) .map(|e| e.common().handle) diff --git a/src/app/command_driver/utilities.rs b/src/app/command_driver/utilities.rs index 943c3630d..84ee63d47 100644 --- a/src/app/command_driver/utilities.rs +++ b/src/app/command_driver/utilities.rs @@ -798,9 +798,14 @@ impl OpenCADStudio { { return self.dispatch_command(cmd); } + // Close an earlier step first so only what the probe opens is dropped below. + self.finish_pending_history(i); let start_task = self.dispatch_command(tokens[0]); if self.tabs[i].active_cmd.is_none() { // Not an interactive tool — an inline-argument command (`PDMODE 3`). + // The bare word only probed it: a step it kept without changing + // anything is not an undo step of its own. + self.drop_unchanged_pending_history(i); return self.dispatch_command(cmd); } let toks: Vec = tokens.iter().map(|s| s.to_string()).collect(); diff --git a/src/app/history.rs b/src/app/history.rs index ffe243c3c..3510ea040 100644 --- a/src/app/history.rs +++ b/src/app/history.rs @@ -341,6 +341,15 @@ impl OpenCADStudio { self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta))); } + /// Finish the pending history step, dropping it when it changed nothing + /// even if it was opened as a mark ([`Self::push_undo_mark`]). + pub(super) fn drop_unchanged_pending_history(&mut self, i: usize) { + if let Some(pending) = self.tabs[i].history.pending.as_mut() { + pending.keep_empty = false; + } + self.finish_pending_history(i); + } + pub(super) fn finish_all_pending_history(&mut self) { for i in 0..self.tabs.len() { self.finish_pending_history(i);