forked from 1b-agent/opencadstudio-upstream
feat(block-editor): wire Linear, Polar and XY parameter placement
Enable the three existing palette rows and BPARAMETER choices. Pick a base, endpoint and label location to create the appropriate native parameter, one end grip and its location expressions in the shared authoring graph. Render editor dimensions and labels, preserve them when reframing the block, and include them in editor extents. Reuse local undo/redo, save/discard and native DWG/DXF persistence. Reject incomplete or invalid placement without changing document data or history. This milestone implements parameter placement. Action association/evaluation, parameter property editing, alternate grip counts and parameter sets remain separate work. Validation: focused placement and palette interaction checks; Block Editor and dock regressions; native append and DWG/DXF round trips in all 17 research fixtures, preserving existing graph data and source files; app-view renders.
This commit is contained in:
1 parent
cdfb4ed632
commit
6dab5a7229
16 files changed
+1378
-26
No files matched your search
@@ -74,6 +74,150 @@ fn authored_points(app: &OpenCADStudio) -> Vec<codec::objects::BlockPointParamet
|
||||
.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()
|
||||
}
|
||||
|
||||
#[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();
|
||||
|
||||
@@ -575,6 +575,9 @@ impl OpenCADStudio {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
result @ CmdResult::PlaceBlockDistanceParameter { .. } => {
|
||||
return self.handle_place_block_distance_parameter(result);
|
||||
}
|
||||
result @ CmdResult::PlaceBlockPointParameter { .. } => {
|
||||
return self.handle_place_block_point_parameter(result);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,66 @@ use codec::{CadDocument, EntityType, Handle};
|
||||
use iced::Task;
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(in crate::app) fn handle_place_block_distance_parameter(
|
||||
&mut self,
|
||||
result: crate::command::CmdResult,
|
||||
) -> Task<Message> {
|
||||
let crate::command::CmdResult::PlaceBlockDistanceParameter {
|
||||
document_id,
|
||||
definition,
|
||||
kind,
|
||||
base,
|
||||
end,
|
||||
label,
|
||||
} = 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_distance_parameter::points_error(
|
||||
kind,
|
||||
vector(base),
|
||||
vector(end),
|
||||
vector(label),
|
||||
)
|
||||
}) {
|
||||
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_distance_parameter::create(
|
||||
&mut self.tabs[i].scene.document,
|
||||
definition,
|
||||
kind,
|
||||
vector(base),
|
||||
vector(end),
|
||||
vector(label),
|
||||
)
|
||||
.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 point_parameter_placement_error(&self) -> Option<&'static str> {
|
||||
self.parameter_placement_error("POINT")
|
||||
}
|
||||
@@ -21,10 +81,16 @@ impl OpenCADStudio {
|
||||
&tab.scene.document,
|
||||
session.br_handle,
|
||||
),
|
||||
"LINEAR" | "L" | "POLAR" | "PO" | "XY" => {
|
||||
crate::scene::block_distance_parameter::placement_error(
|
||||
&tab.scene.document,
|
||||
session.br_handle,
|
||||
)
|
||||
}
|
||||
"VISIBILITY" | "V" => {
|
||||
visibility::placement_error(&tab.scene.document, session.br_handle)
|
||||
}
|
||||
_ => Some("Only Point and Visibility parameter placement is currently supported."),
|
||||
_ => Some("Supported parameter types: Point, Linear, Polar, XY, Visibility."),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,6 +333,14 @@ impl OpenCADStudio {
|
||||
return Some(Task::none());
|
||||
};
|
||||
let parameter_type = cmd.strip_prefix("BPARAMETER").unwrap().trim();
|
||||
if let Some(kind) =
|
||||
crate::scene::block_distance_parameter::Kind::parse(parameter_type)
|
||||
{
|
||||
let command = crate::modules::block_editor::distance_parameter::DistanceParameterCommand::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 parameter_type.eq_ignore_ascii_case("POINT")
|
||||
|| parameter_type.eq_ignore_ascii_case("P")
|
||||
{
|
||||
@@ -285,7 +359,7 @@ impl OpenCADStudio {
|
||||
{
|
||||
self.command_line.push_error(
|
||||
crate::t!(
|
||||
"Only Point and Visibility parameter placement is currently supported."
|
||||
"Supported parameter types: Point, Linear, Polar, XY, Visibility."
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
|
||||
@@ -827,6 +827,42 @@ fn point_parameter_placement_starts_from_the_actual_palette_row() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distance_parameter_batch_starts_from_actual_palette_rows() {
|
||||
with_stack(|| {
|
||||
for (id, name) in [
|
||||
("authoring-tool-linear", "linear"),
|
||||
("authoring-tool-polar", "polar"),
|
||||
("authoring-tool-xy", "xy"),
|
||||
] {
|
||||
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 ["0.25,-0.15", "0.65,0.15"] {
|
||||
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("0.3,0.2".into()));
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle;
|
||||
let found = crate::scene::block_distance_parameter::parameters(
|
||||
&app.tabs[i].scene.document,
|
||||
definition,
|
||||
);
|
||||
assert_eq!(found.len(), 1);
|
||||
assert!(found[0].kind.name().eq_ignore_ascii_case(name));
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), 1);
|
||||
assert!(app.tabs[i].scene.preview_wires.is_empty());
|
||||
let _ = app.run_command_line("ZOOM E");
|
||||
authoring_capture(&app, &format!("{name}-parameter-placed"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn authoring_app() -> OpenCADStudio {
|
||||
use codec::{
|
||||
entities::{Insert, Line},
|
||||
|
||||
@@ -1075,6 +1075,64 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let authoring_dimensions = tab
|
||||
.active_block_edit_session()
|
||||
.map(|session| {
|
||||
crate::scene::block_distance_parameter::parameters(
|
||||
&tab.scene.document,
|
||||
session.br_handle,
|
||||
)
|
||||
.iter()
|
||||
.filter_map(|parameter| {
|
||||
let project = |p: codec::types::Vector3| {
|
||||
let p = tab
|
||||
.scene
|
||||
.camera
|
||||
.borrow()
|
||||
.project(glam::DVec3::new(p.x, p.y, p.z), vp_bounds)?;
|
||||
(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()
|
||||
.map(|[a, b]| Some([project(a)?, project(b)?]))
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
let labels = graphic
|
||||
.labels
|
||||
.into_iter()
|
||||
.map(|(point, label)| Some((project(point)?, label)))
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
let grips = graphic
|
||||
.grips
|
||||
.into_iter()
|
||||
.map(|(point, oriented)| {
|
||||
Some((
|
||||
project(point)?,
|
||||
if oriented {
|
||||
Some(project(parameter.native.definition_base_point)?)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
))
|
||||
})
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
Some(crate::ui::overlay::AuthoringDimension {
|
||||
lines,
|
||||
dimensions: graphic
|
||||
.dimensions
|
||||
.into_iter()
|
||||
.map(|[a, b]| Some([project(a)?, project(b)?]))
|
||||
.collect::<Option<Vec<_>>>()?,
|
||||
labels,
|
||||
grips,
|
||||
clip: vp_bounds,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
crate::ui::overlay::selection_overlay(
|
||||
std::sync::Arc::clone(&tab.scene.selection),
|
||||
snap_info,
|
||||
@@ -1141,6 +1199,7 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
constraint_cursor_badge,
|
||||
authoring_visibility,
|
||||
authoring_points,
|
||||
authoring_dimensions,
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user