Files
OneCAD-Draft/src/app/block_editor_tests.rs
T
Karim Jerbi 042f26a4cc feat(blocks): wire Stretch actions and independent reference evaluation
Author a native Stretch action from a Linear, Polar or XY parameter, a two-corner stretch frame, an object selection and a marker. Preview and retain its frame in BEDIT, with selectable and movable action markers.

Freeze line and planar lightweight-polyline vertex bindings, preserve curved-segment height, move wholly selected objects, and evaluate independent saved INSERTs from their definition. Retain native DWG/DXF data, copied-reference isolation and undo/redo.

Validate with synthetic frame, topology, parameter, history, Test Block, drawing-reference, COPY and roundtrip regressions: 340 block tests pass (6 existing opt-in ignored), plus 41 dock and 27 viewport tests. Other entity types, Polar Stretch, custom multipliers and chaining remain unsupported.
2026-10-17 10:03:59 +01:00

3583 lines
125 KiB
Rust

use super::{Message, ModalKind, OpenCADStudio};
use codec::entities::{Insert, Line};
use codec::types::Vector3;
use codec::{EntityType, Handle};
fn fixture() -> (OpenCADStudio, Handle) {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
let i = app.active_tab;
app.tabs[i].scene.define_block_raw(
"EDIT_ME",
Vector3::ZERO,
vec![EntityType::Line(Line::from_points(
Vector3::ZERO,
Vector3::UNIT_X,
))],
);
let reference = app.tabs[i]
.scene
.document
.add_entity(EntityType::Insert(Insert::new("EDIT_ME", Vector3::ZERO)))
.unwrap();
app.tabs[i].dirty = false;
(app, reference)
}
fn begin(app: &mut OpenCADStudio, reference: Handle) {
let _ = app.dispatch_command(&format!("BEDIT_BEGIN:{}", reference.value()));
assert!(app.tabs[app.active_tab].active_block_edit.is_some());
}
fn circle(app: &mut OpenCADStudio, x: f64) {
let result = app.automation_op(&format!(r#"{{"op":"run","cmd":"CIRCLE {x},0 2"}}"#));
assert_eq!(result["ok"], true, "{result}");
app.finish_pending_history(app.active_tab);
}
fn members(app: &OpenCADStudio) -> Vec<Handle> {
app.tabs[app.active_tab]
.scene
.document
.block_records
.get("EDIT_ME")
.unwrap()
.entity_handles
.clone()
}
fn main_depth(app: &OpenCADStudio) -> usize {
let tab = &app.tabs[app.active_tab];
tab.block_edit_history
.as_ref()
.map(|h| h.main.undo_stack.len())
.unwrap_or(tab.history.undo_stack.len())
}
fn feed(app: &mut OpenCADStudio, text: &str) {
let _ = app.update(Message::CommandInput(text.to_string()));
let _ = app.update(Message::CommandSubmit);
}
#[test]
fn block_actions_stretch_frame_selection_history_test_and_saved_grip() {
use crate::scene::block_actions::{self as actions, Kind};
let (mut app, reference) = fixture();
begin(&mut app, reference);
let i = app.active_tab;
let def = app.tabs[i].active_block_edit_session().unwrap().br_handle;
let _ = app.run_command_line("BPARAMETER LINEAR 0,0 10,0 5,2");
let p = actions::parameters(&app.tabs[i].scene.document, def, Kind::Stretch)[0].clone();
let before = app.tabs[i].scene.document.clone();
// A half-authored frame and Escape create neither objects nor history.
let depth = app.tabs[i].history.undo_stack.len();
let _ = app.dispatch_command("BACTION Stretch");
feed(&mut app, &p.name);
feed(&mut app, "0.5,-1");
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("BACTION Stretch");
feed(&mut app, &p.name);
assert!(!app.tabs[i]
.active_cmd
.as_ref()
.unwrap()
.is_selection_gathering());
feed(&mut app, "0.5,-1");
feed(&mut app, "0.5,1"); // zero-width error retains first corner
assert!(!app.tabs[i]
.active_cmd
.as_ref()
.unwrap()
.is_selection_gathering());
feed(&mut app, "2,1");
assert!(app.tabs[i]
.active_cmd
.as_ref()
.unwrap()
.is_selection_gathering());
let targets = members(&app);
let _ = app.feed_command(crate::command::StepInput::SelectionComplete(
targets.clone(),
));
let _ = app.feed_command(crate::command::StepInput::Enter);
feed(&mut app, "3,4");
assert!(app.tabs[i].active_cmd.is_none());
assert_eq!(app.tabs[i].history.undo_stack.len(), depth + 1);
let action = actions::actions(&app.tabs[i].scene.document, def)[0].0;
assert_eq!(
actions::frame_outline(&app.tabs[i].scene.document, action).len(),
4
);
app.undo_steps(1);
assert!(actions::actions(&app.tabs[i].scene.document, def).is_empty());
app.redo_steps(1);
let editor = app.tabs[i].scene.document.clone();
let _ = app.dispatch_command("BTESTBLOCK");
let ti = app.active_tab;
let test_insert = app.tabs[ti].block_test.as_ref().unwrap().insert;
app.tabs[ti].scene.selected.insert(test_insert);
app.refresh_properties();
let grip = app.tabs[ti]
.selected_grips
.iter()
.find(|g| super::block_actions::is_test_grip(g.id))
.unwrap()
.id;
assert!(app.start_test_action_grip(test_insert, grip));
feed(&mut app, "12,0");
let EntityType::Line(line) = app.tabs[ti].scene.document.get_entity(targets[0]).unwrap() else {
panic!()
};
assert_eq!(line.start, Vector3::ZERO);
assert_eq!(line.end, Vector3::new(3., 0., 0.));
let _ = app.dispatch_command("BCLOSETESTBLOCK");
assert_eq!(app.tabs[i].scene.document, editor);
let _ = app.dispatch_command("BSAVE");
let _ = app.dispatch_command("BCLOSE");
app.tabs[i].scene.selected.insert(reference);
app.refresh_properties();
let grip = app.tabs[i]
.selected_grips
.iter()
.find(|g| super::block_actions::is_test_grip(g.id))
.unwrap()
.id;
let depth = app.tabs[i].history.undo_stack.len();
assert!(app.start_test_action_grip(reference, grip));
feed(&mut app, "15,0");
assert_eq!(app.tabs[i].history.undo_stack.len(), depth + 1);
let changed = app.tabs[i].scene.document.clone();
let EntityType::Insert(insert) = changed.get_entity(reference).unwrap() else {
panic!()
};
let member = changed
.block_records
.get(&insert.block_name)
.unwrap()
.entity_handles[0];
let EntityType::Line(line) = changed.get_entity(member).unwrap() else {
panic!()
};
assert_eq!(line.start, Vector3::ZERO);
assert_eq!(line.end, Vector3::new(6., 0., 0.));
assert_eq!(
changed.get_entity(targets[0]),
editor.get_entity(targets[0])
);
app.undo_steps(1);
assert_eq!(
app.tabs[i].scene.document.get_entity(reference),
editor.get_entity(reference)
);
app.redo_steps(1);
assert_eq!(
app.tabs[i].scene.document.get_entity(member),
changed.get_entity(member)
);
let bytes = codec::DwgWriter::write_to_vec(&changed).unwrap();
app.tabs[i].scene.document = codec::DwgReader::from_stream(std::io::Cursor::new(bytes))
.read()
.unwrap();
begin(&mut app, reference);
assert_eq!(
app.tabs[i].active_block_edit_session().unwrap().br_handle,
def
);
assert_eq!(
actions::frame_outline(&app.tabs[i].scene.document, action).len(),
4
);
}
#[test]
fn block_parameters_rotation_only_save_close_reopen_then_flip() {
use crate::scene::block_actions::{self as actions, Kind};
for save_command in ["BEDIT_SAVE", "BSAVE"] {
let (mut app, reference) = fixture();
begin(&mut app, reference);
let i = app.active_tab;
let def = app.tabs[i].active_block_edit_session().unwrap().br_handle;
let _ = app.run_command_line("BPARAMETER ROTATION 0,0 10 0");
assert_eq!(
actions::parameters(&app.tabs[i].scene.document, def, Kind::Rotate).len(),
1
);
let _ = app.dispatch_command(save_command);
if save_command == "BSAVE" {
let _ = app.dispatch_command("BCLOSE");
}
assert!(app.tabs[i].active_block_edit.is_none());
begin(&mut app, reference);
assert_eq!(
actions::parameters(&app.tabs[i].scene.document, def, Kind::Rotate).len(),
1,
"{save_command}"
);
let _ = app.run_command_line("BPARAMETER FLIP 0,0 10,0 -2,2");
assert_eq!(
actions::parameters(&app.tabs[i].scene.document, def, Kind::Flip).len(),
1
);
let _ = app.dispatch_command("BEDIT_SAVE");
begin(&mut app, reference);
assert_eq!(
actions::parameters(&app.tabs[i].scene.document, def, Kind::Rotate).len(),
1
);
assert_eq!(
actions::parameters(&app.tabs[i].scene.document, def, Kind::Flip).len(),
1
);
}
}
#[test]
fn block_actions_save_close_and_reedit_evaluated_reference() {
use crate::scene::block_actions::{self as actions, Kind};
let (mut app, reference) = fixture();
begin(&mut app, reference);
let i = app.active_tab;
let def = app.tabs[i].active_block_edit_session().unwrap().br_handle;
let _ = app.run_command_line("BPARAMETER LINEAR 0,0 10,0 5,2");
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("BCLOSE");
assert!(app.tabs[i].active_block_edit.is_none());
assert_eq!(
actions::parameters(&app.tabs[i].scene.document, def, Kind::Move).len(),
1
);
assert_eq!(actions::actions(&app.tabs[i].scene.document, def).len(), 1);
app.tabs[i].scene.selected.insert(reference);
app.refresh_properties();
let grip = app.tabs[i]
.selected_grips
.iter()
.find(|g| super::block_actions::is_test_grip(g.id))
.unwrap()
.id;
assert!(app.start_test_action_grip(reference, grip));
feed(&mut app, "12,0");
let EntityType::Insert(insert) = app.tabs[i].scene.document.get_entity(reference).unwrap()
else {
panic!()
};
assert_ne!(insert.block_name, "EDIT_ME");
// Native save/reload must preserve the link from the evaluated variant to
// the definition that owns the parameters and actions.
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.tabs[i].scene.selected.insert(reference);
let _ = app.dispatch_command("BEDIT");
assert_eq!(
app.tabs[i].active_block_edit_session().unwrap().br_handle,
def
);
assert_eq!(
actions::parameters(&app.tabs[i].scene.document, def, Kind::Move).len(),
1
);
assert_eq!(actions::actions(&app.tabs[i].scene.document, def).len(), 1);
let _ = app.dispatch_command("BSAVE");
let _ = app.dispatch_command("BCLOSE");
app.tabs[i].scene.selected.insert(reference);
app.refresh_properties();
assert!(app.tabs[i]
.selected_grips
.iter()
.any(|g| super::block_actions::is_test_grip(g.id)));
}
#[test]
fn block_actions_rotate_flip_mouse_grips_authoring_history_and_reopen() {
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 ROTATION 0,0 10 0");
let _ = app.run_command_line("BPARAMETER FLIP 0,0 10,0 -2,2");
let def = app.tabs[source]
.active_block_edit_session()
.unwrap()
.br_handle;
let rotate =
actions::parameters(&app.tabs[source].scene.document, def, Kind::Rotate)[0].clone();
let flip = actions::parameters(&app.tabs[source].scene.document, def, Kind::Flip)[0].clone();
let targets = members(&app);
for (kind, p) in [("Rotate", &rotate), ("Flip", &flip)] {
let depth = app.tabs[source].history.undo_stack.len();
let _ = app.dispatch_command(&format!("BACTION {kind}"));
feed(&mut app, &p.name);
let _ = app.feed_command(crate::command::StepInput::SelectionComplete(
targets.clone(),
));
let _ = app.feed_command(crate::command::StepInput::Enter);
feed(&mut app, "3,4");
assert!(app.tabs[source].active_cmd.is_none());
assert_eq!(app.tabs[source].history.undo_stack.len(), depth + 1);
}
let baseline = app.tabs[source].scene.document.clone();
let click = |app: &mut OpenCADStudio, world: glam::DVec3| {
let scene = &app.tabs[app.active_tab].scene;
let (w, h) = scene.selection.borrow().view.vp_size;
let tile = scene.active_model_tile_bounds(w, h);
let at = scene
.camera
.borrow()
.project(
world,
iced::Rectangle {
x: 0.,
y: 0.,
width: tile.width,
height: tile.height,
},
)
.unwrap();
let _ = app.update(Message::ViewportLeftPressAt(iced::Point::new(
at.x + tile.x,
at.y + tile.y,
)));
let _ = app.update(Message::ViewportLeftRelease);
};
let check = |app: &OpenCADStudio, insert: Handle, expected: Vector3| {
let doc = &app.tabs[app.active_tab].scene.document;
let EntityType::Insert(ins) = doc.get_entity(insert).unwrap() else {
panic!()
};
let h = doc
.block_records
.get(&ins.block_name)
.unwrap()
.entity_handles[0];
let EntityType::Line(line) = doc.get_entity(h).unwrap() else {
panic!()
};
assert!(
line.end.distance(&expected) < 1e-5,
"{:?} != {expected:?}",
line.end
);
};
// Exercise the exact viewport grip press/release path in both contexts.
for test in [true, false] {
let insert = if test {
let _ = app.dispatch_command("BTESTBLOCK");
app.tabs[app.active_tab].block_test.as_ref().unwrap().insert
} else {
app.active_tab = source;
let _ = app.dispatch_command("BSAVE");
let _ = app.dispatch_command("BEDIT_DISCARD");
reference
};
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(-4., -3., 0.),
glam::DVec3::new(13., 13., 0.),
1600. / 900.,
);
app.tabs[i].scene.selected.insert(insert);
app.refresh_properties();
click(&mut app, rotate.end);
assert!(app.tabs[i].active_cmd.is_some());
let depth = app.tabs[i].history.undo_stack.len();
click(&mut app, glam::DVec3::new(0., 10., 0.));
assert_eq!(app.tabs[i].history.undo_stack.len(), depth + 1);
check(&app, insert, Vector3::UNIT_Y);
app.tabs[i].scene.selected.insert(insert);
app.refresh_properties();
let depth = app.tabs[i].history.undo_stack.len();
let flip_grip = app.tabs[i]
.selected_grips
.iter()
.find(|g| {
super::block_actions::is_test_grip(g.id) && g.world.distance(flip.reference) < 1e-5
})
.unwrap();
assert_eq!(
flip_grip.shape,
crate::scene::model::object::GripShape::Triangle
);
assert!(flip_grip.dir.unwrap().distance(glam::DVec3::Y) < 1e-5);
click(&mut app, flip.reference);
assert!(
app.tabs[i].active_cmd.is_none(),
"Flip toggles immediately without point input"
);
check(&app, insert, -Vector3::UNIT_Y);
app.tabs[i].scene.selected.insert(insert);
app.refresh_properties();
let flip_grip = app.tabs[i]
.selected_grips
.iter()
.find(|g| {
super::block_actions::is_test_grip(g.id) && g.world.distance(flip.reference) < 1e-5
})
.unwrap();
assert!(flip_grip.dir.unwrap().distance(-glam::DVec3::Y) < 1e-5);
assert_eq!(app.tabs[i].history.undo_stack.len(), depth + 1);
app.undo_steps(1);
check(&app, insert, Vector3::UNIT_Y);
app.redo_steps(1);
check(&app, insert, -Vector3::UNIT_Y);
if test {
assert_eq!(app.tabs[source].scene.document, baseline);
let _ = app.dispatch_command("BCLOSETESTBLOCK");
} else {
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.tabs[i].scene.selected.insert(insert);
app.refresh_properties();
click(&mut app, flip.reference);
check(&app, insert, Vector3::UNIT_Y);
let EntityType::Line(original) =
app.tabs[i].scene.document.get_entity(targets[0]).unwrap()
else {
panic!()
};
assert_eq!(original.end, Vector3::UNIT_X);
}
}
}
#[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,
value: crate::scene::block_actions::Value::Point(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,
frame: None,
});
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,
value: crate::scene::block_actions::Value::Point(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,
value: crate::scene::block_actions::Value::Point(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<codec::objects::BlockPointParameter> {
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::<Vec<_>>(),
["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::<Vec<_>>()
};
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::<Vec<_>>();
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)
);
}