forked from 1b-agent/opencadstudio-upstream
1336 lines
43 KiB
Rust
1336 lines
43 KiB
Rust
use super::{Message, ModalKind, OpenCADStudio};
|
|
use codec::entities::{Insert, Line};
|
|
use codec::types::Vector3;
|
|
use codec::{EntityType, Handle};
|
|
|
|
fn fixture() -> (OpenCADStudio, Handle) {
|
|
let mut app = OpenCADStudio::new_for_test();
|
|
app.automation_op(r#"{"op":"new"}"#);
|
|
let i = app.active_tab;
|
|
app.tabs[i].scene.define_block_raw(
|
|
"EDIT_ME",
|
|
Vector3::ZERO,
|
|
vec![EntityType::Line(Line::from_points(
|
|
Vector3::ZERO,
|
|
Vector3::UNIT_X,
|
|
))],
|
|
);
|
|
let reference = app.tabs[i]
|
|
.scene
|
|
.document
|
|
.add_entity(EntityType::Insert(Insert::new("EDIT_ME", Vector3::ZERO)))
|
|
.unwrap();
|
|
app.tabs[i].dirty = false;
|
|
(app, reference)
|
|
}
|
|
|
|
fn begin(app: &mut OpenCADStudio, reference: Handle) {
|
|
let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", reference.value()));
|
|
assert!(app.tabs[app.active_tab].active_block_edit.is_some());
|
|
}
|
|
|
|
fn circle(app: &mut OpenCADStudio, x: f64) {
|
|
let result = app.automation_op(&format!(r#"{{"op":"run","cmd":"CIRCLE {x},0 2"}}"#));
|
|
assert_eq!(result["ok"], true, "{result}");
|
|
app.finish_pending_history(app.active_tab);
|
|
}
|
|
|
|
fn members(app: &OpenCADStudio) -> Vec<Handle> {
|
|
app.tabs[app.active_tab]
|
|
.scene
|
|
.document
|
|
.block_records
|
|
.get("EDIT_ME")
|
|
.unwrap()
|
|
.entity_handles
|
|
.clone()
|
|
}
|
|
|
|
fn main_depth(app: &OpenCADStudio) -> usize {
|
|
let tab = &app.tabs[app.active_tab];
|
|
tab.block_edit_history
|
|
.as_ref()
|
|
.map(|h| h.main.undo_stack.len())
|
|
.unwrap_or(tab.history.undo_stack.len())
|
|
}
|
|
|
|
fn feed(app: &mut OpenCADStudio, text: &str) {
|
|
let _ = app.update(Message::CommandInput(text.to_string()));
|
|
let _ = app.update(Message::CommandSubmit);
|
|
}
|
|
|
|
#[test]
|
|
fn local_undo_preserves_imported_native_networks_when_constraints_are_unchanged() {
|
|
use codec::objects::{
|
|
AssocNetwork, AssociativeData, AssociativeObject, Dictionary, ObjectType,
|
|
};
|
|
let (mut app, reference) = fixture();
|
|
let i = app.active_tab;
|
|
let doc = &mut app.tabs[i].scene.document;
|
|
let owner = doc.block_records.get("EDIT_ME").unwrap().handle;
|
|
let dictionary_handle = doc.allocate_handle();
|
|
let network_handle = doc.allocate_handle();
|
|
let mut dictionary = Dictionary::new();
|
|
dictionary.handle = dictionary_handle;
|
|
dictionary.owner = owner;
|
|
dictionary.hard_owner = true;
|
|
dictionary.add_entry("ACAD_ASSOCNETWORK", network_handle);
|
|
doc.objects
|
|
.insert(dictionary_handle, ObjectType::Dictionary(dictionary));
|
|
let network = AssociativeObject {
|
|
handle: network_handle,
|
|
owner: dictionary_handle,
|
|
reactors: vec![dictionary_handle],
|
|
dxf_name: "ASSOCNETWORK".into(),
|
|
cpp_class_name: "AcDbAssocNetwork".into(),
|
|
data: AssociativeData::Network(AssocNetwork::default()),
|
|
..Default::default()
|
|
};
|
|
doc.objects
|
|
.insert(network_handle, ObjectType::Associative(network));
|
|
begin(&mut app, reference);
|
|
let original_objects = app.tabs[i].scene.document.objects.clone();
|
|
circle(&mut app, 1.0);
|
|
assert!(!app.save_block_edit_as(i, "COPY"));
|
|
assert_eq!(app.tabs[i].scene.document.objects, original_objects);
|
|
app.undo_steps(1);
|
|
assert_eq!(members(&app).len(), 1);
|
|
assert_eq!(app.tabs[i].scene.document.objects, original_objects);
|
|
app.redo_steps(1);
|
|
assert_eq!(members(&app).len(), 2);
|
|
assert_eq!(app.tabs[i].scene.document.objects, original_objects);
|
|
}
|
|
|
|
#[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();
|
|
circle(&mut app, 50.0);
|
|
let depth = main_depth(&app);
|
|
begin(&mut app, reference);
|
|
assert_eq!(main_depth(&app), depth);
|
|
circle(&mut app, 1.0);
|
|
assert_eq!(members(&app).len(), 2);
|
|
app.undo_steps(1);
|
|
assert_eq!(members(&app).len(), 1);
|
|
assert!(!app.block_edit_dirty(app.active_tab));
|
|
app.undo_steps(100);
|
|
assert_eq!(main_depth(&app), depth);
|
|
assert!(app.tabs[app.active_tab].active_block_edit.is_some());
|
|
app.redo_steps(1);
|
|
assert_eq!(members(&app).len(), 2);
|
|
assert!(members(&app).iter().all(|h| app.tabs[app.active_tab]
|
|
.scene
|
|
.document
|
|
.get_entity(*h)
|
|
.is_some()));
|
|
}
|
|
|
|
#[test]
|
|
fn repeated_saves_stay_open_and_commit_one_drawing_step_each() {
|
|
let (mut app, reference) = fixture();
|
|
begin(&mut app, reference);
|
|
circle(&mut app, 1.0);
|
|
circle(&mut app, 2.0);
|
|
let _ = app.dispatch_command("BSAVE");
|
|
assert!(app.tabs[app.active_tab].active_block_edit.is_some());
|
|
assert_eq!(main_depth(&app), 1);
|
|
assert!(!app.block_edit_dirty(app.active_tab));
|
|
let _ = app.dispatch_command("BSAVE");
|
|
assert_eq!(main_depth(&app), 1, "a clean save is not a new edit");
|
|
circle(&mut app, 3.0);
|
|
let _ = app.dispatch_command("BSAVE");
|
|
assert_eq!(main_depth(&app), 2);
|
|
circle(&mut app, 4.0);
|
|
let _ = app.dispatch_command("BEDIT_DISCARD");
|
|
assert_eq!(members(&app).len(), 4, "discard restores the last save");
|
|
assert_eq!(main_depth(&app), 2);
|
|
assert_eq!(
|
|
app.tabs[app.active_tab]
|
|
.history
|
|
.undo_stack
|
|
.last()
|
|
.unwrap()
|
|
.label(),
|
|
"Edit block EDIT_ME"
|
|
);
|
|
app.undo_steps(1);
|
|
assert_eq!(members(&app).len(), 3);
|
|
app.undo_steps(1);
|
|
assert_eq!(members(&app).len(), 1);
|
|
app.redo_steps(2);
|
|
assert_eq!(members(&app).len(), 4);
|
|
}
|
|
|
|
#[test]
|
|
fn discard_preserves_drawing_redo_and_never_leaks_local_edits() {
|
|
let (mut app, reference) = fixture();
|
|
circle(&mut app, 50.0);
|
|
app.undo_steps(1);
|
|
let main_redo = app.tabs[app.active_tab].history.redo_stack.len();
|
|
begin(&mut app, reference);
|
|
circle(&mut app, 1.0);
|
|
let _ = app.dispatch_command("BEDIT_DISCARD");
|
|
assert_eq!(members(&app).len(), 1);
|
|
assert_eq!(app.tabs[app.active_tab].history.redo_stack.len(), main_redo);
|
|
app.redo_steps(1);
|
|
assert_eq!(members(&app).len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn dirty_close_cancel_discard_and_save_follow_the_checkpoint() {
|
|
let (mut app, reference) = fixture();
|
|
begin(&mut app, reference);
|
|
circle(&mut app, 1.0);
|
|
let _ = app.dispatch_command("BCLOSE");
|
|
assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges));
|
|
let _ = app.update(Message::BlockEditChangesCancel);
|
|
assert!(app.tabs[app.active_tab].active_block_edit.is_some());
|
|
assert_eq!(members(&app).len(), 2);
|
|
let _ = app.dispatch_command("BCLOSE");
|
|
let _ = app.update(Message::BlockEditChangesDiscard);
|
|
assert!(app.tabs[app.active_tab].active_block_edit.is_none());
|
|
assert_eq!(members(&app).len(), 1);
|
|
assert_eq!(main_depth(&app), 0);
|
|
begin(&mut app, reference);
|
|
circle(&mut app, 2.0);
|
|
let _ = app.dispatch_command("BCLOSE");
|
|
let _ = app.update(Message::BlockEditChangesSave);
|
|
assert!(app.tabs[app.active_tab].active_block_edit.is_none());
|
|
assert_eq!(members(&app).len(), 2);
|
|
assert_eq!(main_depth(&app), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn clean_close_does_not_prompt_or_dirty_the_drawing() {
|
|
let (mut app, reference) = fixture();
|
|
begin(&mut app, reference);
|
|
assert!(!app.tabs[app.active_tab].dirty);
|
|
let _ = app.dispatch_command("BCLOSE");
|
|
assert!(app.tabs[app.active_tab].active_block_edit.is_none());
|
|
assert!(app.active_modal.is_none());
|
|
assert!(!app.tabs[app.active_tab].dirty);
|
|
assert_eq!(main_depth(&app), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn dirty_switch_can_cancel_and_never_nests_sessions() {
|
|
let (mut app, reference) = fixture();
|
|
let i = app.active_tab;
|
|
app.tabs[i]
|
|
.scene
|
|
.define_block_raw("OTHER", Vector3::ZERO, vec![]);
|
|
let other = app.tabs[i]
|
|
.scene
|
|
.document
|
|
.add_entity(EntityType::Insert(Insert::new("OTHER", Vector3::ZERO)))
|
|
.unwrap();
|
|
begin(&mut app, reference);
|
|
circle(&mut app, 1.0);
|
|
let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", other.value()));
|
|
assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges));
|
|
let _ = app.update(Message::BlockEditChangesCancel);
|
|
assert_eq!(
|
|
app.tabs[i].active_block_edit_session().unwrap().block_name,
|
|
"EDIT_ME"
|
|
);
|
|
let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", other.value()));
|
|
let _ = app.update(Message::BlockEditChangesSave);
|
|
assert_eq!(app.tabs[i].block_edits.len(), 1);
|
|
assert_eq!(
|
|
app.tabs[i].active_block_edit_session().unwrap().block_name,
|
|
"OTHER"
|
|
);
|
|
assert_eq!(main_depth(&app), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn tab_close_prompts_for_block_edits_before_drawing_changes() {
|
|
let (mut app, reference) = fixture();
|
|
let id = app.tabs[app.active_tab].id;
|
|
begin(&mut app, reference);
|
|
circle(&mut app, 1.0);
|
|
let _ = app.update(Message::TabClose(id));
|
|
assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges));
|
|
let _ = app.update(Message::BlockEditChangesCancel);
|
|
assert!(app.tabs.iter().any(|t| t.id == id));
|
|
let _ = app.update(Message::TabClose(id));
|
|
let _ = app.update(Message::BlockEditChangesDiscard);
|
|
assert!(!app.tabs.iter().any(|t| t.id == id));
|
|
}
|
|
|
|
#[test]
|
|
fn save_refuses_a_cycle_introduced_outside_the_insert_command() {
|
|
let (mut app, reference) = fixture();
|
|
let i = app.active_tab;
|
|
app.tabs[i].scene.define_block_raw(
|
|
"OTHER",
|
|
Vector3::ZERO,
|
|
vec![EntityType::Insert(Insert::new("EDIT_ME", Vector3::ZERO))],
|
|
);
|
|
begin(&mut app, reference);
|
|
app.tabs[i]
|
|
.scene
|
|
.add_entity(EntityType::Insert(Insert::new("OTHER", Vector3::ZERO)));
|
|
assert!(!app.save_block_edit(i));
|
|
assert_eq!(main_depth(&app), 0);
|
|
assert!(app.tabs[i].active_block_edit.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn drawing_history_retains_the_paper_layout_after_save_and_undo() {
|
|
let (mut app, reference) = fixture();
|
|
let i = app.active_tab;
|
|
app.tabs[i].scene.add_layout("Sheet").unwrap();
|
|
app.tabs[i].scene.set_current_layout("Sheet".into());
|
|
begin(&mut app, reference);
|
|
circle(&mut app, 1.0);
|
|
let _ = app.dispatch_command("BEDIT_SAVE");
|
|
assert_eq!(app.tabs[i].scene.current_layout, "Sheet");
|
|
assert!(!app.tabs[i].scene.document.header.show_model_space);
|
|
app.undo_steps(1);
|
|
assert_eq!(app.tabs[i].scene.current_layout, "Sheet");
|
|
assert!(!app.tabs[i].scene.document.header.show_model_space);
|
|
assert_eq!(
|
|
crate::io::saved_active_layout(&app.tabs[i].scene.document).as_deref(),
|
|
Some("Sheet")
|
|
);
|
|
assert_eq!(members(&app).len(), 1);
|
|
app.redo_steps(1);
|
|
assert!(!app.tabs[i].scene.document.header.show_model_space);
|
|
assert_eq!(members(&app).len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn local_parameters_are_saved_and_discarded_with_the_definition() {
|
|
let (mut app, reference) = fixture();
|
|
begin(&mut app, reference);
|
|
let i = app.active_tab;
|
|
let scope = app.tabs[i].current_parametric_scope();
|
|
app.tabs[i]
|
|
.scene
|
|
.parametric_constraint_set_mut(scope)
|
|
.local_parameters
|
|
.set("Width", "7")
|
|
.unwrap();
|
|
assert!(app.save_block_edit(i));
|
|
app.tabs[i]
|
|
.scene
|
|
.parametric_constraint_set_mut(scope)
|
|
.local_parameters
|
|
.set("Width", "9")
|
|
.unwrap();
|
|
let _ = app.dispatch_command("BEDIT_DISCARD");
|
|
assert_eq!(
|
|
app.tabs[i]
|
|
.scene
|
|
.parametric_constraint_set(scope)
|
|
.unwrap()
|
|
.local_parameters
|
|
.resolve("Width")
|
|
.unwrap(),
|
|
7.0
|
|
);
|
|
app.undo_steps(1);
|
|
assert!(!app.tabs[i]
|
|
.scene
|
|
.parametric_constraint_set(scope)
|
|
.unwrap()
|
|
.local_parameters
|
|
.contains("Width"));
|
|
app.redo_steps(1);
|
|
assert_eq!(
|
|
app.tabs[i]
|
|
.scene
|
|
.parametric_constraint_set(scope)
|
|
.unwrap()
|
|
.local_parameters
|
|
.resolve("Width")
|
|
.unwrap(),
|
|
7.0
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn quit_prompts_for_block_edits_before_unsaved_drawing_changes() {
|
|
let (mut app, reference) = fixture();
|
|
begin(&mut app, reference);
|
|
circle(&mut app, 1.0);
|
|
let _ = app.request_quit_with_block_edits();
|
|
assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges));
|
|
let _ = app.update(Message::BlockEditChangesCancel);
|
|
assert!(app.pending_close.is_none());
|
|
assert!(app.tabs[app.active_tab].active_block_edit.is_some());
|
|
let _ = app.request_quit_with_block_edits();
|
|
let _ = app.update(Message::BlockEditChangesSave);
|
|
assert!(app.tabs[app.active_tab].active_block_edit.is_none());
|
|
assert_eq!(app.active_modal, Some(ModalKind::Unsaved));
|
|
assert!(matches!(app.pending_close, Some(super::PendingClose::Quit)));
|
|
}
|
|
|
|
#[test]
|
|
fn document_replacement_is_refused_until_the_editor_is_closed() {
|
|
let (mut app, reference) = fixture();
|
|
begin(&mut app, reference);
|
|
assert_eq!(app.automation_op(r#"{"op":"new"}"#)["ok"], false);
|
|
assert!(app.tabs[app.active_tab].active_block_edit.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn erasing_the_last_member_saves_an_empty_definition_and_undo_restores_it() {
|
|
let (mut app, reference) = fixture();
|
|
begin(&mut app, reference);
|
|
let i = app.active_tab;
|
|
let line = members(&app)[0];
|
|
app.push_undo_snapshot(i, "Erase block member");
|
|
app.tabs[i].scene.erase_entities(&[line]);
|
|
app.finish_pending_history(i);
|
|
assert!(members(&app).is_empty());
|
|
app.undo_steps(1);
|
|
assert_eq!(members(&app), vec![line]);
|
|
app.redo_steps(1);
|
|
assert!(members(&app).is_empty());
|
|
let _ = app.dispatch_command("BEDIT_SAVE");
|
|
assert!(members(&app).is_empty());
|
|
app.undo_steps(1);
|
|
assert_eq!(members(&app), vec![line]);
|
|
}
|
|
|
|
#[test]
|
|
fn close_all_resumes_after_discarding_a_dirty_block() {
|
|
let (mut app, reference) = fixture();
|
|
let first = app.tabs[app.active_tab].id;
|
|
app.tabs.push(super::document::DocumentTab::new_drawing(2));
|
|
let second = app.tabs.last().unwrap().id;
|
|
begin(&mut app, reference);
|
|
circle(&mut app, 1.0);
|
|
let _ = app.update(Message::DocTabCloseAll);
|
|
assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges));
|
|
let _ = app.update(Message::BlockEditChangesDiscard);
|
|
assert!(!app
|
|
.tabs
|
|
.iter()
|
|
.any(|tab| tab.id == first || tab.id == second));
|
|
assert!(app.pending_tab_closes.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn explode_inside_session_edits_the_definition_and_marks_dirty() {
|
|
let (mut app, reference) = fixture();
|
|
let i = app.active_tab;
|
|
app.tabs[i].scene.define_block_raw(
|
|
"CHILD",
|
|
Vector3::ZERO,
|
|
vec![EntityType::Line(Line::from_points(
|
|
Vector3::ZERO,
|
|
Vector3::new(3.0, 0.0, 0.0),
|
|
))],
|
|
);
|
|
begin(&mut app, reference);
|
|
let _ = app.dispatch_command("-INSERT");
|
|
feed(&mut app, "CHILD");
|
|
feed(&mut app, "0,5");
|
|
feed(&mut app, "1");
|
|
feed(&mut app, "1");
|
|
feed(&mut app, "0");
|
|
app.finish_pending_history(i);
|
|
let before = members(&app);
|
|
assert_eq!(before.len(), 2, "the nested INSERT joins the definition");
|
|
let insert_handle = *before.last().unwrap();
|
|
assert!(
|
|
matches!(
|
|
app.tabs[i].scene.document.get_entity(insert_handle),
|
|
Some(EntityType::Insert(_))
|
|
),
|
|
"expected the member just added to be the INSERT"
|
|
);
|
|
let depth = main_depth(&app);
|
|
app.tabs[i].scene.select_entity(insert_handle, true);
|
|
let _ = app.dispatch_command("EXPLODE");
|
|
assert!(
|
|
app.block_edit_dirty(i),
|
|
"the explode marks the session dirty"
|
|
);
|
|
assert_eq!(main_depth(&app), depth, "the main stack is untouched");
|
|
let _ = app.dispatch_command("BSAVE");
|
|
let saved = members(&app);
|
|
assert!(
|
|
saved
|
|
.iter()
|
|
.all(|h| app.tabs[i].scene.document.get_entity(*h).is_some()),
|
|
"the saved definition carries no dangling member handle: {saved:?}"
|
|
);
|
|
assert_eq!(saved.len(), 2, "the INSERT is replaced by its piece");
|
|
assert!(
|
|
!saved.contains(&insert_handle),
|
|
"the exploded INSERT leaves the saved definition"
|
|
);
|
|
assert!(
|
|
saved.iter().any(|h| {
|
|
matches!(
|
|
app.tabs[i].scene.document.get_entity(*h),
|
|
Some(EntityType::Line(_))
|
|
)
|
|
}),
|
|
"the child line lands as a member of the edited block"
|
|
);
|
|
let _ = app.dispatch_command("BEDIT_DISCARD");
|
|
}
|
|
|
|
#[test]
|
|
fn xclip_inside_session_stays_in_the_definition() {
|
|
let (mut app, reference) = fixture();
|
|
let i = app.active_tab;
|
|
app.tabs[i].scene.define_block_raw(
|
|
"CHILD",
|
|
Vector3::ZERO,
|
|
vec![EntityType::Line(Line::from_points(
|
|
Vector3::ZERO,
|
|
Vector3::new(3.0, 0.0, 0.0),
|
|
))],
|
|
);
|
|
begin(&mut app, reference);
|
|
let _ = app.dispatch_command("-INSERT");
|
|
feed(&mut app, "CHILD");
|
|
feed(&mut app, "0,5");
|
|
feed(&mut app, "1");
|
|
feed(&mut app, "1");
|
|
feed(&mut app, "0");
|
|
app.finish_pending_history(i);
|
|
let before = members(&app);
|
|
let insert_handle = *before.last().unwrap();
|
|
let depth = main_depth(&app);
|
|
app.tabs[i].scene.select_entity(insert_handle, true);
|
|
let _ = app.dispatch_command("XCLIP");
|
|
feed(&mut app, "N");
|
|
feed(&mut app, "R");
|
|
feed(&mut app, "0,0");
|
|
feed(&mut app, "10,10");
|
|
app.finish_pending_history(i);
|
|
assert!(
|
|
crate::scene::pick::xclip::filter_handle(&app.tabs[i].scene.document, insert_handle)
|
|
.is_some(),
|
|
"the clip filter attaches to the in-block reference"
|
|
);
|
|
assert!(
|
|
members(&app).contains(&insert_handle),
|
|
"the clipped reference stays a member of the definition"
|
|
);
|
|
assert!(app.block_edit_dirty(i), "the clip marks the session dirty");
|
|
assert_eq!(main_depth(&app), depth, "the main stack is untouched");
|
|
let _ = app.dispatch_command("BEDIT_DISCARD");
|
|
}
|
|
|
|
#[test]
|
|
fn attdef_inside_session_lands_in_the_definition_and_undo_stays_local() {
|
|
let (mut app, reference) = fixture();
|
|
let i = app.active_tab;
|
|
let depth = main_depth(&app);
|
|
begin(&mut app, reference);
|
|
assert_eq!(members(&app).len(), 1);
|
|
let _ = app.dispatch_command("-ATTDEF");
|
|
feed(&mut app, ""); // modes: done
|
|
feed(&mut app, "TAG1");
|
|
feed(&mut app, "prompt text");
|
|
feed(&mut app, ""); // default value
|
|
feed(&mut app, ""); // single line
|
|
feed(&mut app, "0,5");
|
|
feed(&mut app, "2");
|
|
feed(&mut app, "0");
|
|
app.finish_pending_history(i);
|
|
let after = members(&app);
|
|
assert_eq!(after.len(), 2, "the ATTDEF joins the definition");
|
|
assert!(
|
|
matches!(
|
|
app.tabs[i]
|
|
.scene
|
|
.document
|
|
.get_entity(*after.last().unwrap()),
|
|
Some(EntityType::AttributeDefinition(_))
|
|
),
|
|
"the new member is the ATTDEF"
|
|
);
|
|
assert!(
|
|
app.block_edit_dirty(i),
|
|
"the ATTDEF marks the session dirty"
|
|
);
|
|
assert_eq!(main_depth(&app), depth, "the main stack is untouched");
|
|
app.undo_steps(1);
|
|
assert_eq!(members(&app).len(), 1, "session undo removes the ATTDEF");
|
|
assert_eq!(main_depth(&app), depth, "the main stack is still untouched");
|
|
let _ = app.dispatch_command("BEDIT_DISCARD");
|
|
}
|
|
|
|
#[test]
|
|
fn save_after_session_undo_does_not_write_a_dangling_member() {
|
|
let (mut app, reference) = fixture();
|
|
begin(&mut app, reference);
|
|
circle(&mut app, 1.0);
|
|
assert_eq!(members(&app).len(), 2);
|
|
app.undo_steps(1);
|
|
assert_eq!(members(&app).len(), 1);
|
|
let depth = main_depth(&app);
|
|
let _ = app.dispatch_command("BSAVE");
|
|
assert!(app.tabs[app.active_tab].active_block_edit.is_some());
|
|
let saved = members(&app);
|
|
assert!(
|
|
saved.iter().all(|h| app.tabs[app.active_tab]
|
|
.scene
|
|
.document
|
|
.get_entity(*h)
|
|
.is_some()),
|
|
"save after a session undo must not write a member handle whose entity is gone: {saved:?}"
|
|
);
|
|
assert_eq!(
|
|
main_depth(&app),
|
|
depth,
|
|
"a clean save commits no new drawing step"
|
|
);
|
|
let _ = app.dispatch_command("BEDIT_DISCARD");
|
|
assert_eq!(members(&app).len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn locked_bedit_no_selection_opens_edit_block_definition_dialog() {
|
|
let mut app = OpenCADStudio::new_for_test();
|
|
app.automation_op(r#"{"op":"new"}"#);
|
|
let tab = app.active_tab;
|
|
app.tabs[tab].scene.define_block_raw(
|
|
"DOOR",
|
|
Vector3::ZERO,
|
|
vec![EntityType::Line(Line::from_points(
|
|
Vector3::ZERO,
|
|
Vector3::UNIT_X,
|
|
))],
|
|
);
|
|
app.tabs[tab]
|
|
.scene
|
|
.document
|
|
.block_records
|
|
.get_mut("DOOR")
|
|
.unwrap()
|
|
.description = "Wooden door".to_string();
|
|
app.tabs[tab]
|
|
.scene
|
|
.define_block_raw("WINDOW", Vector3::ZERO, vec![]);
|
|
|
|
app.tabs[tab].scene.deselect_all();
|
|
let _ = app.dispatch_command("BEDIT");
|
|
|
|
assert_eq!(app.active_modal, Some(ModalKind::BlockEditPicker));
|
|
let picker = app.block_edit_picker.as_ref().expect("picker state set");
|
|
assert_eq!(picker.existing_names, vec!["DOOR", "WINDOW"]);
|
|
assert_eq!(picker.name, "DOOR");
|
|
assert_eq!(picker.description, "Wooden door");
|
|
assert!(picker.error.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn locked_bedit_dialog_select_existing_block() {
|
|
let mut app = OpenCADStudio::new_for_test();
|
|
app.automation_op(r#"{"op":"new"}"#);
|
|
let tab = app.active_tab;
|
|
app.tabs[tab]
|
|
.scene
|
|
.define_block_raw("DOOR", Vector3::ZERO, vec![]);
|
|
app.tabs[tab]
|
|
.scene
|
|
.define_block_raw("WINDOW", Vector3::ZERO, vec![]);
|
|
|
|
app.tabs[tab].scene.deselect_all();
|
|
let _ = app.dispatch_command("BEDIT");
|
|
|
|
let _ = app.update(Message::BlockEditPickerSelect("WINDOW".to_string()));
|
|
let picker = app.block_edit_picker.as_ref().unwrap();
|
|
assert_eq!(picker.name, "WINDOW");
|
|
|
|
let _ = app.update(Message::BlockEditPickerApply);
|
|
assert_eq!(app.active_modal, None);
|
|
assert!(app.tabs[tab].active_block_edit.is_some());
|
|
assert_eq!(
|
|
app.tabs[tab]
|
|
.active_block_edit_session()
|
|
.unwrap()
|
|
.block_name,
|
|
"WINDOW"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn locked_bedit_dialog_name_validation() {
|
|
let mut app = OpenCADStudio::new_for_test();
|
|
app.automation_op(r#"{"op":"new"}"#);
|
|
let tab = app.active_tab;
|
|
app.tabs[tab].scene.deselect_all();
|
|
let _ = app.dispatch_command("BEDIT");
|
|
|
|
let _ = app.update(Message::BlockEditPickerName("INVALID/NAME".to_string()));
|
|
assert!(app.block_edit_picker.as_ref().unwrap().error.is_some());
|
|
|
|
let _ = app.update(Message::BlockEditPickerApply);
|
|
assert_eq!(app.active_modal, Some(ModalKind::BlockEditPicker));
|
|
assert!(app.tabs[tab].active_block_edit.is_none());
|
|
|
|
let _ = app.update(Message::BlockEditPickerName("".to_string()));
|
|
let _ = app.update(Message::BlockEditPickerApply);
|
|
assert_eq!(app.active_modal, Some(ModalKind::BlockEditPicker));
|
|
assert!(app.tabs[tab].active_block_edit.is_none());
|
|
|
|
let _ = app.update(Message::CloseModal);
|
|
assert_eq!(app.active_modal, None);
|
|
}
|
|
|
|
#[test]
|
|
fn locked_bedit_dialog_create_new_block_closes_without_saving() {
|
|
let mut app = OpenCADStudio::new_for_test();
|
|
app.automation_op(r#"{"op":"new"}"#);
|
|
let tab = app.active_tab;
|
|
app.tabs[tab].scene.deselect_all();
|
|
let _ = app.dispatch_command("BEDIT");
|
|
|
|
let _ = app.update(Message::BlockEditPickerName("BRAND_NEW".to_string()));
|
|
assert!(app.block_edit_picker.as_ref().unwrap().error.is_none());
|
|
|
|
let _ = app.update(Message::BlockEditPickerApply);
|
|
assert_eq!(app.active_modal, None);
|
|
assert!(app.tabs[tab].active_block_edit.is_some());
|
|
assert_eq!(
|
|
app.tabs[tab]
|
|
.active_block_edit_session()
|
|
.unwrap()
|
|
.block_name,
|
|
"BRAND_NEW"
|
|
);
|
|
assert!(!app.block_edit_dirty(tab));
|
|
|
|
let _ = app.dispatch_command("BCLOSE");
|
|
assert_eq!(app.active_modal, None);
|
|
assert!(app.tabs[tab].active_block_edit.is_none());
|
|
assert!(
|
|
app.tabs[tab]
|
|
.scene
|
|
.document
|
|
.block_records
|
|
.get("BRAND_NEW")
|
|
.is_none(),
|
|
"unpersisted new block closed without saving must not remain in drawing"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn locked_bedit_dialog_create_new_block_discard_dirty_leaves_no_block() {
|
|
let mut app = OpenCADStudio::new_for_test();
|
|
app.automation_op(r#"{"op":"new"}"#);
|
|
let tab = app.active_tab;
|
|
app.tabs[tab].scene.deselect_all();
|
|
let _ = app.dispatch_command("BEDIT");
|
|
|
|
let _ = app.update(Message::BlockEditPickerName("BRAND_NEW2".to_string()));
|
|
let _ = app.update(Message::BlockEditPickerApply);
|
|
assert!(app.tabs[tab].active_block_edit.is_some());
|
|
|
|
circle(&mut app, 1.0);
|
|
assert!(app.block_edit_dirty(tab));
|
|
|
|
let _ = app.dispatch_command("BCLOSE");
|
|
assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges));
|
|
|
|
let _ = app.update(Message::BlockEditChangesDiscard);
|
|
assert_eq!(app.active_modal, None);
|
|
assert!(app.tabs[tab].active_block_edit.is_none());
|
|
assert!(
|
|
app.tabs[tab]
|
|
.scene
|
|
.document
|
|
.block_records
|
|
.get("BRAND_NEW2")
|
|
.is_none(),
|
|
"discarded new block must not remain in drawing"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn locked_bedit_dialog_create_new_block_save_persists_and_undoes() {
|
|
let mut app = OpenCADStudio::new_for_test();
|
|
app.automation_op(r#"{"op":"new"}"#);
|
|
let tab = app.active_tab;
|
|
app.tabs[tab].scene.deselect_all();
|
|
let _ = app.dispatch_command("BEDIT");
|
|
|
|
let _ = app.update(Message::BlockEditPickerName("SAVED_BLOCK".to_string()));
|
|
let _ = app.update(Message::BlockEditPickerApply);
|
|
assert!(app.tabs[tab].active_block_edit.is_some());
|
|
|
|
circle(&mut app, 1.0);
|
|
let _ = app.dispatch_command("BSAVE");
|
|
assert!(app.tabs[tab]
|
|
.scene
|
|
.document
|
|
.block_records
|
|
.get("SAVED_BLOCK")
|
|
.is_some());
|
|
|
|
let _ = app.dispatch_command("BCLOSE");
|
|
assert!(app.tabs[tab].active_block_edit.is_none());
|
|
assert!(app.tabs[tab]
|
|
.scene
|
|
.document
|
|
.block_records
|
|
.get("SAVED_BLOCK")
|
|
.is_some());
|
|
|
|
app.undo_steps(1);
|
|
assert!(
|
|
app.tabs[tab]
|
|
.scene
|
|
.document
|
|
.block_records
|
|
.get("SAVED_BLOCK")
|
|
.is_none(),
|
|
"undo must revert block creation"
|
|
);
|
|
|
|
app.redo_steps(1);
|
|
assert!(
|
|
app.tabs[tab]
|
|
.scene
|
|
.document
|
|
.block_records
|
|
.get("SAVED_BLOCK")
|
|
.is_some(),
|
|
"redo must restore block creation"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn locked_bedit_with_preselected_insert_opens_directly() {
|
|
let (mut app, reference) = fixture();
|
|
let tab = app.active_tab;
|
|
app.tabs[tab].scene.deselect_all();
|
|
app.tabs[tab].scene.select_entity(reference, true);
|
|
|
|
let _ = app.dispatch_command("BEDIT");
|
|
assert_eq!(app.active_modal, None);
|
|
assert!(app.tabs[tab].active_block_edit.is_some());
|
|
assert_eq!(
|
|
app.tabs[tab]
|
|
.active_block_edit_session()
|
|
.unwrap()
|
|
.block_name,
|
|
"EDIT_ME"
|
|
);
|
|
}
|