forked from 1b-agent/opencadstudio-upstream
Verify disposable reference geometry follows selected table rows while the editor definition remains unchanged.
450 lines
16 KiB
Rust
450 lines
16 KiB
Rust
use super::{Message, ModalKind, OpenCADStudio};
|
|
use codec::entities::{Insert, Line};
|
|
use codec::types::Vector3;
|
|
use codec::{EntityType, Handle};
|
|
|
|
fn fixture() -> (OpenCADStudio, usize, 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(
|
|
"SAMPLE",
|
|
Vector3::new(10.0, 20.0, 0.0),
|
|
vec![EntityType::Line(Line::from_points(
|
|
Vector3::new(10.0, 20.0, 0.0),
|
|
Vector3::new(15.0, 20.0, 0.0),
|
|
))],
|
|
);
|
|
app.tabs[i]
|
|
.scene
|
|
.document
|
|
.block_records
|
|
.get_mut("SAMPLE")
|
|
.unwrap()
|
|
.base_point = Vector3::new(10.0, 20.0, 0.0);
|
|
let reference = app.tabs[i]
|
|
.scene
|
|
.document
|
|
.add_entity(EntityType::Insert(Insert::new(
|
|
"SAMPLE",
|
|
Vector3::new(100.0, 200.0, 0.0),
|
|
)))
|
|
.unwrap();
|
|
let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", reference.value()));
|
|
(app, i, reference)
|
|
}
|
|
|
|
#[test]
|
|
fn block_test_uses_unsaved_definition_and_preserves_editor_history_and_view() {
|
|
let (mut app, source, _) = fixture();
|
|
assert_eq!(
|
|
app.automation_op(r#"{"op":"run","cmd":"CIRCLE 12,20 2"}"#)["ok"],
|
|
true
|
|
);
|
|
app.finish_pending_history(source);
|
|
let document = app.tabs[source].scene.document.clone();
|
|
let depth = app.tabs[source].history.undo_stack.len();
|
|
let camera = app.tabs[source].scene.camera.borrow().clone();
|
|
let source_id = app.tabs[source].id;
|
|
assert_eq!(
|
|
app.automation_op(r#"{"op":"run","cmd":"BTESTBLOCK"}"#)["ok"],
|
|
true
|
|
);
|
|
let test = app.active_tab;
|
|
assert_ne!(app.tabs[test].id, source_id);
|
|
assert!(app.tabs[test].active_block_edit.is_none());
|
|
assert_eq!(
|
|
app.tabs[test].scene.document.block_records.get("SAMPLE"),
|
|
document.block_records.get("SAMPLE")
|
|
);
|
|
let model = app.tabs[test]
|
|
.scene
|
|
.document
|
|
.header
|
|
.model_space_block_handle;
|
|
let top: Vec<_> = app.tabs[test]
|
|
.scene
|
|
.document
|
|
.entities()
|
|
.filter(|e| {
|
|
e.common().owner_handle == model
|
|
&& !matches!(e, EntityType::Block(_) | EntityType::BlockEnd(_))
|
|
})
|
|
.collect();
|
|
assert_eq!(top.len(), 1);
|
|
let EntityType::Insert(reference) = top[0] else {
|
|
panic!("missing test reference")
|
|
};
|
|
assert_eq!(reference.insert_point, Vector3::ZERO);
|
|
assert_eq!(reference.block_name, "SAMPLE");
|
|
assert!(app.tabs[test].history.undo_stack.is_empty());
|
|
assert_eq!(
|
|
app.automation_op(r#"{"op":"run","cmd":"CIRCLE 1000,1000 8"}"#)["ok"],
|
|
true
|
|
);
|
|
assert_eq!(
|
|
app.automation_op(r#"{"op":"run","cmd":"BCLOSETESTBLOCK"}"#)["ok"],
|
|
true
|
|
);
|
|
assert_eq!(app.active_tab, source);
|
|
assert_eq!(app.tabs[source].scene.document, document);
|
|
assert_eq!(app.tabs[source].history.undo_stack.len(), depth);
|
|
assert_eq!(app.tabs[source].scene.camera.borrow().target, camera.target);
|
|
assert!(app.active_modal.is_none());
|
|
app.undo_steps(1);
|
|
assert_eq!(
|
|
app.tabs[source]
|
|
.scene
|
|
.document
|
|
.entities_in_block("SAMPLE")
|
|
.count(),
|
|
1
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn block_test_keytips_follow_context_and_offer_close() {
|
|
let (mut app, source, _) = fixture();
|
|
let tab_tip = |app: &OpenCADStudio, target: &str| {
|
|
app.ribbon.keytips_json(false)["tips"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.any(|tip| tip["target"] == target)
|
|
};
|
|
app.ribbon.show_keytips(false);
|
|
assert!(!tab_tip(&app, "block_test"));
|
|
let _ = app.ribbon.keytip_type(Some("H"));
|
|
assert!(tab_tip(&app, "LINE"));
|
|
assert!(!tab_tip(&app, "BCLOSETESTBLOCK"));
|
|
let _ = app.ribbon.hide_keytips();
|
|
let _ = app.open_block_test(source);
|
|
app.ribbon.show_keytips(false);
|
|
assert_eq!(app.ribbon.keytips_json(false)["tab"], "draw");
|
|
assert!(!tab_tip(&app, "block_test"));
|
|
assert!(!tab_tip(&app, "block_editor"));
|
|
let _ = app.ribbon.keytip_type(Some("H"));
|
|
assert!(tab_tip(&app, "LINE"));
|
|
assert!(tab_tip(&app, "Close"));
|
|
assert!(tab_tip(&app, "BCLOSETESTBLOCK"));
|
|
let _ = app.ribbon.keytip_type(Some("C"));
|
|
let crate::ui::ribbon::KeyTipStep::Run(close) = app.ribbon.keytip_type(Some("T")) else {
|
|
panic!("close key tip did not dispatch a ribbon action");
|
|
};
|
|
assert!(matches!(
|
|
&close,
|
|
Message::RibbonToolClick { event: crate::modules::ModuleEvent::Command(command), .. }
|
|
if command == "BCLOSETESTBLOCK"
|
|
));
|
|
let _ = app.ribbon.hide_keytips();
|
|
let _ = app.update(close);
|
|
app.ribbon.show_keytips(false);
|
|
assert!(!tab_tip(&app, "block_test"));
|
|
assert!(tab_tip(&app, "block_editor"));
|
|
assert_eq!(app.ribbon.keytips_json(false)["tab"], "block_editor");
|
|
let _ = app.ribbon.keytip_type(Some("H"));
|
|
assert!(!tab_tip(&app, "Close"));
|
|
assert!(!tab_tip(&app, "BCLOSETESTBLOCK"));
|
|
let _ = app.ribbon.hide_keytips();
|
|
}
|
|
|
|
#[test]
|
|
fn block_test_close_x_discards_dirty_edits_and_repeated_test_refreshes() {
|
|
let (mut app, source, _) = fixture();
|
|
let _ = app.open_block_test(source);
|
|
let first = app.tabs[app.active_tab].id;
|
|
let _ = app.open_block_test(app.active_tab);
|
|
assert!(!app.tabs.iter().any(|tab| tab.id == first));
|
|
assert_eq!(
|
|
app.tabs
|
|
.iter()
|
|
.filter(|tab| tab.block_test.is_some())
|
|
.count(),
|
|
1
|
|
);
|
|
let i = app.active_tab;
|
|
app.tabs[i].dirty = true;
|
|
let _ = app.on_tab_close(i);
|
|
assert_eq!(app.active_tab, source);
|
|
assert!(app.pending_close.is_none());
|
|
assert!(app.active_modal.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn block_test_refuses_persistence_and_replacing_the_scratch_drawing() {
|
|
let (mut app, source, _) = fixture();
|
|
let _ = app.open_block_test(source);
|
|
let test = app.active_tab;
|
|
for command in [
|
|
"SAVE",
|
|
"QSAVE",
|
|
"SAVEAS",
|
|
"SAVEALL",
|
|
"BSAVE",
|
|
"BSAVEAS:OTHER",
|
|
"BEDIT_OPEN:SAMPLE",
|
|
"REFEDIT",
|
|
] {
|
|
let request = serde_json::json!({"op":"run", "cmd":command});
|
|
assert_eq!(
|
|
app.automation_op(&request.to_string())["ok"],
|
|
false,
|
|
"{command}"
|
|
);
|
|
}
|
|
assert_eq!(
|
|
app.automation_op(r#"{"op":"save","path":"blocked-test.dwg"}"#)["ok"],
|
|
false
|
|
);
|
|
assert_eq!(app.automation_op(r#"{"op":"new"}"#)["ok"], false);
|
|
let _ = app.update(Message::SaveFile);
|
|
let _ = app.update(Message::SaveAs);
|
|
assert!(app.active_modal.is_none());
|
|
assert_eq!(app.active_tab, test);
|
|
assert!(app.tabs[test].current_path.is_none());
|
|
assert!(app.active_save_jobs.is_empty());
|
|
app.tabs[test].dirty = true;
|
|
let _ = app.update(Message::AutoSave);
|
|
assert!(app.active_save_jobs.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn block_test_parent_close_cancel_keeps_test_and_discard_removes_it() {
|
|
let (mut app, source, _) = fixture();
|
|
app.automation_op(r#"{"op":"run","cmd":"CIRCLE 12,20 2"}"#);
|
|
let _ = app.open_block_test(source);
|
|
let test_id = app.tabs[app.active_tab].id;
|
|
let _ = app.on_tab_close(source);
|
|
assert_eq!(app.active_modal, Some(ModalKind::BlockEditChanges));
|
|
let _ = app.update(Message::BlockEditChangesCancel);
|
|
assert!(app.tabs.iter().any(|tab| tab.id == test_id));
|
|
let _ = app.on_tab_close(source);
|
|
let _ = app.update(Message::BlockEditChangesDiscard);
|
|
assert!(!app.tabs.iter().any(|tab| tab.id == test_id));
|
|
assert!(!app.tabs.iter().any(|tab| tab.block_test.is_some()));
|
|
}
|
|
|
|
#[test]
|
|
fn block_test_no_editor_is_refused() {
|
|
let mut app = OpenCADStudio::new_for_test();
|
|
app.automation_op(r#"{"op":"new"}"#);
|
|
let count = app.tabs.len();
|
|
let _ = app.open_block_test(app.active_tab);
|
|
assert_eq!(app.tabs.len(), count);
|
|
assert!(app.command_line.last_error.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn block_test_reordering_before_its_source_keeps_close_targets_stable() {
|
|
let (mut app, source, _) = fixture();
|
|
let source_id = app.tabs[source].id;
|
|
let _ = app.open_block_test(source);
|
|
let test = app.active_tab;
|
|
app.tabs.swap(source, test);
|
|
app.active_tab = test;
|
|
let _ = app.dispatch_command("BEDIT_DISCARD");
|
|
assert!(!app.tabs.iter().any(|tab| tab.block_test.is_some()));
|
|
assert_eq!(app.tabs[app.active_tab].id, source_id);
|
|
assert!(app.tabs[app.active_tab].active_block_edit.is_none());
|
|
assert_eq!(app.tabs.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn block_test_unsaved_visibility_states_work_without_changing_the_editor() {
|
|
let (mut app, source, _) = fixture();
|
|
let doc = &mut app.tabs[source].scene.document;
|
|
let owner = doc.block_records.get("SAMPLE").unwrap().handle;
|
|
let first = doc.block_records.get("SAMPLE").unwrap().entity_handles[0];
|
|
let mut line = Line::from_points(Vector3::new(10.0, 23.0, 0.0), Vector3::new(15.0, 23.0, 0.0));
|
|
line.common.owner_handle = owner;
|
|
let second = doc.add_entity(EntityType::Line(line)).unwrap();
|
|
let parameter = crate::scene::block_visibility::create(doc, owner, Vector3::ZERO).unwrap();
|
|
crate::scene::block_visibility::add_state(doc, parameter, "Alternative", 0).unwrap();
|
|
crate::scene::block_visibility::set_visible(doc, parameter, 0, &[second], false);
|
|
crate::scene::block_visibility::set_visible(doc, parameter, 1, &[first], false);
|
|
let original = app.tabs[source].scene.document.clone();
|
|
let _ = app.open_block_test(source);
|
|
let test = app.active_tab;
|
|
let doc = &app.tabs[test].scene.document;
|
|
let model = doc.header.model_space_block_handle;
|
|
let reference = doc
|
|
.entities()
|
|
.find(|e| e.common().owner_handle == model && matches!(e, EntityType::Insert(_)))
|
|
.unwrap()
|
|
.common()
|
|
.handle;
|
|
assert!(doc.dynamic_visibility_for_insert(reference).is_some());
|
|
app.apply_visibility_state(reference, 1);
|
|
app.finish_pending_history(test);
|
|
let doc = &app.tabs[test].scene.document;
|
|
let EntityType::Insert(insert) = doc.get_entity(reference).unwrap() else {
|
|
unreachable!()
|
|
};
|
|
assert_ne!(insert.block_name, "SAMPLE");
|
|
let visible: Vec<_> = doc
|
|
.entities_in_block(&insert.block_name)
|
|
.filter(|e| !e.common().invisible)
|
|
.collect();
|
|
assert_eq!(visible.len(), 1);
|
|
let EntityType::Line(line) = visible[0] else {
|
|
panic!("expected the alternative line")
|
|
};
|
|
assert_eq!(line.start.y, 23.0);
|
|
assert_eq!(app.tabs[source].scene.document, original);
|
|
app.visibility_popup = Some(super::visibility::VisibilityPopup {
|
|
insert_handle: reference,
|
|
anchor: iced::Point::ORIGIN,
|
|
items: vec!["Alternative".into()],
|
|
current: Some(1),
|
|
});
|
|
let _ = app.close_block_test(test);
|
|
assert!(app.visibility_popup.is_none());
|
|
assert_eq!(app.tabs[source].scene.document, original);
|
|
}
|
|
|
|
#[test]
|
|
fn block_test_lookup_choice_evaluates_temporary_reference_only() {
|
|
use crate::scene::model::object::PropValue;
|
|
use crate::scene::{block_actions, block_distance_parameter as distance, block_lookup};
|
|
use codec::objects::BlockLookupAction;
|
|
|
|
let (mut app, source, _) = fixture();
|
|
let definition = app.tabs[source]
|
|
.active_block_edit_session()
|
|
.unwrap()
|
|
.br_handle;
|
|
let line = app.tabs[source]
|
|
.scene
|
|
.document
|
|
.block_records
|
|
.get("SAMPLE")
|
|
.unwrap()
|
|
.entity_handles[0];
|
|
let doc = &mut app.tabs[source].scene.document;
|
|
let parameter = distance::create(
|
|
doc,
|
|
definition,
|
|
distance::Kind::Linear,
|
|
Vector3::new(10.0, 20.0, 0.0),
|
|
Vector3::new(12.0, 20.0, 0.0),
|
|
Vector3::new(10.0, 21.0, 0.0),
|
|
)
|
|
.unwrap();
|
|
block_actions::create(
|
|
doc,
|
|
definition,
|
|
block_actions::Kind::Move,
|
|
parameter,
|
|
&[line],
|
|
glam::DVec3::new(10.0, 20.0, 0.0),
|
|
)
|
|
.unwrap();
|
|
let lookup_parameter =
|
|
block_lookup::create(doc, definition, Vector3::new(11.0, 21.0, 0.0)).unwrap();
|
|
let mut table = block_lookup::empty_table(
|
|
block_lookup::parameters(doc, definition)
|
|
.into_iter()
|
|
.find(|(handle, _)| *handle == lookup_parameter)
|
|
.unwrap()
|
|
.1,
|
|
);
|
|
table.columns.insert(
|
|
0,
|
|
block_lookup::properties(doc, definition)[0].column.clone(),
|
|
);
|
|
table.column_count = 2;
|
|
table.row_count = 2;
|
|
table.expressions = vec!["2".into(), "Small".into(), "4".into(), "Large".into()];
|
|
block_lookup::apply_table(doc, definition, lookup_parameter, table).unwrap();
|
|
|
|
let source_before = app.tabs[source].scene.document.clone();
|
|
let _ = app.open_block_test(source);
|
|
let test = app.active_tab;
|
|
let insert = app.tabs[test].block_test.as_ref().unwrap().insert;
|
|
app.tabs[test].scene.selected.insert(insert);
|
|
app.tabs[test].selected_handle = Some(insert);
|
|
app.refresh_properties();
|
|
let lookup_property = app.tabs[test]
|
|
.properties
|
|
.sections
|
|
.iter()
|
|
.flat_map(|section| §ion.props)
|
|
.find(|property| property.field == "dyn_lookup")
|
|
.expect("Test Block should expose the Lookup table choice");
|
|
let PropValue::Choice { selected, options } = &lookup_property.value else {
|
|
panic!("Lookup table property should be a choice")
|
|
};
|
|
assert_eq!(selected, "Small");
|
|
assert_eq!(options, &["Small", "Large"]);
|
|
|
|
let _ = app.update(super::Message::PropGeomChoiceChanged {
|
|
field: "dyn_lookup",
|
|
value: "Large".into(),
|
|
});
|
|
let EntityType::Insert(evaluated) = app.tabs[test].scene.document.get_entity(insert).unwrap()
|
|
else {
|
|
panic!("expected the temporary Test Block reference")
|
|
};
|
|
assert_ne!(evaluated.block_name, "SAMPLE");
|
|
let member = app.tabs[test]
|
|
.scene
|
|
.document
|
|
.block_records
|
|
.get(&evaluated.block_name)
|
|
.unwrap()
|
|
.entity_handles[0];
|
|
let EntityType::Line(line) = app.tabs[test].scene.document.get_entity(member).unwrap() else {
|
|
panic!("expected the materialized line")
|
|
};
|
|
assert_eq!(line.start, Vector3::new(12.0, 20.0, 0.0));
|
|
assert_eq!(line.end, Vector3::new(17.0, 20.0, 0.0));
|
|
assert_eq!(app.tabs[source].scene.document, source_before);
|
|
assert!(matches!(
|
|
block_lookup::table(&app.tabs[test].scene.document, definition, lookup_parameter).unwrap(),
|
|
Some((_, BlockLookupAction { row_count: 2, .. }))
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn block_test_attributes_use_defaults_and_the_stored_base_point() {
|
|
use codec::entities::AttributeDefinition;
|
|
let (mut app, source, _) = fixture();
|
|
let doc = &mut app.tabs[source].scene.document;
|
|
let owner = doc.block_records.get("SAMPLE").unwrap().handle;
|
|
let mut attribute = AttributeDefinition::new("TAG".into(), "Value".into(), "default".into());
|
|
attribute.insertion_point = Vector3::new(12.0, 23.0, 0.0);
|
|
attribute.common.owner_handle = owner;
|
|
let definition = doc
|
|
.add_entity(EntityType::AttributeDefinition(attribute))
|
|
.unwrap();
|
|
let _ = app.open_block_test(source);
|
|
let doc = &app.tabs[app.active_tab].scene.document;
|
|
let model = doc.header.model_space_block_handle;
|
|
let insert = doc
|
|
.entities()
|
|
.find_map(|entity| match entity {
|
|
EntityType::Insert(insert) if insert.common.owner_handle == model => Some(insert),
|
|
_ => None,
|
|
})
|
|
.unwrap();
|
|
assert_eq!(insert.attributes.len(), 1);
|
|
let attribute = &insert.attributes[0];
|
|
assert_eq!(attribute.value, "default");
|
|
assert_eq!(attribute.insertion_point, Vector3::new(2.0, 3.0, 0.0));
|
|
assert_eq!(attribute.attdef_handle, definition);
|
|
assert!(!attribute.common.handle.is_null());
|
|
assert_eq!(attribute.common.owner_handle, insert.common.handle);
|
|
}
|
|
|
|
#[test]
|
|
fn block_test_close_others_discards_the_test_without_prompting() {
|
|
let (mut app, source, _) = fixture();
|
|
let _ = app.open_block_test(source);
|
|
app.tabs[app.active_tab].dirty = true;
|
|
let test = app.tabs[app.active_tab].id;
|
|
let _ = app.update(Message::DocTabCloseOthers(source));
|
|
assert!(!app.tabs.iter().any(|tab| tab.id == test));
|
|
assert!(app.active_modal.is_none());
|
|
assert!(app.tabs[source].active_block_edit.is_some());
|
|
}
|