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 { 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 block_actions_drawing_grips_history_cancel_transform_and_reopen() { use crate::scene::block_actions::{self as actions, Kind}; let (mut app, reference) = fixture(); begin(&mut app, reference); let i = app.active_tab; let _ = app.run_command_line("BPARAMETER LINEAR 0,0 10,0 5,2"); let def = app.tabs[i].active_block_edit_session().unwrap().br_handle; let p = actions::parameters(&app.tabs[i].scene.document, def, Kind::Move)[0].clone(); let targets = members(&app); actions::create( &mut app.tabs[i].scene.document, def, Kind::Move, p.handle, &targets, glam::DVec3::ZERO, ) .unwrap(); let _ = app.dispatch_command("BSAVE"); let _ = app.dispatch_command("BEDIT_DISCARD"); let sibling = app.tabs[i] .scene .document .add_entity(EntityType::Insert(Insert::new( "EDIT_ME", Vector3::new(40., 0., 0.), ))) .unwrap(); let EntityType::Insert(ins) = app.tabs[i] .scene .document .get_entity_mut(reference) .unwrap() else { panic!() }; ins.insert_point = Vector3::new(20., 30., 0.); ins.rotation = std::f64::consts::FRAC_PI_2; ins.set_x_scale(2.); ins.set_y_scale(2.); ins.set_z_scale(2.); let original = app.tabs[i].scene.document.clone(); app.tabs[i].scene.selected.insert(reference); app.tabs[i].selected_handle = Some(reference); app.refresh_properties(); let grip = app.tabs[i] .selected_grips .iter() .find(|g| super::block_actions::is_test_grip(g.id)) .unwrap() .clone(); assert!(grip.world.distance(glam::DVec3::new(20., 50., 0.)) < 1e-8); let depth = app.tabs[i].history.undo_stack.len(); assert!(app.start_test_action_grip(reference, grip.id)); let _ = app.update(Message::CommandEscape); assert_eq!(app.tabs[i].scene.document, original); assert_eq!(app.tabs[i].history.undo_stack.len(), depth); // Escape also clears the ordinary drawing selection. app.tabs[i].scene.selected.insert(reference); app.refresh_properties(); assert!(app.start_test_action_grip(reference, grip.id)); feed(&mut app, "20,70"); assert!(app.tabs[i].active_cmd.is_none()); let changed = app.tabs[i].scene.document.clone(); assert_eq!(app.tabs[i].history.undo_stack.len(), depth + 1); let EntityType::Insert(ins) = changed.get_entity(reference).unwrap() else { panic!() }; assert_ne!(ins.block_name, "EDIT_ME"); let member = changed .block_records .get(&ins.block_name) .unwrap() .entity_handles[0]; let EntityType::Line(line) = changed.get_entity(member).unwrap() else { panic!() }; assert!((line.start.x - 10.).abs() < 1e-8); assert_eq!( changed.get_entity(targets[0]), original.get_entity(targets[0]) ); assert_eq!(changed.get_entity(sibling), original.get_entity(sibling)); app.undo_steps(1); assert_eq!( app.tabs[i].scene.document.get_entity(reference), original.get_entity(reference) ); assert!(app.tabs[i].scene.document.get_entity(member).is_none()); app.redo_steps(1); assert_eq!( app.tabs[i].scene.document.get_entity(member), changed.get_entity(member) ); assert_eq!( app.tabs[i].scene.document.get_entity(reference), changed.get_entity(reference) ); let bytes = codec::DwgWriter::write_to_vec(&app.tabs[i].scene.document).unwrap(); app.tabs[i].scene.document = codec::DwgReader::from_stream(std::io::Cursor::new(bytes)) .read() .unwrap(); app.refresh_properties(); let grip = app.tabs[i] .selected_grips .iter() .find(|g| super::block_actions::is_test_grip(g.id)) .unwrap(); assert!(grip.world.distance(glam::DVec3::new(20., 70., 0.)) < 1e-8); let id = grip.id; assert!(app.start_test_action_grip(reference, id)); feed(&mut app, "20,50"); let EntityType::Line(line) = app.tabs[i].scene.document.get_entity(member).unwrap() else { panic!() }; assert!(line.start.x.abs() < 1e-8); } #[test] fn authoring_edit_basepoint_attribute_relocation_undo_and_cancel() { let (mut app, reference) = fixture(); begin(&mut app, reference); let i = app.active_tab; let _ = app.run_command_line("BPARAMETER BASEPOINT 2,3"); let def = app.tabs[i].active_block_edit_session().unwrap().br_handle; let handle = crate::scene::block_basepoint::parameter(&app.tabs[i].scene.document, def) .unwrap() .0; let mut attribute = codec::entities::AttributeEntity::new("TAG".into(), "value".into()); attribute.insertion_point = Vector3::new(7., 8., 0.); let EntityType::Insert(insert) = app.tabs[i] .scene .document .get_entity_mut(reference) .unwrap() else { panic!() }; insert.attributes.push(attribute); let before = app.tabs[i].scene.document.clone(); let result = crate::command::CmdResult::EditBlockAuthoring { document: app.tabs[i].id, definition: def, handle, part: crate::scene::block_authoring::edit::Part::Point, position: glam::DVec3::new(4., 6., 0.), }; let _ = app.handle_authoring_edit(result); assert_ne!( app.tabs[i].scene.document.get_entity(reference), before.get_entity(reference) ); app.undo_steps(1); assert_eq!(app.tabs[i].scene.document, before); app.redo_steps(1); assert_ne!( app.tabs[i].scene.document.get_entity(reference), before.get_entity(reference) ); app.tabs[i].selected_authoring = Some(handle); app.refresh_properties(); let id = app.tabs[i].selected_grips[0].id; assert!(app.start_authoring_grip(handle, id)); let after = app.tabs[i].scene.document.clone(); let depth = app.tabs[i].history.undo_stack.len(); let _ = app.update(Message::CommandEscape); assert_eq!(app.tabs[i].scene.document, after); assert_eq!(app.tabs[i].history.undo_stack.len(), depth); let _ = app.update(Message::CommandEscape); assert_eq!(app.tabs[i].selected_authoring, None); } #[test] fn authoring_edit_mouse_selection_endpoint_label_marker_history_and_reopen() { use crate::scene::block_authoring::edit::{self, Part}; use crate::scene::block_actions::{self as actions, Kind}; let (mut app, reference) = fixture(); begin(&mut app, reference); let i = app.active_tab; let _ = app.run_command_line("BPARAMETER LINEAR 2,2 6,2 4,3"); let def = app.tabs[i].active_block_edit_session().unwrap().br_handle; let p = actions::parameters(&app.tabs[i].scene.document, def, Kind::Move)[0].clone(); let targets = members(&app); let a = actions::create( &mut app.tabs[i].scene.document, def, Kind::Move, p.handle, &targets, glam::DVec3::new(5., 5., 0.), ) .unwrap(); app.tabs[i].scene.selection.borrow_mut().view.vp_size = (1600., 900.); app.tabs[i].scene.camera.borrow_mut().fit_to_bounds_f64( glam::DVec3::new(-1., -1., 0.), glam::DVec3::new(10., 7., 0.), 1600. / 900., ); let click = |app: &mut OpenCADStudio, point: Vector3| { let scene = &app.tabs[app.active_tab].scene; let (w, h) = scene.selection.borrow().view.vp_size; let b = scene.active_model_tile_bounds(w, h); let s = scene .camera .borrow() .project( glam::DVec3::new(point.x, point.y, point.z), iced::Rectangle { x: 0., y: 0., width: b.width, height: b.height, }, ) .unwrap(); let _ = app.update(Message::ViewportLeftPressAt(iced::Point::new( s.x + b.x, s.y + b.y, ))); let _ = app.update(Message::ViewportLeftRelease); }; click(&mut app, Vector3::new(4., 3., 0.)); assert_eq!(app.tabs[i].selected_authoring, Some(p.handle)); assert_eq!(app.tabs[i].selected_grips.len(), 3); let before = app.tabs[i].scene.document.clone(); let depth = app.tabs[i].history.undo_stack.len(); click(&mut app, Vector3::new(6., 2., 0.)); assert_eq!( app.tabs[i].active_cmd.as_ref().map(|c| c.name()), Some("BEDITGRIP") ); feed(&mut app, "8,2"); assert!(app.tabs[i].active_cmd.is_none()); assert_eq!( actions::parameters(&app.tabs[i].scene.document, def, Kind::Move)[0].end, glam::DVec3::new(8., 2., 0.) ); assert_eq!(app.tabs[i].history.undo_stack.len(), depth + 1); app.undo_steps(1); assert_eq!(app.tabs[i].scene.document, before); app.redo_steps(1); let label = edit::items(&app.tabs[i].scene.document, def) .into_iter() .find(|i| i.handle == p.handle) .unwrap() .grips .into_iter() .find(|(p, _)| *p == Part::Label(0)) .unwrap() .1; click(&mut app, label); feed(&mut app, "5,4"); let graphic = edit::items(&app.tabs[i].scene.document, def) .into_iter() .find(|i| i.handle == p.handle) .unwrap(); assert_eq!(graphic.graphic.labels[0].0, Vector3::new(5., 4., 0.)); let _ = app.update(Message::CommandEscape); click(&mut app, Vector3::new(5., 5., 0.)); assert_eq!(app.tabs[i].selected_authoring, Some(a)); click(&mut app, Vector3::new(5., 5., 0.)); feed(&mut app, "7,5"); let action = actions::actions(&app.tabs[i].scene.document, def); assert_eq!(action[0].1.display_location, Vector3::new(7., 5., 0.)); assert_eq!(action[0].1.dependencies, targets); let _ = app.dispatch_command("BSAVE"); let _ = app.dispatch_command("BEDIT_DISCARD"); assert!(app.tabs[i] .selected_grips .iter() .all(|g| !super::block_authoring_edit::is_editor_grip(g.id))); // Persisted native coordinates, rather than session-only display positions. use std::io::Cursor; let bytes = codec::DwgWriter::write_to_vec(&app.tabs[i].scene.document).unwrap(); let loaded = codec::DwgReader::from_stream(Cursor::new(bytes)) .read() .unwrap(); assert_eq!( actions::parameters(&loaded, def, Kind::Move)[0].end, glam::DVec3::new(8., 2., 0.) ); assert_eq!( actions::actions(&loaded, def)[0].1.display_location, Vector3::new(7., 5., 0.) ); app.tabs[i].scene.document = loaded; begin(&mut app, reference); click(&mut app, Vector3::new(8., 2., 0.)); assert_eq!(app.tabs[i].selected_authoring, Some(p.handle)); } #[test] fn block_actions_mouse_selection_waits_for_enter_for_move_and_scale() { use crate::scene::block_actions::{self as actions, Kind}; for (kind, command) in [(Kind::Move, "BACTION MOVE"), (Kind::Scale, "BACTION SCALE")] { let (mut app, reference) = fixture(); begin(&mut app, reference); let source = app.active_tab; let _ = app.run_command_line("CIRCLE 0.5,0.5 0.2"); let _ = app.run_command_line("BPARAMETER LINEAR 0,0 2,0 1,-0.2"); let definition = app.tabs[source] .active_block_edit_session() .unwrap() .br_handle; app.tabs[source].scene.selection.borrow_mut().view.vp_size = (1600., 900.); app.tabs[source] .scene .camera .borrow_mut() .fit_to_bounds_f64( glam::DVec3::new(-0.5, -0.5, 0.), glam::DVec3::new(2.5, 1.5, 0.), 1600. / 900., ); let _ = app.dispatch_command(command); let click = |app: &mut OpenCADStudio, point: [f64; 3], modifiers: &str| { let scene = &app.tabs[app.active_tab].scene; let (vw, vh) = scene.selection.borrow().view.vp_size; let tile = scene.active_model_tile_bounds(vw, vh); let screen = scene .camera .borrow() .project( glam::DVec3::from_array(point), iced::Rectangle { x: 0., y: 0., width: tile.width, height: tile.height, }, ) .unwrap(); let cursor = iced::Point::new(screen.x + tile.x, screen.y + tile.y); let previous = (app.ctrl_down, app.shift_down); app.ctrl_down = modifiers.contains("ctrl"); app.shift_down = modifiers.contains("shift"); let _ = app.update(Message::ViewportLeftPressAt(cursor)); let _ = app.update(Message::ViewportLeftRelease); (app.ctrl_down, app.shift_down) = previous; }; click(&mut app, [2., 0., 0.], ""); assert!(app.tabs[source] .active_cmd .as_ref() .unwrap() .is_selection_gathering()); feed(&mut app, ""); // Empty selection does not cancel or advance. assert!(app.tabs[source] .active_cmd .as_ref() .unwrap() .is_selection_gathering()); for (point, modifiers, count) in [ ([0.5, 0., 0.], "", 1), ([0.5, 0.7, 0.], "", 2), ([0.5, 0., 0.], "shift", 1), ([0.5, 0., 0.], "", 2), ] { click(&mut app, point, modifiers); assert!( app.tabs[source] .active_cmd .as_ref() .unwrap() .is_selection_gathering(), "every mouse pick/removal stays in object selection" ); assert_eq!(app.tabs[source].scene.selected.len(), count); assert!(actions::actions(&app.tabs[source].scene.document, definition).is_empty()); } feed(&mut app, ""); assert!(!app.tabs[source] .active_cmd .as_ref() .unwrap() .is_selection_gathering()); click(&mut app, [1.5, 1., 0.], ""); assert!(app.tabs[source].active_cmd.is_none()); let authored = actions::actions(&app.tabs[source].scene.document, definition); assert_eq!(authored.len(), 1); assert_eq!(authored[0].2, kind); assert_eq!(authored[0].1.dependencies.len(), 2); let original = app.tabs[source].scene.document.clone(); let _ = app.dispatch_command("BTESTBLOCK"); let i = app.active_tab; app.tabs[i].scene.selection.borrow_mut().view.vp_size = (1600., 900.); app.tabs[i].scene.camera.borrow_mut().fit_to_bounds_f64( glam::DVec3::new(-0.5, -0.5, 0.), glam::DVec3::new(4.5, 1.5, 0.), 1600. / 900., ); click(&mut app, [0.5, 0., 0.], ""); click(&mut app, [2., 0., 0.], ""); assert_eq!( app.tabs[i].active_cmd.as_ref().map(|c| c.name()), Some("BTESTBLOCK") ); click(&mut app, [4., 0., 0.], ""); assert!(app.tabs[i].active_cmd.is_none()); let circle = app.tabs[i] .scene .document .entities_in_block("EDIT_ME") .find_map(|e| { if let EntityType::Circle(c) = e { Some(c) } else { None } }) .unwrap(); let (center, radius) = if kind == Kind::Move { (Vector3::new(2.5, 0.5, 0.), 0.2) } else { (Vector3::new(1., 1., 0.), 0.4) }; // Mouse projection uses f32 screen coordinates; allow sub-pixel roundoff. assert!( (circle.center - center).length() < 1e-5, "{kind:?}: actual {:?}, expected {center:?}", circle.center ); assert!((circle.radius - radius).abs() < 1e-5); assert_eq!(app.tabs[source].scene.document, original); } } #[test] fn block_actions_command_selection_cancel_undo_and_test_block_isolation() { use crate::scene::block_actions::{self as actions, Kind}; let (mut app, reference) = fixture(); begin(&mut app, reference); let source = app.active_tab; let _ = app.run_command_line("BPARAMETER LINEAR 0,0 10,0 5,2"); let definition = app.tabs[source] .active_block_edit_session() .unwrap() .br_handle; let p = actions::parameters(&app.tabs[source].scene.document, definition, Kind::Move)[0].clone(); let targets = members(&app); let before = app.tabs[source].scene.document.clone(); let depth = app.tabs[source].history.undo_stack.len(); let _ = app.dispatch_command("BACTION MOVE"); feed(&mut app, &p.name); assert!(app.tabs[source] .active_cmd .as_ref() .unwrap() .is_selection_gathering()); let _ = app.feed_command(crate::command::StepInput::SelectionComplete( targets.clone(), )); let _ = app.update(Message::CommandEscape); assert_eq!(app.tabs[source].scene.document, before); assert_eq!(app.tabs[source].history.undo_stack.len(), depth); for kind in ["Move", "Scale"] { let _ = app.dispatch_command(&format!("BACTION {kind}")); feed(&mut app, &p.name); let _ = app.feed_command(crate::command::StepInput::SelectionComplete( targets.clone(), )); assert!(app.tabs[source] .active_cmd .as_ref() .unwrap() .is_selection_gathering()); let _ = app.feed_command(crate::command::StepInput::Enter); feed(&mut app, "3,4"); assert!(app.tabs[source].active_cmd.is_none()); } assert_eq!( actions::actions(&app.tabs[source].scene.document, definition).len(), 2 ); assert_eq!(app.tabs[source].history.undo_stack.len(), depth + 2); app.undo_steps(1); assert_eq!( actions::actions(&app.tabs[source].scene.document, definition).len(), 1 ); app.redo_steps(1); assert_eq!( actions::actions(&app.tabs[source].scene.document, definition).len(), 2 ); let original = app.tabs[source].scene.document.clone(); let _ = app.dispatch_command("BTESTBLOCK"); let test = app.active_tab; let insert = app.tabs[test].block_test.as_ref().unwrap().insert; app.tabs[test].selected_handle = Some(insert); app.refresh_test_action_grips([0.; 3]); let grip = app.tabs[test].selected_grips.last().unwrap().id; assert!(app.start_test_action_grip(insert, grip)); feed(&mut app, "20,5"); let EntityType::Line(line) = app.tabs[test] .scene .document .get_entity(targets[0]) .unwrap() else { panic!() }; assert_eq!(line.start, Vector3::new(20., 0., 0.)); assert_eq!(line.end, Vector3::new(22., 0., 0.)); assert_eq!(app.tabs[source].scene.document, original); app.undo_steps(1); let EntityType::Line(line) = app.tabs[test] .scene .document .get_entity(targets[0]) .unwrap() else { panic!() }; assert_eq!(line.start, Vector3::ZERO); app.redo_steps(1); let EntityType::Line(line) = app.tabs[test] .scene .document .get_entity(targets[0]) .unwrap() else { panic!() }; assert_eq!(line.start, Vector3::new(20., 0., 0.)); app.tabs[test] .scene .document .add_entity(EntityType::Insert(Insert::new( "EDIT_ME", Vector3::new(50., 0., 0.), ))) .unwrap(); let shared_before = app.tabs[test].scene.document.clone(); let id = app.tabs[test].id; let _ = app.handle_block_action_result(crate::command::CmdResult::EvaluateBlockParameter { insert: None, document: id, definition, parameter: p.handle, position: glam::DVec3::new(30., 0., 0.), }); assert_eq!( app.tabs[test].scene.document, shared_before, "shared references are refused rather than silently modified together" ); let _ = app.dispatch_command("BCLOSETESTBLOCK"); assert_eq!(app.active_tab, source); assert_eq!(app.tabs[source].scene.document, original); } #[test] fn block_actions_rejected_outside_editor_preserve_running_command_and_stale_results_are_inert() { use crate::scene::block_actions::Kind; let (mut app, reference) = fixture(); let i = app.active_tab; let _ = app.dispatch_command("LINE"); let before = app.tabs[i].scene.document.clone(); let _ = app.dispatch_command("BACTION MOVE"); assert_eq!(app.tabs[i].active_cmd.as_ref().unwrap().name(), "LINE"); assert_eq!(app.tabs[i].scene.document, before); let _ = app.update(Message::CommandEscape); begin(&mut app, reference); let _ = app.run_command_line("BPARAMETER POINT 2,3 4,5"); let def = app.tabs[i].active_block_edit_session().unwrap().br_handle; let p = crate::scene::block_actions::parameters(&app.tabs[i].scene.document, def, Kind::Move) [0] .handle; let before = app.tabs[i].scene.document.clone(); let _ = app.handle_block_action_result(crate::command::CmdResult::PlaceBlockAction { document: app.tabs[i].id + 1, definition: def, kind: Kind::Move, parameter: p, targets: members(&app), location: glam::DVec3::ZERO, }); assert_eq!(app.tabs[i].scene.document, before); let _ = app.dispatch_command("LINE"); let _ = app.dispatch_command("BACTION SCALE"); assert_eq!(app.tabs[i].active_cmd.as_ref().unwrap().name(), "LINE"); } #[test] fn block_actions_test_reference_transform_visibility_and_noop_history() { use crate::scene::block_actions::{self as actions, Kind}; let (mut app, reference) = fixture(); begin(&mut app, reference); let source = app.active_tab; let _ = app.run_command_line("BPARAMETER POINT 2,3 4,5"); let _ = app.run_command_line("BPARAMETER VISIBILITY 8,8"); let _ = app.run_command_line("BPARAMETER BASEPOINT 1,2"); let definition = app.tabs[source] .active_block_edit_session() .unwrap() .br_handle; let p = actions::parameters(&app.tabs[source].scene.document, definition, Kind::Move)[0].clone(); let targets = members(&app); let _ = app.dispatch_command("BACTION MOVE"); feed(&mut app, &p.name); let _ = app.feed_command(crate::command::StepInput::SelectionComplete(targets)); let _ = app.feed_command(crate::command::StepInput::Enter); feed(&mut app, "4,6"); let _ = app.dispatch_command("BTESTBLOCK"); let i = app.active_tab; let insert = app.tabs[i].block_test.as_ref().unwrap().insert; let depth = app.tabs[i].history.undo_stack.len(); let id = app.tabs[i].id; let EntityType::Insert(ins) = app.tabs[i].scene.document.get_entity(insert).unwrap() else { panic!() }; let current_world = actions::v( crate::scene::render_graph::insert_transform(&app.tabs[i].scene.document, ins) .apply(actions::native(p.end)), ); let _ = app.handle_block_action_result(crate::command::CmdResult::EvaluateBlockParameter { insert: None, document: id, definition, parameter: p.handle, position: current_world, }); assert_eq!(app.tabs[i].history.undo_stack.len(), depth); // Existing visibility UI creates a per-reference anonymous definition. app.apply_visibility_state(insert, 0); app.finish_pending_history(i); let EntityType::Insert(ins) = app.tabs[i].scene.document.get_entity(insert).unwrap() else { panic!() }; let after_world = actions::v( crate::scene::render_graph::insert_transform(&app.tabs[i].scene.document, ins) .apply(actions::native(p.end)), ); assert_eq!( after_world, current_world, "visibility representation retains the native Basepoint anchor" ); let EntityType::Insert(ins) = app.tabs[i].scene.document.get_entity_mut(insert).unwrap() else { panic!() }; ins.insert_point = Vector3::new(100., 200., 0.); ins.rotation = std::f64::consts::FRAC_PI_2; ins.set_x_scale(2.); ins.set_y_scale(3.); let EntityType::Insert(ins) = app.tabs[i].scene.document.get_entity(insert).unwrap() else { panic!() }; let name = ins.block_name.clone(); assert_ne!(name, "EDIT_ME"); let world = actions::v( crate::scene::render_graph::insert_transform(&app.tabs[i].scene.document, ins) .apply(Vector3::new(12., 13., 0.)), ); let before = app.tabs[i].scene.document.clone(); let _ = app.handle_block_action_result(crate::command::CmdResult::EvaluateBlockParameter { insert: None, document: id, definition, parameter: p.handle, position: world, }); let anon = app.tabs[i] .scene .document .entities_in_block(&name) .find_map(|e| { if let EntityType::Line(l) = e { Some(l) } else { None } }) .unwrap(); assert!((anon.start.x - 10.).abs() < 1e-9 && (anon.start.y - 10.).abs() < 1e-9); app.undo_steps(1); assert_eq!(app.tabs[i].scene.document, before); } fn authored_points(app: &OpenCADStudio) -> Vec { let tab = &app.tabs[app.active_tab]; let definition = tab .scene .document .block_records .get("EDIT_ME") .unwrap() .handle; crate::scene::block_point_parameter::parameters(&tab.scene.document, definition) .into_iter() .map(|(_, parameter)| parameter.clone()) .collect() } fn distance_parameter_count(app: &OpenCADStudio) -> usize { let tab = &app.tabs[app.active_tab]; let definition = tab .scene .document .block_records .get("EDIT_ME") .unwrap() .handle; crate::scene::block_distance_parameter::parameters(&tab.scene.document, definition).len() } fn orientation_parameter_count(app: &OpenCADStudio) -> usize { let tab = &app.tabs[app.active_tab]; let definition = tab .scene .document .block_records .get("EDIT_ME") .unwrap() .handle; crate::scene::block_orientation_parameter::parameters(&tab.scene.document, definition).len() } #[test] fn lookup_basepoint_placement_history_test_block_and_saved_parent_undo() { use crate::scene::{block_basepoint, block_lookup, render_graph}; let (mut app, reference) = fixture(); begin(&mut app, reference); let i = app.active_tab; let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle; let members_before = members(&app); let _ = app.run_command_line("BPARAMETER BASEPOINT 3,4"); let (handle, p) = block_basepoint::parameter(&app.tabs[i].scene.document, definition).unwrap(); assert_eq!(p.point, Vector3::new(3.0, 4.0, 0.0)); let depth = app.tabs[i].history.undo_stack.len(); assert_eq!(depth, 1); let _ = app.run_command_line("BPARAMETER BASEPOINT 3,4"); assert_eq!( app.tabs[i].history.undo_stack.len(), depth, "same anchor is a no-op" ); let _ = app.run_command_line("BPARAMETER BASEPOINT 5,6"); assert_eq!( block_basepoint::parameter(&app.tabs[i].scene.document, definition) .unwrap() .0, handle ); app.undo_steps(1); assert_eq!( block_basepoint::parameter(&app.tabs[i].scene.document, definition) .unwrap() .1 .point, Vector3::new(3.0, 4.0, 0.0) ); app.redo_steps(1); let _ = app.dispatch_command("BPARAMETER"); feed(&mut app, "K"); feed(&mut app, "2,2"); assert_eq!( block_lookup::parameters(&app.tabs[i].scene.document, definition).len(), 1 ); assert_eq!( members(&app), members_before, "authoring objects are not drawing entities" ); let before_test = app.tabs[i].scene.document.clone(); let _ = app.dispatch_command("BTESTBLOCK"); let doc = &app.tabs[app.active_tab].scene.document; let insert = doc .entities() .find_map(|e| match e { EntityType::Insert(i) => Some(i), _ => None, }) .unwrap(); assert!( (render_graph::insert_transform(doc, insert).apply(Vector3::new(5.0, 6.0, 0.0)) - insert.insert_point) .length() < 1e-9 ); let _ = app.dispatch_command("BCLOSETESTBLOCK"); assert_eq!(app.tabs[i].scene.document, before_test); let _ = app.dispatch_command("BSAVE"); let _ = app.dispatch_command("BEDIT_DISCARD"); assert!(block_basepoint::parameter(&app.tabs[i].scene.document, definition).is_some()); app.undo_steps(1); assert!(block_basepoint::parameter(&app.tabs[i].scene.document, definition).is_none()); app.redo_steps(1); assert!(block_basepoint::parameter(&app.tabs[i].scene.document, definition).is_some()); } fn lookup_editor_fixture() -> (OpenCADStudio, Handle, Handle) { let (mut app, reference) = fixture(); begin(&mut app, reference); let _ = app.run_command_line("BPARAMETER LINEAR 0,0 2,0 0,1"); let _ = app.run_command_line("BPARAMETER LOOKUP 1,1"); let i = app.active_tab; let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle; let parameter = crate::scene::block_lookup::parameters(&app.tabs[i].scene.document, definition)[0].0; (app, definition, parameter) } fn edit_lookup(app: &mut OpenCADStudio) { use crate::ui::window::block_lookup::Action as A; let _ = app.update(Message::BlockLookup(A::Property(0))); let _ = app.update(Message::BlockLookup(A::AddProperty)); let _ = app.update(Message::BlockLookup(A::AddRow)); let _ = app.update(Message::BlockLookup(A::Cell(0, 1, "Small".into()))); } #[test] fn lookup_editor_cancel_apply_one_undo_and_stale_guard() { use crate::ui::window::block_lookup::Action as A; use crate::scene::block_lookup; let (mut app, definition, parameter) = lookup_editor_fixture(); let i = app.active_tab; let before = app.tabs[i].scene.document.clone(); let depth = app.tabs[i].history.undo_stack.len(); for cancel in [Message::CloseModal, Message::CommandEscape] { let _ = app.dispatch_command("BLOOKUPTABLE"); edit_lookup(&mut app); assert_eq!(app.tabs[i].scene.document, before, "draft is isolated"); let _ = app.update(cancel); assert!(app.block_lookup.is_none()); assert_eq!(app.tabs[i].scene.document, before); assert_eq!(app.tabs[i].history.undo_stack.len(), depth); } let _ = app.dispatch_command("BLOOKUPTABLE"); let _ = app.update(Message::BlockLookup(A::Apply)); assert_eq!( app.tabs[i].history.undo_stack.len(), depth, "unchanged OK is a no-op" ); let _ = app.dispatch_command("BLOOKUPTABLE"); edit_lookup(&mut app); let _ = app.update(Message::BlockLookup(A::Audit)); assert!(app.block_lookup.as_ref().unwrap().error.is_none()); let _ = app.update(Message::BlockLookup(A::Apply)); assert!(app.block_lookup.is_none()); assert_eq!(app.tabs[i].history.undo_stack.len(), depth + 1); assert_eq!( block_lookup::table(&app.tabs[i].scene.document, definition, parameter) .unwrap() .unwrap() .1 .expressions, vec!["2", "Small"] ); let saved = app.tabs[i].scene.document.clone(); app.undo_steps(1); assert_eq!(app.tabs[i].scene.document, before); app.redo_steps(1); assert_eq!(app.tabs[i].scene.document, saved); let _ = app.dispatch_command("BLOOKUPTABLE Lookup1"); let _ = app.update(Message::BlockLookup(A::Cell(0, 1, "Changed".into()))); app.tabs[i].scene.bump_geometry(); let _ = app.update(Message::BlockLookup(A::Apply)); assert!(app .block_lookup .as_ref() .unwrap() .error .as_ref() .unwrap() .contains("changed")); assert_eq!(app.tabs[i].scene.document, saved); let _ = app.update(Message::CloseModal); } #[test] fn lookup_editor_multiple_tables_apply_atomically_and_parameter_cancel_is_clean() { use crate::ui::window::block_lookup::Action as A; let (mut app, definition, _) = lookup_editor_fixture(); let i = app.active_tab; let _ = app.run_command_line("BPARAMETER LOOKUP 3,3"); let before = app.tabs[i].scene.document.clone(); let depth = app.tabs[i].history.undo_stack.len(); for kind in ["BASEPOINT", "LOOKUP"] { let _ = app.dispatch_command(&format!("BPARAMETER {kind}")); let _ = app.update(Message::CommandEscape); assert_eq!(app.tabs[i].scene.document, before); assert_eq!(app.tabs[i].history.undo_stack.len(), depth); } let _ = app.dispatch_command("BLOOKUPTABLE"); edit_lookup(&mut app); let _ = app.update(Message::BlockLookup(A::Select(1))); edit_lookup(&mut app); let _ = app.update(Message::BlockLookup(A::Cell(0, 0, "NaN".into()))); let _ = app.update(Message::BlockLookup(A::Apply)); assert!(app.block_lookup.as_ref().unwrap().error.is_some()); assert_eq!(app.tabs[i].scene.document, before); let _ = app.update(Message::BlockLookup(A::Cell(0, 0, "5".into()))); let _ = app.update(Message::BlockLookup(A::Apply)); assert!(app.block_lookup.is_none()); assert_eq!(app.tabs[i].history.undo_stack.len(), depth + 1); for (h, _) in crate::scene::block_lookup::parameters(&app.tabs[i].scene.document, definition) { assert!( crate::scene::block_lookup::table(&app.tabs[i].scene.document, definition, h) .unwrap() .is_some() ); } app.undo_steps(1); assert_eq!(app.tabs[i].scene.document, before); let _ = app.dispatch_command("BEDIT_DISCARD"); let _ = app.dispatch_command("LINE"); for cmd in ["BPARAMETER BASEPOINT", "BPARAMETER LOOKUP", "BLOOKUPTABLE"] { let _ = app.dispatch_command(cmd); assert_eq!(app.tabs[i].active_cmd.as_ref().unwrap().name(), "LINE"); } } #[test] fn orientation_parameter_batch_numeric_rotation_local_history_save_and_parent_undo() { let (mut app, reference) = fixture(); begin(&mut app, reference); let i = app.active_tab; for (index, (kind, inputs)) in [ ("ROTATION", vec!["0,0", "2", "90"]), ("ALIGNMENT", vec!["0,0", "0,2"]), ("FLIP", vec!["0,0", "0,2", "3,3"]), ] .into_iter() .enumerate() { let before = app.tabs[i].scene.document.clone(); let _ = app.dispatch_command(&format!("BPARAMETER {kind}")); for text in &inputs[..inputs.len() - 1] { feed(&mut app, text); assert_eq!(app.tabs[i].scene.document, before, "incomplete placement"); } feed(&mut app, inputs.last().unwrap()); assert_eq!(orientation_parameter_count(&app), index + 1, "{kind}"); assert_eq!(app.tabs[i].history.undo_stack.len(), index + 1); assert!(app.tabs[i].scene.preview_wires.is_empty()); app.undo_steps(1); assert_eq!(orientation_parameter_count(&app), index); app.redo_steps(1); assert_eq!(orientation_parameter_count(&app), index + 1); } let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle; let p = &crate::scene::block_orientation_parameter::parameters( &app.tabs[i].scene.document, definition, )[0]; assert!((p.native.definition_end_point - Vector3::new(0.0, 2.0, 0.0)).length() < 1e-10); assert_eq!(p.reference, Vector3::new(2.0, 0.0, 0.0)); let editor = app.tabs[i].scene.document.clone(); let _ = app.dispatch_command("BTESTBLOCK"); assert!(app.tabs[app.active_tab] .active_block_edit_session() .is_none()); assert_eq!( app.tabs[app.active_tab] .scene .document .entities_in_block("EDIT_ME") .count(), 1 ); let _ = app.dispatch_command("BCLOSETESTBLOCK"); assert_eq!(app.tabs[i].scene.document, editor); let _ = app.run_command_line("UCS ORIGIN 1,2,0"); for p in crate::scene::block_orientation_parameter::parameters( &app.tabs[i].scene.document, definition, ) { assert_eq!( p.native.definition_base_point, Vector3::new(-1.0, -2.0, 0.0) ); } let _ = app.dispatch_command("BSAVE"); let _ = app.dispatch_command("BEDIT_DISCARD"); assert_eq!(orientation_parameter_count(&app), 3); app.undo_steps(1); assert_eq!(orientation_parameter_count(&app), 0); app.redo_steps(1); assert_eq!(orientation_parameter_count(&app), 3); } #[test] fn orientation_parameter_chooser_cancels_invalid_picks_and_rotation_default_angle() { let (mut app, reference) = fixture(); begin(&mut app, reference); let i = app.active_tab; let before = app.tabs[i].scene.document.clone(); for (kind, maximum) in [("R", 2), ("A", 1), ("F", 2)] { for picks in 0..=maximum { let _ = app.dispatch_command("BPARAMETER"); feed(&mut app, kind); if picks > 0 { feed(&mut app, "0,0"); feed(&mut app, "0,0"); assert_eq!(app.tabs[i].scene.document, before); } if picks > 1 { feed(&mut app, "0,2"); } let _ = app.update(Message::CommandEscape); assert_eq!(app.tabs[i].scene.document, before); assert!(app.tabs[i].history.undo_stack.is_empty()); } } let _ = app.dispatch_command("BPARAMETER ROTATION"); feed(&mut app, "0,0"); feed(&mut app, "-2"); assert_eq!(app.tabs[i].scene.document, before); feed(&mut app, "2"); feed(&mut app, ""); assert_eq!(orientation_parameter_count(&app), 1); let _ = app.dispatch_command("BEDIT_DISCARD"); assert_eq!(orientation_parameter_count(&app), 0); let _ = app.dispatch_command("LINE"); for kind in ["ROTATION", "ALIGNMENT", "FLIP"] { let _ = app.dispatch_command(&format!("BPARAMETER {kind}")); assert_eq!(app.tabs[i].active_cmd.as_ref().unwrap().name(), "LINE"); } } #[test] fn orientation_parameter_stale_context_and_invalid_result_do_not_start_history() { use crate::scene::block_orientation_parameter::Kind; let (mut app, reference) = fixture(); begin(&mut app, reference); let i = app.active_tab; let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle; let id = app.tabs[i].id; let before = app.tabs[i].scene.document.clone(); for (document_id, end) in [ (id + 1, glam::DVec3::Y), (id, glam::DVec3::ZERO), (id, glam::DVec3::splat(f64::NAN)), ] { let _ = app.handle_place_block_orientation_parameter( crate::command::CmdResult::PlaceBlockOrientationParameter { document_id, definition, kind: Kind::Flip, base: glam::DVec3::ZERO, end, reference: glam::DVec3::X, }, ); assert_eq!(app.tabs[i].scene.document, before); assert!(app.tabs[i].history.undo_stack.is_empty()); } } #[test] fn distance_parameter_batch_three_picks_local_undo_redo_and_saved_parent_undo() { let (mut app, reference) = fixture(); begin(&mut app, reference); let i = app.active_tab; for (index, kind) in ["LINEAR", "POLAR", "XY"].iter().enumerate() { let before = app.tabs[i].scene.document.clone(); let _ = app.dispatch_command(&format!("BPARAMETER {kind}")); feed(&mut app, "2,3"); feed(&mut app, "6,7"); assert_eq!( app.tabs[i].scene.document, before, "base and endpoint are preview only" ); feed(&mut app, "1,8"); assert_eq!(distance_parameter_count(&app), index + 1); assert_eq!(app.tabs[i].history.undo_stack.len(), index + 1); assert!(app.tabs[i].scene.preview_wires.is_empty()); app.undo_steps(1); assert_eq!(distance_parameter_count(&app), index); app.redo_steps(1); assert_eq!(distance_parameter_count(&app), index + 1); } let _ = app.run_command_line("BPARAMETER POINT 0,0 1,1"); let _ = app.run_command_line("BPARAMETER VISIBILITY 0,0"); let editor = app.tabs[i].scene.document.clone(); let _ = app.dispatch_command("BTESTBLOCK"); assert!(app.tabs[app.active_tab] .active_block_edit_session() .is_none()); assert_eq!( app.tabs[app.active_tab] .scene .document .entities_in_block("EDIT_ME") .count(), 1, "parameters and their editor graphics add no drawing geometry" ); assert_eq!(distance_parameter_count(&app), 3); let _ = app.dispatch_command("BCLOSETESTBLOCK"); assert_eq!(app.active_tab, i); assert_eq!(app.tabs[i].scene.document, editor); let _ = app.run_command_line("UCS ORIGIN 1,2,0"); let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle; for p in crate::scene::block_distance_parameter::parameters(&app.tabs[i].scene.document, definition) { assert_eq!(p.native.definition_base_point, Vector3::new(1.0, 1.0, 0.0)); assert_eq!(p.native.definition_end_point, Vector3::new(5.0, 5.0, 0.0)); } let _ = app.dispatch_command("BSAVE"); let _ = app.dispatch_command("BEDIT_DISCARD"); assert_eq!(distance_parameter_count(&app), 3); app.undo_steps(1); assert_eq!(distance_parameter_count(&app), 0); app.redo_steps(1); assert_eq!(distance_parameter_count(&app), 3); } #[test] fn distance_parameter_cancel_chooser_invalid_endpoint_and_discard_are_safe() { let (mut app, reference) = fixture(); begin(&mut app, reference); let i = app.active_tab; let before = app.tabs[i].scene.document.clone(); for kind in ["Linear", "Polar", "XY"] { for picks in 0..3 { let _ = app.dispatch_command("BPARAMETER"); feed(&mut app, kind); if picks > 0 { feed(&mut app, "2,3"); } if picks > 1 { feed(&mut app, "6,7"); } let _ = app.update(Message::CommandEscape); assert_eq!(app.tabs[i].scene.document, before); assert!(app.tabs[i].history.undo_stack.is_empty()); } let _ = app.dispatch_command(&format!("BPARAMETER {kind}")); feed(&mut app, "2,3"); feed(&mut app, "2,3"); assert_eq!(app.tabs[i].scene.document, before); assert!( app.tabs[i].active_cmd.is_some(), "invalid endpoint can be retried" ); feed(&mut app, "6,7"); feed(&mut app, "1,8"); assert_eq!(distance_parameter_count(&app), 1); app.undo_steps(1); } let _ = app.run_command_line("BPARAMETER LINEAR 0,0 1,1 2,2"); let _ = app.dispatch_command("BEDIT_DISCARD"); assert_eq!(distance_parameter_count(&app), 0); let _ = app.dispatch_command("LINE"); for kind in ["LINEAR", "POLAR", "XY"] { let _ = app.dispatch_command(&format!("BPARAMETER {kind}")); assert_eq!(app.tabs[i].active_cmd.as_ref().unwrap().name(), "LINE"); } } #[test] fn distance_parameter_stale_context_and_invalid_result_do_not_start_history() { use crate::scene::block_distance_parameter::Kind; let (mut app, reference) = fixture(); begin(&mut app, reference); let i = app.active_tab; let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle; let id = app.tabs[i].id; let before = app.tabs[i].scene.document.clone(); for (document_id, end) in [ (id + 1, glam::DVec3::ONE), (id, glam::DVec3::ZERO), (id, glam::DVec3::splat(f64::NAN)), ] { let _ = app.handle_place_block_distance_parameter( crate::command::CmdResult::PlaceBlockDistanceParameter { document_id, definition, kind: Kind::Linear, base: glam::DVec3::ZERO, end, label: glam::DVec3::ZERO, }, ); assert_eq!(app.tabs[i].scene.document, before); assert!(app.tabs[i].history.undo_stack.is_empty()); } } #[test] fn point_parameter_placement_two_picks_one_undo_and_repeated_names() { let (mut app, reference) = fixture(); begin(&mut app, reference); let i = app.active_tab; let before = app.tabs[i].scene.document.clone(); let _ = app.dispatch_command("BPARAMETER POINT"); feed(&mut app, "3,4,0"); assert_eq!( app.tabs[i].scene.document, before, "first pick is preview only" ); feed(&mut app, "6,7,0"); assert_eq!(authored_points(&app)[0].position_name, "Position1"); assert_eq!( authored_points(&app)[0].definition_label_point, Vector3::new(6.0, 7.0, 0.0) ); assert_eq!(app.tabs[i].history.undo_stack.len(), 1); assert!(app.block_edit_dirty(i)); app.undo_steps(1); assert!(authored_points(&app).is_empty()); app.redo_steps(1); assert_eq!(authored_points(&app).len(), 1); let _ = app.run_command_line("BPARAMETER POINT 8,9 10,11"); assert_eq!(authored_points(&app)[1].position_name, "Position2"); assert_eq!(app.tabs[i].history.undo_stack.len(), 2); } #[test] fn point_parameter_placement_cancel_at_both_prompts_is_inert() { let (mut app, reference) = fixture(); begin(&mut app, reference); let i = app.active_tab; let before = app.tabs[i].scene.document.clone(); for picked in [false, true] { let _ = app.dispatch_command("BPARAMETER POINT"); if picked { feed(&mut app, "3,4"); } let _ = app.update(Message::CommandEscape); assert_eq!(app.tabs[i].scene.document, before); assert!(app.tabs[i].history.undo_stack.is_empty()); assert!(!app.block_edit_dirty(i)); } let _ = app.dispatch_command("BPARAMETER POINT"); feed(&mut app, "3,4"); feed(&mut app, ""); assert_eq!(app.tabs[i].scene.document, before); assert!(app.tabs[i].history.undo_stack.is_empty()); } #[test] fn point_parameter_placement_chooser_works_with_existing_visibility_and_reframes() { let (mut app, reference) = fixture(); begin(&mut app, reference); let i = app.active_tab; let _ = app.run_command_line("BPARAMETER VISIBILITY 0,0"); let _ = app.dispatch_command("BPARAMETER"); feed(&mut app, "Point"); feed(&mut app, "3,4,5"); feed(&mut app, "6,7,5"); assert_eq!(authored_points(&app).len(), 1); assert!(app.block_visibility_context().is_some()); let _ = app.run_command_line("UCS ORIGIN 1,2,3"); assert_eq!( authored_points(&app)[0].parameter.definition_point, Vector3::new(2.0, 2.0, 2.0) ); assert_eq!( authored_points(&app)[0].definition_label_point, Vector3::new(5.0, 5.0, 2.0) ); app.undo_steps(1); assert_eq!( authored_points(&app)[0].parameter.definition_point, Vector3::new(3.0, 4.0, 5.0) ); let _ = app.dispatch_command("BSAVE"); assert!(!app.block_edit_dirty(i)); let _ = app.dispatch_command("BEDIT_DISCARD"); assert_eq!(authored_points(&app).len(), 1); app.undo_steps(1); assert!( authored_points(&app).is_empty(), "saved block undo restores the parent drawing" ); } #[test] fn point_parameter_placement_discard_and_stale_context_leave_no_objects() { let (mut app, reference) = fixture(); let i = app.active_tab; let before = app.tabs[i].scene.document.clone(); begin(&mut app, reference); let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle; let start = app.tabs[i].scene.document.clone(); let id = app.tabs[i].id; for (document, location) in [ (id + 1, glam::DVec3::ZERO), (id, glam::DVec3::splat(f64::NAN)), ] { let _ = app.handle_place_block_point_parameter( crate::command::CmdResult::PlaceBlockPointParameter { document_id: document, definition, location, label: glam::DVec3::ZERO, }, ); assert_eq!(app.tabs[i].scene.document, start); } let _ = app.run_command_line("BPARAMETER POINT 3,4 6,7"); let _ = app.dispatch_command("BEDIT_DISCARD"); let mut after = app.tabs[i].scene.document.clone(); after.vports = before.vports.clone(); assert_eq!(after, before); let closed = app.tabs[i].scene.document.clone(); let _ = app.dispatch_command("LINE"); let _ = app.dispatch_command("BPARAMETER POINT"); assert_eq!(app.tabs[i].active_cmd.as_ref().unwrap().name(), "LINE"); assert_eq!(app.tabs[i].scene.document, closed); } fn visibility_editor() -> (OpenCADStudio, crate::ui::window::block_visibility::Target) { let (mut app, reference) = fixture(); begin(&mut app, reference); let _ = app.run_command_line("BPARAMETER VISIBILITY 0,0"); let target = app .block_visibility_context() .expect("placed parameter") .target; (app, target) } fn visibility_action(app: &mut OpenCADStudio, action: crate::ui::window::block_visibility::Action) { let _ = app.update(Message::BlockVisibility(action)); } #[test] fn visibility_ribbon_manager_cancel_validation_and_single_undo() { use crate::ui::window::block_visibility::Action as A; let (mut app, target) = visibility_editor(); let i = app.active_tab; let before = app.tabs[i].scene.document.clone(); let depth = app.tabs[i].history.undo_stack.len(); visibility_action(&mut app, A::Open(target)); visibility_action(&mut app, A::Name("Alternate".into())); visibility_action(&mut app, A::New); assert_eq!(app.tabs[i].scene.document, before, "dialog is staged"); let _ = app.update(Message::CommandEscape); assert!(app.block_visibility.is_none()); assert_eq!(app.tabs[i].history.undo_stack.len(), depth); assert_eq!(app.tabs[i].scene.document, before); visibility_action(&mut app, A::Open(target)); visibility_action(&mut app, A::Name("visibilitystate0".into())); visibility_action(&mut app, A::New); assert!(app.block_visibility.as_ref().unwrap().error.is_some()); visibility_action(&mut app, A::Delete); assert_eq!( app.block_visibility.as_ref().unwrap().edited.states.len(), 1 ); visibility_action(&mut app, A::Name("Alternate".into())); visibility_action(&mut app, A::New); visibility_action(&mut app, A::SetCurrent); visibility_action(&mut app, A::Name("Alternate renamed".into())); visibility_action(&mut app, A::Rename); visibility_action(&mut app, A::Move(true)); visibility_action(&mut app, A::Apply); assert_eq!(app.tabs[i].history.undo_stack.len(), depth + 1); assert_eq!( app.block_visibility_context().unwrap().current.as_deref(), Some("Alternate renamed") ); app.undo_steps(1); assert_eq!( app.block_visibility_context().unwrap().names, ["VisibilityState0"] ); app.redo_steps(1); assert_eq!( app.block_visibility_context().unwrap().names, ["Alternate renamed", "VisibilityState0"] ); assert_eq!( app.block_visibility_context().unwrap().current.as_deref(), Some("Alternate renamed") ); } #[test] fn visibility_ribbon_new_modes_state_selection_and_native_roundtrip() { use crate::ui::window::block_visibility::{Action as A, NewVisibility as V}; let (mut app, target) = visibility_editor(); let i = app.active_tab; let member = members(&app)[0]; visibility_action(&mut app, A::Open(target)); for (name, mode) in [("Hidden", V::Hide), ("Shown", V::Show), ("Copy", V::Keep)] { visibility_action(&mut app, A::Name(name.into())); visibility_action(&mut app, A::NewVisibility(mode)); visibility_action(&mut app, A::New); } visibility_action(&mut app, A::Apply); let parameter = crate::scene::block_visibility::parameter(&app.tabs[i].scene.document, target.definition) .unwrap(); assert!(parameter.states[0].visible_blocks.contains(&member)); assert!(parameter.states[1].visible_blocks.is_empty()); assert!(parameter.states[2].visible_blocks.contains(&member)); assert!(parameter.states[3].visible_blocks.contains(&member)); visibility_action(&mut app, A::Current(target, "Hidden".into())); assert!( app.tabs[i] .scene .document .get_entity(member) .unwrap() .common() .invisible ); visibility_action(&mut app, A::Current(target, "Shown".into())); visibility_action(&mut app, A::Current(target, "Copy".into())); app.undo_steps(1); assert_eq!( app.block_visibility_context().unwrap().current.as_deref(), Some("Shown"), "identical states retain their identity in history" ); app.redo_steps(1); assert_eq!( app.block_visibility_context().unwrap().current.as_deref(), Some("Copy") ); app.save_block_edit(i); for ext in ["dwg", "dxf"] { let document = &app.tabs[i].scene.document; let bytes = crate::io::save_to_bytes(document, ext, document.version).unwrap(); let loaded = crate::io::load_bytes(&format!("visibility.{ext}"), bytes).unwrap(); let parameter = crate::scene::block_visibility::parameter(&loaded, target.definition).unwrap(); assert_eq!( parameter .states .iter() .map(|state| state.name.as_str()) .collect::>(), ["VisibilityState0", "Hidden", "Shown", "Copy"] ); assert!(parameter.states[1].visible_blocks.is_empty()); assert!(parameter.states[2].visible_blocks.contains(&member)); } } #[test] fn visibility_ribbon_mode_and_make_visible_cancel_do_not_change_saved_flags() { use crate::ui::window::block_visibility::Action as A; let (mut app, target) = visibility_editor(); let i = app.active_tab; let member = members(&app)[0]; app.tabs[i].scene.select_entity(member, true); let _ = app.run_command_line("BVHIDE"); app.finish_pending_history(i); let before = app.tabs[i].scene.document.clone(); let depth = app.tabs[i].history.undo_stack.len(); let dirty = app.tabs[i].dirty; let wires = |app: &OpenCADStudio| { app.tabs[i] .scene .entity_wires() .iter() .filter(|wire| crate::scene::Scene::handle_from_wire_name(&wire.name) == Some(member)) .cloned() .collect::>() }; assert!(wires(&app).is_empty()); visibility_action(&mut app, A::Mode(target)); assert!( !wires(&app).is_empty(), "hidden member is rendered for picking" ); assert_eq!(app.tabs[i].scene.document, before); visibility_action(&mut app, A::Mode(target)); assert!(wires(&app).is_empty()); let _ = app.run_command_line("BVSHOW"); app.sync_block_visibility_display(); assert!(!wires(&app).is_empty()); let _ = app.update(Message::CommandEscape); assert!(wires(&app).is_empty()); assert_eq!(app.tabs[i].scene.document, before); assert_eq!(app.tabs[i].history.undo_stack.len(), depth); assert_eq!(app.tabs[i].dirty, dirty); let _ = app.run_command_line("BVMODE 1"); assert!(app.block_visibility_mode); let _ = app.run_command_line("SETVAR BVMODE 2"); assert!(app.block_visibility_mode); let _ = app.run_command_line("BVMODE 0"); assert!(!app.block_visibility_mode); assert!( app.pending_setvar.is_none(), "inline variable must close its probe prompt" ); let _ = app.run_command_line("BVSHOW"); app.tabs[i].scene.select_entity(member, true); let result = app.tabs[i] .active_cmd .as_mut() .unwrap() .on_selection_complete(vec![member]); let _ = app.apply_cmd_result(result); feed(&mut app, ""); assert!( !app.tabs[i] .scene .document .get_entity(member) .unwrap() .common() .invisible ); app.undo_steps(1); assert!( app.tabs[i] .scene .document .get_entity(member) .unwrap() .common() .invisible, "undo uses the hidden flags from before picking" ); } #[test] fn visibility_ribbon_manager_rejects_stale_target_and_stale_document() { use crate::ui::window::block_visibility::Action as A; let (mut app, target) = visibility_editor(); visibility_action(&mut app, A::Open(target)); visibility_action(&mut app, A::Name("Alternate".into())); visibility_action(&mut app, A::New); app.tabs[app.active_tab].scene.bump_geometry(); visibility_action(&mut app, A::Apply); assert!(app.block_visibility.as_ref().unwrap().error.is_some()); assert_eq!( app.block_visibility_context().unwrap().names, ["VisibilityState0"] ); let _ = app.update(Message::CloseModal); let mut stale = target; stale.document += 1; visibility_action(&mut app, A::Open(stale)); assert!(app.block_visibility.is_none()); assert!(app.active_modal.is_none()); let _ = app.run_command_line("BVSTATE DIALOG"); assert_eq!(app.active_modal, Some(ModalKind::BlockVisibility)); } #[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" ); } #[test] fn locked_bedit_clean_close_preserves_block_epoch_and_render_caches() { let (mut app, reference) = fixture(); let tab = app.active_tab; let epoch_before = app.tabs[tab].scene.block_epoch; begin(&mut app, reference); let _ = app.dispatch_command("BCLOSE"); assert_eq!(app.tabs[tab].scene.block_epoch, epoch_before); } #[test] fn locked_bedit_discard_preserves_block_epoch_and_render_caches() { let (mut app, reference) = fixture(); let tab = app.active_tab; let epoch_before = app.tabs[tab].scene.block_epoch; begin(&mut app, reference); circle(&mut app, 1.0); let _ = app.dispatch_command("BCLOSE"); let _ = app.update(Message::BlockEditChangesDiscard); assert_eq!(app.tabs[tab].scene.block_epoch, epoch_before); } fn placed_visibility(app: &OpenCADStudio) -> &codec::objects::BlockVisibilityParameter { let tab = &app.tabs[app.active_tab]; let definition = tab .scene .document .block_records .get("EDIT_ME") .unwrap() .handle; crate::scene::block_visibility::parameter(&tab.scene.document, definition) .expect("visibility parameter") } #[test] fn visibility_parameter_placement_can_cancel_without_document_or_history_changes() { let (mut app, reference) = fixture(); begin(&mut app, reference); let i = app.active_tab; let before = app.tabs[i].scene.document.clone(); let history = app.tabs[i].history.undo_stack.len(); let _ = app.dispatch_command("BPARAMETER"); feed(&mut app, "Visibility"); assert_eq!( app.tabs[i].active_cmd.as_ref().unwrap().name(), "BPARAMETER" ); assert_eq!(app.tabs[i].scene.document, before); let _ = app.apply_cmd_result(crate::command::CmdResult::Cancel); assert_eq!(app.tabs[i].scene.document, before); assert_eq!(app.tabs[i].history.undo_stack.len(), history); assert!(!app.block_edit_dirty(i)); } #[test] fn visibility_parameter_placement_has_local_undo_redo_save_and_parent_undo() { let (mut app, reference) = fixture(); begin(&mut app, reference); let i = app.active_tab; let before = app.tabs[i].scene.document.clone(); let _ = app.dispatch_command("BPARAMETER VISIBILITY"); feed(&mut app, "3,4,0"); assert!(app.tabs[i].active_cmd.is_none()); assert_eq!( placed_visibility(&app).def_point, Vector3::new(3.0, 4.0, 0.0) ); assert_eq!(placed_visibility(&app).states[0].name, "VisibilityState0"); assert_eq!( members(&app), before.block_records.get("EDIT_ME").unwrap().entity_handles ); assert_eq!(app.tabs[i].history.undo_stack.len(), 1); assert!(app.block_edit_dirty(i)); app.undo_steps(1); assert_eq!(app.tabs[i].scene.document.objects, before.objects); assert!(!app.block_edit_dirty(i)); app.redo_steps(1); assert_eq!( placed_visibility(&app).def_point, Vector3::new(3.0, 4.0, 0.0) ); let _ = app.dispatch_command("BSAVE"); assert!(!app.block_edit_dirty(i)); let _ = app.dispatch_command("BEDIT_DISCARD"); assert_eq!( placed_visibility(&app).def_point, Vector3::new(3.0, 4.0, 0.0) ); app.undo_steps(1); let definition = app.tabs[i] .scene .document .block_records .get("EDIT_ME") .unwrap() .handle; assert!( crate::scene::block_visibility::parameter(&app.tabs[i].scene.document, definition) .is_none() ); } #[test] fn visibility_parameter_placement_discard_removes_the_unsaved_graph() { let (mut app, reference) = fixture(); let before = app.tabs[app.active_tab].scene.document.clone(); begin(&mut app, reference); let _ = app.dispatch_command("BPARAMETER VISIBILITY"); feed(&mut app, "7,8,0"); let _ = app.dispatch_command("BEDIT_DISCARD"); // Closing the editor synchronizes the restored camera to the active VPORT. let mut after = app.tabs[app.active_tab].scene.document.clone(); after.vports = before.vports.clone(); assert_eq!(after, before); } #[test] fn visibility_parameter_placement_rejects_duplicates_stale_context_and_nonfinite_locations() { let (mut app, reference) = fixture(); begin(&mut app, reference); let i = app.active_tab; let document_id = app.tabs[i].id; let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle; let before = app.tabs[i].scene.document.clone(); for (document_id, location) in [ (document_id + 1, glam::DVec3::ZERO), (document_id, glam::DVec3::splat(f64::NAN)), ] { let _ = app.handle_place_block_visibility_parameter(document_id, definition, location); assert_eq!(app.tabs[i].scene.document, before); assert!(app.tabs[i].history.undo_stack.is_empty()); } let _ = app.dispatch_command("BPARAMETER VISIBILITY"); feed(&mut app, "3,4,0"); let before = app.tabs[i].scene.document.clone(); let depth = app.tabs[i].history.undo_stack.len(); let _ = app.dispatch_command("LINE"); let _ = app.dispatch_command("BPARAMETER VISIBILITY"); assert_eq!(app.tabs[i].active_cmd.as_ref().unwrap().name(), "LINE"); assert_eq!(app.tabs[i].scene.document, before); assert_eq!(app.tabs[i].history.undo_stack.len(), depth); let _ = app.dispatch_command("BTESTBLOCK"); let test = app.active_tab; let before = app.tabs[test].scene.document.clone(); let _ = app.dispatch_command("LINE"); let _ = app.dispatch_command("BPARAMETER VISIBILITY"); assert_eq!(app.tabs[test].active_cmd.as_ref().unwrap().name(), "LINE"); let _ = app.handle_place_block_visibility_parameter(document_id, definition, glam::DVec3::ZERO); assert_eq!(app.tabs[test].scene.document, before); } #[test] fn visibility_parameter_placement_preserves_an_opaque_authoring_graph() { use codec::objects::{DynamicBlockObject, DynamicBlockData, ObjectType}; let (mut app, reference) = fixture(); let i = app.active_tab; let doc = &mut app.tabs[i].scene.document; let definition = doc.block_records.get("EDIT_ME").unwrap().handle; let dictionary = doc.ensure_extension_dictionary(definition); let graph_handle = doc.allocate_handle(); let mut graph = DynamicBlockObject::new("ACAD_EVALUATION_GRAPH", "AcDbEvalGraph"); graph.handle = graph_handle; graph.owner = dictionary; graph.data = DynamicBlockData::Unknown; doc.objects .insert(graph_handle, ObjectType::DynamicBlock(graph)); if let Some(ObjectType::Dictionary(dictionary)) = doc.objects.get_mut(&dictionary) { dictionary.add_entry("ACAD_ENHANCEDBLOCK", graph_handle); } begin(&mut app, reference); let before = app.tabs[i].scene.document.clone(); for command in [ "BPARAMETER VISIBILITY", "BVSTATE", "_BVSTATE\u{1f}NEW\u{1f}Other", ] { let _ = app.dispatch_command(command); assert_eq!(app.tabs[i].scene.document, before); assert!(app.tabs[i].history.undo_stack.is_empty()); } assert!(crate::scene::block_visibility::create( &mut app.tabs[i].scene.document, definition, Vector3::ZERO ) .is_none()); assert_eq!(app.tabs[i].scene.document, before); } #[test] fn visibility_parameter_placement_roundtrips_its_location_and_reference_grip_in_dwg_and_dxf() { let (mut app, reference) = fixture(); begin(&mut app, reference); let _ = app.dispatch_command("BPARAMETER VISIBILITY"); feed(&mut app, "3,4,0"); assert_eq!( app.automation_op(r#"{"op":"run","cmd":"UCS ORIGIN 1,2,0"}"#)["ok"], true ); let _ = app.dispatch_command("BSAVE"); let _ = app.dispatch_command("BEDIT_DISCARD"); let doc = &app.tabs[app.active_tab].scene.document; for extension in ["dwg", "dxf"] { let bytes = crate::io::save_to_bytes(doc, extension, doc.version).unwrap(); let read = crate::io::load_bytes(&format!("visibility.{extension}"), bytes).unwrap(); let (definition, parameter) = read .dynamic_visibility_for_insert(reference) .expect(extension); assert_eq!( read.block_records.get("EDIT_ME").unwrap().handle, definition ); assert_eq!(parameter.def_point, Vector3::new(2.0, 2.0, 0.0)); assert_eq!(parameter.states[0].name, "VisibilityState0"); let grips = read .objects .values() .filter_map(|object| match object { codec::objects::ObjectType::DynamicBlock(object) if object.owner == parameter.owner => { match &object.data { codec::objects::DynamicBlockData::VisibilityGrip(grip) => { Some(grip.location) } _ => None, } } _ => None, }) .collect::>(); assert_eq!(grips, [Vector3::new(2.0, 2.0, 0.0)], "{extension}"); } } #[test] fn visibility_parameter_placement_follows_the_editor_coordinate_frame() { let (mut app, reference) = fixture(); begin(&mut app, reference); assert_eq!( app.automation_op(r#"{"op":"run","cmd":"UCS ORIGIN 100,200,300"}"#)["ok"], true ); assert_eq!( app.automation_op(r#"{"op":"run","cmd":"UCS Z 90"}"#)["ok"], true ); // BEDIT bakes UCS changes into the definition and returns to local WCS. assert!(app.tabs[app.active_tab].active_ucs.is_none()); let _ = app.dispatch_command("BPARAMETER VISIBILITY"); feed(&mut app, "3,4,0"); assert_eq!( placed_visibility(&app).def_point, Vector3::new(3.0, 4.0, 0.0) ); assert_eq!( app.automation_op(r#"{"op":"run","cmd":"UCS ORIGIN 1,2,0"}"#)["ok"], true ); assert_eq!( placed_visibility(&app).def_point, Vector3::new(2.0, 2.0, 0.0) ); } #[test] fn visibility_parameter_placement_reframes_an_empty_definition_and_undoes_it() { let (mut app, reference) = fixture(); let i = app.active_tab; let handles = app.tabs[i] .scene .document .block_records .get("EDIT_ME") .unwrap() .entity_handles .clone(); for handle in handles { app.tabs[i].scene.document.remove_entity(handle); } begin(&mut app, reference); let _ = app.dispatch_command("BPARAMETER VISIBILITY"); feed(&mut app, "3,4,0"); let before = app.tabs[i].scene.document.objects.clone(); assert_eq!( app.automation_op(r#"{"op":"run","cmd":"UCS ORIGIN 1,2,0"}"#)["ok"], true ); assert_eq!( placed_visibility(&app).def_point, Vector3::new(2.0, 2.0, 0.0) ); app.undo_steps(1); assert_eq!( placed_visibility(&app).def_point, Vector3::new(3.0, 4.0, 0.0) ); assert_eq!(app.tabs[i].scene.document.objects, before); app.redo_steps(1); assert_eq!( placed_visibility(&app).def_point, Vector3::new(2.0, 2.0, 0.0) ); }