Files
OneCAD-Draft/src/app/commands/refedit.rs
T

589 lines
23 KiB
Rust

//! 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<Task<Message>> {
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<Handle> {
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<Message> {
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<Handle> = 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::<u64>().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<String> = 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::<u64>().ok())
.map(Handle::new)
else {
return;
};
let handles = |s: Option<&str>| -> Vec<Handle> {
s.unwrap_or("")
.split(',')
.filter_map(|v| v.parse::<u64>().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<Handle> = 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<EntityType> = 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::<rustc_hash::FxHashSet<_>>()));
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<Handle> = 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<Handle> = 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<Handle> = 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<Handle> = created
.iter()
.chain(session.added.iter())
.copied()
.filter(|h| session.in_working_set(*h))
.collect::<HashSet<_>>()
.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<Handle> = 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<Handle> = 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<EntityType> = 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<Handle> = 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<EntityType>,
) -> 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<EntityType> = 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<Handle> = 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<Handle> = [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<Handle> = 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);
}
}
_ => {}
}
}
}