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,
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@@ -1514,6 +1514,15 @@ impl DimensionPreview {
|
||||
/// Returned by every `CadCommand` method to tell main.rs what to do.
|
||||
#[allow(dead_code)]
|
||||
pub enum CmdResult {
|
||||
/// Place a native two-point parameter after its base, end and label picks.
|
||||
PlaceBlockDistanceParameter {
|
||||
document_id: u64,
|
||||
definition: Handle,
|
||||
kind: crate::scene::block_distance_parameter::Kind,
|
||||
base: DVec3,
|
||||
end: DVec3,
|
||||
label: DVec3,
|
||||
},
|
||||
/// Place a native Point parameter after both authoring picks complete.
|
||||
PlaceBlockPointParameter {
|
||||
document_id: u64,
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
//! Three-pick authoring shared by Linear, Polar and XY parameters.
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::scene::{
|
||||
block_distance_parameter::{self, Kind},
|
||||
WireModel,
|
||||
};
|
||||
use codec::{types::Vector3, Handle};
|
||||
use glam::DVec3;
|
||||
|
||||
pub struct DistanceParameterCommand {
|
||||
document_id: u64,
|
||||
definition: Handle,
|
||||
kind: Kind,
|
||||
base: Option<DVec3>,
|
||||
end: Option<DVec3>,
|
||||
}
|
||||
|
||||
impl DistanceParameterCommand {
|
||||
pub(crate) fn new(document_id: u64, definition: Handle, kind: Kind) -> Self {
|
||||
Self {
|
||||
document_id,
|
||||
definition,
|
||||
kind,
|
||||
base: None,
|
||||
end: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for DistanceParameterCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"BPARAMETER"
|
||||
}
|
||||
fn prompt(&self) -> String {
|
||||
let step = if self.base.is_none() {
|
||||
"Specify parameter base point:"
|
||||
} else if self.end.is_none() {
|
||||
"Specify parameter endpoint:"
|
||||
} else {
|
||||
"Specify parameter label location:"
|
||||
};
|
||||
crate::tf!(
|
||||
"BPARAMETER {} {}",
|
||||
crate::t!(self.kind.name()),
|
||||
crate::t!(step)
|
||||
)
|
||||
.into_owned()
|
||||
}
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
if !point.is_finite() {
|
||||
return CmdResult::ReportError(
|
||||
crate::t!("Specify a finite parameter location.").into_owned(),
|
||||
);
|
||||
}
|
||||
let Some(base) = self.base else {
|
||||
self.base = Some(point);
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
let vector = |p: DVec3| Vector3::new(p.x, p.y, p.z);
|
||||
if let Some(error) = block_distance_parameter::points_error(
|
||||
self.kind,
|
||||
vector(base),
|
||||
vector(self.end.unwrap_or(point)),
|
||||
vector(point),
|
||||
) {
|
||||
return CmdResult::ReportError(crate::t!(error).into_owned());
|
||||
}
|
||||
let Some(end) = self.end else {
|
||||
self.end = Some(point);
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
CmdResult::PlaceBlockDistanceParameter {
|
||||
document_id: self.document_id,
|
||||
definition: self.definition,
|
||||
kind: self.kind,
|
||||
base,
|
||||
end,
|
||||
label: point,
|
||||
}
|
||||
}
|
||||
fn on_preview_wires(&mut self, point: DVec3) -> Vec<WireModel> {
|
||||
let Some(base) = self.base.filter(|_| point.is_finite()) else {
|
||||
return vec![];
|
||||
};
|
||||
let end = self.end.unwrap_or(point);
|
||||
let mut lines = vec![[base, end]];
|
||||
if self.kind == Kind::XY {
|
||||
lines = vec![
|
||||
[base, DVec3::new(end.x, base.y, base.z)],
|
||||
[base, DVec3::new(base.x, end.y, base.z)],
|
||||
];
|
||||
}
|
||||
if self.end.is_some() {
|
||||
if self.kind == Kind::XY {
|
||||
let x = DVec3::new(base.x, point.y, base.z);
|
||||
let y = DVec3::new(point.x, base.y, base.z);
|
||||
lines.extend([
|
||||
[base, x],
|
||||
[x, DVec3::new(end.x, point.y, base.z)],
|
||||
[base, y],
|
||||
[y, DVec3::new(point.x, end.y, base.z)],
|
||||
]);
|
||||
} else {
|
||||
let d = end - base;
|
||||
let n = DVec3::new(-d.y, d.x, 0.0).normalize_or_zero();
|
||||
let offset = n * (point - base).dot(n);
|
||||
lines = vec![
|
||||
[base, base + offset],
|
||||
[base + offset, end + offset],
|
||||
[end + offset, end],
|
||||
];
|
||||
}
|
||||
}
|
||||
lines
|
||||
.into_iter()
|
||||
.map(|line| {
|
||||
WireModel::solid_f64(
|
||||
"preview_distance_parameter".into(),
|
||||
line.into_iter().map(|point| point.to_array()).collect(),
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem,
|
||||
|
||||
pub mod visibility_parameter;
|
||||
pub mod point_parameter;
|
||||
pub mod distance_parameter;
|
||||
|
||||
pub struct BlockEditorModule;
|
||||
|
||||
|
||||
@@ -27,8 +27,10 @@ impl CadCommand for VisibilityParameterCommand {
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.choosing_type {
|
||||
crate::t!("BPARAMETER Enter parameter type [Point/Visibility] <Visibility>:")
|
||||
.into_owned()
|
||||
crate::t!(
|
||||
"BPARAMETER Enter parameter type [Point/Linear/Polar/XY/Visibility] <Visibility>:"
|
||||
)
|
||||
.into_owned()
|
||||
} else {
|
||||
crate::t!("BPARAMETER Specify visibility parameter location:").into_owned()
|
||||
}
|
||||
@@ -38,6 +40,9 @@ impl CadCommand for VisibilityParameterCommand {
|
||||
if self.choosing_type {
|
||||
vec![
|
||||
CmdOption::new(crate::t!("Point").as_ref(), "P"),
|
||||
CmdOption::new(crate::t!("Linear").as_ref(), "L"),
|
||||
CmdOption::new(crate::t!("Polar").as_ref(), "PO"),
|
||||
CmdOption::new(crate::t!("XY").as_ref(), "XY"),
|
||||
CmdOption::new(crate::t!("Visibility").as_ref(), "V"),
|
||||
]
|
||||
} else {
|
||||
@@ -60,16 +65,17 @@ impl CadCommand for VisibilityParameterCommand {
|
||||
.to_ascii_uppercase()
|
||||
.as_str()
|
||||
{
|
||||
"L" | "LINEAR" => CmdResult::Dispatch("BPARAMETER LINEAR".into()),
|
||||
"PO" | "POLAR" => CmdResult::Dispatch("BPARAMETER POLAR".into()),
|
||||
"XY" => CmdResult::Dispatch("BPARAMETER XY".into()),
|
||||
"P" | "POINT" => CmdResult::Dispatch("BPARAMETER POINT".into()),
|
||||
"" | "V" | "VISIBILITY" => {
|
||||
self.choosing_type = false;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => CmdResult::ReportError(
|
||||
crate::t!(
|
||||
"Only Point and Visibility parameter placement is currently supported."
|
||||
)
|
||||
.into_owned(),
|
||||
crate::t!("Supported parameter types: Point, Linear, Polar, XY, Visibility.")
|
||||
.into_owned(),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -183,7 +183,8 @@ pub(super) fn prepare(
|
||||
Ok((handle, slots(None)?))
|
||||
}
|
||||
|
||||
/// A one-point parameter, its XY/visibility grip and two position expressions.
|
||||
/// A parameter, its single grip and two position expressions. Supports one-point
|
||||
/// parameters and two-point parameters configured with one end grip.
|
||||
/// Graph node IDs, expression IDs and edge IDs are separate namespaces.
|
||||
pub(super) fn append(
|
||||
document: &mut CadDocument,
|
||||
|
||||
@@ -0,0 +1,794 @@
|
||||
//! Native Linear, Polar and XY parameter authoring with one end grip.
|
||||
use super::block_authoring::{self as authoring, *};
|
||||
use codec::objects::{
|
||||
BlockConnection, BlockElement, BlockEvalValue, BlockGrip, BlockGripExpression,
|
||||
BlockLinearParameter, BlockOrientedGrip, BlockParameter, BlockParameterProperty,
|
||||
BlockParameterValueSet, BlockPolarParameter, BlockTwoPointParameter, BlockXYParameter,
|
||||
DynamicBlockData, ObjectType,
|
||||
};
|
||||
use codec::types::{Transform, Vector3};
|
||||
use codec::{CadDocument, Handle};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Kind {
|
||||
Linear,
|
||||
Polar,
|
||||
XY,
|
||||
}
|
||||
|
||||
impl Kind {
|
||||
pub(crate) fn parse(text: &str) -> Option<Self> {
|
||||
match text.to_ascii_uppercase().as_str() {
|
||||
"LINEAR" | "L" => Some(Self::Linear),
|
||||
"POLAR" | "PO" => Some(Self::Polar),
|
||||
"XY" => Some(Self::XY),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub(crate) fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Linear => "Linear",
|
||||
Self::Polar => "Polar",
|
||||
Self::XY => "XY",
|
||||
}
|
||||
}
|
||||
fn class(self) -> (&'static str, &'static str) {
|
||||
match self {
|
||||
Self::Linear => ("BLOCKLINEARPARAMETER", "AcDbBlockLinearParameter"),
|
||||
Self::Polar => ("BLOCKPOLARPARAMETER", "AcDbBlockPolarParameter"),
|
||||
Self::XY => ("BLOCKXYPARAMETER", "AcDbBlockXYParameter"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Parameter<'a> {
|
||||
pub handle: Handle,
|
||||
pub kind: Kind,
|
||||
pub native: &'a BlockTwoPointParameter,
|
||||
pub labels: Vec<String>,
|
||||
pub offsets: [f64; 2],
|
||||
}
|
||||
|
||||
fn graph_handle(document: &CadDocument, definition: Handle) -> Option<Handle> {
|
||||
match document
|
||||
.objects
|
||||
.get(&document.extension_dictionary_handle(definition)?)?
|
||||
{
|
||||
ObjectType::Dictionary(dictionary) => dictionary.get("ACAD_ENHANCEDBLOCK"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parameters(document: &CadDocument, definition: Handle) -> Vec<Parameter<'_>> {
|
||||
let Some(graph) = graph_handle(document, definition) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(ObjectType::DynamicBlock(object)) = document.objects.get(&graph) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let DynamicBlockData::EvaluationGraph(data) = &object.data else {
|
||||
return Vec::new();
|
||||
};
|
||||
data.nodes
|
||||
.iter()
|
||||
.filter_map(|node| {
|
||||
let ObjectType::DynamicBlock(object) = document.objects.get(&node.expression)? else {
|
||||
return None;
|
||||
};
|
||||
if object.owner != graph {
|
||||
return None;
|
||||
}
|
||||
let (kind, native, labels, offsets) = match &object.data {
|
||||
DynamicBlockData::LinearParameter(p) => (
|
||||
Kind::Linear,
|
||||
&p.parameter,
|
||||
vec![p.distance_name.clone()],
|
||||
[p.distance, 0.0],
|
||||
),
|
||||
DynamicBlockData::PolarParameter(p) => (
|
||||
Kind::Polar,
|
||||
&p.parameter,
|
||||
vec![p.distance_name.clone(), p.angle_name.clone()],
|
||||
[p.offset, 0.0],
|
||||
),
|
||||
DynamicBlockData::XYParameter(p) => (
|
||||
Kind::XY,
|
||||
&p.parameter,
|
||||
vec![p.x_label.clone(), p.y_label.clone()],
|
||||
[p.x_value, p.y_value],
|
||||
),
|
||||
_ => return None,
|
||||
};
|
||||
Some(Parameter {
|
||||
handle: object.handle,
|
||||
kind,
|
||||
native,
|
||||
labels,
|
||||
offsets,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn placement_error(document: &CadDocument, definition: Handle) -> Option<&'static str> {
|
||||
authoring::graph(document, definition).err()
|
||||
}
|
||||
|
||||
pub(crate) fn points_error(
|
||||
kind: Kind,
|
||||
base: Vector3,
|
||||
end: Vector3,
|
||||
label: Vector3,
|
||||
) -> Option<&'static str> {
|
||||
if ![base, end, label]
|
||||
.iter()
|
||||
.all(|p| [p.x, p.y, p.z].iter().all(|v| v.is_finite()))
|
||||
{
|
||||
return Some("Specify finite parameter and label locations.");
|
||||
}
|
||||
let delta = end - base;
|
||||
if !delta.x.hypot(delta.y).is_finite() {
|
||||
return Some("The parameter dimensions are too large.");
|
||||
}
|
||||
if delta.x.hypot(delta.y) <= 1e-10
|
||||
|| (end.z - base.z).abs() > 1e-8
|
||||
|| (label.z - base.z).abs() > 1e-8
|
||||
{
|
||||
return Some("Specify distinct parameter points in the same block XY plane.");
|
||||
}
|
||||
if kind == Kind::XY && (delta.x.abs() <= 1e-10 || delta.y.abs() <= 1e-10) {
|
||||
return Some("Specify an XY endpoint with both X and Y distances.");
|
||||
}
|
||||
let offsets = offsets(kind, base, end, label);
|
||||
if !offsets.iter().all(|value| value.is_finite()) {
|
||||
return Some("The parameter dimensions are too large.");
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn offsets(kind: Kind, base: Vector3, end: Vector3, label: Vector3) -> [f64; 2] {
|
||||
if kind == Kind::XY {
|
||||
[label.y - base.y, label.x - base.x]
|
||||
} else {
|
||||
[(label - base).dot(&dimension_normal(base, end)), 0.0]
|
||||
}
|
||||
}
|
||||
|
||||
fn dimension_normal(base: Vector3, end: Vector3) -> Vector3 {
|
||||
let d = end - base;
|
||||
let length = d.x.hypot(d.y);
|
||||
// A definition can be reframed onto an upright UCS after placement.
|
||||
if length <= 1e-10 {
|
||||
Vector3::UNIT_X
|
||||
} else {
|
||||
Vector3::new(-d.y / length, d.x / length, 0.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn create(
|
||||
document: &mut CadDocument,
|
||||
definition: Handle,
|
||||
kind: Kind,
|
||||
base: Vector3,
|
||||
end: Vector3,
|
||||
label: Vector3,
|
||||
) -> Result<Handle, &'static str> {
|
||||
if let Some(error) = points_error(kind, base, end, label) {
|
||||
return Err(error);
|
||||
}
|
||||
let names = parameters(document, definition)
|
||||
.into_iter()
|
||||
.flat_map(|p| p.labels)
|
||||
.collect::<Vec<_>>();
|
||||
let unique = |prefix: &str| {
|
||||
(1..)
|
||||
.map(|n| format!("{prefix}{n}"))
|
||||
.find(|name| names.iter().all(|old| !old.eq_ignore_ascii_case(name)))
|
||||
.unwrap()
|
||||
};
|
||||
let distance_name = unique("Distance");
|
||||
let angle_name = unique("Angle");
|
||||
let x_label = unique("DistanceX");
|
||||
let y_label = unique("DistanceY");
|
||||
let values = offsets(kind, base, end, label);
|
||||
let (graph, slots) = authoring::prepare(document, definition)?;
|
||||
let (class, cpp) = kind.class();
|
||||
let (grip_class, grip_cpp) = match kind {
|
||||
Kind::Linear => ("BLOCKLINEARGRIP", "AcDbBlockLinearGrip"),
|
||||
Kind::Polar => ("BLOCKPOLARGRIP", "AcDbBlockPolarGrip"),
|
||||
Kind::XY => ("BLOCKXYGRIP", "AcDbBlockXYGrip"),
|
||||
};
|
||||
register_classes(
|
||||
document,
|
||||
&[
|
||||
(class, cpp, 55),
|
||||
(grip_class, grip_cpp, 55),
|
||||
("BLOCKGRIPLOCATIONCOMPONENT", "AcDbBlockGripExpr", 20),
|
||||
],
|
||||
);
|
||||
let [parameter, grip, x, y] = [(); 4].map(|_| document.allocate_handle());
|
||||
let element = |offset, name: &str| BlockElement {
|
||||
eval: expression(slots.eval + offset, NO_VALUE, BlockEvalValue::None),
|
||||
name: name.into(),
|
||||
major: MAJOR,
|
||||
minor: MINOR,
|
||||
eed_1071: 0,
|
||||
};
|
||||
let native = BlockTwoPointParameter {
|
||||
parameter: BlockParameter {
|
||||
element: element(0, kind.name()),
|
||||
show_properties: true,
|
||||
chain_actions: false,
|
||||
},
|
||||
definition_base_point: base,
|
||||
definition_end_point: end,
|
||||
properties: std::array::from_fn(|i| BlockParameterProperty {
|
||||
connections: if i < 2 {
|
||||
vec![]
|
||||
} else {
|
||||
vec![BlockConnection {
|
||||
code: slots.eval + 1,
|
||||
name: if i == 2 {
|
||||
"DisplacementX"
|
||||
} else {
|
||||
"DisplacementY"
|
||||
}
|
||||
.into(),
|
||||
}]
|
||||
},
|
||||
}),
|
||||
property_states: [0, slots.eval + 1, 0, 0],
|
||||
parameter_base_location: 0,
|
||||
..Default::default()
|
||||
};
|
||||
let value_set = || BlockParameterValueSet {
|
||||
flags: 1,
|
||||
..Default::default()
|
||||
};
|
||||
let data = match kind {
|
||||
Kind::Linear => DynamicBlockData::LinearParameter(BlockLinearParameter {
|
||||
parameter: native,
|
||||
distance_name,
|
||||
distance: values[0],
|
||||
value_set: value_set(),
|
||||
..Default::default()
|
||||
}),
|
||||
Kind::Polar => DynamicBlockData::PolarParameter(BlockPolarParameter {
|
||||
parameter: native,
|
||||
distance_name,
|
||||
angle_name,
|
||||
offset: values[0],
|
||||
distance_value_set: value_set(),
|
||||
angle_value_set: BlockParameterValueSet {
|
||||
flags: 3,
|
||||
maximum: std::f64::consts::TAU,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}),
|
||||
Kind::XY => DynamicBlockData::XYParameter(BlockXYParameter {
|
||||
parameter: native,
|
||||
x_label,
|
||||
y_label,
|
||||
x_value: values[0],
|
||||
y_value: values[1],
|
||||
x_value_set: value_set(),
|
||||
y_value_set: value_set(),
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
document.objects.insert(
|
||||
parameter,
|
||||
ObjectType::DynamicBlock(dynamic(parameter, graph, class, cpp, data)),
|
||||
);
|
||||
let native_grip = BlockGrip {
|
||||
element: element(1, "End Grip"),
|
||||
flags_91: slots.eval + 2,
|
||||
flags_92: slots.eval + 3,
|
||||
location: end,
|
||||
insert_cycling: true,
|
||||
insert_cycling_weight: -1,
|
||||
};
|
||||
let data = match kind {
|
||||
Kind::Linear => {
|
||||
let d = end - base;
|
||||
let length = d.x.hypot(d.y);
|
||||
DynamicBlockData::LinearGrip(BlockOrientedGrip {
|
||||
grip: native_grip,
|
||||
orientation: Vector3::new(d.x / length, d.y / length, 0.0),
|
||||
})
|
||||
}
|
||||
Kind::Polar => DynamicBlockData::PolarGrip(native_grip),
|
||||
Kind::XY => DynamicBlockData::XYGrip(native_grip),
|
||||
};
|
||||
document.objects.insert(
|
||||
grip,
|
||||
ObjectType::DynamicBlock(dynamic(grip, graph, grip_class, grip_cpp, data)),
|
||||
);
|
||||
for (handle, offset, axis) in [(x, 2, "UpdatedEndX"), (y, 3, "UpdatedEndY")] {
|
||||
document.objects.insert(
|
||||
handle,
|
||||
ObjectType::DynamicBlock(dynamic(
|
||||
handle,
|
||||
graph,
|
||||
"BLOCKGRIPLOCATIONCOMPONENT",
|
||||
"AcDbBlockGripExpr",
|
||||
DynamicBlockData::GripLocationComponent(BlockGripExpression {
|
||||
eval: expression(slots.eval + offset, 40, BlockEvalValue::Real(0.0)),
|
||||
grip_type: slots.eval,
|
||||
expression: axis.into(),
|
||||
}),
|
||||
)),
|
||||
);
|
||||
}
|
||||
authoring::append(document, graph, slots, [parameter, grip, x, y]);
|
||||
Ok(parameter)
|
||||
}
|
||||
|
||||
/// World-space editor graphics, derived entirely from the persisted native fields.
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Graphic {
|
||||
pub lines: Vec<[Vector3; 2]>,
|
||||
pub dimensions: Vec<[Vector3; 2]>,
|
||||
pub labels: Vec<(Vector3, String)>,
|
||||
pub grips: Vec<(Vector3, bool)>, // Linear uses a direction arrow; others a square.
|
||||
}
|
||||
|
||||
pub(crate) fn graphic(parameter: &Parameter<'_>) -> Graphic {
|
||||
let base = parameter.native.definition_base_point;
|
||||
let end = parameter.native.definition_end_point;
|
||||
let mut result = Graphic::default();
|
||||
let mut dimension = |a: Vector3, b: Vector3, offset: Vector3, label: String| {
|
||||
let start = a + offset;
|
||||
let finish = b + offset;
|
||||
result
|
||||
.lines
|
||||
.extend([[a, start], [b, finish], [start, finish]]);
|
||||
result.dimensions.push([start, finish]);
|
||||
result.labels.push(((start + finish) * 0.5, label));
|
||||
};
|
||||
if parameter.kind == Kind::XY {
|
||||
dimension(
|
||||
base,
|
||||
Vector3::new(end.x, base.y, base.z),
|
||||
Vector3::new(0.0, parameter.offsets[0], 0.0),
|
||||
parameter.labels[0].clone(),
|
||||
);
|
||||
dimension(
|
||||
base,
|
||||
Vector3::new(base.x, end.y, base.z),
|
||||
Vector3::new(parameter.offsets[1], 0.0, 0.0),
|
||||
parameter.labels[1].clone(),
|
||||
);
|
||||
} else {
|
||||
let d = end - base;
|
||||
let length = d.x.hypot(d.y).hypot(d.z);
|
||||
if length <= 1e-10 || !length.is_finite() {
|
||||
return result;
|
||||
}
|
||||
dimension(
|
||||
base,
|
||||
end,
|
||||
dimension_normal(base, end) * parameter.offsets[0],
|
||||
parameter.labels[0].clone(),
|
||||
);
|
||||
if parameter.kind == Kind::Polar {
|
||||
let angle = d.y.atan2(d.x);
|
||||
let radius = length * 0.25;
|
||||
let at = |angle: f64| base + Vector3::new(angle.cos(), angle.sin(), 0.0) * radius;
|
||||
result.lines.push([base, at(0.0)]);
|
||||
result.lines.push([base, end]);
|
||||
for step in 0..24 {
|
||||
result.lines.push([
|
||||
at(angle * step as f64 / 24.0),
|
||||
at(angle * (step + 1) as f64 / 24.0),
|
||||
]);
|
||||
}
|
||||
result
|
||||
.labels
|
||||
.push((at(angle * 0.5), parameter.labels[1].clone()));
|
||||
}
|
||||
}
|
||||
result.grips.push((end, parameter.kind == Kind::Linear));
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn reframe(
|
||||
document: &mut CadDocument,
|
||||
definition: Handle,
|
||||
transform: &Transform,
|
||||
) -> usize {
|
||||
let Some(graph) = graph_handle(document, definition) else {
|
||||
return 0;
|
||||
};
|
||||
let owned = parameters(document, definition)
|
||||
.iter()
|
||||
.map(|p| {
|
||||
(
|
||||
p.handle,
|
||||
p.native.property_states,
|
||||
p.kind,
|
||||
graphic(p)
|
||||
.labels
|
||||
.iter()
|
||||
.map(|(point, _)| *point)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut changed = 0;
|
||||
for (handle, grip_ids, kind, labels) in owned {
|
||||
if let Some(ObjectType::DynamicBlock(object)) = document.objects.get_mut(&handle) {
|
||||
let (native, x, y) = match &mut object.data {
|
||||
DynamicBlockData::LinearParameter(p) => (&mut p.parameter, &mut p.distance, None),
|
||||
DynamicBlockData::PolarParameter(p) => (&mut p.parameter, &mut p.offset, None),
|
||||
DynamicBlockData::XYParameter(p) => {
|
||||
(&mut p.parameter, &mut p.x_value, Some(&mut p.y_value))
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
native.definition_base_point = transform.apply(native.definition_base_point);
|
||||
native.definition_end_point = transform.apply(native.definition_end_point);
|
||||
for point in [
|
||||
&mut native.updated_base_point,
|
||||
&mut native.base_point,
|
||||
&mut native.updated_end_point,
|
||||
&mut native.end_point,
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
*point = transform.apply(*point);
|
||||
}
|
||||
if let Some(label) = labels.first() {
|
||||
*x = offsets(
|
||||
kind,
|
||||
native.definition_base_point,
|
||||
native.definition_end_point,
|
||||
transform.apply(*label),
|
||||
)[0];
|
||||
}
|
||||
if let (Some(y), Some(label)) = (y, labels.get(1)) {
|
||||
*y = transform.apply(*label).x - native.definition_base_point.x;
|
||||
}
|
||||
changed += 1;
|
||||
}
|
||||
for object in document.objects.values_mut() {
|
||||
if let ObjectType::DynamicBlock(object) = object {
|
||||
if object.owner != graph {
|
||||
continue;
|
||||
}
|
||||
let grip = match &mut object.data {
|
||||
DynamicBlockData::LinearGrip(p)
|
||||
if grip_ids.contains(&p.grip.element.eval.node_id) =>
|
||||
{
|
||||
p.orientation = transform.apply_rotation(p.orientation);
|
||||
&mut p.grip
|
||||
}
|
||||
DynamicBlockData::PolarGrip(p) | DynamicBlockData::XYGrip(p)
|
||||
if grip_ids.contains(&p.element.eval.node_id)
|
||||
&& p.element.eval.node_id > 0 =>
|
||||
{
|
||||
p
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
grip.location = transform.apply(grip.location);
|
||||
changed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::scene::{block_point_parameter, block_visibility, Scene};
|
||||
use codec::{entities::Line, EntityType};
|
||||
|
||||
fn fixture() -> (Scene, Handle) {
|
||||
let mut scene = Scene::new();
|
||||
scene.define_block_raw(
|
||||
"PARAMETERS",
|
||||
Vector3::ZERO,
|
||||
vec![EntityType::Line(Line::from_points(
|
||||
Vector3::ZERO,
|
||||
Vector3::UNIT_X,
|
||||
))],
|
||||
);
|
||||
let definition = scene
|
||||
.document
|
||||
.block_records
|
||||
.get("PARAMETERS")
|
||||
.unwrap()
|
||||
.handle;
|
||||
(scene, definition)
|
||||
}
|
||||
|
||||
fn append_batch(document: &mut CadDocument, definition: Handle) -> Vec<Handle> {
|
||||
[Kind::Linear, Kind::Polar, Kind::XY]
|
||||
.map(|kind| {
|
||||
create(
|
||||
document,
|
||||
definition,
|
||||
kind,
|
||||
Vector3::new(2.0, 3.0, 0.0),
|
||||
Vector3::new(6.0, 7.0, 0.0),
|
||||
Vector3::new(1.0, 8.0, 0.0),
|
||||
)
|
||||
.unwrap()
|
||||
})
|
||||
.to_vec()
|
||||
}
|
||||
|
||||
fn assert_native(document: &CadDocument, handles: &[Handle]) {
|
||||
for handle in handles {
|
||||
let ObjectType::DynamicBlock(object) = &document.objects[handle] else {
|
||||
panic!()
|
||||
};
|
||||
let native = match &object.data {
|
||||
DynamicBlockData::LinearParameter(p) => {
|
||||
assert!(!p.distance_name.is_empty());
|
||||
assert!((p.distance - 6.0 / 2.0_f64.sqrt()).abs() < 1e-12);
|
||||
&p.parameter
|
||||
}
|
||||
DynamicBlockData::PolarParameter(p) => {
|
||||
assert!(!p.distance_name.is_empty());
|
||||
assert!(!p.angle_name.is_empty());
|
||||
assert!((p.offset - 6.0 / 2.0_f64.sqrt()).abs() < 1e-12);
|
||||
&p.parameter
|
||||
}
|
||||
DynamicBlockData::XYParameter(p) => {
|
||||
assert_ne!(p.x_label, p.y_label);
|
||||
assert_eq!([p.x_value, p.y_value], [5.0, -1.0]);
|
||||
&p.parameter
|
||||
}
|
||||
_ => panic!("native parameter"),
|
||||
};
|
||||
assert_eq!(native.definition_base_point, Vector3::new(2.0, 3.0, 0.0));
|
||||
assert_eq!(native.definition_end_point, Vector3::new(6.0, 7.0, 0.0));
|
||||
let end = native.property_states[1];
|
||||
assert!(end > 0);
|
||||
assert_eq!(native.property_states, [0, end, 0, 0]);
|
||||
assert!(native.properties[..2]
|
||||
.iter()
|
||||
.all(|p| p.connections.is_empty()));
|
||||
for (index, axis) in [(2, "DisplacementX"), (3, "DisplacementY")] {
|
||||
assert_eq!(
|
||||
native.properties[index].connections,
|
||||
vec![BlockConnection {
|
||||
code: end,
|
||||
name: axis.into()
|
||||
}]
|
||||
);
|
||||
}
|
||||
let grip = document
|
||||
.objects
|
||||
.values()
|
||||
.find_map(|o| match o {
|
||||
ObjectType::DynamicBlock(o) if o.owner == object.owner => match &o.data {
|
||||
DynamicBlockData::LinearGrip(g) => Some(&g.grip),
|
||||
DynamicBlockData::PolarGrip(g) | DynamicBlockData::XYGrip(g) => Some(g),
|
||||
_ => None,
|
||||
}
|
||||
.filter(|g| g.element.eval.node_id == end),
|
||||
_ => None,
|
||||
})
|
||||
.expect("native end grip");
|
||||
assert_eq!(grip.location, native.definition_end_point);
|
||||
for (id, axis) in [
|
||||
(grip.flags_91, "UpdatedEndX"),
|
||||
(grip.flags_92, "UpdatedEndY"),
|
||||
] {
|
||||
assert!(document.objects.values().any(|o| matches!(o, ObjectType::DynamicBlock(o) if o.owner == object.owner && matches!(&o.data, DynamicBlockData::GripLocationComponent(c) if c.eval.node_id == id && c.grip_type == native.parameter.element.eval.node_id && c.expression == axis))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distance_parameter_batch_native_graph_preserves_point_visibility_and_roundtrips() {
|
||||
let (mut scene, definition) = fixture();
|
||||
block_point_parameter::create(
|
||||
&mut scene.document,
|
||||
definition,
|
||||
Vector3::ZERO,
|
||||
Vector3::UNIT_X,
|
||||
)
|
||||
.unwrap();
|
||||
block_visibility::create(&mut scene.document, definition, Vector3::UNIT_Y).unwrap();
|
||||
let graph = graph_handle(&scene.document, definition).unwrap();
|
||||
let before = scene.document.clone();
|
||||
let handles = append_batch(&mut scene.document, definition);
|
||||
for (handle, object) in &before.objects {
|
||||
if *handle != graph {
|
||||
assert_eq!(scene.document.objects.get(handle), Some(object));
|
||||
}
|
||||
}
|
||||
assert_native(&scene.document, &handles);
|
||||
for extension in ["dwg", "dxf"] {
|
||||
let bytes =
|
||||
crate::io::save_to_bytes(&scene.document, extension, scene.document.version)
|
||||
.unwrap();
|
||||
let document =
|
||||
crate::io::load_bytes(&format!("parameters.{extension}"), bytes).unwrap();
|
||||
assert_native(&document, &handles);
|
||||
assert!(authoring::graph(&document, definition).is_ok());
|
||||
assert_eq!(parameters(&document, definition).len(), 3);
|
||||
assert_eq!(
|
||||
block_point_parameter::parameters(&document, definition).len(),
|
||||
1
|
||||
);
|
||||
assert!(block_visibility::parameter(&document, definition).is_some());
|
||||
}
|
||||
append_batch(&mut scene.document, definition);
|
||||
let labels = parameters(&scene.document, definition)
|
||||
.into_iter()
|
||||
.flat_map(|p| p.labels)
|
||||
.collect::<Vec<_>>();
|
||||
let unique = labels.iter().collect::<std::collections::HashSet<_>>();
|
||||
assert_eq!(
|
||||
labels.len(),
|
||||
unique.len(),
|
||||
"labels are unique across parameter types"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distance_parameter_rejects_degenerate_nonfinite_and_full_graph_without_mutation() {
|
||||
let (mut scene, definition) = fixture();
|
||||
for kind in [Kind::Linear, Kind::Polar, Kind::XY] {
|
||||
for end in [
|
||||
Vector3::ZERO,
|
||||
Vector3::new(f64::NAN, 1.0, 0.0),
|
||||
Vector3::new(1.0, 1.0, 1.0),
|
||||
] {
|
||||
let before = scene.document.clone();
|
||||
assert!(create(
|
||||
&mut scene.document,
|
||||
definition,
|
||||
kind,
|
||||
Vector3::ZERO,
|
||||
end,
|
||||
Vector3::ZERO
|
||||
)
|
||||
.is_err());
|
||||
assert_eq!(scene.document, before);
|
||||
}
|
||||
}
|
||||
assert!(create(
|
||||
&mut scene.document,
|
||||
definition,
|
||||
Kind::XY,
|
||||
Vector3::ZERO,
|
||||
Vector3::UNIT_X,
|
||||
Vector3::ZERO
|
||||
)
|
||||
.is_err());
|
||||
append_batch(&mut scene.document, definition);
|
||||
let graph = graph_handle(&scene.document, definition).unwrap();
|
||||
if let ObjectType::DynamicBlock(o) = scene.document.objects.get_mut(&graph).unwrap() {
|
||||
if let DynamicBlockData::EvaluationGraph(g) = &mut o.data {
|
||||
g.first_node_id = i32::MAX;
|
||||
}
|
||||
}
|
||||
let before = scene.document.clone();
|
||||
assert!(create(
|
||||
&mut scene.document,
|
||||
definition,
|
||||
Kind::Linear,
|
||||
Vector3::ZERO,
|
||||
Vector3::UNIT_X,
|
||||
Vector3::ZERO
|
||||
)
|
||||
.is_err());
|
||||
assert_eq!(scene.document, before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distance_parameter_upright_reframe_keeps_finite_label_and_grip() {
|
||||
let (mut scene, definition) = fixture();
|
||||
create(
|
||||
&mut scene.document,
|
||||
definition,
|
||||
Kind::Linear,
|
||||
Vector3::ZERO,
|
||||
Vector3::UNIT_Y,
|
||||
Vector3::new(1.0, 0.5, 0.0),
|
||||
)
|
||||
.unwrap();
|
||||
let before = graphic(¶meters(&scene.document, definition)[0]);
|
||||
let transform = Transform::from_rotation(Vector3::UNIT_X, std::f64::consts::FRAC_PI_2);
|
||||
reframe(&mut scene.document, definition, &transform);
|
||||
let parameter = ¶meters(&scene.document, definition)[0];
|
||||
assert!(parameter.offsets[0].is_finite());
|
||||
let after = graphic(parameter);
|
||||
let expected = transform.apply(before.labels[0].0);
|
||||
assert!((after.labels[0].0 - expected).length() < 1e-10);
|
||||
assert!((after.grips[0].0 - transform.apply(before.grips[0].0)).length() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distance_parameter_reframe_and_editor_only_extents() {
|
||||
let (mut scene, definition) = fixture();
|
||||
append_batch(&mut scene.document, definition);
|
||||
let before = parameters(&scene.document, definition)
|
||||
.iter()
|
||||
.map(graphic)
|
||||
.collect::<Vec<_>>();
|
||||
let transform = Transform::from_translation(Vector3::new(10.0, 20.0, 0.0));
|
||||
assert_eq!(reframe(&mut scene.document, definition, &transform), 6);
|
||||
for (parameter, original) in parameters(&scene.document, definition).iter().zip(before) {
|
||||
let new = graphic(parameter);
|
||||
for ((point, _), (old, _)) in new.labels.iter().zip(original.labels) {
|
||||
assert_eq!(*point, transform.apply(old));
|
||||
}
|
||||
assert_eq!(new.grips[0].0, Vector3::new(16.0, 27.0, 0.0));
|
||||
}
|
||||
scene.block_edit_block = Some(definition);
|
||||
let (_, max) = scene.visible_block_bounds(definition).unwrap();
|
||||
assert!(max.x >= 16.0 && max.y >= 28.0);
|
||||
scene.block_edit_block = None;
|
||||
let (_, max) = scene.visible_block_bounds(definition).unwrap();
|
||||
assert!(max.x <= 1.0 && max.y < 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires OCS_DYNAMIC_BLOCK_FIXTURES with local DWG fixtures"]
|
||||
fn distance_parameter_fixture_batch_roundtrips() {
|
||||
use sha2::{Digest, Sha256};
|
||||
let directory = std::env::var_os("OCS_DYNAMIC_BLOCK_FIXTURES").expect("fixture directory");
|
||||
let mut paths = std::fs::read_dir(directory)
|
||||
.unwrap()
|
||||
.map(|p| p.unwrap().path())
|
||||
.filter(|p| p.extension().is_some_and(|e| e.eq_ignore_ascii_case("dwg")))
|
||||
.collect::<Vec<_>>();
|
||||
paths.sort();
|
||||
assert_eq!(paths.len(), 17);
|
||||
for path in paths {
|
||||
let bytes = std::fs::read(&path).unwrap();
|
||||
let checksum = Sha256::digest(&bytes);
|
||||
let filename = path.file_name().unwrap().to_string_lossy();
|
||||
let mut document = crate::io::load_bytes(&filename, bytes).unwrap();
|
||||
let definition = document
|
||||
.block_records
|
||||
.iter()
|
||||
.map(|b| b.handle)
|
||||
.find(|h| authoring::graph(&document, *h).is_ok_and(|g| g.is_some()))
|
||||
.expect("editable existing graph");
|
||||
let graph = graph_handle(&document, definition).unwrap();
|
||||
let before = document.clone();
|
||||
let handles = append_batch(&mut document, definition);
|
||||
for (h, o) in &before.objects {
|
||||
if *h != graph {
|
||||
assert_eq!(
|
||||
document.objects.get(h),
|
||||
Some(o),
|
||||
"{filename}: preserved {h}"
|
||||
);
|
||||
}
|
||||
}
|
||||
if let (ObjectType::DynamicBlock(old), ObjectType::DynamicBlock(new)) =
|
||||
(&before.objects[&graph], &document.objects[&graph])
|
||||
{
|
||||
if let (
|
||||
DynamicBlockData::EvaluationGraph(old),
|
||||
DynamicBlockData::EvaluationGraph(new),
|
||||
) = (&old.data, &new.data)
|
||||
{
|
||||
assert_eq!(&new.nodes[..old.nodes.len()], &old.nodes);
|
||||
assert_eq!(&new.edges[..old.edges.len()], &old.edges);
|
||||
}
|
||||
}
|
||||
for extension in ["dwg", "dxf"] {
|
||||
let bytes =
|
||||
crate::io::save_to_bytes(&document, extension, document.version).unwrap();
|
||||
let read = crate::io::load_bytes(&format!("batch.{extension}"), bytes).unwrap();
|
||||
assert_native(&read, &handles);
|
||||
assert!(authoring::graph(&read, definition).is_ok());
|
||||
}
|
||||
assert_eq!(Sha256::digest(std::fs::read(&path).unwrap()), checksum);
|
||||
eprintln!("{filename}: Linear/Polar/XY appended to existing graph; DWG/DXF passed; original objects/checksum preserved");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1677,6 +1677,16 @@ impl Scene {
|
||||
fold!(Some((point, point)));
|
||||
}
|
||||
}
|
||||
for parameter in super::block_distance_parameter::parameters(&self.document, block) {
|
||||
let graphic = super::block_distance_parameter::graphic(¶meter);
|
||||
for point in graphic.lines.into_iter().flatten().chain([
|
||||
parameter.native.definition_base_point,
|
||||
parameter.native.definition_end_point,
|
||||
]) {
|
||||
let point = glam::DVec3::new(point.x, point.y, point.z);
|
||||
fold!(Some((point, point)));
|
||||
}
|
||||
}
|
||||
if let Some(parameter) = super::block_visibility::parameter(&self.document, block) {
|
||||
let point = parameter.def_point;
|
||||
let point = glam::DVec3::new(point.x, point.y, point.z);
|
||||
|
||||
@@ -12,6 +12,7 @@ pub mod block_service;
|
||||
pub mod block_visibility;
|
||||
pub(crate) mod block_authoring;
|
||||
pub(crate) mod block_point_parameter;
|
||||
pub(crate) mod block_distance_parameter;
|
||||
pub mod cache;
|
||||
pub mod compare;
|
||||
pub mod convert;
|
||||
|
||||
@@ -559,6 +559,12 @@ impl Scene {
|
||||
block_record,
|
||||
local_from_old,
|
||||
);
|
||||
let parameter_changes = parameter_changes
|
||||
+ super::block_distance_parameter::reframe(
|
||||
&mut self.document,
|
||||
block_record,
|
||||
local_from_old,
|
||||
);
|
||||
if !changed.is_empty() || parameter_changes > 0 {
|
||||
self.rebuild_derived_caches();
|
||||
}
|
||||
|
||||
+88
-1
@@ -946,7 +946,7 @@ impl canvas::Program<Message> for GridCanvas {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selection_overlay<'a>(
|
||||
pub(crate) fn selection_overlay<'a>(
|
||||
selection: Arc<RefCell<SelectionState>>,
|
||||
snap: Option<(Point, SnapType)>,
|
||||
snap_ext_base: Option<Point>,
|
||||
@@ -979,6 +979,7 @@ pub fn selection_overlay<'a>(
|
||||
constraint_cursor_badge: Option<String>,
|
||||
authoring_visibility: Option<(Point, String, iced::Rectangle)>,
|
||||
authoring_points: Vec<(Point, Point, String, iced::Rectangle)>,
|
||||
authoring_dimensions: Vec<AuthoringDimension>,
|
||||
) -> Element<'a, Message> {
|
||||
canvas(SelectionCanvas {
|
||||
selection,
|
||||
@@ -1009,12 +1010,21 @@ pub fn selection_overlay<'a>(
|
||||
constraint_cursor_badge,
|
||||
authoring_visibility,
|
||||
authoring_points,
|
||||
authoring_dimensions,
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub(crate) struct AuthoringDimension {
|
||||
pub lines: Vec<[Point; 2]>,
|
||||
pub dimensions: Vec<[Point; 2]>,
|
||||
pub labels: Vec<(Point, String)>,
|
||||
pub grips: Vec<(Point, Option<Point>)>,
|
||||
pub clip: iced::Rectangle,
|
||||
}
|
||||
|
||||
struct SelectionCanvas {
|
||||
selection: Arc<RefCell<SelectionState>>,
|
||||
snap: Option<(Point, SnapType)>,
|
||||
@@ -1087,6 +1097,7 @@ struct SelectionCanvas {
|
||||
constraint_cursor_badge: Option<String>,
|
||||
authoring_visibility: Option<(Point, String, iced::Rectangle)>,
|
||||
authoring_points: Vec<(Point, Point, String, iced::Rectangle)>,
|
||||
authoring_dimensions: Vec<AuthoringDimension>,
|
||||
}
|
||||
|
||||
/// The move gizmo: an arrow per axis (X red, Y green, Z blue) from the
|
||||
@@ -2351,6 +2362,82 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||
});
|
||||
});
|
||||
}
|
||||
for dimension in &self.authoring_dimensions {
|
||||
frame.with_clip(dimension.clip, |frame| {
|
||||
let color = theme.palette().primary.base.color;
|
||||
for [a, b] in &dimension.lines {
|
||||
frame.stroke(
|
||||
&canvas::Path::line(*a, *b),
|
||||
canvas::Stroke::default().with_color(color),
|
||||
);
|
||||
}
|
||||
for [a, b] in &dimension.dimensions {
|
||||
let dx = b.x - a.x;
|
||||
let dy = b.y - a.y;
|
||||
let length = dx.hypot(dy).max(1.0);
|
||||
let (x, y) = (dx / length, dy / length);
|
||||
for (point, sign) in [(a, 1.0), (b, -1.0)] {
|
||||
let tip = Point::new(point.x + sign * 7.0 * x, point.y + sign * 7.0 * y);
|
||||
let arrow = canvas::Path::new(|path| {
|
||||
path.move_to(*point);
|
||||
path.line_to(Point::new(tip.x - 2.5 * y, tip.y + 2.5 * x));
|
||||
path.line_to(Point::new(tip.x + 2.5 * y, tip.y - 2.5 * x));
|
||||
path.close();
|
||||
});
|
||||
frame.fill(&arrow, color);
|
||||
}
|
||||
}
|
||||
for (point, label) in &dimension.labels {
|
||||
// Keep diagonal dimension lines from crossing the label.
|
||||
let background = Color::from_rgba(
|
||||
self.crosshair_bg[0],
|
||||
self.crosshair_bg[1],
|
||||
self.crosshair_bg[2],
|
||||
self.crosshair_bg[3],
|
||||
);
|
||||
frame.fill(
|
||||
&canvas::Path::rectangle(
|
||||
Point::new(point.x + 2.0, point.y - 19.0),
|
||||
iced::Size::new(label.chars().count() as f32 * 8.0 + 6.0, 19.0),
|
||||
),
|
||||
background,
|
||||
);
|
||||
frame.fill_text(canvas::Text {
|
||||
content: label.clone(),
|
||||
position: Point::new(point.x + 5.0, point.y - 17.0),
|
||||
color,
|
||||
size: iced::Pixels(13.0),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
for (point, base) in &dimension.grips {
|
||||
let marker = if let Some(base) = base {
|
||||
let dx = point.x - base.x;
|
||||
let dy = point.y - base.y;
|
||||
let length = dx.hypot(dy).max(1.0);
|
||||
let (x, y) = (dx / length, dy / length);
|
||||
canvas::Path::new(|path| {
|
||||
path.move_to(Point::new(point.x + 5.0 * x, point.y + 5.0 * y));
|
||||
path.line_to(Point::new(
|
||||
point.x - 4.0 * x - 4.0 * y,
|
||||
point.y - 4.0 * y + 4.0 * x,
|
||||
));
|
||||
path.line_to(Point::new(
|
||||
point.x - 4.0 * x + 4.0 * y,
|
||||
point.y - 4.0 * y - 4.0 * x,
|
||||
));
|
||||
path.close();
|
||||
})
|
||||
} else {
|
||||
canvas::Path::rectangle(
|
||||
Point::new(point.x - 3.0, point.y - 3.0),
|
||||
iced::Size::new(6.0, 6.0),
|
||||
)
|
||||
};
|
||||
frame.fill(&marker, color);
|
||||
}
|
||||
});
|
||||
}
|
||||
// Draw + hover + tooltip all read the memoised entries' precomputed
|
||||
// `top_left`/`size` (one shared `hovered` lookup below) — no offset
|
||||
// recompute, no label clones on this path.
|
||||
|
||||
@@ -208,8 +208,8 @@ fn catalog(
|
||||
column(entries.iter().map(|(label, icon)| {
|
||||
let visibility = tab == Tab::Parameters && *label == "Visibility";
|
||||
let point = tab == Tab::Parameters && *label == "Point";
|
||||
let enabled =
|
||||
(visibility && visibility_error.is_none()) || (point && point_error.is_none());
|
||||
let distance = tab == Tab::Parameters && matches!(*label, "Linear" | "Polar" | "XY");
|
||||
let enabled = (visibility && visibility_error.is_none()) || ((point || distance) && point_error.is_none());
|
||||
let base = if enabled {
|
||||
icons::semantic(icon, 28.0)
|
||||
} else {
|
||||
@@ -232,20 +232,8 @@ fn catalog(
|
||||
base
|
||||
};
|
||||
let message = enabled.then(|| Message::RibbonToolClick {
|
||||
tool_id: if point {
|
||||
"BPARAMETER_POINT"
|
||||
} else {
|
||||
"BPARAMETER_VISIBILITY"
|
||||
}
|
||||
.into(),
|
||||
event: crate::modules::ModuleEvent::Command(
|
||||
if point {
|
||||
"BPARAMETER POINT"
|
||||
} else {
|
||||
"BPARAMETER VISIBILITY"
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
tool_id: format!("BPARAMETER_{}", label.to_ascii_uppercase()),
|
||||
event: crate::modules::ModuleEvent::Command(format!("BPARAMETER {}", label.to_ascii_uppercase())),
|
||||
});
|
||||
let description = if visibility {
|
||||
crate::t!(visibility_error
|
||||
@@ -254,6 +242,8 @@ fn catalog(
|
||||
} else if point {
|
||||
crate::t!(point_error.unwrap_or("Place a point parameter and its position label."))
|
||||
.into_owned()
|
||||
} else if distance {
|
||||
crate::t!(point_error.unwrap_or("Place the base point, endpoint, and dimension label. Creates one end grip; associate an action to change geometry.")).into_owned()
|
||||
} else {
|
||||
unavailable.to_string()
|
||||
};
|
||||
@@ -262,6 +252,8 @@ fn catalog(
|
||||
container(entry).id("authoring-tool-visibility").into()
|
||||
} else if point {
|
||||
container(entry).id("authoring-tool-point").into()
|
||||
} else if distance {
|
||||
container(entry).id(match *label { "Linear" => "authoring-tool-linear", "Polar" => "authoring-tool-polar", _ => "authoring-tool-xy" }).into()
|
||||
} else {
|
||||
entry
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user