forked from 1b-agent/opencadstudio-upstream
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.
3583 lines
125 KiB
Rust
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)
|
|
);
|
|
}
|