feat(block-editor): wire Rotation, Alignment and Flip parameter placement

Enable the existing palette rows and BPARAMETER choices. Rotation accepts a
center, picked or numeric radius, and picked or numeric default angle.
Alignment places a tangent direction from its base point. Flip places a
reflection line and state label, with native grip and state expressions.

Extend shared graph append/capacity checks for Flip's additional state node
while preserving existing subgraphs. Keep Polar/Rotation angle labels unique
in either placement order. Add editor graphics, extents and definition
reframing through the existing overlay/history/save paths.

This milestone implements native parameter placement and persistence.
Rotate/Flip action association, reference alignment/grip editing, parameter
property editing and parameter sets remain separate work.

Validation covers command and palette placement, cancellation, local/parent
undo, save/discard, native DWG/DXF objects, existing graph preservation and
the 17 research fixtures. Broader block/dock regressions and editor renders
are also checked.
This commit is contained in:
Karim Jerbi committed 2026-10-15 16:50:47 +01:00
1 parent 6dab5a7229
commit 72c762e944
19 files changed
+1483 -25

No files matched your search

+149
View File
@@ -86,6 +86,155 @@ fn distance_parameter_count(app: &OpenCADStudio) -> usize {
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 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();