//! REFEDIT, -REFEDIT, REFSET and REFCLOSE: in-place editing of a block //! reference or an external reference, with the reference's prompts and //! messages. The session itself lives in `DocumentTab::refedit_session`. use crate::app::{Message, OpenCADStudio}; use crate::modules::draw::modify::refedit::{ apply_insert_inverse_transform, apply_insert_transform, level_hits, nesting_chain, RefCloseCommand, RefEditOptionsCommand, RefEditPickCommand, RefEditSession, RefSetCommand, NOT_CHECKED_OUT, }; use codec::{EntityType, Handle}; use iced::Task; use std::collections::HashSet; /// `** Command not allowed, NAME already checked out for editing **` fn already_checked_out(name: &str) -> String { crate::tf!("** Command not allowed, {} already checked out for editing **", name).into_owned() } impl OpenCADStudio { pub(super) fn dispatch_refedit(&mut self, cmd: &str, i: usize) -> Option> { match cmd { "REFEDIT" | "-REFEDIT" => Some(self.start_refedit(i, cmd == "-REFEDIT")), "REFSET" => { if self.tabs[i].refedit_session.is_none() { self.command_line.push_error(crate::t!(NOT_CHECKED_OUT).as_ref()); return Some(Task::none()); } self.command_line.push_info( crate::t!("Transfer objects between the RefEdit working set and host drawing...") .as_ref(), ); let command = RefSetCommand::new(); use crate::command::CadCommand; self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); Some(Task::none()) } "REFCLOSE" => { if self.tabs[i].refedit_session.is_none() { self.command_line.push_error(crate::t!(NOT_CHECKED_OUT).as_ref()); return Some(Task::none()); } let command = RefCloseCommand::new(); use crate::command::CadCommand; self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); Some(Task::none()) } "REFCLOSE_SAVE" => { self.close_refedit(i, true); Some(Task::none()) } "REFCLOSE_DISCARD" => { self.close_refedit(i, false); Some(Task::none()) } _ if cmd.starts_with("REFEDIT_PICKED:") => { self.refedit_picked(i, &cmd["REFEDIT_PICKED:".len()..]); Some(Task::none()) } _ if cmd.starts_with("REFEDIT_BEGIN:") => { self.begin_refedit(i, &cmd["REFEDIT_BEGIN:".len()..]); Some(Task::none()) } _ if cmd.starts_with("REFSET_APPLY:") => { self.apply_refset(i, cmd.ends_with("ADD")); Some(Task::none()) } _ => None, } } /// Block references and xrefs of the current space. fn refedit_references(&self, i: usize) -> HashSet { let space = self.tabs[i].scene.current_layout_block_handle_pub(); self.tabs[i] .scene .document .entities() .filter(|e| matches!(e, EntityType::Insert(_)) && e.common().owner_handle == space) .map(|e| e.common().handle) .collect() } fn start_refedit(&mut self, i: usize, command_line: bool) -> Task { if let Some(session) = &self.tabs[i].refedit_session { let message = already_checked_out(&session.block_name); self.command_line.push_error(&message); return Task::none(); } let references = self.refedit_references(i); // A single preselected reference is taken as the pick. let selected: Vec = self.tabs[i] .scene .selected_entities() .into_iter() .map(|(h, _)| h) .collect(); if let [handle] = selected[..] { if references.contains(&handle) { self.tabs[i].scene.deselect_all(); self.refedit_picked( i, &format!("{}:{}", command_line as u8, handle.value()), ); return Task::none(); } } use crate::command::CadCommand; let command = RefEditPickCommand::new(command_line, references); self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); Task::none() } /// `cli:handle[:x:y:z]` — the reference was picked (at the point). fn refedit_picked(&mut self, i: usize, args: &str) { let parts: Vec<&str> = args.split(':').collect(); let command_line = parts.first() == Some(&"1"); let Some(top) = parts.get(1).and_then(|v| v.parse::().ok()).map(Handle::new) else { return; }; let point = (parts.len() >= 5) .then(|| { Some(glam::DVec3::new( parts[2].parse().ok()?, parts[3].parse().ok()?, parts[4].parse().ok()?, )) }) .flatten(); self.tabs[i].scene.deselect_all(); let chain = nesting_chain(&self.tabs[i].scene.document, top, point); if chain.is_empty() { self.command_line .push_error(crate::t!("Object is not a valid reference").as_ref()); return; } if command_line { let hits = chain .iter() .map(|level| level_hits(&self.tabs[i].scene.document, level)) .collect(); let command = RefEditOptionsCommand::new(top, chain, hits); use crate::command::CadCommand; self.command_line.push_info(&command.prompt()); self.tabs[i].active_cmd = Some(Box::new(command)); } else { // The dialog's defaults: the innermost reference, all objects, // attribute definitions not displayed. let path: Vec = chain[1..] .iter() .map(|l| l.insert.value().to_string()) .collect(); let _ = self.dispatch_command(&format!( "REFEDIT_BEGIN:{}:{}:0:", top.value(), path.join(",") )); } } /// `top[:path[:attributes[:nested]]]` — open the session. fn begin_refedit(&mut self, i: usize, args: &str) { let mut parts = args.split(':'); let Some(top) = parts .next() .and_then(|v| v.parse::().ok()) .map(Handle::new) else { return; }; let handles = |s: Option<&str>| -> Vec { s.unwrap_or("") .split(',') .filter_map(|v| v.parse::().ok()) .map(Handle::new) .collect() }; let path = handles(parts.next()); let attributes = parts.next() == Some("1"); let nested = handles(parts.next()); if self.tabs[i].refedit_session.is_some() || self.reject_locked_edit(i, top) { return; } let document = &self.tabs[i].scene.document; let mut chain = nesting_chain(document, top, None); if chain.is_empty() { self.command_line .push_error(crate::t!("Object is not a valid reference").as_ref()); return; } // Follow the stored path of definition-owned references. for handle in path { let Some(EntityType::Insert(inner)) = document.get_entity(handle) else { break; }; let base = crate::scene::render_graph::block_base_point(document, &inner.block_name); let local = crate::scene::render_graph::insert_transform_with_base(inner, base); let outer = chain.last().expect("non-empty chain").forward.clone(); chain.push(crate::modules::draw::modify::refedit::ChainLevel { insert: handle, block_name: inner.block_name.clone(), forward: local.then(&outer), }); } let level = chain.last().expect("non-empty chain").clone(); let Some(br) = document.block_records.get(&level.block_name) else { return; }; let br_handle = br.handle; let xref_path = (br.flags.is_xref || br.flags.is_xref_overlay) .then(|| br.xref_path.clone()) .filter(|p| !p.is_empty()); let extracted: Vec = br .entity_handles .iter() .copied() .filter(|h| match document.get_entity(*h) { Some(EntityType::Block(_) | EntityType::BlockEnd(_)) | None => false, Some(EntityType::AttributeDefinition(_)) => attributes, Some(_) => nested.is_empty() || nested.contains(h), }) .collect(); let Some(inverse) = crate::modules::draw::modify::refedit::inverse_placement(&level.forward) else { return; }; let session = RefEditSession { block_name: level.block_name.clone(), br_handle, temp_handles: Vec::new(), extracted: extracted.clone(), added: HashSet::new(), removed: HashSet::new(), forward: level.forward.clone(), inverse, // Everything allocated from here on is part of the session. handle_watermark: document.next_handle().max(document.header.handle_seed), xref_path, }; let copies: Vec = extracted .iter() .filter_map(|h| document.get_entity(*h).cloned()) .collect(); self.push_undo_snapshot(i, "REFEDIT"); let mut temp_handles = Vec::new(); for mut entity in copies { apply_insert_transform(&mut entity, &session); entity.common_mut().handle = Handle::NULL; entity.common_mut().owner_handle = Handle::NULL; temp_handles.push(self.tabs[i].scene.add_entity(entity)); } let mut session = session; session.temp_handles = temp_handles.clone(); self.tabs[i].refedit_session = Some(session); self.tabs[i].refedit_closed.clear(); // Fade everything outside the working set by XFADECTL. (#136) self.tabs[i] .scene .set_refedit_keep(Some(temp_handles.iter().copied().collect::>())); self.tabs[i].scene.refedit_lock = true; self.tabs[i].scene.deselect_all(); self.tabs[i].dirty = true; // No active command — the definition's objects are edited freely // (move, grips, draw, erase…) until REFCLOSE. self.tabs[i].active_cmd = None; } /// REFSET Add / Remove on the current selection. fn apply_refset(&mut self, i: usize, add: bool) { let Some(mut session) = self.tabs[i].refedit_session.take() else { self.command_line.push_error(crate::t!(NOT_CHECKED_OUT).as_ref()); return; }; let picked: Vec = self.tabs[i] .scene .selected_entities() .into_iter() .map(|(h, _)| h) .collect(); let mut moved = 0usize; for h in picked { let inside = session.in_working_set(h); if add && !inside { session.removed.remove(&h); if !session.in_working_set(h) { session.added.insert(h); } moved += 1; } else if !add && inside { session.added.remove(&h); if session.in_working_set(h) { session.removed.insert(h); } moved += 1; } } let keep: rustc_hash::FxHashSet = self.tabs[i] .scene .document .entities() .map(|e| e.common().handle) .filter(|h| session.in_working_set(*h)) .collect(); self.tabs[i].refedit_session = Some(session); self.tabs[i].scene.set_refedit_keep(Some(keep)); self.tabs[i].scene.deselect_all(); self.command_line.push_output(&if add { crate::tf!("{} Added to working set", moved).into_owned() } else { crate::tf!("{} Removed from working set", moved).into_owned() }); } /// REFCLOSE Save (`save`) or Discard. fn close_refedit(&mut self, i: usize, save: bool) { let Some(session) = self.tabs[i].refedit_session.take() else { self.command_line.push_error(crate::t!(NOT_CHECKED_OUT).as_ref()); return; }; self.push_undo_snapshot(i, "REFCLOSE"); self.command_line .push_output(crate::t!("Regenerating model.").as_ref()); let space = self.tabs[i].scene.current_layout_block_handle_pub(); // The working set: the copies still in it, everything created in this // space during the session (#423) and the host objects added with // REFSET — less what REFSET removed. let created: Vec = self.tabs[i] .scene .document .entities() .filter(|e| { let c = e.common(); c.owner_handle == space && c.handle.value() >= session.handle_watermark }) .map(|e| e.common().handle) .collect(); let working: Vec = created .iter() .chain(session.added.iter()) .copied() .filter(|h| session.in_working_set(*h)) .collect::>() .into_iter() .collect(); if !save { // The copies and new objects go; host objects added with REFSET // stay where they are, and copies moved out with REFSET Remove // stay in the host. let drop: Vec = working .iter() .copied() .filter(|h| !session.added.contains(h)) .collect(); self.tabs[i].scene.erase_entities(&drop); self.finish_refedit_close(i, session); return; } use crate::modules::draw::modify::explode::normalize_entity_for_block; let originals: HashSet = session.temp_handles.iter().copied().collect(); let added_count = working.iter().filter(|h| !originals.contains(h)).count(); let removed_count = session.extracted.len() - working.iter().filter(|h| originals.contains(h)).count(); let edited: Vec = working .iter() .filter_map(|h| self.tabs[i].scene.document.get_entity(*h).cloned()) .map(|mut entity| { apply_insert_inverse_transform(&mut entity, &session); let mut entity = normalize_entity_for_block(entity); entity.common_mut().handle = Handle::NULL; entity.common_mut().owner_handle = session.br_handle; entity }) .collect(); self.tabs[i].scene.erase_entities(&working); // The extracted definition objects are replaced; the rest stay. for h in &session.extracted { self.tabs[i].scene.document.remove_entity(*h); } if let Some(br) = self.tabs[i] .scene .document .block_records .get_mut(&session.block_name) { let extracted: HashSet = session.extracted.iter().copied().collect(); br.entity_handles.retain(|h| !extracted.contains(h)); } for entity in edited.iter().cloned() { let _ = self.tabs[i].scene.document.add_entity(entity); } let instances = self.tabs[i] .scene .document .entities() .filter(|e| { matches!(e, EntityType::Insert(ins) if ins.block_name.eq_ignore_ascii_case(&session.block_name)) }) .count(); let name = session.block_name.clone(); match added_count { 0 => {} 1 => self .command_line .push_output(crate::tf!("1 object added to {}", name).as_ref()), n => self .command_line .push_output(crate::tf!("{} objects added to {}", n, name).as_ref()), } match removed_count { 0 => {} 1 => self .command_line .push_output(crate::tf!("1 object removed from {}", name).as_ref()), n => self .command_line .push_output(crate::tf!("{} objects removed from {}", n, name).as_ref()), } let reloaded = match &session.xref_path { Some(raw) => match self.write_back_xref(i, &session, raw, edited) { Ok(()) => true, Err(error) => { self.command_line.push_error(&error); false } }, None => false, }; let updated = match (session.xref_path.is_some(), instances) { (true, 1) => crate::t!("1 xref instance updated").into_owned(), (true, n) => crate::tf!("{} xref instances updated", n).into_owned(), (false, 1) => crate::t!("1 block instance updated").into_owned(), (false, n) => crate::tf!("{} block instances updated", n).into_owned(), }; self.command_line.push_output(&updated); self.command_line.push_output(&if reloaded { crate::tf!("{} redefined and reloaded.", name).into_owned() } else { crate::tf!("{} redefined.", name).into_owned() }); self.finish_refedit_close(i, session); // Rebuild hatch/image/mesh caches since block content changed. self.tabs[i].scene.rebuild_derived_caches(); } fn finish_refedit_close(&mut self, i: usize, session: RefEditSession) { self.tabs[i].refedit_closed.push(session); self.tabs[i].scene.refedit_lock = false; self.tabs[i].scene.set_refedit_keep(None); self.tabs[i].scene.deselect_all(); self.tabs[i].dirty = true; } /// Write the edited definition into the referenced drawing and reload it. fn write_back_xref( &mut self, i: usize, session: &RefEditSession, raw: &str, edited: Vec, ) -> Result<(), String> { let base_dir = self.tabs[i] .current_path .as_ref() .and_then(|p| p.parent().map(std::path::Path::to_path_buf)) .unwrap_or_default(); let path = crate::io::xref::resolve_path(raw, &base_dir) .ok_or_else(|| format!("\"{raw}\" cannot be found."))?; let mut source = crate::io::load_file(&path)?; let prefix = format!("{}|", session.block_name); let strip = |name: &str| -> String { if name.len() > prefix.len() && name.is_char_boundary(prefix.len()) && name[..prefix.len()].eq_ignore_ascii_case(&prefix) { name[prefix.len()..].to_string() } else { name.to_string() } }; // Dependencies come from the host (where they carry the xref prefix) // and go back under their own names. let mut deps = crate::app::ClipboardDeps::capture(&self.tabs[i].scene.document, &edited); { use codec::TableEntry; for layer in &mut deps.layers { let name = strip(layer.name()); layer.name = name; } for linetype in &mut deps.linetypes { let name = strip(linetype.name()); linetype.name = name; } for style in &mut deps.text_styles { let name = strip(style.name()); style.name = name; } } for block in &mut deps.blocks { block.name = strip(&block.name); } let entities: Vec = edited .into_iter() .map(|mut entity| { let common = entity.common_mut(); common.layer = strip(&common.layer); common.linetype = strip(&common.linetype); common.layer_handle = None; common.linetype_handle = None; common.owner_handle = Handle::NULL; match &mut entity { EntityType::Insert(insert) => insert.block_name = strip(&insert.block_name), EntityType::Text(text) => text.style = strip(&text.style), EntityType::MText(text) => text.style = strip(&text.style), _ => {} } entity }) .collect(); let model: Vec = source .model_space_entities() .map(|e| e.common().handle) .collect(); for h in model { source.remove_entity(h); } crate::app::command_driver::clipboard::paste_entities_kernel( &mut source, &entities, &deps, None, crate::app::command_driver::clipboard::PasteSpace::Model, ); crate::io::save(&source, &path)?; let keys: rustc_hash::FxHashSet = [session.br_handle].into_iter().collect(); self.tabs[i].xref_stat_cache.0.remove(&session.br_handle.value()); let _ = crate::io::xref::resolve_xrefs_for_keys( &mut self.tabs[i].scene.document, &base_dir, &keys, ); Ok(()) } /// Undo of REFCLOSE checks the reference out again; undo of REFEDIT ends /// the session (their redo does the reverse). pub(in crate::app) fn refedit_after_history_step(&mut self, i: usize, label: &str, undo: bool) { match (label, undo) { ("REFCLOSE", true) | ("REFEDIT", false) => { let reopened = if undo { self.tabs[i].refedit_closed.pop() } else { self.tabs[i].refedit_reopen.pop() }; if let Some(session) = reopened { let keep: rustc_hash::FxHashSet = self.tabs[i] .scene .document .entities() .map(|e| e.common().handle) .filter(|h| session.in_working_set(*h)) .collect(); self.tabs[i].refedit_session = Some(session); self.tabs[i].scene.refedit_lock = true; self.tabs[i].scene.set_refedit_keep(Some(keep)); } } ("REFCLOSE", false) | ("REFEDIT", true) => { if let Some(session) = self.tabs[i].refedit_session.take() { if undo { self.tabs[i].refedit_reopen.push(session); } else { self.tabs[i].refedit_closed.push(session); } self.tabs[i].scene.refedit_lock = false; self.tabs[i].scene.set_refedit_keep(None); } } _ => {} } } }