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()
|
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]
|
#[test]
|
||||||
fn distance_parameter_batch_three_picks_local_undo_redo_and_saved_parent_undo() {
|
fn distance_parameter_batch_three_picks_local_undo_redo_and_saved_parent_undo() {
|
||||||
let (mut app, reference) = fixture();
|
let (mut app, reference) = fixture();
|
||||||
|
|||||||
@@ -575,6 +575,9 @@ impl OpenCADStudio {
|
|||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
result @ CmdResult::PlaceBlockOrientationParameter { .. } => {
|
||||||
|
return self.handle_place_block_orientation_parameter(result);
|
||||||
|
}
|
||||||
result @ CmdResult::PlaceBlockDistanceParameter { .. } => {
|
result @ CmdResult::PlaceBlockDistanceParameter { .. } => {
|
||||||
return self.handle_place_block_distance_parameter(result);
|
return self.handle_place_block_distance_parameter(result);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,66 @@ use codec::{CadDocument, EntityType, Handle};
|
|||||||
use iced::Task;
|
use iced::Task;
|
||||||
|
|
||||||
impl OpenCADStudio {
|
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(
|
pub(in crate::app) fn handle_place_block_distance_parameter(
|
||||||
&mut self,
|
&mut self,
|
||||||
result: crate::command::CmdResult,
|
result: crate::command::CmdResult,
|
||||||
@@ -87,10 +147,11 @@ impl OpenCADStudio {
|
|||||||
session.br_handle,
|
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" | "V" => {
|
||||||
visibility::placement_error(&tab.scene.document, session.br_handle)
|
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());
|
return Some(Task::none());
|
||||||
};
|
};
|
||||||
let parameter_type = cmd.strip_prefix("BPARAMETER").unwrap().trim();
|
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) =
|
if let Some(kind) =
|
||||||
crate::scene::block_distance_parameter::Kind::parse(parameter_type)
|
crate::scene::block_distance_parameter::Kind::parse(parameter_type)
|
||||||
{
|
{
|
||||||
@@ -359,7 +428,7 @@ impl OpenCADStudio {
|
|||||||
{
|
{
|
||||||
self.command_line.push_error(
|
self.command_line.push_error(
|
||||||
crate::t!(
|
crate::t!(
|
||||||
"Supported parameter types: Point, Linear, Polar, XY, Visibility."
|
"Supported parameter types: Point, Linear, Polar, XY, Rotation, Alignment, Flip, Visibility."
|
||||||
)
|
)
|
||||||
.as_ref(),
|
.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 {
|
fn authoring_app() -> OpenCADStudio {
|
||||||
use codec::{
|
use codec::{
|
||||||
entities::{Insert, Line},
|
entities::{Insert, Line},
|
||||||
|
|||||||
@@ -408,6 +408,7 @@ impl OpenCADStudio {
|
|||||||
chrome,
|
chrome,
|
||||||
self.visibility_placement_error(),
|
self.visibility_placement_error(),
|
||||||
self.point_parameter_placement_error(),
|
self.point_parameter_placement_error(),
|
||||||
|
self.parameter_placement_error("FLIP"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
crate::ui::dock::PanelId::BlockPalette => {
|
crate::ui::dock::PanelId::BlockPalette => {
|
||||||
|
|||||||
+26
-3
@@ -1083,7 +1083,26 @@ bg={bg_ms:.1}ms n={view_count}"
|
|||||||
session.br_handle,
|
session.br_handle,
|
||||||
)
|
)
|
||||||
.iter()
|
.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 project = |p: codec::types::Vector3| {
|
||||||
let p = tab
|
let p = tab
|
||||||
.scene
|
.scene
|
||||||
@@ -1093,7 +1112,6 @@ bg={bg_ms:.1}ms n={view_count}"
|
|||||||
(p.x.is_finite() && p.y.is_finite())
|
(p.x.is_finite() && p.y.is_finite())
|
||||||
.then_some(iced::Point::new(vp_bounds.x + p.x, vp_bounds.y + p.y))
|
.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
|
let lines = graphic
|
||||||
.lines
|
.lines
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -1111,7 +1129,7 @@ bg={bg_ms:.1}ms n={view_count}"
|
|||||||
Some((
|
Some((
|
||||||
project(point)?,
|
project(point)?,
|
||||||
if oriented {
|
if oriented {
|
||||||
Some(project(parameter.native.definition_base_point)?)
|
Some(project(marker_tail)?)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
@@ -1120,6 +1138,11 @@ bg={bg_ms:.1}ms n={view_count}"
|
|||||||
.collect::<Option<Vec<_>>>()?;
|
.collect::<Option<Vec<_>>>()?;
|
||||||
Some(crate::ui::overlay::AuthoringDimension {
|
Some(crate::ui::overlay::AuthoringDimension {
|
||||||
lines,
|
lines,
|
||||||
|
round_grips: graphic
|
||||||
|
.round_grips
|
||||||
|
.into_iter()
|
||||||
|
.map(project)
|
||||||
|
.collect::<Option<Vec<_>>>()?,
|
||||||
dimensions: graphic
|
dimensions: graphic
|
||||||
.dimensions
|
.dimensions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -1514,6 +1514,15 @@ impl DimensionPreview {
|
|||||||
/// Returned by every `CadCommand` method to tell main.rs what to do.
|
/// Returned by every `CadCommand` method to tell main.rs what to do.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub enum CmdResult {
|
pub enum CmdResult {
|
||||||
|
/// Place a native Rotation, Alignment or Flip parameter.
|
||||||
|
PlaceBlockOrientationParameter {
|
||||||
|
document_id: u64,
|
||||||
|
definition: Handle,
|
||||||
|
kind: crate::scene::block_orientation_parameter::Kind,
|
||||||
|
base: DVec3,
|
||||||
|
end: DVec3,
|
||||||
|
reference: DVec3,
|
||||||
|
},
|
||||||
/// Place a native two-point parameter after its base, end and label picks.
|
/// Place a native two-point parameter after its base, end and label picks.
|
||||||
PlaceBlockDistanceParameter {
|
PlaceBlockDistanceParameter {
|
||||||
document_id: u64,
|
document_id: u64,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem,
|
|||||||
pub mod visibility_parameter;
|
pub mod visibility_parameter;
|
||||||
pub mod point_parameter;
|
pub mod point_parameter;
|
||||||
pub mod distance_parameter;
|
pub mod distance_parameter;
|
||||||
|
pub mod orientation_parameter;
|
||||||
|
|
||||||
pub struct BlockEditorModule;
|
pub struct BlockEditorModule;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
//! Interactive native Rotation, Alignment and Flip placement.
|
||||||
|
use crate::command::{CadCommand, CmdResult};
|
||||||
|
use crate::scene::{
|
||||||
|
block_orientation_parameter::{self, Kind},
|
||||||
|
WireModel,
|
||||||
|
};
|
||||||
|
use codec::{types::Vector3, Handle};
|
||||||
|
use glam::DVec3;
|
||||||
|
|
||||||
|
pub struct OrientationParameterCommand {
|
||||||
|
document_id: u64,
|
||||||
|
definition: Handle,
|
||||||
|
kind: Kind,
|
||||||
|
base: Option<DVec3>,
|
||||||
|
second: Option<DVec3>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OrientationParameterCommand {
|
||||||
|
pub(crate) fn new(document_id: u64, definition: Handle, kind: Kind) -> Self {
|
||||||
|
Self {
|
||||||
|
document_id,
|
||||||
|
definition,
|
||||||
|
kind,
|
||||||
|
base: None,
|
||||||
|
second: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn finish(&self, end: DVec3, reference: DVec3) -> CmdResult {
|
||||||
|
let base = self.base.unwrap();
|
||||||
|
let vector = |p: DVec3| Vector3::new(p.x, p.y, p.z);
|
||||||
|
if let Some(error) = block_orientation_parameter::points_error(
|
||||||
|
self.kind,
|
||||||
|
vector(base),
|
||||||
|
vector(end),
|
||||||
|
vector(reference),
|
||||||
|
) {
|
||||||
|
return CmdResult::ReportError(crate::t!(error).into_owned());
|
||||||
|
}
|
||||||
|
CmdResult::PlaceBlockOrientationParameter {
|
||||||
|
document_id: self.document_id,
|
||||||
|
definition: self.definition,
|
||||||
|
kind: self.kind,
|
||||||
|
base,
|
||||||
|
end,
|
||||||
|
reference,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn rotation_at(&self, angle: f64) -> CmdResult {
|
||||||
|
let base = self.base.unwrap();
|
||||||
|
let reference = self.second.unwrap();
|
||||||
|
let radius = (reference - base).length();
|
||||||
|
self.finish(
|
||||||
|
base + DVec3::new(angle.cos(), angle.sin(), 0.0) * radius,
|
||||||
|
reference,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CadCommand for OrientationParameterCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"BPARAMETER"
|
||||||
|
}
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
let step = if self.base.is_none() {
|
||||||
|
"Specify parameter base point:"
|
||||||
|
} else {
|
||||||
|
match (self.kind, self.second.is_some()) {
|
||||||
|
(Kind::Rotation, false) => "Specify parameter radius or pick a radius point:",
|
||||||
|
(Kind::Rotation, true) => "Specify default rotation angle or pick direction <0>:",
|
||||||
|
(Kind::Alignment, _) => "Specify alignment direction (tangent):",
|
||||||
|
(Kind::Flip, false) => "Specify reflection line endpoint:",
|
||||||
|
(Kind::Flip, true) => "Specify flip 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;
|
||||||
|
};
|
||||||
|
if (point.z - base.z).abs() > 1e-8 {
|
||||||
|
return CmdResult::ReportError(
|
||||||
|
crate::t!("Specify points in the same block XY plane.").into_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if self.kind == Kind::Flip && self.second.is_some() {
|
||||||
|
return self.finish(self.second.unwrap(), point);
|
||||||
|
}
|
||||||
|
let d = point - base;
|
||||||
|
let radius = d.x.hypot(d.y);
|
||||||
|
if radius <= 1e-10 || !radius.is_finite() || !d.length().is_finite() {
|
||||||
|
return CmdResult::ReportError(
|
||||||
|
crate::t!("Specify a finite nonzero parameter direction or radius.").into_owned(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
match self.kind {
|
||||||
|
Kind::Alignment => self.finish(point, point),
|
||||||
|
Kind::Flip => {
|
||||||
|
self.second = Some(point);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
Kind::Rotation => {
|
||||||
|
if self.second.is_some() {
|
||||||
|
self.rotation_at(d.y.atan2(d.x))
|
||||||
|
} else {
|
||||||
|
self.second = Some(base + DVec3::X * radius);
|
||||||
|
CmdResult::NeedPoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
|
if self.kind != Kind::Rotation || self.base.is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let value = text.trim().parse::<f64>().ok()?;
|
||||||
|
if !value.is_finite() {
|
||||||
|
return Some(CmdResult::ReportError(
|
||||||
|
crate::t!("Specify a finite radius or angle.").into_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Some(if self.second.is_some() {
|
||||||
|
self.rotation_at(value.rem_euclid(360.0).to_radians())
|
||||||
|
} else if value > 1e-10 {
|
||||||
|
self.on_point(self.base.unwrap() + DVec3::X * value)
|
||||||
|
} else {
|
||||||
|
CmdResult::ReportError(crate::t!("Specify a positive parameter radius.").into_owned())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn on_preview_wires(&mut self, point: DVec3) -> Vec<WireModel> {
|
||||||
|
let Some(base) = self.base.filter(|_| point.is_finite()) else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let mut lines = vec![];
|
||||||
|
if self.kind == Kind::Rotation {
|
||||||
|
let radius = self
|
||||||
|
.second
|
||||||
|
.map(|p| (p - base).length())
|
||||||
|
.unwrap_or((point - base).length());
|
||||||
|
if !radius.is_finite() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
let angle = (point.y - base.y).atan2(point.x - base.x);
|
||||||
|
let end = base + DVec3::new(angle.cos(), angle.sin(), 0.0) * radius;
|
||||||
|
lines.push(vec![base.to_array(), end.to_array()]);
|
||||||
|
lines.push(
|
||||||
|
(0..=64)
|
||||||
|
.map(|i| {
|
||||||
|
let a = std::f64::consts::TAU * i as f64 / 64.0;
|
||||||
|
(base + DVec3::new(a.cos(), a.sin(), 0.0) * radius).to_array()
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
lines.push(vec![
|
||||||
|
base.to_array(),
|
||||||
|
self.second.unwrap_or(point).to_array(),
|
||||||
|
]);
|
||||||
|
if let Some(end) = self.second {
|
||||||
|
lines.push(vec![end.to_array(), point.to_array()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines
|
||||||
|
.into_iter()
|
||||||
|
.map(|points| {
|
||||||
|
WireModel::solid_f64(
|
||||||
|
"preview_orientation_parameter".into(),
|
||||||
|
points,
|
||||||
|
WireModel::CYAN,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
if self.kind == Kind::Rotation && self.second.is_some() {
|
||||||
|
self.rotation_at(0.0)
|
||||||
|
} else {
|
||||||
|
CmdResult::Cancel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ impl CadCommand for VisibilityParameterCommand {
|
|||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
if self.choosing_type {
|
if self.choosing_type {
|
||||||
crate::t!(
|
crate::t!(
|
||||||
"BPARAMETER Enter parameter type [Point/Linear/Polar/XY/Visibility] <Visibility>:"
|
"BPARAMETER Enter parameter type [Point/Linear/Polar/XY/Rotation/Alignment/Flip/Visibility] <Visibility>:"
|
||||||
)
|
)
|
||||||
.into_owned()
|
.into_owned()
|
||||||
} else {
|
} else {
|
||||||
@@ -43,6 +43,9 @@ impl CadCommand for VisibilityParameterCommand {
|
|||||||
CmdOption::new(crate::t!("Linear").as_ref(), "L"),
|
CmdOption::new(crate::t!("Linear").as_ref(), "L"),
|
||||||
CmdOption::new(crate::t!("Polar").as_ref(), "PO"),
|
CmdOption::new(crate::t!("Polar").as_ref(), "PO"),
|
||||||
CmdOption::new(crate::t!("XY").as_ref(), "XY"),
|
CmdOption::new(crate::t!("XY").as_ref(), "XY"),
|
||||||
|
CmdOption::new(crate::t!("Rotation").as_ref(), "R"),
|
||||||
|
CmdOption::new(crate::t!("Alignment").as_ref(), "A"),
|
||||||
|
CmdOption::new(crate::t!("Flip").as_ref(), "F"),
|
||||||
CmdOption::new(crate::t!("Visibility").as_ref(), "V"),
|
CmdOption::new(crate::t!("Visibility").as_ref(), "V"),
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
@@ -68,13 +71,16 @@ impl CadCommand for VisibilityParameterCommand {
|
|||||||
"L" | "LINEAR" => CmdResult::Dispatch("BPARAMETER LINEAR".into()),
|
"L" | "LINEAR" => CmdResult::Dispatch("BPARAMETER LINEAR".into()),
|
||||||
"PO" | "POLAR" => CmdResult::Dispatch("BPARAMETER POLAR".into()),
|
"PO" | "POLAR" => CmdResult::Dispatch("BPARAMETER POLAR".into()),
|
||||||
"XY" => CmdResult::Dispatch("BPARAMETER XY".into()),
|
"XY" => CmdResult::Dispatch("BPARAMETER XY".into()),
|
||||||
|
"R" | "ROTATION" => CmdResult::Dispatch("BPARAMETER ROTATION".into()),
|
||||||
|
"A" | "ALIGNMENT" => CmdResult::Dispatch("BPARAMETER ALIGNMENT".into()),
|
||||||
|
"F" | "FLIP" => CmdResult::Dispatch("BPARAMETER FLIP".into()),
|
||||||
"P" | "POINT" => CmdResult::Dispatch("BPARAMETER POINT".into()),
|
"P" | "POINT" => CmdResult::Dispatch("BPARAMETER POINT".into()),
|
||||||
"" | "V" | "VISIBILITY" => {
|
"" | "V" | "VISIBILITY" => {
|
||||||
self.choosing_type = false;
|
self.choosing_type = false;
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
_ => CmdResult::ReportError(
|
_ => CmdResult::ReportError(
|
||||||
crate::t!("Supported parameter types: Point, Linear, Polar, XY, Visibility.")
|
crate::t!("Supported parameter types: Point, Linear, Polar, XY, Rotation, Alignment, Flip, Visibility.")
|
||||||
.into_owned(),
|
.into_owned(),
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -183,6 +183,30 @@ pub(super) fn prepare(
|
|||||||
Ok((handle, slots(None)?))
|
Ok((handle, slots(None)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Preflight the larger subgraph needed by parameters with state expressions.
|
||||||
|
pub(super) fn capacity(
|
||||||
|
document: &CadDocument,
|
||||||
|
definition: Handle,
|
||||||
|
nodes: i32,
|
||||||
|
edges: i32,
|
||||||
|
) -> Result<(), &'static str> {
|
||||||
|
let graph = graph(document, definition)?.and_then(|handle| match &document.objects[&handle] {
|
||||||
|
ObjectType::DynamicBlock(object) => match &object.data {
|
||||||
|
DynamicBlockData::EvaluationGraph(graph) => Some(graph),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
let start = slots(graph)?;
|
||||||
|
if start.node.checked_add(nodes - 1).is_none()
|
||||||
|
|| start.eval.checked_add(nodes - 1).is_none()
|
||||||
|
|| start.edge.checked_add(edges - 1).is_none()
|
||||||
|
{
|
||||||
|
return Err("The block authoring graph has no free node IDs.");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// A parameter, its single grip and two position expressions. Supports one-point
|
/// A parameter, its single grip and two position expressions. Supports one-point
|
||||||
/// parameters and two-point parameters configured with one end grip.
|
/// parameters and two-point parameters configured with one end grip.
|
||||||
/// Graph node IDs, expression IDs and edge IDs are separate namespaces.
|
/// Graph node IDs, expression IDs and edge IDs are separate namespaces.
|
||||||
@@ -191,6 +215,18 @@ pub(super) fn append(
|
|||||||
handle: Handle,
|
handle: Handle,
|
||||||
slots: Slots,
|
slots: Slots,
|
||||||
expressions: [Handle; 4],
|
expressions: [Handle; 4],
|
||||||
|
) {
|
||||||
|
append_components(document, handle, slots, &expressions, &[2, 3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Append a parameter and grip, followed by its location/state expressions.
|
||||||
|
/// `components` gives the expression offsets in native dependency order.
|
||||||
|
pub(super) fn append_components(
|
||||||
|
document: &mut CadDocument,
|
||||||
|
handle: Handle,
|
||||||
|
slots: Slots,
|
||||||
|
expressions: &[Handle],
|
||||||
|
components: &[usize],
|
||||||
) {
|
) {
|
||||||
use codec::objects::{BlockEvaluationEdge, BlockEvaluationNode};
|
use codec::objects::{BlockEvaluationEdge, BlockEvaluationNode};
|
||||||
let ObjectType::DynamicBlock(object) = document.objects.get_mut(&handle).unwrap() else {
|
let ObjectType::DynamicBlock(object) = document.objects.get_mut(&handle).unwrap() else {
|
||||||
@@ -201,13 +237,14 @@ pub(super) fn append(
|
|||||||
};
|
};
|
||||||
let n = slots.node;
|
let n = slots.node;
|
||||||
let e = slots.edge;
|
let e = slots.edge;
|
||||||
let links = [
|
let mut links = vec![[-1; 4]; expressions.len()];
|
||||||
[e, e, e + 1, e + 2],
|
links[0] = [e, e, e + 1, e + components.len() as i32];
|
||||||
[-1, -1, e, e],
|
links[1] = [-1, -1, e, e];
|
||||||
[e + 1, e + 1, -1, -1],
|
for (index, component) in components.iter().enumerate() {
|
||||||
[e + 2, e + 2, -1, -1],
|
let edge = e + 1 + index as i32;
|
||||||
];
|
links[*component] = [edge, edge, -1, -1];
|
||||||
for (offset, expression) in expressions.into_iter().enumerate() {
|
}
|
||||||
|
for (offset, expression) in expressions.iter().copied().enumerate() {
|
||||||
graph.nodes.push(BlockEvaluationNode {
|
graph.nodes.push(BlockEvaluationNode {
|
||||||
id: n + offset as i32,
|
id: n + offset as i32,
|
||||||
edge_flags: 32,
|
edge_flags: 32,
|
||||||
@@ -217,11 +254,24 @@ pub(super) fn append(
|
|||||||
active_cycles: None,
|
active_cycles: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (offset, source_node, destination_node, outgoing_edges) in [
|
let dependencies = std::iter::once((0, n + 1, n, [-1; 5])).chain(
|
||||||
(0, n + 1, n, [-1; 5]),
|
components.iter().enumerate().map(|(index, component)| {
|
||||||
(1, n, n + 2, [-1, -1, -1, e + 2, -1]),
|
let offset = index as i32 + 1;
|
||||||
(2, n, n + 3, [-1, -1, e + 1, -1, -1]),
|
let previous = if index == 0 { -1 } else { e + offset - 1 };
|
||||||
] {
|
let next = if index + 1 == components.len() {
|
||||||
|
-1
|
||||||
|
} else {
|
||||||
|
e + offset + 1
|
||||||
|
};
|
||||||
|
(
|
||||||
|
offset,
|
||||||
|
n,
|
||||||
|
n + *component as i32,
|
||||||
|
[-1, -1, previous, next, -1],
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
for (offset, source_node, destination_node, outgoing_edges) in dependencies {
|
||||||
graph.edges.push(BlockEvaluationEdge {
|
graph.edges.push(BlockEvaluationEdge {
|
||||||
id: e + offset,
|
id: e + offset,
|
||||||
next_id: 0,
|
next_id: 0,
|
||||||
@@ -231,7 +281,7 @@ pub(super) fn append(
|
|||||||
outgoing_edges,
|
outgoing_edges,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
graph.first_node_id = slots.eval + 3;
|
graph.first_node_id = slots.eval + (expressions.len() as i32 - 1);
|
||||||
graph.first_node_id_copy = graph.first_node_id;
|
graph.first_node_id_copy = graph.first_node_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,10 +176,15 @@ pub(crate) fn create(
|
|||||||
if let Some(error) = points_error(kind, base, end, label) {
|
if let Some(error) = points_error(kind, base, end, label) {
|
||||||
return Err(error);
|
return Err(error);
|
||||||
}
|
}
|
||||||
let names = parameters(document, definition)
|
let mut names = parameters(document, definition)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|p| p.labels)
|
.flat_map(|p| p.labels)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
names.extend(
|
||||||
|
super::block_orientation_parameter::parameters(document, definition)
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| p.label),
|
||||||
|
);
|
||||||
let unique = |prefix: &str| {
|
let unique = |prefix: &str| {
|
||||||
(1..)
|
(1..)
|
||||||
.map(|n| format!("{prefix}{n}"))
|
.map(|n| format!("{prefix}{n}"))
|
||||||
@@ -329,6 +334,7 @@ pub(crate) fn create(
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(crate) struct Graphic {
|
pub(crate) struct Graphic {
|
||||||
pub lines: Vec<[Vector3; 2]>,
|
pub lines: Vec<[Vector3; 2]>,
|
||||||
|
pub round_grips: Vec<Vector3>,
|
||||||
pub dimensions: Vec<[Vector3; 2]>,
|
pub dimensions: Vec<[Vector3; 2]>,
|
||||||
pub labels: Vec<(Vector3, String)>,
|
pub labels: Vec<(Vector3, String)>,
|
||||||
pub grips: Vec<(Vector3, bool)>, // Linear uses a direction arrow; others a square.
|
pub grips: Vec<(Vector3, bool)>, // Linear uses a direction arrow; others a square.
|
||||||
|
|||||||
@@ -0,0 +1,528 @@
|
|||||||
|
//! Native Rotation, Alignment and Flip parameter placement.
|
||||||
|
use super::block_authoring::{self as authoring, *};
|
||||||
|
use super::block_distance_parameter::Graphic;
|
||||||
|
use codec::objects::{
|
||||||
|
BlockAlignmentParameter, BlockConnection, BlockElement, BlockEvalValue, BlockFlipGrip,
|
||||||
|
BlockFlipParameter, BlockGrip, BlockGripExpression, BlockOrientedGrip, BlockParameter,
|
||||||
|
BlockParameterProperty, BlockParameterValueSet, BlockRotationParameter, BlockTwoPointParameter,
|
||||||
|
DynamicBlockData, ObjectType,
|
||||||
|
};
|
||||||
|
use codec::types::{Transform, Vector3};
|
||||||
|
use codec::{CadDocument, Handle};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Kind {
|
||||||
|
Rotation,
|
||||||
|
Alignment,
|
||||||
|
Flip,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Kind {
|
||||||
|
pub(crate) fn parse(text: &str) -> Option<Self> {
|
||||||
|
match text.to_ascii_uppercase().as_str() {
|
||||||
|
"ROTATION" | "R" => Some(Self::Rotation),
|
||||||
|
"ALIGNMENT" | "A" => Some(Self::Alignment),
|
||||||
|
"FLIP" | "F" => Some(Self::Flip),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub(crate) fn name(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Rotation => "Rotation",
|
||||||
|
Self::Alignment => "Alignment",
|
||||||
|
Self::Flip => "Flip",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn classes(self) -> (&'static str, &'static str, &'static str, &'static str) {
|
||||||
|
match self {
|
||||||
|
Self::Rotation => (
|
||||||
|
"BLOCKROTATIONPARAMETER",
|
||||||
|
"AcDbBlockRotationParameter",
|
||||||
|
"BLOCKROTATIONGRIP",
|
||||||
|
"AcDbBlockRotationGrip",
|
||||||
|
),
|
||||||
|
Self::Alignment => (
|
||||||
|
"BLOCKALIGNMENTPARAMETER",
|
||||||
|
"AcDbBlockAlignmentParameter",
|
||||||
|
"BLOCKALIGNMENTGRIP",
|
||||||
|
"AcDbBlockAlignmentGrip",
|
||||||
|
),
|
||||||
|
Self::Flip => (
|
||||||
|
"BLOCKFLIPPARAMETER",
|
||||||
|
"AcDbBlockFlipParameter",
|
||||||
|
"BLOCKFLIPGRIP",
|
||||||
|
"AcDbBlockFlipGrip",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Parameter<'a> {
|
||||||
|
pub handle: Handle,
|
||||||
|
pub kind: Kind,
|
||||||
|
pub native: &'a BlockTwoPointParameter,
|
||||||
|
pub reference: Vector3,
|
||||||
|
pub label: String,
|
||||||
|
pub offset: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
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![];
|
||||||
|
};
|
||||||
|
let Some(ObjectType::DynamicBlock(object)) = document.objects.get(&graph) else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
let DynamicBlockData::EvaluationGraph(data) = &object.data else {
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
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, reference, label, offset) = match &object.data {
|
||||||
|
DynamicBlockData::RotationParameter(p) => (
|
||||||
|
Kind::Rotation,
|
||||||
|
&p.parameter,
|
||||||
|
p.definition_base_angle_point,
|
||||||
|
p.angle_name.clone(),
|
||||||
|
p.angle,
|
||||||
|
),
|
||||||
|
DynamicBlockData::AlignmentParameter(p) => (
|
||||||
|
Kind::Alignment,
|
||||||
|
&p.parameter,
|
||||||
|
p.parameter.definition_end_point,
|
||||||
|
p.parameter.parameter.element.name.clone(),
|
||||||
|
0.0,
|
||||||
|
),
|
||||||
|
DynamicBlockData::FlipParameter(p) => (
|
||||||
|
Kind::Flip,
|
||||||
|
&p.parameter,
|
||||||
|
p.definition_label_point,
|
||||||
|
p.flip_label.clone(),
|
||||||
|
0.0,
|
||||||
|
),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(Parameter {
|
||||||
|
handle: object.handle,
|
||||||
|
kind,
|
||||||
|
native,
|
||||||
|
reference,
|
||||||
|
label,
|
||||||
|
offset,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn placement_error(
|
||||||
|
document: &CadDocument,
|
||||||
|
definition: Handle,
|
||||||
|
kind: Kind,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
let nodes = if kind == Kind::Flip { 5 } else { 4 };
|
||||||
|
authoring::capacity(document, definition, nodes, nodes - 1).err()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn points_error(
|
||||||
|
kind: Kind,
|
||||||
|
base: Vector3,
|
||||||
|
end: Vector3,
|
||||||
|
reference: Vector3,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
if ![base, end, reference]
|
||||||
|
.iter()
|
||||||
|
.all(|p| [p.x, p.y, p.z].iter().all(|v| v.is_finite()))
|
||||||
|
{
|
||||||
|
return Some("Specify finite parameter locations.");
|
||||||
|
}
|
||||||
|
let d = end - base;
|
||||||
|
let length = d.x.hypot(d.y);
|
||||||
|
if !length.is_finite() {
|
||||||
|
return Some("The parameter dimensions are too large.");
|
||||||
|
}
|
||||||
|
if length <= 1e-10 || (end.z - base.z).abs() > 1e-8 || (reference.z - base.z).abs() > 1e-8 {
|
||||||
|
return Some("Specify distinct parameter points in the same block XY plane.");
|
||||||
|
}
|
||||||
|
if kind == Kind::Rotation {
|
||||||
|
let r = reference - base;
|
||||||
|
if !r.length().is_finite() || !d.length().is_finite() {
|
||||||
|
return Some("The parameter dimensions are too large.");
|
||||||
|
}
|
||||||
|
if !r.x.hypot(r.y).is_finite() || (r.x.hypot(r.y) - length).abs() > length.max(1.0) * 1e-8 {
|
||||||
|
return Some("The rotation grip and base-angle point must share the specified radius.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn create(
|
||||||
|
document: &mut CadDocument,
|
||||||
|
definition: Handle,
|
||||||
|
kind: Kind,
|
||||||
|
base: Vector3,
|
||||||
|
end: Vector3,
|
||||||
|
reference: Vector3,
|
||||||
|
) -> Result<Handle, &'static str> {
|
||||||
|
if let Some(error) = points_error(kind, base, end, reference)
|
||||||
|
.or_else(|| placement_error(document, definition, kind))
|
||||||
|
{
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
let mut names = super::block_distance_parameter::parameters(document, definition)
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|p| p.labels)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
names.extend(
|
||||||
|
parameters(document, definition)
|
||||||
|
.into_iter()
|
||||||
|
.map(|p| p.label),
|
||||||
|
);
|
||||||
|
let prefix = match kind {
|
||||||
|
Kind::Rotation => "Angle",
|
||||||
|
Kind::Alignment => "Alignment",
|
||||||
|
Kind::Flip => "Flip state",
|
||||||
|
};
|
||||||
|
let label = (1..)
|
||||||
|
.map(|n| format!("{prefix}{n}"))
|
||||||
|
.find(|name| names.iter().all(|old| !old.eq_ignore_ascii_case(name)))
|
||||||
|
.unwrap();
|
||||||
|
let (graph, slots) = authoring::prepare(document, definition)?;
|
||||||
|
let (class, cpp, grip_class, grip_cpp) = kind.classes();
|
||||||
|
register_classes(
|
||||||
|
document,
|
||||||
|
&[
|
||||||
|
(class, cpp, 55),
|
||||||
|
(grip_class, grip_cpp, 55),
|
||||||
|
("BLOCKGRIPLOCATIONCOMPONENT", "AcDbBlockGripExpr", 20),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
let count = if kind == Kind::Flip { 5 } else { 4 };
|
||||||
|
let handles = (0..count)
|
||||||
|
.map(|_| document.allocate_handle())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
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,
|
||||||
|
if kind == Kind::Alignment {
|
||||||
|
&label
|
||||||
|
} else {
|
||||||
|
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 kind == Kind::Rotation && i >= 2 {
|
||||||
|
vec![BlockConnection {
|
||||||
|
code: slots.eval + 1,
|
||||||
|
name: if i == 2 {
|
||||||
|
"DisplacementX"
|
||||||
|
} else {
|
||||||
|
"DisplacementY"
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
}]
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
property_states: if kind == Kind::Rotation {
|
||||||
|
[0, slots.eval + 1, 0, 0]
|
||||||
|
} else {
|
||||||
|
[slots.eval + 1, 0, 0, 0]
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let data = match kind {
|
||||||
|
Kind::Rotation => DynamicBlockData::RotationParameter(BlockRotationParameter {
|
||||||
|
parameter: native,
|
||||||
|
definition_base_angle_point: reference,
|
||||||
|
angle_name: label,
|
||||||
|
angle: (end - base).length() * 0.15,
|
||||||
|
value_set: BlockParameterValueSet {
|
||||||
|
flags: 3,
|
||||||
|
maximum: std::f64::consts::TAU,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
Kind::Alignment => DynamicBlockData::AlignmentParameter(BlockAlignmentParameter {
|
||||||
|
parameter: native,
|
||||||
|
align_perpendicular: false,
|
||||||
|
}),
|
||||||
|
Kind::Flip => DynamicBlockData::FlipParameter(BlockFlipParameter {
|
||||||
|
parameter: native,
|
||||||
|
flip_label: label,
|
||||||
|
base_state_label: "Not flipped".into(),
|
||||||
|
flipped_state_label: "Flipped".into(),
|
||||||
|
definition_label_point: reference,
|
||||||
|
flags_96: slots.eval + 1,
|
||||||
|
tooltip: "UpdatedFlip".into(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
document.objects.insert(
|
||||||
|
handles[0],
|
||||||
|
ObjectType::DynamicBlock(dynamic(handles[0], graph, class, cpp, data)),
|
||||||
|
);
|
||||||
|
let x_offset = if kind == Kind::Flip { 3 } else { 2 };
|
||||||
|
let grip = BlockGrip {
|
||||||
|
element: element(1, "Grip"),
|
||||||
|
flags_91: slots.eval + x_offset,
|
||||||
|
flags_92: slots.eval + x_offset + 1,
|
||||||
|
location: if kind == Kind::Rotation { end } else { base },
|
||||||
|
insert_cycling: kind == Kind::Alignment,
|
||||||
|
insert_cycling_weight: -1,
|
||||||
|
};
|
||||||
|
let data = match kind {
|
||||||
|
Kind::Rotation => DynamicBlockData::RotationGrip(grip),
|
||||||
|
Kind::Alignment => DynamicBlockData::AlignmentGrip(BlockOrientedGrip {
|
||||||
|
grip,
|
||||||
|
orientation: end - base,
|
||||||
|
}),
|
||||||
|
Kind::Flip => DynamicBlockData::FlipGrip(BlockFlipGrip {
|
||||||
|
grip,
|
||||||
|
combined_state: slots.eval + 2,
|
||||||
|
orientation: end - base,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
document.objects.insert(
|
||||||
|
handles[1],
|
||||||
|
ObjectType::DynamicBlock(dynamic(handles[1], graph, grip_class, grip_cpp, data)),
|
||||||
|
);
|
||||||
|
for (offset, axis, code, value) in [
|
||||||
|
(
|
||||||
|
x_offset,
|
||||||
|
if kind == Kind::Rotation {
|
||||||
|
"UpdatedEndX"
|
||||||
|
} else {
|
||||||
|
"UpdatedBaseX"
|
||||||
|
},
|
||||||
|
40,
|
||||||
|
BlockEvalValue::Real(0.0),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
x_offset + 1,
|
||||||
|
if kind == Kind::Rotation {
|
||||||
|
"UpdatedEndY"
|
||||||
|
} else {
|
||||||
|
"UpdatedBaseY"
|
||||||
|
},
|
||||||
|
40,
|
||||||
|
BlockEvalValue::Real(0.0),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.chain((kind == Kind::Flip).then_some((2, "UpdatedFlip", 70, BlockEvalValue::Short(0))))
|
||||||
|
{
|
||||||
|
let handle = handles[offset as usize];
|
||||||
|
document.objects.insert(
|
||||||
|
handle,
|
||||||
|
ObjectType::DynamicBlock(dynamic(
|
||||||
|
handle,
|
||||||
|
graph,
|
||||||
|
"BLOCKGRIPLOCATIONCOMPONENT",
|
||||||
|
"AcDbBlockGripExpr",
|
||||||
|
DynamicBlockData::GripLocationComponent(BlockGripExpression {
|
||||||
|
eval: expression(slots.eval + offset, code, value),
|
||||||
|
grip_type: slots.eval,
|
||||||
|
expression: axis.into(),
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let components: &[usize] = if kind == Kind::Flip {
|
||||||
|
&[3, 4, 2]
|
||||||
|
} else {
|
||||||
|
&[2, 3]
|
||||||
|
};
|
||||||
|
authoring::append_components(document, graph, slots, &handles, components);
|
||||||
|
Ok(handles[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn graphic(parameter: &Parameter<'_>) -> Graphic {
|
||||||
|
let base = parameter.native.definition_base_point;
|
||||||
|
let end = parameter.native.definition_end_point;
|
||||||
|
let d = end - base;
|
||||||
|
let radius = d.x.hypot(d.y).hypot(d.z);
|
||||||
|
let mut result = Graphic::default();
|
||||||
|
if !radius.is_finite() || radius <= 1e-10 {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
match parameter.kind {
|
||||||
|
Kind::Rotation => {
|
||||||
|
let reference = parameter.reference - base;
|
||||||
|
let u = reference.normalize();
|
||||||
|
let mut v = if u.z.abs() < 1e-10 && d.z.abs() < 1e-10 {
|
||||||
|
Vector3::new(-u.y, u.x, 0.0)
|
||||||
|
} else {
|
||||||
|
d - u * d.dot(&u)
|
||||||
|
};
|
||||||
|
if v.length() <= 1e-10 {
|
||||||
|
v = Vector3::new(-u.y, u.x, 0.0);
|
||||||
|
}
|
||||||
|
if v.length() <= 1e-10 {
|
||||||
|
v = Vector3::UNIT_Y;
|
||||||
|
}
|
||||||
|
let v = v.normalize();
|
||||||
|
let mut angle = d.dot(&v).atan2(d.dot(&u)).rem_euclid(std::f64::consts::TAU);
|
||||||
|
if angle.abs() <= 1e-10 {
|
||||||
|
angle = std::f64::consts::TAU;
|
||||||
|
}
|
||||||
|
let at = |angle: f64, r: f64| base + (u * angle.cos() + v * angle.sin()) * r;
|
||||||
|
result
|
||||||
|
.lines
|
||||||
|
.extend([[base, parameter.reference], [base, end]]);
|
||||||
|
for step in 0..48 {
|
||||||
|
result.lines.push([
|
||||||
|
at(angle * step as f64 / 48.0, radius),
|
||||||
|
at(angle * (step + 1) as f64 / 48.0, radius),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
result.labels.push((
|
||||||
|
at(angle * 0.5, radius + parameter.offset),
|
||||||
|
parameter.label.clone(),
|
||||||
|
));
|
||||||
|
result.round_grips.push(end);
|
||||||
|
}
|
||||||
|
Kind::Alignment => {
|
||||||
|
result.lines.push([base, end]);
|
||||||
|
result.labels.push((end, parameter.label.clone()));
|
||||||
|
result.grips.push((base, true));
|
||||||
|
}
|
||||||
|
Kind::Flip => {
|
||||||
|
for step in (0..24).step_by(2) {
|
||||||
|
result.lines.push([
|
||||||
|
base + d * (step as f64 / 24.0),
|
||||||
|
base + d * ((step + 1) as f64 / 24.0),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
result.lines.push([end, parameter.reference]);
|
||||||
|
result
|
||||||
|
.labels
|
||||||
|
.push((parameter.reference, parameter.label.clone()));
|
||||||
|
result.grips.push((base, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tail position used to orient a base grip in the shared screen overlay.
|
||||||
|
pub(crate) fn marker_tail(parameter: &Parameter<'_>) -> Vector3 {
|
||||||
|
let base = parameter.native.definition_base_point;
|
||||||
|
let d = parameter.native.definition_end_point - base;
|
||||||
|
if parameter.kind == Kind::Flip {
|
||||||
|
base - Vector3::new(-d.y, d.x, 0.0)
|
||||||
|
} else {
|
||||||
|
base - d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut changed = 0;
|
||||||
|
for (handle, ids) in owned {
|
||||||
|
if let Some(ObjectType::DynamicBlock(object)) = document.objects.get_mut(&handle) {
|
||||||
|
let native = match &mut object.data {
|
||||||
|
DynamicBlockData::RotationParameter(p) => {
|
||||||
|
p.definition_base_angle_point = transform.apply(p.definition_base_angle_point);
|
||||||
|
&mut p.parameter
|
||||||
|
}
|
||||||
|
DynamicBlockData::AlignmentParameter(p) => &mut p.parameter,
|
||||||
|
DynamicBlockData::FlipParameter(p) => {
|
||||||
|
p.definition_label_point = transform.apply(p.definition_label_point);
|
||||||
|
&mut p.parameter
|
||||||
|
}
|
||||||
|
_ => 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);
|
||||||
|
}
|
||||||
|
changed += 1;
|
||||||
|
}
|
||||||
|
for object in document.objects.values_mut() {
|
||||||
|
let ObjectType::DynamicBlock(object) = object else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if object.owner != graph {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let grip = match &mut object.data {
|
||||||
|
DynamicBlockData::RotationGrip(g)
|
||||||
|
if g.element.eval.node_id > 0 && ids.contains(&g.element.eval.node_id) =>
|
||||||
|
{
|
||||||
|
g
|
||||||
|
}
|
||||||
|
DynamicBlockData::AlignmentGrip(g)
|
||||||
|
if g.grip.element.eval.node_id > 0
|
||||||
|
&& ids.contains(&g.grip.element.eval.node_id) =>
|
||||||
|
{
|
||||||
|
g.orientation = transform.apply_rotation(g.orientation);
|
||||||
|
&mut g.grip
|
||||||
|
}
|
||||||
|
DynamicBlockData::FlipGrip(g)
|
||||||
|
if g.grip.element.eval.node_id > 0
|
||||||
|
&& ids.contains(&g.grip.element.eval.node_id) =>
|
||||||
|
{
|
||||||
|
g.orientation = transform.apply_rotation(g.orientation);
|
||||||
|
&mut g.grip
|
||||||
|
}
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
grip.location = transform.apply(grip.location);
|
||||||
|
changed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::scene::{block_distance_parameter, 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 handle = scene
|
||||||
|
.document
|
||||||
|
.block_records
|
||||||
|
.get("PARAMETERS")
|
||||||
|
.unwrap()
|
||||||
|
.handle;
|
||||||
|
(scene, handle)
|
||||||
|
}
|
||||||
|
fn append_batch(document: &mut CadDocument, definition: Handle) {
|
||||||
|
for kind in [Kind::Rotation, Kind::Alignment, Kind::Flip] {
|
||||||
|
create(
|
||||||
|
document,
|
||||||
|
definition,
|
||||||
|
kind,
|
||||||
|
Vector3::ZERO,
|
||||||
|
Vector3::new(0.0, 2.0, 0.0),
|
||||||
|
if kind == Kind::Rotation {
|
||||||
|
Vector3::new(2.0, 0.0, 0.0)
|
||||||
|
} else {
|
||||||
|
Vector3::new(3.0, 3.0, 0.0)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn graph_data(document: &CadDocument, definition: Handle) -> &codec::objects::BlockEvaluationGraph {
|
||||||
|
let ObjectType::DynamicBlock(o) =
|
||||||
|
&document.objects[&graph_handle(document, definition).unwrap()]
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
let DynamicBlockData::EvaluationGraph(g) = &o.data else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
g
|
||||||
|
}
|
||||||
|
fn assert_roundtrips(document: &CadDocument, definition: Handle, before: &CadDocument) {
|
||||||
|
let graph = graph_handle(document, definition).unwrap();
|
||||||
|
if let Some(old_graph) = graph_handle(before, definition) {
|
||||||
|
for (h, o) in &before.objects {
|
||||||
|
if *h != old_graph {
|
||||||
|
assert_eq!(document.objects.get(h), Some(o), "preserved {h}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let old = graph_data(before, definition);
|
||||||
|
let new = graph_data(document, definition);
|
||||||
|
assert_eq!(&new.nodes[..old.nodes.len()], &old.nodes);
|
||||||
|
assert_eq!(&new.edges[..old.edges.len()], &old.edges);
|
||||||
|
}
|
||||||
|
let objects = document
|
||||||
|
.objects
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(h, o)| match o {
|
||||||
|
ObjectType::DynamicBlock(o) if o.owner == graph && !before.objects.contains_key(h) => {
|
||||||
|
Some((*h, o.data.clone()))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(objects.len(), 13);
|
||||||
|
let g = graph_data(document, definition);
|
||||||
|
for node in g
|
||||||
|
.nodes
|
||||||
|
.iter()
|
||||||
|
.filter(|n| objects.iter().any(|(h, _)| *h == n.expression))
|
||||||
|
{
|
||||||
|
let incoming = g
|
||||||
|
.edges
|
||||||
|
.iter()
|
||||||
|
.filter(|e| e.destination_node == node.id)
|
||||||
|
.map(|e| e.id)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let outgoing = g
|
||||||
|
.edges
|
||||||
|
.iter()
|
||||||
|
.filter(|e| e.source_node == node.id)
|
||||||
|
.map(|e| e.id)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(
|
||||||
|
node.node_data,
|
||||||
|
[
|
||||||
|
incoming.first().copied().unwrap_or(-1),
|
||||||
|
incoming.last().copied().unwrap_or(-1),
|
||||||
|
outgoing.first().copied().unwrap_or(-1),
|
||||||
|
outgoing.last().copied().unwrap_or(-1)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for extension in ["dwg", "dxf"] {
|
||||||
|
let bytes = crate::io::save_to_bytes(document, extension, document.version).unwrap();
|
||||||
|
let read = crate::io::load_bytes(&format!("orientation.{extension}"), bytes).unwrap();
|
||||||
|
for (h, data) in &objects {
|
||||||
|
let ObjectType::DynamicBlock(object) = &read.objects[h] else {
|
||||||
|
panic!("{extension}: native {h}")
|
||||||
|
};
|
||||||
|
assert_eq!(&object.data, data, "{extension}: {h}");
|
||||||
|
assert_eq!(object.owner, graph);
|
||||||
|
}
|
||||||
|
assert!(authoring::graph(&read, definition).is_ok());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn orientation_parameter_native_batch_coexists_and_roundtrips_complete_objects() {
|
||||||
|
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::ZERO).unwrap();
|
||||||
|
block_distance_parameter::create(
|
||||||
|
&mut scene.document,
|
||||||
|
definition,
|
||||||
|
block_distance_parameter::Kind::Polar,
|
||||||
|
Vector3::ZERO,
|
||||||
|
Vector3::UNIT_Y,
|
||||||
|
Vector3::UNIT_X,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let before = scene.document.clone();
|
||||||
|
append_batch(&mut scene.document, definition);
|
||||||
|
assert_roundtrips(&scene.document, definition, &before);
|
||||||
|
let parameters = parameters(&scene.document, definition);
|
||||||
|
assert_eq!(parameters[0].label, "Angle2", "share labels with Polar");
|
||||||
|
for p in ¶meters {
|
||||||
|
let id = p.native.parameter.element.eval.node_id;
|
||||||
|
assert_eq!(
|
||||||
|
p.native.property_states,
|
||||||
|
if p.kind == Kind::Rotation {
|
||||||
|
[0, id + 1, 0, 0]
|
||||||
|
} else {
|
||||||
|
[id + 1, 0, 0, 0]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let flip = parameters.iter().find(|p| p.kind == Kind::Flip).unwrap();
|
||||||
|
let id = flip.native.parameter.element.eval.node_id;
|
||||||
|
assert!(scene.document.objects.values().any(|o|matches!(o,ObjectType::DynamicBlock(o) if matches!(&o.data,DynamicBlockData::FlipGrip(g) if g.combined_state==id+2&&g.grip.flags_91==id+3&&g.grip.flags_92==id+4))));
|
||||||
|
assert!(scene.document.objects.values().any(|o|matches!(o,ObjectType::DynamicBlock(o) if matches!(&o.data,DynamicBlockData::GripLocationComponent(c) if c.expression=="UpdatedFlip"&&c.grip_type==id&&c.eval.value_code==70&&c.eval.value==BlockEvalValue::Short(0)))));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn orientation_parameter_rotation_and_polar_share_unique_labels_in_both_orders() {
|
||||||
|
for rotation_first in [false, true] {
|
||||||
|
let (mut scene, definition) = fixture();
|
||||||
|
for rotation in [rotation_first, !rotation_first] {
|
||||||
|
if rotation {
|
||||||
|
create(
|
||||||
|
&mut scene.document,
|
||||||
|
definition,
|
||||||
|
Kind::Rotation,
|
||||||
|
Vector3::ZERO,
|
||||||
|
Vector3::UNIT_Y,
|
||||||
|
Vector3::UNIT_X,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
} else {
|
||||||
|
block_distance_parameter::create(
|
||||||
|
&mut scene.document,
|
||||||
|
definition,
|
||||||
|
block_distance_parameter::Kind::Polar,
|
||||||
|
Vector3::ZERO,
|
||||||
|
Vector3::UNIT_Y,
|
||||||
|
Vector3::UNIT_X,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let rotation = parameters(&scene.document, definition)[0].label.clone();
|
||||||
|
let polar =
|
||||||
|
block_distance_parameter::parameters(&scene.document, definition)[0].labels[1].clone();
|
||||||
|
assert_ne!(rotation, polar);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn orientation_parameter_flip_capacity_and_invalid_points_leave_document_intact() {
|
||||||
|
let (mut scene, definition) = fixture();
|
||||||
|
for kind in [Kind::Rotation, Kind::Alignment, Kind::Flip] {
|
||||||
|
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::UNIT_X
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
assert_eq!(scene.document, before);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 - 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let before = scene.document.clone();
|
||||||
|
assert!(placement_error(&scene.document, definition, Kind::Flip).is_some());
|
||||||
|
assert!(create(
|
||||||
|
&mut scene.document,
|
||||||
|
definition,
|
||||||
|
Kind::Flip,
|
||||||
|
Vector3::ZERO,
|
||||||
|
Vector3::UNIT_X,
|
||||||
|
Vector3::ZERO
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
assert_eq!(scene.document, before);
|
||||||
|
assert!(placement_error(&scene.document, definition, Kind::Rotation).is_none());
|
||||||
|
create(
|
||||||
|
&mut scene.document,
|
||||||
|
definition,
|
||||||
|
Kind::Rotation,
|
||||||
|
Vector3::ZERO,
|
||||||
|
Vector3::UNIT_Y,
|
||||||
|
Vector3::UNIT_X,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
graph_data(&scene.document, definition).first_node_id,
|
||||||
|
i32::MAX
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn orientation_parameter_reframe_and_editor_extents_preserve_native_points() {
|
||||||
|
let (mut scene, definition) = fixture();
|
||||||
|
append_batch(&mut scene.document, definition);
|
||||||
|
let original = parameters(&scene.document, definition)
|
||||||
|
.iter()
|
||||||
|
.map(|p| {
|
||||||
|
(
|
||||||
|
p.native.definition_base_point,
|
||||||
|
p.native.definition_end_point,
|
||||||
|
p.reference,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.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 (p, (base, end, reference)) in parameters(&scene.document, definition).iter().zip(original)
|
||||||
|
{
|
||||||
|
assert_eq!(p.native.definition_base_point, transform.apply(base));
|
||||||
|
assert_eq!(p.native.definition_end_point, transform.apply(end));
|
||||||
|
assert_eq!(p.reference, transform.apply(reference));
|
||||||
|
assert!(graphic(p)
|
||||||
|
.lines
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.all(|p| p.x.is_finite() && p.y.is_finite() && p.z.is_finite()));
|
||||||
|
}
|
||||||
|
scene.block_edit_block = Some(definition);
|
||||||
|
let (_, max) = scene.visible_block_bounds(definition).unwrap();
|
||||||
|
assert!(max.x >= 13.0 && max.y >= 23.0);
|
||||||
|
scene.block_edit_block = None;
|
||||||
|
let (_, max) = scene.visible_block_bounds(definition).unwrap();
|
||||||
|
assert!(max.x <= 1.0 && max.y < 1.0);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn orientation_parameter_rotation_270_graphic_retains_counterclockwise_arc() {
|
||||||
|
let (mut scene, definition) = fixture();
|
||||||
|
create(
|
||||||
|
&mut scene.document,
|
||||||
|
definition,
|
||||||
|
Kind::Rotation,
|
||||||
|
Vector3::ZERO,
|
||||||
|
-Vector3::UNIT_Y,
|
||||||
|
Vector3::UNIT_X,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let g = graphic(¶meters(&scene.document, definition)[0]);
|
||||||
|
assert!(g.lines.iter().flatten().any(|p| p.x < -0.99));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires OCS_DYNAMIC_BLOCK_FIXTURES with local DWG fixtures"]
|
||||||
|
fn orientation_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 digest = 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())
|
||||||
|
&& placement_error(&document, *h, Kind::Flip).is_none()
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let before = document.clone();
|
||||||
|
append_batch(&mut document, definition);
|
||||||
|
assert_roundtrips(&document, definition, &before);
|
||||||
|
assert_eq!(Sha256::digest(std::fs::read(&path).unwrap()), digest);
|
||||||
|
eprintln!("{filename}: Rotation/Alignment/Flip native DWG/DXF objects and graph append passed; original graph/objects/checksum preserved");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1687,6 +1687,22 @@ impl Scene {
|
|||||||
fold!(Some((point, point)));
|
fold!(Some((point, point)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for parameter in super::block_orientation_parameter::parameters(&self.document, block) {
|
||||||
|
let graphic = super::block_orientation_parameter::graphic(¶meter);
|
||||||
|
for point in graphic
|
||||||
|
.lines
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.chain(graphic.labels.into_iter().map(|(p, _)| p))
|
||||||
|
.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) {
|
if let Some(parameter) = super::block_visibility::parameter(&self.document, block) {
|
||||||
let point = parameter.def_point;
|
let point = parameter.def_point;
|
||||||
let point = glam::DVec3::new(point.x, point.y, point.z);
|
let point = glam::DVec3::new(point.x, point.y, point.z);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ pub mod block_visibility;
|
|||||||
pub(crate) mod block_authoring;
|
pub(crate) mod block_authoring;
|
||||||
pub(crate) mod block_point_parameter;
|
pub(crate) mod block_point_parameter;
|
||||||
pub(crate) mod block_distance_parameter;
|
pub(crate) mod block_distance_parameter;
|
||||||
|
pub(crate) mod block_orientation_parameter;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
pub mod compare;
|
pub mod compare;
|
||||||
pub mod convert;
|
pub mod convert;
|
||||||
|
|||||||
@@ -565,6 +565,12 @@ impl Scene {
|
|||||||
block_record,
|
block_record,
|
||||||
local_from_old,
|
local_from_old,
|
||||||
);
|
);
|
||||||
|
let parameter_changes = parameter_changes
|
||||||
|
+ super::block_orientation_parameter::reframe(
|
||||||
|
&mut self.document,
|
||||||
|
block_record,
|
||||||
|
local_from_old,
|
||||||
|
);
|
||||||
if !changed.is_empty() || parameter_changes > 0 {
|
if !changed.is_empty() || parameter_changes > 0 {
|
||||||
self.rebuild_derived_caches();
|
self.rebuild_derived_caches();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1019,6 +1019,7 @@ pub(crate) fn selection_overlay<'a>(
|
|||||||
|
|
||||||
pub(crate) struct AuthoringDimension {
|
pub(crate) struct AuthoringDimension {
|
||||||
pub lines: Vec<[Point; 2]>,
|
pub lines: Vec<[Point; 2]>,
|
||||||
|
pub round_grips: Vec<Point>,
|
||||||
pub dimensions: Vec<[Point; 2]>,
|
pub dimensions: Vec<[Point; 2]>,
|
||||||
pub labels: Vec<(Point, String)>,
|
pub labels: Vec<(Point, String)>,
|
||||||
pub grips: Vec<(Point, Option<Point>)>,
|
pub grips: Vec<(Point, Option<Point>)>,
|
||||||
@@ -2410,6 +2411,9 @@ impl canvas::Program<Message> for SelectionCanvas {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
for point in &dimension.round_grips {
|
||||||
|
frame.fill(&canvas::Path::circle(*point, 3.5), color);
|
||||||
|
}
|
||||||
for (point, base) in &dimension.grips {
|
for (point, base) in &dimension.grips {
|
||||||
let marker = if let Some(base) = base {
|
let marker = if let Some(base) = base {
|
||||||
let dx = point.x - base.x;
|
let dx = point.x - base.x;
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ fn catalog(
|
|||||||
tab: Tab,
|
tab: Tab,
|
||||||
visibility_error: Option<&'static str>,
|
visibility_error: Option<&'static str>,
|
||||||
point_error: Option<&'static str>,
|
point_error: Option<&'static str>,
|
||||||
|
flip_error: Option<&'static str>,
|
||||||
) -> Element<'static, Message> {
|
) -> Element<'static, Message> {
|
||||||
let (entries, unavailable) = match tab {
|
let (entries, unavailable) = match tab {
|
||||||
Tab::Parameters => (
|
Tab::Parameters => (
|
||||||
@@ -209,7 +210,9 @@ fn catalog(
|
|||||||
let visibility = tab == Tab::Parameters && *label == "Visibility";
|
let visibility = tab == Tab::Parameters && *label == "Visibility";
|
||||||
let point = tab == Tab::Parameters && *label == "Point";
|
let point = tab == Tab::Parameters && *label == "Point";
|
||||||
let distance = tab == Tab::Parameters && matches!(*label, "Linear" | "Polar" | "XY");
|
let distance = tab == Tab::Parameters && matches!(*label, "Linear" | "Polar" | "XY");
|
||||||
let enabled = (visibility && visibility_error.is_none()) || ((point || distance) && point_error.is_none());
|
let orientation = tab == Tab::Parameters && matches!(*label,"Rotation"|"Alignment"|"Flip");
|
||||||
|
let orientation_error = if *label=="Flip" {flip_error} else {point_error};
|
||||||
|
let enabled = (orientation && orientation_error.is_none()) || (visibility && visibility_error.is_none()) || ((point || distance) && point_error.is_none());
|
||||||
let base = if enabled {
|
let base = if enabled {
|
||||||
icons::semantic(icon, 28.0)
|
icons::semantic(icon, 28.0)
|
||||||
} else {
|
} else {
|
||||||
@@ -244,6 +247,12 @@ fn catalog(
|
|||||||
.into_owned()
|
.into_owned()
|
||||||
} else if distance {
|
} 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()
|
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 if orientation {
|
||||||
|
crate::t!(orientation_error.unwrap_or(match *label {
|
||||||
|
"Rotation" => "Place the center, specify a radius, then the default rotation angle.",
|
||||||
|
"Alignment" => "Place the alignment base point and tangent direction.",
|
||||||
|
_ => "Place a reflection line and its flip-state label. Associate a Flip action to mirror geometry.",
|
||||||
|
})).into_owned()
|
||||||
} else {
|
} else {
|
||||||
unavailable.to_string()
|
unavailable.to_string()
|
||||||
};
|
};
|
||||||
@@ -254,6 +263,8 @@ fn catalog(
|
|||||||
container(entry).id("authoring-tool-point").into()
|
container(entry).id("authoring-tool-point").into()
|
||||||
} else if distance {
|
} else if distance {
|
||||||
container(entry).id(match *label { "Linear" => "authoring-tool-linear", "Polar" => "authoring-tool-polar", _ => "authoring-tool-xy" }).into()
|
container(entry).id(match *label { "Linear" => "authoring-tool-linear", "Polar" => "authoring-tool-polar", _ => "authoring-tool-xy" }).into()
|
||||||
|
} else if orientation {
|
||||||
|
container(entry).id(match *label {"Rotation"=>"authoring-tool-rotation","Alignment"=>"authoring-tool-alignment",_=>"authoring-tool-flip"}).into()
|
||||||
} else {
|
} else {
|
||||||
entry
|
entry
|
||||||
}
|
}
|
||||||
@@ -366,6 +377,7 @@ pub fn view(
|
|||||||
chrome: Chrome,
|
chrome: Chrome,
|
||||||
visibility_error: Option<&'static str>,
|
visibility_error: Option<&'static str>,
|
||||||
point_error: Option<&'static str>,
|
point_error: Option<&'static str>,
|
||||||
|
flip_error: Option<&'static str>,
|
||||||
) -> Element<'static, Message> {
|
) -> Element<'static, Message> {
|
||||||
let tabs = column(Tab::ALL.into_iter().map(|category| {
|
let tabs = column(Tab::ALL.into_iter().map(|category| {
|
||||||
let label = category.label();
|
let label = category.label();
|
||||||
@@ -415,9 +427,14 @@ pub fn view(
|
|||||||
chrome,
|
chrome,
|
||||||
),
|
),
|
||||||
row![
|
row![
|
||||||
scrollable(catalog(selected_tab, visibility_error, point_error))
|
scrollable(catalog(
|
||||||
.width(Fill)
|
selected_tab,
|
||||||
.height(Fill),
|
visibility_error,
|
||||||
|
point_error,
|
||||||
|
flip_error
|
||||||
|
))
|
||||||
|
.width(Fill)
|
||||||
|
.height(Fill),
|
||||||
tabs
|
tabs
|
||||||
]
|
]
|
||||||
.spacing(4)
|
.spacing(4)
|
||||||
|
|||||||
Reference in new issue
Block a user