forked from 1b-agent/opencadstudio-upstream
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:
1 parent
6dab5a7229
commit
72c762e944
19 files changed
+1483
-25
No files matched your search
@@ -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();
|
||||
|
||||
@@ -575,6 +575,9 @@ impl OpenCADStudio {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
result @ CmdResult::PlaceBlockOrientationParameter { .. } => {
|
||||
return self.handle_place_block_orientation_parameter(result);
|
||||
}
|
||||
result @ CmdResult::PlaceBlockDistanceParameter { .. } => {
|
||||
return self.handle_place_block_distance_parameter(result);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,66 @@ use codec::{CadDocument, EntityType, Handle};
|
||||
use iced::Task;
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(in crate::app) fn handle_place_block_orientation_parameter(
|
||||
&mut self,
|
||||
result: crate::command::CmdResult,
|
||||
) -> Task<Message> {
|
||||
let crate::command::CmdResult::PlaceBlockOrientationParameter {
|
||||
document_id,
|
||||
definition,
|
||||
kind,
|
||||
base,
|
||||
end,
|
||||
reference,
|
||||
} = result
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let i = self.active_tab;
|
||||
if self.tabs[i].id != document_id
|
||||
|| self.tabs[i]
|
||||
.active_block_edit_session()
|
||||
.map(|s| s.br_handle)
|
||||
!= Some(definition)
|
||||
{
|
||||
self.command_line.push_error(
|
||||
crate::t!("The parameter's Block Editor session is no longer active.").as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
let vector = |p: glam::DVec3| codec::types::Vector3::new(p.x, p.y, p.z);
|
||||
if let Some(error) = self.parameter_placement_error(kind.name()).or_else(|| {
|
||||
crate::scene::block_orientation_parameter::points_error(
|
||||
kind,
|
||||
vector(base),
|
||||
vector(end),
|
||||
vector(reference),
|
||||
)
|
||||
}) {
|
||||
self.command_line.push_error(crate::t!(error).as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
self.push_undo_snapshot(i, &format!("{} parameter", kind.name()));
|
||||
crate::scene::block_orientation_parameter::create(
|
||||
&mut self.tabs[i].scene.document,
|
||||
definition,
|
||||
kind,
|
||||
vector(base),
|
||||
vector(end),
|
||||
vector(reference),
|
||||
)
|
||||
.expect("validated parameter placement");
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.finish_pending_history(i);
|
||||
self.refresh_properties();
|
||||
self.command_line
|
||||
.push_output(crate::tf!("{} parameter placed.", crate::t!(kind.name())).as_ref());
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(in crate::app) fn handle_place_block_distance_parameter(
|
||||
&mut self,
|
||||
result: crate::command::CmdResult,
|
||||
@@ -87,10 +147,11 @@ impl OpenCADStudio {
|
||||
session.br_handle,
|
||||
)
|
||||
}
|
||||
"ROTATION" | "R" | "ALIGNMENT" | "A" | "FLIP" | "F" => crate::scene::block_orientation_parameter::placement_error(&tab.scene.document, session.br_handle, crate::scene::block_orientation_parameter::Kind::parse(kind).unwrap()),
|
||||
"VISIBILITY" | "V" => {
|
||||
visibility::placement_error(&tab.scene.document, session.br_handle)
|
||||
}
|
||||
_ => Some("Supported parameter types: Point, Linear, Polar, XY, Visibility."),
|
||||
_ => Some("Supported parameter types: Point, Linear, Polar, XY, Rotation, Alignment, Flip, Visibility."),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,6 +394,14 @@ impl OpenCADStudio {
|
||||
return Some(Task::none());
|
||||
};
|
||||
let parameter_type = cmd.strip_prefix("BPARAMETER").unwrap().trim();
|
||||
if let Some(kind) =
|
||||
crate::scene::block_orientation_parameter::Kind::parse(parameter_type)
|
||||
{
|
||||
let command = crate::modules::block_editor::orientation_parameter::OrientationParameterCommand::new(self.tabs[i].id,block,kind);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
return Some(Task::none());
|
||||
}
|
||||
if let Some(kind) =
|
||||
crate::scene::block_distance_parameter::Kind::parse(parameter_type)
|
||||
{
|
||||
@@ -359,7 +428,7 @@ impl OpenCADStudio {
|
||||
{
|
||||
self.command_line.push_error(
|
||||
crate::t!(
|
||||
"Supported parameter types: Point, Linear, Polar, XY, Visibility."
|
||||
"Supported parameter types: Point, Linear, Polar, XY, Rotation, Alignment, Flip, Visibility."
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
|
||||
@@ -863,6 +863,54 @@ fn distance_parameter_batch_starts_from_actual_palette_rows() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn orientation_parameter_batch_starts_from_actual_palette_rows() {
|
||||
with_stack(|| {
|
||||
for (id, name, inputs) in [
|
||||
(
|
||||
"authoring-tool-rotation",
|
||||
"rotation",
|
||||
vec!["0.35,-0.12", "0.65,-0.12", "0.35,0.2"],
|
||||
),
|
||||
(
|
||||
"authoring-tool-alignment",
|
||||
"alignment",
|
||||
vec!["0.25,-0.1", "0.6,0.15"],
|
||||
),
|
||||
(
|
||||
"authoring-tool-flip",
|
||||
"flip",
|
||||
vec!["0.35,-0.15", "0.35,0.15", "0.6,0.15"],
|
||||
),
|
||||
] {
|
||||
let mut app = authoring_app();
|
||||
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||||
let point = authoring_control(&app, id);
|
||||
authoring_click(&mut app, point);
|
||||
let i = app.active_tab;
|
||||
let before = app.tabs[i].scene.document.clone();
|
||||
for text in &inputs[..inputs.len() - 1] {
|
||||
let _ = app.update(Message::CommandInput((*text).into()));
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
assert_eq!(app.tabs[i].scene.document, before);
|
||||
}
|
||||
let _ = app.update(Message::CommandInput(inputs.last().unwrap().to_string()));
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle;
|
||||
let found = crate::scene::block_orientation_parameter::parameters(
|
||||
&app.tabs[i].scene.document,
|
||||
definition,
|
||||
);
|
||||
assert_eq!(found.len(), 1);
|
||||
assert!(found[0].kind.name().eq_ignore_ascii_case(name));
|
||||
assert!(app.tabs[i].scene.preview_wires.is_empty());
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), 1);
|
||||
let _ = app.run_command_line("ZOOM E");
|
||||
authoring_capture(&app, &format!("{name}-parameter-placed"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn authoring_app() -> OpenCADStudio {
|
||||
use codec::{
|
||||
entities::{Insert, Line},
|
||||
|
||||
@@ -408,6 +408,7 @@ impl OpenCADStudio {
|
||||
chrome,
|
||||
self.visibility_placement_error(),
|
||||
self.point_parameter_placement_error(),
|
||||
self.parameter_placement_error("FLIP"),
|
||||
)
|
||||
}
|
||||
crate::ui::dock::PanelId::BlockPalette => {
|
||||
|
||||
+26
-3
@@ -1083,7 +1083,26 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
session.br_handle,
|
||||
)
|
||||
.iter()
|
||||
.filter_map(|parameter| {
|
||||
.map(|p| {
|
||||
(
|
||||
crate::scene::block_distance_parameter::graphic(p),
|
||||
p.native.definition_base_point,
|
||||
)
|
||||
})
|
||||
.chain(
|
||||
crate::scene::block_orientation_parameter::parameters(
|
||||
&tab.scene.document,
|
||||
session.br_handle,
|
||||
)
|
||||
.iter()
|
||||
.map(|p| {
|
||||
(
|
||||
crate::scene::block_orientation_parameter::graphic(p),
|
||||
crate::scene::block_orientation_parameter::marker_tail(p),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.filter_map(|(graphic, marker_tail)| {
|
||||
let project = |p: codec::types::Vector3| {
|
||||
let p = tab
|
||||
.scene
|
||||
@@ -1093,7 +1112,6 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
(p.x.is_finite() && p.y.is_finite())
|
||||
.then_some(iced::Point::new(vp_bounds.x + p.x, vp_bounds.y + p.y))
|
||||
};
|
||||
let graphic = crate::scene::block_distance_parameter::graphic(parameter);
|
||||
let lines = graphic
|
||||
.lines
|
||||
.into_iter()
|
||||
@@ -1111,7 +1129,7 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
Some((
|
||||
project(point)?,
|
||||
if oriented {
|
||||
Some(project(parameter.native.definition_base_point)?)
|
||||
Some(project(marker_tail)?)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
@@ -1120,6 +1138,11 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
Some(crate::ui::overlay::AuthoringDimension {
|
||||
lines,
|
||||
round_grips: graphic
|
||||
.round_grips
|
||||
.into_iter()
|
||||
.map(project)
|
||||
.collect::<Option<Vec<_>>>()?,
|
||||
dimensions: graphic
|
||||
.dimensions
|
||||
.into_iter()
|
||||
|
||||
Reference in new issue
Block a user