forked from 1b-agent/opencadstudio-upstream
feat(block-editor): add native Point parameter placement
Enable Point in Authoring Palettes and BPARAMETER. Pick parameter and label positions, then create the native Point, XY grip and position expressions in one undo transaction. Draw editor-only markers and clear placement previews. Share graph append construction with Visibility so both types coexist in either order. Allocate node, expression and edge IDs independently; preserve existing objects, nodes and relationships and reject malformed/opaque graphs. Reframe parameter, label and grip with the edited definition. Include authoring positions in editor framing while keeping reference geometry extents unchanged. Retain save/discard and parent saved-edit undo. Action association/evaluation and parameter property editing remain separate milestones. Validation: 10 targeted, 286 block and 60 dock checks passed (overlapping filters). Appending a Point to an existing native graph passed DWG/DXF roundtrips in all 17 local fixtures; old objects and original checksums stayed unchanged. Real-view palette interaction and rendered marker/label verified.
This commit is contained in:
1 parent
daa26007f9
commit
cdfb4ed632
19 files changed
+1457
-303
No files matched your search
@@ -59,6 +59,145 @@ fn feed(app: &mut OpenCADStudio, text: &str) {
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
#[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);
|
||||
@@ -1721,8 +1860,8 @@ fn visibility_parameter_placement_rejects_duplicates_stale_context_and_nonfinite
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visibility_parameter_placement_preserves_an_existing_authoring_graph() {
|
||||
use codec::objects::{DynamicBlockObject, DynamicBlockData, BlockEvaluationGraph, ObjectType};
|
||||
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;
|
||||
@@ -1732,7 +1871,7 @@ fn visibility_parameter_placement_preserves_an_existing_authoring_graph() {
|
||||
let mut graph = DynamicBlockObject::new("ACAD_EVALUATION_GRAPH", "AcDbEvalGraph");
|
||||
graph.handle = graph_handle;
|
||||
graph.owner = dictionary;
|
||||
graph.data = DynamicBlockData::EvaluationGraph(BlockEvaluationGraph::default());
|
||||
graph.data = DynamicBlockData::Unknown;
|
||||
doc.objects
|
||||
.insert(graph_handle, ObjectType::DynamicBlock(graph));
|
||||
if let Some(ObjectType::Dictionary(dictionary)) = doc.objects.get_mut(&dictionary) {
|
||||
|
||||
@@ -575,6 +575,9 @@ impl OpenCADStudio {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
result @ CmdResult::PlaceBlockPointParameter { .. } => {
|
||||
return self.handle_place_block_point_parameter(result);
|
||||
}
|
||||
CmdResult::PlaceBlockVisibilityParameter {
|
||||
document_id,
|
||||
definition,
|
||||
|
||||
@@ -7,6 +7,80 @@ use codec::{CadDocument, EntityType, Handle};
|
||||
use iced::Task;
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(in crate::app) fn point_parameter_placement_error(&self) -> Option<&'static str> {
|
||||
self.parameter_placement_error("POINT")
|
||||
}
|
||||
|
||||
pub(in crate::app) fn parameter_placement_error(&self, kind: &str) -> Option<&'static str> {
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
let Some(session) = tab.active_block_edit_session() else {
|
||||
return Some("Open a block in the Block Editor before placing a parameter.");
|
||||
};
|
||||
match kind.to_ascii_uppercase().as_str() {
|
||||
"POINT" | "P" | "" => crate::scene::block_point_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."),
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::app) fn handle_place_block_point_parameter(
|
||||
&mut self,
|
||||
result: crate::command::CmdResult,
|
||||
) -> Task<Message> {
|
||||
let crate::command::CmdResult::PlaceBlockPointParameter {
|
||||
document_id,
|
||||
definition,
|
||||
location,
|
||||
label,
|
||||
} = result
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let i = self.active_tab;
|
||||
if self.tabs[i].id != document_id
|
||||
|| self.tabs[i]
|
||||
.active_block_edit_session()
|
||||
.map(|session| session.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();
|
||||
}
|
||||
if let Some(error) = self.point_parameter_placement_error() {
|
||||
self.command_line.push_error(crate::t!(error).as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
if !location.is_finite() || !label.is_finite() {
|
||||
self.command_line
|
||||
.push_error(crate::t!("Specify finite parameter and label locations.").as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
self.push_undo_snapshot(i, "Point parameter");
|
||||
crate::scene::block_point_parameter::create(
|
||||
&mut self.tabs[i].scene.document,
|
||||
definition,
|
||||
codec::types::Vector3::new(location.x, location.y, location.z),
|
||||
codec::types::Vector3::new(label.x, label.y, label.z),
|
||||
)
|
||||
.expect("validated Point 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::t!("Point parameter placed.").as_ref());
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(in crate::app) fn visibility_placement_error(&self) -> Option<&'static str> {
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
let Some(session) = tab.active_block_edit_session() else {
|
||||
@@ -193,13 +267,27 @@ impl OpenCADStudio {
|
||||
return Some(Task::none());
|
||||
};
|
||||
let parameter_type = cmd.strip_prefix("BPARAMETER").unwrap().trim();
|
||||
if parameter_type.eq_ignore_ascii_case("POINT")
|
||||
|| parameter_type.eq_ignore_ascii_case("P")
|
||||
{
|
||||
let command =
|
||||
crate::modules::block_editor::point_parameter::PointParameterCommand::new(
|
||||
self.tabs[i].id,
|
||||
block,
|
||||
);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
return Some(Task::none());
|
||||
}
|
||||
if !parameter_type.is_empty()
|
||||
&& !parameter_type.eq_ignore_ascii_case("VISIBILITY")
|
||||
&& !parameter_type.eq_ignore_ascii_case("V")
|
||||
{
|
||||
self.command_line.push_error(
|
||||
crate::t!("Only Visibility parameter placement is currently supported.")
|
||||
.as_ref(),
|
||||
crate::t!(
|
||||
"Only Point and Visibility parameter placement is currently supported."
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
|
||||
@@ -174,7 +174,9 @@ impl OpenCADStudio {
|
||||
let resolved = self.resolve_alias(cmd);
|
||||
let cmd = resolved.as_deref().unwrap_or(cmd);
|
||||
if cmd == "BPARAMETER" || cmd.starts_with("BPARAMETER ") {
|
||||
if let Some(error) = self.visibility_placement_error() {
|
||||
if let Some(error) =
|
||||
self.parameter_placement_error(cmd.strip_prefix("BPARAMETER").unwrap().trim())
|
||||
{
|
||||
self.command_line.push_error(crate::t!(error).as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
@@ -783,6 +783,50 @@ fn visibility_ribbon_real_controls_disable_enable_and_open_manager() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_parameter_placement_starts_from_the_actual_palette_row() {
|
||||
with_stack(|| {
|
||||
let mut app = authoring_app();
|
||||
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||||
let i = app.active_tab;
|
||||
let before = app.tabs[i].scene.document.clone();
|
||||
let point = authoring_control(&app, "authoring-tool-point");
|
||||
authoring_click(&mut app, point);
|
||||
assert_eq!(
|
||||
app.tabs[i].active_cmd.as_ref().unwrap().name(),
|
||||
"BPARAMETER"
|
||||
);
|
||||
let _ = app.update(Message::CommandInput("0.25,0.25".into()));
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
assert_eq!(app.tabs[i].scene.document, before);
|
||||
let _ = app.update(Message::CommandInput("0.65,0.15".into()));
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
assert!(
|
||||
app.tabs[i].scene.preview_wires.is_empty(),
|
||||
"placement clears its rubber-band preview"
|
||||
);
|
||||
let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle;
|
||||
assert_eq!(
|
||||
crate::scene::block_point_parameter::parameters(
|
||||
&app.tabs[i].scene.document,
|
||||
definition
|
||||
)
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert!(
|
||||
app.visibility_placement_error().is_none(),
|
||||
"Visibility can join the Point graph"
|
||||
);
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), 1);
|
||||
authoring_capture(&app, "point-parameter-placed");
|
||||
let point = authoring_control(&app, "authoring-tool-point");
|
||||
authoring_click(&mut app, point);
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), 1);
|
||||
});
|
||||
}
|
||||
|
||||
fn authoring_app() -> OpenCADStudio {
|
||||
use codec::{
|
||||
entities::{Insert, Line},
|
||||
|
||||
@@ -407,6 +407,7 @@ impl OpenCADStudio {
|
||||
width,
|
||||
chrome,
|
||||
self.visibility_placement_error(),
|
||||
self.point_parameter_placement_error(),
|
||||
)
|
||||
}
|
||||
crate::ui::dock::PanelId::BlockPalette => {
|
||||
|
||||
@@ -1047,6 +1047,34 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
(point.x.is_finite() && point.y.is_finite())
|
||||
.then(|| (point, parameter.name.clone(), vp_bounds))
|
||||
});
|
||||
let authoring_points =
|
||||
tab.active_block_edit_session()
|
||||
.map(|session| {
|
||||
crate::scene::block_point_parameter::parameters(
|
||||
&tab.scene.document,
|
||||
session.br_handle,
|
||||
)
|
||||
.into_iter()
|
||||
.filter_map(|(_, parameter)| {
|
||||
let project = |point: codec::types::Vector3| {
|
||||
let point = tab.scene.camera.borrow().project(
|
||||
glam::DVec3::new(point.x, point.y, point.z),
|
||||
vp_bounds,
|
||||
)?;
|
||||
(point.x.is_finite() && point.y.is_finite()).then_some(
|
||||
iced::Point::new(vp_bounds.x + point.x, vp_bounds.y + point.y),
|
||||
)
|
||||
};
|
||||
Some((
|
||||
project(parameter.parameter.definition_point)?,
|
||||
project(parameter.definition_label_point)?,
|
||||
parameter.position_name.clone(),
|
||||
vp_bounds,
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
crate::ui::overlay::selection_overlay(
|
||||
std::sync::Arc::clone(&tab.scene.selection),
|
||||
snap_info,
|
||||
@@ -1112,6 +1140,7 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
.map(|kind| crate::t!(kind.label()).into_owned()),
|
||||
constraint_cursor_badge,
|
||||
authoring_visibility,
|
||||
authoring_points,
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user