forked from 1b-agent/opencadstudio-upstream
feat(block-editor): add Save Block As
Create a new named definition from current session edits, restore the source checkpoint, and continue editing the copy with fresh local history. Add the name dialog and Save dropdown, clone supported owned metadata onto independent handles, and refuse unsupported relationships atomically.
This commit is contained in:
1 parent
da8e976a60
commit
78bdfa3c74
13 files changed
+1364
-8
No files matched your search
@@ -224,6 +224,198 @@ impl OpenCADStudio {
|
||||
self.block_edit_delta(i).is_some()
|
||||
}
|
||||
|
||||
pub(super) fn validate_block_save_as_name(
|
||||
&self,
|
||||
i: usize,
|
||||
raw: &str,
|
||||
) -> Result<String, String> {
|
||||
let name =
|
||||
crate::scene::block_service::validate_block_name(raw).map_err(|e| e.to_string())?;
|
||||
if self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get(&name)
|
||||
.is_some()
|
||||
{
|
||||
return Err(
|
||||
crate::scene::block_service::BlockServiceError::NameExists(name).to_string(),
|
||||
);
|
||||
}
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
pub(super) fn open_block_save_as_dialog(&mut self, i: usize) {
|
||||
let Some(session) = self.tabs[i].active_block_edit_session() else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("No block editor is open.").as_ref());
|
||||
return;
|
||||
};
|
||||
let source = session.br_handle;
|
||||
let name = session.block_name.clone();
|
||||
let mut existing_names = self.tabs[i].scene.custom_block_names();
|
||||
existing_names.sort_by_key(|name| name.to_ascii_lowercase());
|
||||
self.block_save_as = Some(crate::ui::window::block_save_as::State {
|
||||
document_id: self.tabs[i].id,
|
||||
source,
|
||||
name: String::new(),
|
||||
existing_names,
|
||||
preview: self.tabs[i].scene.block_preview_wires(&name),
|
||||
error: self.validate_block_save_as_name(i, "").err(),
|
||||
});
|
||||
self.active_modal = Some(ModalKind::BlockSaveAs);
|
||||
self.reset_modal_geometry();
|
||||
}
|
||||
|
||||
pub(super) fn apply_block_save_as_dialog(&mut self) -> Task<Message> {
|
||||
let Some(state) = self.block_save_as.as_ref() else {
|
||||
return Task::none();
|
||||
};
|
||||
let id = state.document_id;
|
||||
let source = state.source;
|
||||
let name = state.name.clone();
|
||||
let Some(i) = self.tabs.iter().position(|tab| tab.id == id) else {
|
||||
return Task::none();
|
||||
};
|
||||
if self.tabs[i]
|
||||
.active_block_edit_session()
|
||||
.is_none_or(|session| session.br_handle != source)
|
||||
{
|
||||
if let Some(state) = self.block_save_as.as_mut() {
|
||||
state.error = Some(
|
||||
crate::t!("The block editor has changed; reopen Save Block As.").into_owned(),
|
||||
);
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
self.active_tab = i;
|
||||
if self.save_block_edit_as(i, &name) {
|
||||
self.block_save_as = None;
|
||||
self.active_modal = None;
|
||||
self.reset_modal_geometry();
|
||||
} else if let Some(state) = self.block_save_as.as_mut() {
|
||||
state.error = self.command_line.last_error.clone();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(super) fn save_block_edit_as(&mut self, i: usize, raw: &str) -> bool {
|
||||
let Some(session) = self.tabs[i].active_block_edit_session() else {
|
||||
self.command_line
|
||||
.push_error(crate::t!("No block editor is open.").as_ref());
|
||||
return false;
|
||||
};
|
||||
let source_name = session.block_name.clone();
|
||||
let source_handle = session.br_handle;
|
||||
let name = match self.validate_block_save_as_name(i, raw) {
|
||||
Ok(name) => name,
|
||||
Err(error) => {
|
||||
self.command_line.push_error(&error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
self.finish_pending_history(i);
|
||||
self.reconcile_block_edit_membership(i);
|
||||
let Some(history) = self.tabs[i].block_edit_history.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
let copied = match super::block_save_as::copy_definition(
|
||||
&self.tabs[i].scene.document,
|
||||
history.checkpoint.document.clone(),
|
||||
&source_name,
|
||||
&name,
|
||||
) {
|
||||
Ok(copied) => copied,
|
||||
Err(error) => {
|
||||
self.command_line.push_error(&error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let mut constraints = history.checkpoint.constraints.clone();
|
||||
for mut set in self.tabs[i].scene.parametric_constraints.iter().filter(|set| {
|
||||
matches!(set.scope, crate::scene::parametric_constraints::ParametricScope::Block(handle) if copied.handles.contains_key(&handle))
|
||||
}).cloned() {
|
||||
if !set.retained_standard_groups.is_empty() || set.constraints.iter().any(|c| c.native_origin.is_some()) {
|
||||
self.command_line.push_error(crate::t!("Save Block As cannot copy retained native constraints yet.").as_ref());
|
||||
return false;
|
||||
}
|
||||
let crate::scene::parametric_constraints::ParametricScope::Block(old) = set.scope else { unreachable!() };
|
||||
set.scope = crate::scene::parametric_constraints::ParametricScope::Block(copied.handles[&old]);
|
||||
for constraint in &mut set.constraints {
|
||||
for reference in constraint.refs.iter_mut().chain(constraint.rigid_points.iter_mut().map(|(r, _)| r)) {
|
||||
let Some(new) = copied.handles.get(&reference.entity) else {
|
||||
self.command_line.push_error(crate::t!("A block constraint refers to geometry outside the definition.").as_ref());
|
||||
return false;
|
||||
};
|
||||
reference.entity = *new;
|
||||
}
|
||||
}
|
||||
for handle in set.dimensions.values_mut() {
|
||||
let Some(new) = copied.handles.get(handle) else {
|
||||
self.command_line.push_error(crate::t!("A block constraint dimension is missing.").as_ref());
|
||||
return false;
|
||||
};
|
||||
*handle = *new;
|
||||
}
|
||||
set.dof = None;
|
||||
set.conflicts.clear();
|
||||
constraints.push(set);
|
||||
}
|
||||
for entity in copied.document.entities_in_block(&name) {
|
||||
if let EntityType::Insert(insert) = entity {
|
||||
if crate::scene::block_service::would_create_cycle(
|
||||
&copied.document,
|
||||
&name,
|
||||
&insert.block_name,
|
||||
) {
|
||||
self.command_line.push_error(
|
||||
&crate::scene::block_service::circular_dependency_error(
|
||||
&name,
|
||||
&insert.block_name,
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
let parameters = self.tabs[i].scene.named_parameters.clone();
|
||||
let active_layer = self.tabs[i].active_layer.clone();
|
||||
self.tabs[i].scene.document = copied.document;
|
||||
self.tabs[i].scene.parametric_constraints = constraints;
|
||||
self.tabs[i].scene.named_parameters = parameters;
|
||||
self.tabs[i].active_layer = active_layer;
|
||||
let mut delta = self
|
||||
.block_edit_delta(i)
|
||||
.expect("a new definition changes the document");
|
||||
delta.label = format!("Save block as {name}");
|
||||
let mut history = self.tabs[i].block_edit_history.take().unwrap();
|
||||
std::mem::swap(&mut self.tabs[i].history, &mut history.main);
|
||||
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
|
||||
std::mem::swap(&mut self.tabs[i].history, &mut history.main);
|
||||
self.tabs[i].history = HistoryState::default();
|
||||
self.tabs[i].dirty = true;
|
||||
let session = self.tabs[i].active_block_edit_session_mut().unwrap();
|
||||
debug_assert_eq!(session.br_handle, source_handle);
|
||||
session.br_handle = copied.record;
|
||||
session.block_name = name.clone();
|
||||
self.tabs[i].scene.block_edit_block = Some(copied.record);
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.tabs[i].active_grip = None;
|
||||
self.grip_hover = None;
|
||||
self.grip_popup = None;
|
||||
self.visibility_popup = None;
|
||||
history.checkpoint = self.block_edit_checkpoint(i);
|
||||
self.tabs[i].block_edit_history = Some(history);
|
||||
self.tabs[i].scene.rebuild_derived_caches();
|
||||
self.tabs[i].refresh_active_ucs();
|
||||
self.refresh_block_palette();
|
||||
self.refresh_properties();
|
||||
self.sync_ribbon_layers();
|
||||
self.command_line
|
||||
.push_output(crate::tf!("Block {} saved.", name).as_ref());
|
||||
true
|
||||
}
|
||||
|
||||
pub(super) fn save_block_edit(&mut self, i: usize) -> bool {
|
||||
let Some(session) = self.tabs[i].active_block_edit_session() else {
|
||||
self.command_line
|
||||
|
||||
@@ -54,6 +54,520 @@ fn main_depth(app: &OpenCADStudio) -> usize {
|
||||
.unwrap_or(tab.history.undo_stack.len())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_as_forks_unsaved_edits_and_undoes_as_one_drawing_step() {
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
circle(&mut app, 1.0);
|
||||
app.save_block_edit(app.active_tab);
|
||||
let saved = members(&app);
|
||||
circle(&mut app, 2.0);
|
||||
let edited = members(&app).len();
|
||||
assert!(app.save_block_edit_as(app.active_tab, " COPY "));
|
||||
let i = app.active_tab;
|
||||
assert_eq!(members(&app), saved);
|
||||
assert_eq!(
|
||||
app.tabs[i].active_block_edit_session().unwrap().block_name,
|
||||
"COPY"
|
||||
);
|
||||
assert_eq!(
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get("COPY")
|
||||
.unwrap()
|
||||
.entity_handles
|
||||
.len(),
|
||||
edited
|
||||
);
|
||||
assert_eq!(main_depth(&app), 2);
|
||||
assert!(app.tabs[i].history.undo_stack.is_empty());
|
||||
assert!(!app.block_edit_dirty(i));
|
||||
app.undo_steps(1);
|
||||
assert!(app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get("COPY")
|
||||
.is_some());
|
||||
circle(&mut app, 3.0);
|
||||
app.close_block_edit(i, true);
|
||||
assert_eq!(
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get("COPY")
|
||||
.unwrap()
|
||||
.entity_handles
|
||||
.len(),
|
||||
edited
|
||||
);
|
||||
app.undo_steps(1);
|
||||
assert!(app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get("COPY")
|
||||
.is_none());
|
||||
assert_eq!(members(&app), saved);
|
||||
app.redo_steps(1);
|
||||
assert_eq!(
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get("COPY")
|
||||
.unwrap()
|
||||
.entity_handles
|
||||
.len(),
|
||||
edited
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_as_name_refusals_do_not_change_the_session() {
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
circle(&mut app, 1.0);
|
||||
let i = app.active_tab;
|
||||
let document = app.tabs[i].scene.document.clone();
|
||||
let depth = app.tabs[i].history.undo_stack.len();
|
||||
for name in ["", "bad/name", "edit_me", "EDIT_ME"] {
|
||||
assert!(!app.save_block_edit_as(i, name));
|
||||
assert_eq!(app.tabs[i].scene.document, document);
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), depth);
|
||||
assert_eq!(main_depth(&app), 0);
|
||||
assert_eq!(
|
||||
app.tabs[i].active_block_edit_session().unwrap().block_name,
|
||||
"EDIT_ME"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
app.command_line.last_error.as_deref(),
|
||||
Some("block \"EDIT_ME\" already exists")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_as_dialog_cancel_and_enter_follow_the_current_session() {
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
circle(&mut app, 1.0);
|
||||
let i = app.active_tab;
|
||||
let document = app.tabs[i].scene.document.clone();
|
||||
let _ = app.update(Message::DropdownSelectItem {
|
||||
dropdown_id: "BLOCK_SAVE",
|
||||
cmd: "BSAVEAS",
|
||||
});
|
||||
assert_eq!(app.active_modal, Some(ModalKind::BlockSaveAs));
|
||||
let _ = app.update(Message::BlockSaveAsName("edit_me".into()));
|
||||
assert!(app.block_save_as.as_ref().unwrap().error.is_some());
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
assert!(app.block_save_as.is_none());
|
||||
assert_eq!(app.tabs[i].scene.document, document);
|
||||
let _ = app.dispatch_command("BSAVEAS");
|
||||
let _ = app.update(Message::BlockSaveAsName("COPY".into()));
|
||||
assert!(app.block_save_as.as_ref().unwrap().error.is_none());
|
||||
let _ = app.update(Message::CommandFinalize);
|
||||
assert!(app.active_modal.is_none());
|
||||
assert_eq!(
|
||||
app.tabs[i].active_block_edit_session().unwrap().block_name,
|
||||
"COPY"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_as_preserves_base_point_description_units_and_nested_attributes() {
|
||||
use codec::entities::AttributeEntity;
|
||||
let (mut app, reference) = fixture();
|
||||
let i = app.active_tab;
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.define_block_raw("CHILD", Vector3::ZERO, vec![]);
|
||||
let owner = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get("EDIT_ME")
|
||||
.unwrap()
|
||||
.handle;
|
||||
let mut nested = Insert::new("CHILD", Vector3::new(4.0, 5.0, 0.0));
|
||||
let mut attribute = AttributeEntity::new("TAG".into(), "value".into());
|
||||
attribute.common.handle = app.tabs[i].scene.document.allocate_handle();
|
||||
nested.attributes.push(attribute);
|
||||
nested.common.owner_handle = owner;
|
||||
nested.seqend_handle = Some(app.tabs[i].scene.document.allocate_handle());
|
||||
let nested = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.add_entity(EntityType::Insert(nested))
|
||||
.unwrap();
|
||||
let base = Vector3::new(12.0, 34.0, 0.0);
|
||||
let record = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get_mut("EDIT_ME")
|
||||
.unwrap();
|
||||
record.base_point = base;
|
||||
record.description = "A reusable definition".into();
|
||||
record.units = 4;
|
||||
let source_marker = record.block_entity_handle;
|
||||
if let Some(EntityType::Block(marker)) =
|
||||
app.tabs[i].scene.document.get_entity_mut(source_marker)
|
||||
{
|
||||
marker.base_point = base;
|
||||
}
|
||||
let source_insert = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(nested)
|
||||
.unwrap()
|
||||
.clone();
|
||||
begin(&mut app, reference);
|
||||
assert!(app.save_block_edit_as(i, "COPY"));
|
||||
let doc = &app.tabs[i].scene.document;
|
||||
let record = doc.block_records.get("COPY").unwrap();
|
||||
assert_eq!(record.base_point, base);
|
||||
assert_eq!(record.units, 4);
|
||||
assert_eq!(record.description, "A reusable definition");
|
||||
assert_eq!(doc.get_entity(nested).unwrap(), &source_insert);
|
||||
let copied = doc
|
||||
.entities_in_block("COPY")
|
||||
.find_map(|e| match e {
|
||||
EntityType::Insert(insert) => Some(insert),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
let EntityType::Insert(original) = &source_insert else {
|
||||
unreachable!()
|
||||
};
|
||||
assert_eq!(copied.block_name, "CHILD");
|
||||
assert_eq!(copied.insert_point, original.insert_point);
|
||||
assert_ne!(
|
||||
copied.attributes[0].common.handle,
|
||||
original.attributes[0].common.handle
|
||||
);
|
||||
assert_eq!(
|
||||
copied.attributes[0].common.owner_handle,
|
||||
copied.common.handle
|
||||
);
|
||||
assert_ne!(copied.seqend_handle, original.seqend_handle);
|
||||
assert_eq!(copied.attributes[0].value, original.attributes[0].value);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_as_clones_extension_dictionaries_and_draw_order_independently() {
|
||||
use codec::objects::{
|
||||
Dictionary, ObjectType, SortEntitiesTable, XRecord, XRecordEntry, XRecordValue,
|
||||
};
|
||||
let (mut app, reference) = fixture();
|
||||
let i = app.active_tab;
|
||||
let owner = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get("EDIT_ME")
|
||||
.unwrap()
|
||||
.handle;
|
||||
let first = members(&app)[0];
|
||||
let mut second = Line::from_points(Vector3::ZERO, Vector3::UNIT_Y);
|
||||
second.common.owner_handle = owner;
|
||||
let second = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.add_entity(EntityType::Line(second))
|
||||
.unwrap();
|
||||
let root = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.ensure_extension_dictionary(owner);
|
||||
let mut record = XRecord::new();
|
||||
record.handle = app.tabs[i].scene.document.allocate_handle();
|
||||
record.owner = root;
|
||||
record
|
||||
.entries
|
||||
.push(XRecordEntry::new(340, XRecordValue::Handle(first)));
|
||||
let leaf = record.handle;
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.insert(leaf, ObjectType::XRecord(record));
|
||||
let mut order = SortEntitiesTable::for_block(owner);
|
||||
order.handle = app.tabs[i].scene.document.allocate_handle();
|
||||
order.owner_handle = root;
|
||||
order.add_entry(first, Handle::new(0x200));
|
||||
order.send_to_back(second);
|
||||
let order_handle = order.handle;
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.insert(order_handle, ObjectType::SortEntitiesTable(order));
|
||||
let Some(ObjectType::Dictionary(dictionary)) =
|
||||
app.tabs[i].scene.document.objects.get_mut(&root)
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
dictionary.add_entry("DATA", leaf);
|
||||
dictionary.add_entry("ACAD_SORTENTS", order_handle);
|
||||
let original: Dictionary = dictionary.clone();
|
||||
begin(&mut app, reference);
|
||||
assert!(app.save_block_edit_as(i, "COPY"));
|
||||
let doc = &app.tabs[i].scene.document;
|
||||
let owner = doc.block_records.get("COPY").unwrap().handle;
|
||||
let copy_members = doc
|
||||
.block_records
|
||||
.get("COPY")
|
||||
.unwrap()
|
||||
.entity_handles
|
||||
.clone();
|
||||
let copied_root = doc.extension_dictionary_handle(owner).unwrap();
|
||||
assert_ne!(root, copied_root);
|
||||
assert_eq!(
|
||||
doc.objects.get(&root),
|
||||
Some(&ObjectType::Dictionary(original))
|
||||
);
|
||||
let Some(ObjectType::Dictionary(dictionary)) = doc.objects.get(&copied_root) else {
|
||||
unreachable!()
|
||||
};
|
||||
let Some(ObjectType::XRecord(record)) = doc.objects.get(&dictionary.get("DATA").unwrap())
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
assert_eq!(record.owner, copied_root);
|
||||
assert_eq!(
|
||||
record.entries[0].value,
|
||||
XRecordValue::Handle(copy_members[0])
|
||||
);
|
||||
let Some(ObjectType::SortEntitiesTable(order)) =
|
||||
doc.objects.get(&dictionary.get("ACAD_SORTENTS").unwrap())
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
assert_eq!(order.block_owner_handle, owner);
|
||||
assert!(
|
||||
order.effective_sort_handle(copy_members[1]) < order.effective_sort_handle(copy_members[0])
|
||||
);
|
||||
app.close_block_edit(i, true);
|
||||
app.undo_steps(1);
|
||||
assert!(!app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.contains_key(&copied_root));
|
||||
app.redo_steps(1);
|
||||
assert!(app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.contains_key(&copied_root));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_as_preserves_block_parameters_and_remaps_constraint_geometry() {
|
||||
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef, ParametricScope};
|
||||
let (mut app, reference) = fixture();
|
||||
let i = app.active_tab;
|
||||
let owner = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get("EDIT_ME")
|
||||
.unwrap()
|
||||
.handle;
|
||||
let member = members(&app)[0];
|
||||
let set = app.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set_mut(ParametricScope::Block(owner));
|
||||
set.local_parameters.set("width", "7").unwrap();
|
||||
set.add(
|
||||
ConstraintKind::Horizontal,
|
||||
vec![ParametricRef::whole(member)],
|
||||
None,
|
||||
);
|
||||
begin(&mut app, reference);
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set_mut(ParametricScope::Block(owner))
|
||||
.local_parameters
|
||||
.set("width", "9")
|
||||
.unwrap();
|
||||
assert!(app.save_block_edit_as(i, "COPY"));
|
||||
let new_owner = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get("COPY")
|
||||
.unwrap()
|
||||
.handle;
|
||||
let copied = app.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(ParametricScope::Block(new_owner))
|
||||
.unwrap();
|
||||
assert_eq!(copied.local_parameters.resolve("width").unwrap(), 9.0);
|
||||
assert_ne!(copied.constraints[0].refs[0].entity, member);
|
||||
assert_eq!(
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(ParametricScope::Block(owner))
|
||||
.unwrap()
|
||||
.local_parameters
|
||||
.resolve("width")
|
||||
.unwrap(),
|
||||
7.0
|
||||
);
|
||||
app.close_block_edit(i, true);
|
||||
app.undo_steps(1);
|
||||
assert!(app.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(ParametricScope::Block(new_owner))
|
||||
.is_none());
|
||||
app.redo_steps(1);
|
||||
assert_eq!(
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(ParametricScope::Block(new_owner))
|
||||
.unwrap()
|
||||
.local_parameters
|
||||
.resolve("width")
|
||||
.unwrap(),
|
||||
9.0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_as_refuses_unsupported_owned_graphs_without_losing_edits() {
|
||||
use codec::objects::{DynamicBlockObject, ObjectType};
|
||||
let (mut app, reference) = fixture();
|
||||
let i = app.active_tab;
|
||||
let mut object = DynamicBlockObject::new("BLOCKLINEARPARAMETER", "LinearParameter");
|
||||
object.handle = app.tabs[i].scene.document.allocate_handle();
|
||||
object.owner = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get("EDIT_ME")
|
||||
.unwrap()
|
||||
.handle;
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.objects
|
||||
.insert(object.handle, ObjectType::DynamicBlock(object));
|
||||
begin(&mut app, reference);
|
||||
circle(&mut app, 1.0);
|
||||
let document = app.tabs[i].scene.document.clone();
|
||||
assert!(!app.save_block_edit_as(i, "COPY"));
|
||||
assert_eq!(app.tabs[i].scene.document, document);
|
||||
assert_eq!(members(&app).len(), 2);
|
||||
assert_eq!(main_depth(&app), 0);
|
||||
assert!(app
|
||||
.command_line
|
||||
.last_error
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.contains("native object relationships"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_as_retains_new_shared_layers_and_nested_definitions() {
|
||||
use codec::entities::{AttributeDefinition, AttributeEntity, MText};
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
let i = app.active_tab;
|
||||
let _ = app.dispatch_command("LAYER NEW COPY_LAYER");
|
||||
let _ = app.dispatch_command("CLAYER COPY_LAYER");
|
||||
let mut definition = AttributeDefinition::new("TAG".into(), "Prompt".into(), "first".into());
|
||||
definition.is_multiline = true;
|
||||
definition.mtext_flag = codec::entities::attribute_definition::MTextFlag::MultiLine;
|
||||
let mut text = MText::new();
|
||||
text.value = "first\\Psecond".into();
|
||||
definition.embedded_mtext = Some(Box::new(text));
|
||||
app.tabs[i].scene.define_block_raw(
|
||||
"NEW_CHILD",
|
||||
Vector3::ZERO,
|
||||
vec![EntityType::AttributeDefinition(definition)],
|
||||
);
|
||||
let source_definition = app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get("NEW_CHILD")
|
||||
.unwrap()
|
||||
.entity_handles[0];
|
||||
let mut child = Insert::new("NEW_CHILD", Vector3::ZERO);
|
||||
let mut attribute = AttributeEntity::new("TAG".into(), "first".into());
|
||||
attribute.attdef_handle = source_definition;
|
||||
child.attributes.push(attribute);
|
||||
child.common.layer = "COPY_LAYER".into();
|
||||
child.common.owner_handle = app.tabs[i].active_block_edit_session().unwrap().br_handle;
|
||||
app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.add_entity(EntityType::Insert(child))
|
||||
.unwrap();
|
||||
assert!(app.save_block_edit_as(i, "COPY"));
|
||||
assert_eq!(app.tabs[i].active_layer, "COPY_LAYER");
|
||||
assert!(app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
.get("COPY_LAYER")
|
||||
.is_some());
|
||||
assert!(app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get("NEW_CHILD")
|
||||
.is_some());
|
||||
assert_eq!(members(&app).len(), 1);
|
||||
let doc = &app.tabs[i].scene.document;
|
||||
let copied_definition = doc.block_records.get("NEW_CHILD").unwrap().entity_handles[0];
|
||||
assert_ne!(copied_definition, source_definition);
|
||||
let EntityType::AttributeDefinition(definition) = doc.get_entity(copied_definition).unwrap()
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let text = definition.embedded_mtext.as_ref().unwrap();
|
||||
assert_eq!(text.value, "first\\Psecond");
|
||||
assert!(!text.common.handle.is_null());
|
||||
let copied_attribute = doc
|
||||
.entities_in_block("COPY")
|
||||
.find_map(|entity| match entity {
|
||||
EntityType::Insert(insert) => Some(&insert.attributes[0]),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(copied_attribute.attdef_handle, copied_definition);
|
||||
assert!(app.tabs[i].scene.document.entities_in_block("COPY").any(|entity| matches!(entity, EntityType::Insert(insert) if insert.block_name=="NEW_CHILD" && insert.common.layer=="COPY_LAYER")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_as_can_create_an_empty_definition() {
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
let i = app.active_tab;
|
||||
let original = members(&app);
|
||||
app.push_undo_snapshot(i, "Erase");
|
||||
app.tabs[i].scene.erase_entities(&original);
|
||||
app.finish_pending_history(i);
|
||||
assert!(app.save_block_edit_as(i, "EMPTY"));
|
||||
assert_eq!(members(&app), original);
|
||||
assert!(app.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get("EMPTY")
|
||||
.unwrap()
|
||||
.entity_handles
|
||||
.is_empty());
|
||||
assert!(!app.block_edit_dirty(i));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_undo_preserves_membership_and_cannot_reach_drawing_history() {
|
||||
let (mut app, reference) = fixture();
|
||||
|
||||
@@ -0,0 +1,517 @@
|
||||
use codec::entities::{Block, BlockEnd, EntityCommon};
|
||||
use codec::objects::ObjectType;
|
||||
use codec::tables::BlockRecord;
|
||||
use codec::{CadDocument, EntityType, Handle};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// The copy is built in the last saved document, so the source definition and
|
||||
/// its references remain at their checkpoint even when the editor is dirty.
|
||||
pub(super) struct DefinitionCopy {
|
||||
pub document: CadDocument,
|
||||
pub handles: FxHashMap<Handle, Handle>,
|
||||
pub record: Handle,
|
||||
}
|
||||
|
||||
fn remap_common(common: &mut EntityCommon, handles: &FxHashMap<Handle, Handle>) {
|
||||
let map = |h: Handle| handles.get(&h).copied().unwrap_or(h);
|
||||
common.handle = map(common.handle);
|
||||
common.owner_handle = map(common.owner_handle);
|
||||
common.xdictionary_handle = common.xdictionary_handle.map(map);
|
||||
common.reactors = common
|
||||
.reactors
|
||||
.iter()
|
||||
.filter_map(|h| handles.get(h).copied())
|
||||
.collect();
|
||||
let mut data = codec::xdata::ExtendedData::new();
|
||||
for mut record in common.extended_data.records().iter().cloned() {
|
||||
for value in &mut record.values {
|
||||
if let codec::xdata::XDataValue::Handle(handle) = value {
|
||||
*handle = map(*handle);
|
||||
}
|
||||
}
|
||||
data.add_record(record);
|
||||
}
|
||||
common.extended_data = data;
|
||||
common.raw_record = None;
|
||||
}
|
||||
|
||||
fn cloneable_object(object: &ObjectType) -> bool {
|
||||
matches!(
|
||||
object,
|
||||
ObjectType::Dictionary(_)
|
||||
| ObjectType::DictionaryWithDefault(_)
|
||||
| ObjectType::DictionaryVariable(_)
|
||||
| ObjectType::SpatialFilter(_)
|
||||
| ObjectType::ObjectContextData(_)
|
||||
| ObjectType::SortEntitiesTable(_)
|
||||
| ObjectType::Field(_)
|
||||
| ObjectType::FieldList(_)
|
||||
| ObjectType::PlaceHolder(_)
|
||||
| ObjectType::ImageDefinitionReactor(_)
|
||||
| ObjectType::XRecord(codec::objects::XRecord {
|
||||
entries_complete: true,
|
||||
..
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn copy_definition(
|
||||
source: &CadDocument,
|
||||
mut target: CadDocument,
|
||||
source_name: &str,
|
||||
name: &str,
|
||||
) -> Result<DefinitionCopy, String> {
|
||||
let root = source
|
||||
.block_records
|
||||
.get(source_name)
|
||||
.ok_or_else(|| format!("block \"{source_name}\" does not exist"))?;
|
||||
// Shared symbol records can be created while editing. Retain them when
|
||||
// restoring the source definition to its checkpoint.
|
||||
target.header = source.header.clone();
|
||||
target.layers = source.layers.clone();
|
||||
target.line_types = source.line_types.clone();
|
||||
target.text_styles = source.text_styles.clone();
|
||||
target.dim_styles = source.dim_styles.clone();
|
||||
target.app_ids = source.app_ids.clone();
|
||||
let mut records = vec![(root.clone(), name.to_string())];
|
||||
let mut names = FxHashSet::default();
|
||||
names.insert(source_name.to_ascii_uppercase());
|
||||
let mut index = 0;
|
||||
let mut anonymous = 0;
|
||||
while index < records.len() {
|
||||
for entity in source.entities_in_block(&records[index].0.name) {
|
||||
if let EntityType::Insert(insert) = entity {
|
||||
if insert.block_name.eq_ignore_ascii_case(name) {
|
||||
return Err(crate::scene::block_service::circular_dependency_error(
|
||||
name,
|
||||
&insert.block_name,
|
||||
));
|
||||
}
|
||||
if target.block_records.get(&insert.block_name).is_none()
|
||||
&& names.insert(insert.block_name.to_ascii_uppercase())
|
||||
{
|
||||
let record = source
|
||||
.block_records
|
||||
.get(&insert.block_name)
|
||||
.ok_or_else(|| format!("block \"{}\" does not exist", insert.block_name))?;
|
||||
records.push((record.clone(), record.name.clone()));
|
||||
}
|
||||
}
|
||||
if let EntityType::Dimension(dimension) = entity {
|
||||
let picture = &dimension.base().block_name;
|
||||
if picture.is_empty() || !names.insert(picture.to_ascii_uppercase()) {
|
||||
continue;
|
||||
}
|
||||
let Some(record) = source.block_records.get(picture) else {
|
||||
return Err(format!("Dimension picture block \"{picture}\" is missing."));
|
||||
};
|
||||
let new_name = loop {
|
||||
let candidate = format!("*D{anonymous}");
|
||||
anonymous += 1;
|
||||
if target.block_records.get(&candidate).is_none()
|
||||
&& !records
|
||||
.iter()
|
||||
.any(|(_, name)| name.eq_ignore_ascii_case(&candidate))
|
||||
{
|
||||
break candidate;
|
||||
}
|
||||
};
|
||||
records.push((record.clone(), new_name));
|
||||
}
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
|
||||
let mut entities = Vec::new();
|
||||
let mut identities = FxHashSet::default();
|
||||
for (record, _) in &records {
|
||||
identities.insert(record.handle);
|
||||
for marker in [record.block_entity_handle, record.block_end_handle] {
|
||||
if let Some(entity) = source.get_entity(marker) {
|
||||
entities.push(entity.clone());
|
||||
}
|
||||
}
|
||||
for entity in source.entities_in_block(&record.name) {
|
||||
if matches!(entity, EntityType::Block(_) | EntityType::BlockEnd(_)) {
|
||||
continue;
|
||||
}
|
||||
if matches!(entity, EntityType::Unknown(_)) {
|
||||
return Err("Save Block As cannot copy opaque entities yet.".to_string());
|
||||
}
|
||||
entities.push(entity.clone());
|
||||
}
|
||||
}
|
||||
for entity in &entities {
|
||||
identities.insert(entity.common().handle);
|
||||
match entity {
|
||||
EntityType::Insert(insert) => {
|
||||
identities.extend(insert.attributes.iter().map(|a| a.common.handle));
|
||||
identities.extend(
|
||||
insert
|
||||
.attributes
|
||||
.iter()
|
||||
.filter_map(|a| a.embedded_mtext.as_ref().map(|text| text.common.handle)),
|
||||
);
|
||||
identities.extend(insert.seqend_handle);
|
||||
}
|
||||
EntityType::AttributeDefinition(attribute) => {
|
||||
identities.extend(
|
||||
attribute
|
||||
.embedded_mtext
|
||||
.as_ref()
|
||||
.map(|text| text.common.handle),
|
||||
);
|
||||
}
|
||||
EntityType::AttributeEntity(attribute) => {
|
||||
identities.extend(
|
||||
attribute
|
||||
.embedded_mtext
|
||||
.as_ref()
|
||||
.map(|text| text.common.handle),
|
||||
);
|
||||
}
|
||||
EntityType::Polyline3D(polyline) => {
|
||||
identities.extend(polyline.vertices.iter().map(|v| v.handle))
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
identities.remove(&Handle::NULL);
|
||||
// Ownership, rather than arbitrary references, determines what must be
|
||||
// independent. Nested INSERT definitions and shared styles stay shared.
|
||||
let mut objects = Vec::new();
|
||||
loop {
|
||||
let added: Vec<_> = source
|
||||
.objects
|
||||
.iter()
|
||||
.filter(|(handle, _)| {
|
||||
!identities.contains(handle)
|
||||
&& source
|
||||
.object_owner(**handle)
|
||||
.is_some_and(|owner| identities.contains(&owner))
|
||||
})
|
||||
.map(|(handle, object)| (*handle, object.clone()))
|
||||
.collect();
|
||||
if added.is_empty() {
|
||||
break;
|
||||
}
|
||||
for (handle, object) in added {
|
||||
if !cloneable_object(&object) {
|
||||
return Err(
|
||||
"Save Block As cannot copy this block's native object relationships yet."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
identities.insert(handle);
|
||||
objects.push((handle, object));
|
||||
}
|
||||
}
|
||||
for owner in &identities {
|
||||
if let Some(dictionary) = source.extension_dictionary_handle(*owner) {
|
||||
if !identities.contains(&dictionary) {
|
||||
return Err(
|
||||
"Save Block As requires an independently owned extension dictionary."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (_, object) in &objects {
|
||||
if let ObjectType::Dictionary(dictionary) = object {
|
||||
if dictionary.entries.iter().any(|(key, handle)| {
|
||||
!handle.is_null()
|
||||
&& (dictionary.hard_owner || dictionary.is_entry_hard_owner(key))
|
||||
&& !identities.contains(handle)
|
||||
}) {
|
||||
return Err(
|
||||
"Save Block As cannot duplicate a dictionary's foreign owned entries."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
identities.remove(&Handle::NULL);
|
||||
let floor = source
|
||||
.next_handle()
|
||||
.max(source.header.handle_seed)
|
||||
.max(target.next_handle());
|
||||
if target.next_handle() < floor {
|
||||
target.header.handle_seed = floor - 1;
|
||||
target.allocate_handle();
|
||||
}
|
||||
let mut identities: Vec<_> = identities.into_iter().collect();
|
||||
identities.sort_by_key(|h| h.value());
|
||||
let handles: FxHashMap<_, _> = identities
|
||||
.into_iter()
|
||||
.map(|h| (h, target.allocate_handle()))
|
||||
.collect();
|
||||
let remap: HashMap<_, _> = handles.iter().map(|(a, b)| (*a, *b)).collect();
|
||||
let block_names: HashMap<_, _> = records
|
||||
.iter()
|
||||
.map(|(r, n)| (r.name.to_ascii_uppercase(), n.clone()))
|
||||
.collect();
|
||||
for (record, new_name) in &records {
|
||||
let mut copied: BlockRecord = record.clone();
|
||||
copied.name = new_name.clone();
|
||||
copied.handle = handles[&record.handle];
|
||||
copied.block_entity_handle = handles
|
||||
.get(&record.block_entity_handle)
|
||||
.copied()
|
||||
.unwrap_or_else(|| target.allocate_handle());
|
||||
copied.block_end_handle = handles
|
||||
.get(&record.block_end_handle)
|
||||
.copied()
|
||||
.unwrap_or_else(|| target.allocate_handle());
|
||||
copied.entity_handles.clear();
|
||||
copied.insert_handles.clear();
|
||||
copied.insert_count_bytes.clear();
|
||||
copied.preview_data.clear();
|
||||
copied.flags.anonymous = new_name.starts_with('*');
|
||||
copied.layout = Handle::NULL;
|
||||
target
|
||||
.block_records
|
||||
.add(copied.clone())
|
||||
.map_err(|e| e.to_string())?;
|
||||
if source.get_entity(record.block_entity_handle).is_none() {
|
||||
let mut marker = Block::new(new_name, record.base_point);
|
||||
marker.common.handle = copied.block_entity_handle;
|
||||
marker.common.owner_handle = copied.handle;
|
||||
target
|
||||
.add_entity(EntityType::Block(marker))
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
if source.get_entity(record.block_end_handle).is_none() {
|
||||
let mut marker = BlockEnd::new();
|
||||
marker.common.handle = copied.block_end_handle;
|
||||
marker.common.owner_handle = copied.handle;
|
||||
target
|
||||
.add_entity(EntityType::BlockEnd(marker))
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
for mut entity in entities {
|
||||
remap_common(entity.common_mut(), &handles);
|
||||
match &mut entity {
|
||||
EntityType::Block(block) => {
|
||||
block.name = block_names[&block.name.to_ascii_uppercase()].clone();
|
||||
}
|
||||
EntityType::Insert(insert) => {
|
||||
insert.seqend_handle = insert.seqend_handle.map(|h| handles[&h]);
|
||||
let copied_definition = block_names
|
||||
.get(&insert.block_name.to_ascii_uppercase())
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case(&insert.block_name));
|
||||
for attribute in &mut insert.attributes {
|
||||
remap_common(&mut attribute.common, &handles);
|
||||
if attribute.common.handle.is_null() {
|
||||
attribute.common.handle = target.allocate_handle();
|
||||
}
|
||||
attribute.common.owner_handle = insert.common.handle;
|
||||
if copied_definition {
|
||||
attribute.attdef_handle = handles
|
||||
.get(&attribute.attdef_handle)
|
||||
.copied()
|
||||
.unwrap_or(attribute.attdef_handle);
|
||||
}
|
||||
if let Some(text) = &mut attribute.embedded_mtext {
|
||||
remap_common(&mut text.common, &handles);
|
||||
if text.common.handle.is_null() {
|
||||
text.common.handle = target.allocate_handle();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EntityType::AttributeDefinition(attribute) => {
|
||||
if let Some(text) = &mut attribute.embedded_mtext {
|
||||
remap_common(&mut text.common, &handles);
|
||||
if text.common.handle.is_null() {
|
||||
text.common.handle = target.allocate_handle();
|
||||
}
|
||||
}
|
||||
}
|
||||
EntityType::AttributeEntity(attribute) => {
|
||||
attribute.attdef_handle = handles
|
||||
.get(&attribute.attdef_handle)
|
||||
.copied()
|
||||
.unwrap_or(attribute.attdef_handle);
|
||||
if let Some(text) = &mut attribute.embedded_mtext {
|
||||
remap_common(&mut text.common, &handles);
|
||||
if text.common.handle.is_null() {
|
||||
text.common.handle = target.allocate_handle();
|
||||
}
|
||||
}
|
||||
}
|
||||
EntityType::Polyline3D(polyline) => {
|
||||
for vertex in &mut polyline.vertices {
|
||||
vertex.handle = handles
|
||||
.get(&vertex.handle)
|
||||
.copied()
|
||||
.unwrap_or_else(|| target.allocate_handle());
|
||||
}
|
||||
}
|
||||
EntityType::Dimension(dimension) => {
|
||||
if let Some(name) =
|
||||
block_names.get(&dimension.base().block_name.to_ascii_uppercase())
|
||||
{
|
||||
dimension.base_mut().block_name = name.clone();
|
||||
}
|
||||
}
|
||||
EntityType::Leader(leader) => {
|
||||
if let Some(handle) = handles.get(&leader.annotation_handle) {
|
||||
leader.annotation_handle = *handle;
|
||||
}
|
||||
}
|
||||
EntityType::RasterImage(image) => {
|
||||
image.definition_reactor_handle = image
|
||||
.definition_reactor_handle
|
||||
.map(|h| handles.get(&h).copied().unwrap_or(h));
|
||||
}
|
||||
EntityType::Wipeout(image) => {
|
||||
image.definition_reactor_handle = image
|
||||
.definition_reactor_handle
|
||||
.map(|h| handles.get(&h).copied().unwrap_or(h));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
target.add_entity(entity).map_err(|e| e.to_string())?;
|
||||
}
|
||||
crate::scene::remap_hatch_associations(&mut target, &handles);
|
||||
for (old, mut object) in objects {
|
||||
let new = handles[&old];
|
||||
match &mut object {
|
||||
ObjectType::SortEntitiesTable(table) => {
|
||||
let source_block = table.block_owner_handle;
|
||||
let mut ordered: Vec<_> = source
|
||||
.entities()
|
||||
.filter(|e| {
|
||||
e.common().owner_handle == source_block
|
||||
&& !matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_))
|
||||
})
|
||||
.map(|e| {
|
||||
(
|
||||
table.effective_sort_handle(e.common().handle),
|
||||
e.common().handle,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
ordered.sort_by_key(|(sort, entity)| (sort.value(), entity.value()));
|
||||
table.handle = new;
|
||||
table.owner_handle = handles[&table.owner_handle];
|
||||
table.block_owner_handle = handles[&source_block];
|
||||
table.clear();
|
||||
table.raw_dxf_codes = None;
|
||||
table.raw_dxf_version = None;
|
||||
for (_, entity) in ordered {
|
||||
table.add_entry(handles[&entity], target.allocate_handle());
|
||||
}
|
||||
}
|
||||
ObjectType::Field(field) => {
|
||||
field.handle = new;
|
||||
field.owner = handles[&field.owner];
|
||||
for h in field
|
||||
.child_fields
|
||||
.iter_mut()
|
||||
.chain(field.referenced_objects.iter_mut())
|
||||
{
|
||||
if let Some(new) = handles.get(h) {
|
||||
*h = *new;
|
||||
}
|
||||
}
|
||||
for value in std::iter::once(&mut field.value)
|
||||
.chain(field.child_values.iter_mut().map(|c| &mut c.value))
|
||||
{
|
||||
if let Some(handle) = value.handle_value.as_mut() {
|
||||
if let Some(new) = handles.get(handle) {
|
||||
*handle = *new;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ObjectType::FieldList(fields) => {
|
||||
fields.handle = new;
|
||||
fields.owner = handles[&fields.owner];
|
||||
for h in &mut fields.fields {
|
||||
if let Some(new) = handles.get(h) {
|
||||
*h = *new;
|
||||
}
|
||||
}
|
||||
}
|
||||
ObjectType::PlaceHolder(placeholder) => {
|
||||
placeholder.handle = new;
|
||||
placeholder.owner = handles[&placeholder.owner];
|
||||
}
|
||||
ObjectType::ImageDefinitionReactor(reactor) => {
|
||||
reactor.handle = new;
|
||||
reactor.owner = handles[&reactor.owner];
|
||||
if let Some(new) = handles.get(&reactor.image_handle) {
|
||||
reactor.image_handle = *new;
|
||||
}
|
||||
}
|
||||
_ => super::command_driver::remap_object(&mut object, new, &remap),
|
||||
}
|
||||
if let ObjectType::XRecord(record) = &mut object {
|
||||
for reference in &mut record.object_references {
|
||||
if let Some(new) = handles.get(&reference.handle) {
|
||||
reference.handle = *new;
|
||||
}
|
||||
}
|
||||
record.reactors = record
|
||||
.reactors
|
||||
.iter()
|
||||
.filter_map(|h| handles.get(h).copied())
|
||||
.collect();
|
||||
record.xdictionary_handle = record
|
||||
.xdictionary_handle
|
||||
.map(|h| handles.get(&h).copied().unwrap_or(h));
|
||||
record.raw_dwg_data = None;
|
||||
record.raw_dwg_version = None;
|
||||
}
|
||||
target.objects.insert(new, object);
|
||||
}
|
||||
for (_, name) in &records {
|
||||
let has_attributes = target
|
||||
.entities_in_block(name)
|
||||
.any(|entity| matches!(entity, EntityType::AttributeDefinition(_)));
|
||||
target
|
||||
.block_records
|
||||
.get_mut(name)
|
||||
.unwrap()
|
||||
.flags
|
||||
.has_attributes = has_attributes;
|
||||
for entity in target.entities_in_block(name) {
|
||||
if let EntityType::Insert(insert) = entity {
|
||||
if crate::scene::block_service::would_create_cycle(
|
||||
&target,
|
||||
name,
|
||||
&insert.block_name,
|
||||
) {
|
||||
return Err(crate::scene::block_service::circular_dependency_error(
|
||||
name,
|
||||
&insert.block_name,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (old, new) in &handles {
|
||||
if let Some(field) = source.fields.get(old) {
|
||||
let mut field = field.clone();
|
||||
field.handle = *new;
|
||||
field.owner = handles.get(&field.owner).copied().unwrap_or(field.owner);
|
||||
for object in &mut field.objects {
|
||||
if let Some(new) = handles.get(object) {
|
||||
*object = *new;
|
||||
}
|
||||
}
|
||||
target.fields.insert(*new, field);
|
||||
}
|
||||
if let Some(scale) = source.context_scales.get(old) {
|
||||
target.context_scales.insert(*new, *scale);
|
||||
}
|
||||
}
|
||||
Ok(DefinitionCopy {
|
||||
record: handles[&root.handle],
|
||||
document: target,
|
||||
handles,
|
||||
})
|
||||
}
|
||||
@@ -95,7 +95,8 @@ mod utilities;
|
||||
mod viewport;
|
||||
|
||||
pub(crate) use clipboard::{
|
||||
copy_to_clipboard_kernel, paste_entities_kernel, remap_ext_subtree_reference, PasteSpace,
|
||||
copy_to_clipboard_kernel, paste_entities_kernel, remap_ext_subtree_reference, remap_object,
|
||||
PasteSpace,
|
||||
};
|
||||
|
||||
impl OpenCADStudio {
|
||||
|
||||
@@ -614,6 +614,12 @@ impl OpenCADStudio {
|
||||
"BSAVE" => {
|
||||
self.save_block_edit(i);
|
||||
}
|
||||
"BSAVEAS" => {
|
||||
self.open_block_save_as_dialog(i);
|
||||
}
|
||||
cmd if cmd.starts_with("BSAVEAS:") => {
|
||||
self.save_block_edit_as(i, &cmd["BSAVEAS:".len()..]);
|
||||
}
|
||||
"BCLOSE" => {
|
||||
if self.tabs[i].active_block_edit.is_none() {
|
||||
self.command_line
|
||||
|
||||
@@ -1035,6 +1035,20 @@ impl OpenCADStudio {
|
||||
scene.bump_constraints_epoch();
|
||||
}
|
||||
for entry in &d.parametric_constraints {
|
||||
if let crate::scene::parametric_constraints::ParametricScope::Block(owner) = entry.scope
|
||||
{
|
||||
if !scene
|
||||
.document
|
||||
.block_records
|
||||
.iter()
|
||||
.any(|record| record.handle == owner)
|
||||
{
|
||||
scene
|
||||
.parametric_constraints
|
||||
.retain(|set| set.scope != entry.scope);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let value = if undo { &entry.before } else { &entry.after };
|
||||
*scene.parametric_constraint_set_mut(entry.scope) = value.clone();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ pub(crate) mod config;
|
||||
pub use automation::{export_headless, serve};
|
||||
mod annotation_data;
|
||||
mod block_editor;
|
||||
mod block_save_as;
|
||||
#[cfg(test)]
|
||||
mod block_editor_tests;
|
||||
mod command_driver;
|
||||
@@ -570,6 +571,7 @@ pub(super) struct OpenCADStudio {
|
||||
drawing_units: Option<crate::ui::window::drawing_units::State>,
|
||||
/// Working copy of the Block Definition dialog; `None` while it is closed.
|
||||
block_definition: Option<crate::ui::window::block_definition::BlockDefinitionState>,
|
||||
block_save_as: Option<crate::ui::window::block_save_as::State>,
|
||||
/// PDF dialogs' working copies; `None` while closed.
|
||||
pdf_attach: Option<crate::ui::window::pdf_dialogs::PdfAttachState>,
|
||||
point_cloud_attach: Option<crate::ui::window::pdf_dialogs::PointCloudAttachState>,
|
||||
@@ -2177,6 +2179,7 @@ pub enum ModalKind {
|
||||
DimStyle,
|
||||
Unsaved,
|
||||
BlockEditChanges,
|
||||
BlockSaveAs,
|
||||
SaveDialog,
|
||||
Recovery,
|
||||
/// Fonts referenced by the drawing are missing on this machine.
|
||||
@@ -2807,6 +2810,8 @@ pub enum Message {
|
||||
BlockEditChangesSave,
|
||||
BlockEditChangesDiscard,
|
||||
BlockEditChangesCancel,
|
||||
BlockSaveAsName(String),
|
||||
BlockSaveAsApply,
|
||||
// ── AEC / unsupported-object drop warning (lossy Save-As) ──────────────
|
||||
/// Save as the source DWG version so the unsupported objects survive.
|
||||
AecDropSameVersion,
|
||||
@@ -4551,6 +4556,7 @@ impl OpenCADStudio {
|
||||
layer_translator: None,
|
||||
drawing_units: None,
|
||||
block_definition: None,
|
||||
block_save_as: None,
|
||||
pdf_attach: None,
|
||||
point_cloud_attach: None,
|
||||
attdef_dialog: None,
|
||||
|
||||
+24
-1
@@ -229,6 +229,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
Some(GeometricTolerance) => self.geometric_tolerance = None,
|
||||
Some(BlockDefinition) => self.block_definition = None,
|
||||
Some(BlockSaveAs) => self.block_save_as = None,
|
||||
Some(PdfAttach) => self.pdf_attach = None,
|
||||
Some(UnderlayLayers) => self.underlay_layers = None,
|
||||
Some(PdfImportSettings) => self.pdf_import_settings = None,
|
||||
@@ -443,6 +444,12 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.active_modal == Some(super::ModalKind::BlockSaveAs)
|
||||
&& (matches!(msg, Message::CommandFinalize)
|
||||
|| matches!(&msg, Message::ShortcutPressed(key) if key.rsplit('+').next() == Some("ENTER") || key.rsplit('+').next() == Some("RETURN")))
|
||||
{
|
||||
return self.apply_block_save_as_dialog();
|
||||
}
|
||||
if self.active_modal == Some(super::ModalKind::WriteBlock) {
|
||||
if matches!(msg, Message::CommandFinalize)
|
||||
|| matches!(&msg, Message::ShortcutPressed(key) if key.rsplit('+').next() == Some("ENTER") || key.rsplit('+').next() == Some("RETURN"))
|
||||
@@ -6539,7 +6546,13 @@ impl OpenCADStudio {
|
||||
self.ribbon.close_dropdown();
|
||||
return Task::none();
|
||||
}
|
||||
self.ribbon.select_dropdown_item(dropdown_id, cmd);
|
||||
// Save As is a secondary operation; the save face keeps BSAVE.
|
||||
let face = if dropdown_id == "BLOCK_SAVE" {
|
||||
"BSAVE"
|
||||
} else {
|
||||
cmd
|
||||
};
|
||||
self.ribbon.select_dropdown_item(dropdown_id, face);
|
||||
self.ribbon.activate_tool(cmd);
|
||||
self.dispatch_command(cmd)
|
||||
}
|
||||
@@ -10442,6 +10455,16 @@ impl OpenCADStudio {
|
||||
|
||||
Message::UnsavedDialogSave => self.on_unsaved_dialog_save(),
|
||||
Message::BlockEditChangesSave => self.resolve_block_edit_close(true),
|
||||
Message::BlockSaveAsName(name) => {
|
||||
let i = self.active_tab;
|
||||
let error = self.validate_block_save_as_name(i, &name).err();
|
||||
if let Some(state) = self.block_save_as.as_mut() {
|
||||
state.name = name;
|
||||
state.error = error;
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::BlockSaveAsApply => self.apply_block_save_as_dialog(),
|
||||
Message::BlockEditChangesDiscard => self.resolve_block_edit_close(false),
|
||||
Message::BlockEditChangesCancel => {
|
||||
self.cancel_block_edit_close();
|
||||
|
||||
@@ -77,6 +77,7 @@ impl OpenCADStudio {
|
||||
Some(K::LayerDeleteWarning) => crate::tr!("modal", "delete-layer"),
|
||||
Some(K::Unsaved) => crate::tr!("modal", "unsaved-changes"),
|
||||
Some(K::BlockEditChanges) => crate::t!("Block Editor").into_owned(),
|
||||
Some(K::BlockSaveAs) => crate::t!("Save Block As").into_owned(),
|
||||
Some(K::PointStyle) => crate::tr!("modal", "point-style"),
|
||||
Some(K::AttributeEditor) => crate::tr!("modal", "attribute-editor"),
|
||||
Some(K::SaveDialog) => crate::tr!("modal", "save-drawing-as"),
|
||||
@@ -498,6 +499,12 @@ impl OpenCADStudio {
|
||||
crate::ui::window::block_definition::view_window(state, flow)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::BlockSaveAs => {
|
||||
let state = self.block_save_as.as_ref()?;
|
||||
sized_flow(ex, 430, 370, |flow| {
|
||||
crate::ui::window::block_save_as::view(state, flow)
|
||||
})
|
||||
}
|
||||
super::super::ModalKind::PdfAttach => {
|
||||
let state = self.pdf_attach.as_ref()?;
|
||||
sized_flow(ex, 880, 540, |flow| {
|
||||
|
||||
@@ -36,11 +36,28 @@ impl CadModule for BlockEditorModule {
|
||||
"Edit Block",
|
||||
include_bytes!("../../../assets/icons/edit_block.svg"),
|
||||
),
|
||||
command(
|
||||
"BSAVE",
|
||||
"Save Block",
|
||||
include_bytes!("../../../assets/icons/mt_ok.svg"),
|
||||
),
|
||||
RibbonItem::LargeDropdown {
|
||||
id: "BLOCK_SAVE",
|
||||
label: "Save Block",
|
||||
icon: IconKind::Svg(include_bytes!("../../../assets/icons/mt_ok.svg")),
|
||||
items: vec![
|
||||
(
|
||||
"BSAVE",
|
||||
"Save Block",
|
||||
IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/mt_ok.svg"
|
||||
)),
|
||||
),
|
||||
(
|
||||
"BSAVEAS",
|
||||
"Save Block As",
|
||||
IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/ui/save.svg"
|
||||
)),
|
||||
),
|
||||
],
|
||||
default: "BSAVE",
|
||||
},
|
||||
command(
|
||||
"BCLOSE",
|
||||
"Close Block Editor",
|
||||
|
||||
@@ -31,6 +31,12 @@ pub fn block_edit_tools() -> Vec<ToolDef> {
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/mt_ok.svg")),
|
||||
event: ModuleEvent::Command("BSAVE".to_string()),
|
||||
},
|
||||
ToolDef {
|
||||
id: "BSAVEAS",
|
||||
label: "Save Block As",
|
||||
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/ui/save.svg")),
|
||||
event: ModuleEvent::Command("BSAVEAS".to_string()),
|
||||
},
|
||||
ToolDef {
|
||||
id: "BCLOSE",
|
||||
label: "Close Block Editor",
|
||||
@@ -111,5 +117,5 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["BEDIT_DISCARD"]
|
||||
});
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["BSAVE", "BCLOSE"]
|
||||
names: &["BSAVE", "BSAVEAS", "BCLOSE"]
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
use crate::app::Message;
|
||||
use crate::scene::WireModel;
|
||||
use iced::widget::{button, column, container, pick_list, row, text, text_input, Space};
|
||||
use iced::{Element, Fill};
|
||||
|
||||
pub(crate) struct State {
|
||||
pub document_id: u64,
|
||||
pub source: codec::Handle,
|
||||
pub name: String,
|
||||
pub existing_names: Vec<String>,
|
||||
pub preview: Vec<WireModel>,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
pub(crate) fn view(state: &State, sizing: crate::ui::modal::ModalSizing) -> Element<'_, Message> {
|
||||
let field = text_input(crate::t!("Block name").as_ref(), &state.name)
|
||||
.on_input(Message::BlockSaveAsName)
|
||||
.on_submit_maybe(state.error.is_none().then_some(Message::BlockSaveAsApply))
|
||||
.style(crate::ui::style::form::field_style);
|
||||
let names = pick_list(
|
||||
None::<String>,
|
||||
state.existing_names.as_slice(),
|
||||
|name: &String| name.clone(),
|
||||
)
|
||||
.on_select(Message::BlockSaveAsName)
|
||||
.placeholder(crate::t!("Existing blocks").into_owned())
|
||||
.width(Fill);
|
||||
container(
|
||||
column![
|
||||
text(crate::t!("Block name").into_owned()).size(13),
|
||||
field,
|
||||
names,
|
||||
super::block_palette::preview_canvas(&state.preview, 160.0),
|
||||
text(state.error.clone().unwrap_or_default())
|
||||
.size(12)
|
||||
.style(text::danger),
|
||||
row![
|
||||
Space::new().width(Fill),
|
||||
button(text(crate::t!("OK").into_owned()))
|
||||
.on_press_maybe(state.error.is_none().then_some(Message::BlockSaveAsApply))
|
||||
.style(button::primary),
|
||||
button(text(crate::t!("Cancel").into_owned())).on_press(Message::CloseModal),
|
||||
]
|
||||
.spacing(8),
|
||||
]
|
||||
.spacing(10),
|
||||
)
|
||||
.padding(16)
|
||||
.width(sizing.width)
|
||||
.height(sizing.height)
|
||||
.into()
|
||||
}
|
||||
@@ -5,6 +5,7 @@ pub mod attdef_dialog;
|
||||
pub mod attribute_editor;
|
||||
pub mod auto_constrain_settings;
|
||||
pub mod block_definition;
|
||||
pub mod block_save_as;
|
||||
pub mod block_palette;
|
||||
pub mod browser;
|
||||
pub mod count_palette;
|
||||
|
||||
Reference in new issue
Block a user