forked from 1b-agent/opencadstudio-upstream
feat(block-editor): add native Point parameter placement
Enable Point in Authoring Palettes and BPARAMETER. Pick parameter and label positions, then create the native Point, XY grip and position expressions in one undo transaction. Draw editor-only markers and clear placement previews. Share graph append construction with Visibility so both types coexist in either order. Allocate node, expression and edge IDs independently; preserve existing objects, nodes and relationships and reject malformed/opaque graphs. Reframe parameter, label and grip with the edited definition. Include authoring positions in editor framing while keeping reference geometry extents unchanged. Retain save/discard and parent saved-edit undo. Action association/evaluation and parameter property editing remain separate milestones. Validation: 10 targeted, 286 block and 60 dock checks passed (overlapping filters). Appending a Point to an existing native graph passed DWG/DXF roundtrips in all 17 local fixtures; old objects and original checksums stayed unchanged. Real-view palette interaction and rendered marker/label verified.
This commit is contained in:
1 parent
daa26007f9
commit
cdfb4ed632
19 files changed
+1457
-303
No files matched your search
@@ -59,6 +59,145 @@ fn feed(app: &mut OpenCADStudio, text: &str) {
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
}
|
||||
|
||||
fn authored_points(app: &OpenCADStudio) -> Vec<codec::objects::BlockPointParameter> {
|
||||
let tab = &app.tabs[app.active_tab];
|
||||
let definition = tab
|
||||
.scene
|
||||
.document
|
||||
.block_records
|
||||
.get("EDIT_ME")
|
||||
.unwrap()
|
||||
.handle;
|
||||
crate::scene::block_point_parameter::parameters(&tab.scene.document, definition)
|
||||
.into_iter()
|
||||
.map(|(_, parameter)| parameter.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_parameter_placement_two_picks_one_undo_and_repeated_names() {
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
let i = app.active_tab;
|
||||
let before = app.tabs[i].scene.document.clone();
|
||||
let _ = app.dispatch_command("BPARAMETER POINT");
|
||||
feed(&mut app, "3,4,0");
|
||||
assert_eq!(
|
||||
app.tabs[i].scene.document, before,
|
||||
"first pick is preview only"
|
||||
);
|
||||
feed(&mut app, "6,7,0");
|
||||
assert_eq!(authored_points(&app)[0].position_name, "Position1");
|
||||
assert_eq!(
|
||||
authored_points(&app)[0].definition_label_point,
|
||||
Vector3::new(6.0, 7.0, 0.0)
|
||||
);
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), 1);
|
||||
assert!(app.block_edit_dirty(i));
|
||||
app.undo_steps(1);
|
||||
assert!(authored_points(&app).is_empty());
|
||||
app.redo_steps(1);
|
||||
assert_eq!(authored_points(&app).len(), 1);
|
||||
let _ = app.run_command_line("BPARAMETER POINT 8,9 10,11");
|
||||
assert_eq!(authored_points(&app)[1].position_name, "Position2");
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_parameter_placement_cancel_at_both_prompts_is_inert() {
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
let i = app.active_tab;
|
||||
let before = app.tabs[i].scene.document.clone();
|
||||
for picked in [false, true] {
|
||||
let _ = app.dispatch_command("BPARAMETER POINT");
|
||||
if picked {
|
||||
feed(&mut app, "3,4");
|
||||
}
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
assert_eq!(app.tabs[i].scene.document, before);
|
||||
assert!(app.tabs[i].history.undo_stack.is_empty());
|
||||
assert!(!app.block_edit_dirty(i));
|
||||
}
|
||||
let _ = app.dispatch_command("BPARAMETER POINT");
|
||||
feed(&mut app, "3,4");
|
||||
feed(&mut app, "");
|
||||
assert_eq!(app.tabs[i].scene.document, before);
|
||||
assert!(app.tabs[i].history.undo_stack.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_parameter_placement_chooser_works_with_existing_visibility_and_reframes() {
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
let i = app.active_tab;
|
||||
let _ = app.run_command_line("BPARAMETER VISIBILITY 0,0");
|
||||
let _ = app.dispatch_command("BPARAMETER");
|
||||
feed(&mut app, "Point");
|
||||
feed(&mut app, "3,4,5");
|
||||
feed(&mut app, "6,7,5");
|
||||
assert_eq!(authored_points(&app).len(), 1);
|
||||
assert!(app.block_visibility_context().is_some());
|
||||
let _ = app.run_command_line("UCS ORIGIN 1,2,3");
|
||||
assert_eq!(
|
||||
authored_points(&app)[0].parameter.definition_point,
|
||||
Vector3::new(2.0, 2.0, 2.0)
|
||||
);
|
||||
assert_eq!(
|
||||
authored_points(&app)[0].definition_label_point,
|
||||
Vector3::new(5.0, 5.0, 2.0)
|
||||
);
|
||||
app.undo_steps(1);
|
||||
assert_eq!(
|
||||
authored_points(&app)[0].parameter.definition_point,
|
||||
Vector3::new(3.0, 4.0, 5.0)
|
||||
);
|
||||
let _ = app.dispatch_command("BSAVE");
|
||||
assert!(!app.block_edit_dirty(i));
|
||||
let _ = app.dispatch_command("BEDIT_DISCARD");
|
||||
assert_eq!(authored_points(&app).len(), 1);
|
||||
app.undo_steps(1);
|
||||
assert!(
|
||||
authored_points(&app).is_empty(),
|
||||
"saved block undo restores the parent drawing"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_parameter_placement_discard_and_stale_context_leave_no_objects() {
|
||||
let (mut app, reference) = fixture();
|
||||
let i = app.active_tab;
|
||||
let before = app.tabs[i].scene.document.clone();
|
||||
begin(&mut app, reference);
|
||||
let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle;
|
||||
let start = app.tabs[i].scene.document.clone();
|
||||
let id = app.tabs[i].id;
|
||||
for (document, location) in [
|
||||
(id + 1, glam::DVec3::ZERO),
|
||||
(id, glam::DVec3::splat(f64::NAN)),
|
||||
] {
|
||||
let _ = app.handle_place_block_point_parameter(
|
||||
crate::command::CmdResult::PlaceBlockPointParameter {
|
||||
document_id: document,
|
||||
definition,
|
||||
location,
|
||||
label: glam::DVec3::ZERO,
|
||||
},
|
||||
);
|
||||
assert_eq!(app.tabs[i].scene.document, start);
|
||||
}
|
||||
let _ = app.run_command_line("BPARAMETER POINT 3,4 6,7");
|
||||
let _ = app.dispatch_command("BEDIT_DISCARD");
|
||||
let mut after = app.tabs[i].scene.document.clone();
|
||||
after.vports = before.vports.clone();
|
||||
assert_eq!(after, before);
|
||||
let closed = app.tabs[i].scene.document.clone();
|
||||
let _ = app.dispatch_command("LINE");
|
||||
let _ = app.dispatch_command("BPARAMETER POINT");
|
||||
assert_eq!(app.tabs[i].active_cmd.as_ref().unwrap().name(), "LINE");
|
||||
assert_eq!(app.tabs[i].scene.document, closed);
|
||||
}
|
||||
|
||||
fn visibility_editor() -> (OpenCADStudio, crate::ui::window::block_visibility::Target) {
|
||||
let (mut app, reference) = fixture();
|
||||
begin(&mut app, reference);
|
||||
@@ -1721,8 +1860,8 @@ fn visibility_parameter_placement_rejects_duplicates_stale_context_and_nonfinite
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn visibility_parameter_placement_preserves_an_existing_authoring_graph() {
|
||||
use codec::objects::{DynamicBlockObject, DynamicBlockData, BlockEvaluationGraph, ObjectType};
|
||||
fn visibility_parameter_placement_preserves_an_opaque_authoring_graph() {
|
||||
use codec::objects::{DynamicBlockObject, DynamicBlockData, ObjectType};
|
||||
let (mut app, reference) = fixture();
|
||||
let i = app.active_tab;
|
||||
let doc = &mut app.tabs[i].scene.document;
|
||||
@@ -1732,7 +1871,7 @@ fn visibility_parameter_placement_preserves_an_existing_authoring_graph() {
|
||||
let mut graph = DynamicBlockObject::new("ACAD_EVALUATION_GRAPH", "AcDbEvalGraph");
|
||||
graph.handle = graph_handle;
|
||||
graph.owner = dictionary;
|
||||
graph.data = DynamicBlockData::EvaluationGraph(BlockEvaluationGraph::default());
|
||||
graph.data = DynamicBlockData::Unknown;
|
||||
doc.objects
|
||||
.insert(graph_handle, ObjectType::DynamicBlock(graph));
|
||||
if let Some(ObjectType::Dictionary(dictionary)) = doc.objects.get_mut(&dictionary) {
|
||||
|
||||
@@ -575,6 +575,9 @@ impl OpenCADStudio {
|
||||
return task;
|
||||
}
|
||||
}
|
||||
result @ CmdResult::PlaceBlockPointParameter { .. } => {
|
||||
return self.handle_place_block_point_parameter(result);
|
||||
}
|
||||
CmdResult::PlaceBlockVisibilityParameter {
|
||||
document_id,
|
||||
definition,
|
||||
|
||||
@@ -7,6 +7,80 @@ use codec::{CadDocument, EntityType, Handle};
|
||||
use iced::Task;
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(in crate::app) fn point_parameter_placement_error(&self) -> Option<&'static str> {
|
||||
self.parameter_placement_error("POINT")
|
||||
}
|
||||
|
||||
pub(in crate::app) fn parameter_placement_error(&self, kind: &str) -> Option<&'static str> {
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
let Some(session) = tab.active_block_edit_session() else {
|
||||
return Some("Open a block in the Block Editor before placing a parameter.");
|
||||
};
|
||||
match kind.to_ascii_uppercase().as_str() {
|
||||
"POINT" | "P" | "" => crate::scene::block_point_parameter::placement_error(
|
||||
&tab.scene.document,
|
||||
session.br_handle,
|
||||
),
|
||||
"VISIBILITY" | "V" => {
|
||||
visibility::placement_error(&tab.scene.document, session.br_handle)
|
||||
}
|
||||
_ => Some("Only Point and Visibility parameter placement is currently supported."),
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::app) fn handle_place_block_point_parameter(
|
||||
&mut self,
|
||||
result: crate::command::CmdResult,
|
||||
) -> Task<Message> {
|
||||
let crate::command::CmdResult::PlaceBlockPointParameter {
|
||||
document_id,
|
||||
definition,
|
||||
location,
|
||||
label,
|
||||
} = result
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
let i = self.active_tab;
|
||||
if self.tabs[i].id != document_id
|
||||
|| self.tabs[i]
|
||||
.active_block_edit_session()
|
||||
.map(|session| session.br_handle)
|
||||
!= Some(definition)
|
||||
{
|
||||
self.command_line.push_error(
|
||||
crate::t!("The parameter's Block Editor session is no longer active.").as_ref(),
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
if let Some(error) = self.point_parameter_placement_error() {
|
||||
self.command_line.push_error(crate::t!(error).as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
if !location.is_finite() || !label.is_finite() {
|
||||
self.command_line
|
||||
.push_error(crate::t!("Specify finite parameter and label locations.").as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
self.push_undo_snapshot(i, "Point parameter");
|
||||
crate::scene::block_point_parameter::create(
|
||||
&mut self.tabs[i].scene.document,
|
||||
definition,
|
||||
codec::types::Vector3::new(location.x, location.y, location.z),
|
||||
codec::types::Vector3::new(label.x, label.y, label.z),
|
||||
)
|
||||
.expect("validated Point placement");
|
||||
self.tabs[i].scene.bump_geometry();
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.finish_pending_history(i);
|
||||
self.refresh_properties();
|
||||
self.command_line
|
||||
.push_output(crate::t!("Point parameter placed.").as_ref());
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(in crate::app) fn visibility_placement_error(&self) -> Option<&'static str> {
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
let Some(session) = tab.active_block_edit_session() else {
|
||||
@@ -193,13 +267,27 @@ impl OpenCADStudio {
|
||||
return Some(Task::none());
|
||||
};
|
||||
let parameter_type = cmd.strip_prefix("BPARAMETER").unwrap().trim();
|
||||
if parameter_type.eq_ignore_ascii_case("POINT")
|
||||
|| parameter_type.eq_ignore_ascii_case("P")
|
||||
{
|
||||
let command =
|
||||
crate::modules::block_editor::point_parameter::PointParameterCommand::new(
|
||||
self.tabs[i].id,
|
||||
block,
|
||||
);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
return Some(Task::none());
|
||||
}
|
||||
if !parameter_type.is_empty()
|
||||
&& !parameter_type.eq_ignore_ascii_case("VISIBILITY")
|
||||
&& !parameter_type.eq_ignore_ascii_case("V")
|
||||
{
|
||||
self.command_line.push_error(
|
||||
crate::t!("Only Visibility parameter placement is currently supported.")
|
||||
.as_ref(),
|
||||
crate::t!(
|
||||
"Only Point and Visibility parameter placement is currently supported."
|
||||
)
|
||||
.as_ref(),
|
||||
);
|
||||
return Some(Task::none());
|
||||
}
|
||||
|
||||
@@ -174,7 +174,9 @@ impl OpenCADStudio {
|
||||
let resolved = self.resolve_alias(cmd);
|
||||
let cmd = resolved.as_deref().unwrap_or(cmd);
|
||||
if cmd == "BPARAMETER" || cmd.starts_with("BPARAMETER ") {
|
||||
if let Some(error) = self.visibility_placement_error() {
|
||||
if let Some(error) =
|
||||
self.parameter_placement_error(cmd.strip_prefix("BPARAMETER").unwrap().trim())
|
||||
{
|
||||
self.command_line.push_error(crate::t!(error).as_ref());
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
@@ -783,6 +783,50 @@ fn visibility_ribbon_real_controls_disable_enable_and_open_manager() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_parameter_placement_starts_from_the_actual_palette_row() {
|
||||
with_stack(|| {
|
||||
let mut app = authoring_app();
|
||||
let _ = app.dispatch_command("BAUTHORPALETTE");
|
||||
let i = app.active_tab;
|
||||
let before = app.tabs[i].scene.document.clone();
|
||||
let point = authoring_control(&app, "authoring-tool-point");
|
||||
authoring_click(&mut app, point);
|
||||
assert_eq!(
|
||||
app.tabs[i].active_cmd.as_ref().unwrap().name(),
|
||||
"BPARAMETER"
|
||||
);
|
||||
let _ = app.update(Message::CommandInput("0.25,0.25".into()));
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
assert_eq!(app.tabs[i].scene.document, before);
|
||||
let _ = app.update(Message::CommandInput("0.65,0.15".into()));
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
assert!(
|
||||
app.tabs[i].scene.preview_wires.is_empty(),
|
||||
"placement clears its rubber-band preview"
|
||||
);
|
||||
let definition = app.tabs[i].active_block_edit_session().unwrap().br_handle;
|
||||
assert_eq!(
|
||||
crate::scene::block_point_parameter::parameters(
|
||||
&app.tabs[i].scene.document,
|
||||
definition
|
||||
)
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert!(
|
||||
app.visibility_placement_error().is_none(),
|
||||
"Visibility can join the Point graph"
|
||||
);
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), 1);
|
||||
authoring_capture(&app, "point-parameter-placed");
|
||||
let point = authoring_control(&app, "authoring-tool-point");
|
||||
authoring_click(&mut app, point);
|
||||
let _ = app.update(Message::CommandEscape);
|
||||
assert_eq!(app.tabs[i].history.undo_stack.len(), 1);
|
||||
});
|
||||
}
|
||||
|
||||
fn authoring_app() -> OpenCADStudio {
|
||||
use codec::{
|
||||
entities::{Insert, Line},
|
||||
|
||||
@@ -407,6 +407,7 @@ impl OpenCADStudio {
|
||||
width,
|
||||
chrome,
|
||||
self.visibility_placement_error(),
|
||||
self.point_parameter_placement_error(),
|
||||
)
|
||||
}
|
||||
crate::ui::dock::PanelId::BlockPalette => {
|
||||
|
||||
@@ -1047,6 +1047,34 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
(point.x.is_finite() && point.y.is_finite())
|
||||
.then(|| (point, parameter.name.clone(), vp_bounds))
|
||||
});
|
||||
let authoring_points =
|
||||
tab.active_block_edit_session()
|
||||
.map(|session| {
|
||||
crate::scene::block_point_parameter::parameters(
|
||||
&tab.scene.document,
|
||||
session.br_handle,
|
||||
)
|
||||
.into_iter()
|
||||
.filter_map(|(_, parameter)| {
|
||||
let project = |point: codec::types::Vector3| {
|
||||
let point = tab.scene.camera.borrow().project(
|
||||
glam::DVec3::new(point.x, point.y, point.z),
|
||||
vp_bounds,
|
||||
)?;
|
||||
(point.x.is_finite() && point.y.is_finite()).then_some(
|
||||
iced::Point::new(vp_bounds.x + point.x, vp_bounds.y + point.y),
|
||||
)
|
||||
};
|
||||
Some((
|
||||
project(parameter.parameter.definition_point)?,
|
||||
project(parameter.definition_label_point)?,
|
||||
parameter.position_name.clone(),
|
||||
vp_bounds,
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
crate::ui::overlay::selection_overlay(
|
||||
std::sync::Arc::clone(&tab.scene.selection),
|
||||
snap_info,
|
||||
@@ -1112,6 +1140,7 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
.map(|kind| crate::t!(kind.label()).into_owned()),
|
||||
constraint_cursor_badge,
|
||||
authoring_visibility,
|
||||
authoring_points,
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@@ -1514,6 +1514,13 @@ impl DimensionPreview {
|
||||
/// Returned by every `CadCommand` method to tell main.rs what to do.
|
||||
#[allow(dead_code)]
|
||||
pub enum CmdResult {
|
||||
/// Place a native Point parameter after both authoring picks complete.
|
||||
PlaceBlockPointParameter {
|
||||
document_id: u64,
|
||||
definition: Handle,
|
||||
location: DVec3,
|
||||
label: DVec3,
|
||||
},
|
||||
/// Place the visibility parameter in the definition that requested it.
|
||||
PlaceBlockVisibilityParameter {
|
||||
document_id: u64,
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::modules::parametric::ParametricModule;
|
||||
use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef};
|
||||
|
||||
pub mod visibility_parameter;
|
||||
pub mod point_parameter;
|
||||
|
||||
pub struct BlockEditorModule;
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
//! Two-point authoring gesture: parameter position followed by label position.
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::scene::WireModel;
|
||||
use codec::Handle;
|
||||
use glam::DVec3;
|
||||
|
||||
pub struct PointParameterCommand {
|
||||
document_id: u64,
|
||||
definition: Handle,
|
||||
location: Option<DVec3>,
|
||||
}
|
||||
|
||||
impl PointParameterCommand {
|
||||
pub fn new(document_id: u64, definition: Handle) -> Self {
|
||||
Self {
|
||||
document_id,
|
||||
definition,
|
||||
location: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for PointParameterCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"BPARAMETER"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
crate::t!(if self.location.is_some() {
|
||||
"BPARAMETER Specify point parameter label location:"
|
||||
} else {
|
||||
"BPARAMETER Specify point parameter location:"
|
||||
})
|
||||
.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(),
|
||||
);
|
||||
}
|
||||
if let Some(location) = self.location {
|
||||
CmdResult::PlaceBlockPointParameter {
|
||||
document_id: self.document_id,
|
||||
definition: self.definition,
|
||||
location,
|
||||
label: point,
|
||||
}
|
||||
} else {
|
||||
self.location = Some(point);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
|
||||
fn on_preview_wires(&mut self, point: DVec3) -> Vec<WireModel> {
|
||||
self.location
|
||||
.filter(|_| point.is_finite())
|
||||
.map(|location| {
|
||||
vec![WireModel::solid_f64(
|
||||
"preview_point_parameter".into(),
|
||||
vec![location.to_array(), point.to_array()],
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
)]
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,8 @@ impl CadCommand for VisibilityParameterCommand {
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.choosing_type {
|
||||
crate::t!("BPARAMETER Enter parameter type [Visibility] <Visibility>:").into_owned()
|
||||
crate::t!("BPARAMETER Enter parameter type [Point/Visibility] <Visibility>:")
|
||||
.into_owned()
|
||||
} else {
|
||||
crate::t!("BPARAMETER Specify visibility parameter location:").into_owned()
|
||||
}
|
||||
@@ -35,7 +36,10 @@ impl CadCommand for VisibilityParameterCommand {
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
if self.choosing_type {
|
||||
vec![CmdOption::new(crate::t!("Visibility").as_ref(), "V")]
|
||||
vec![
|
||||
CmdOption::new(crate::t!("Point").as_ref(), "P"),
|
||||
CmdOption::new(crate::t!("Visibility").as_ref(), "V"),
|
||||
]
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
@@ -56,13 +60,16 @@ impl CadCommand for VisibilityParameterCommand {
|
||||
.to_ascii_uppercase()
|
||||
.as_str()
|
||||
{
|
||||
"P" | "POINT" => CmdResult::Dispatch("BPARAMETER POINT".into()),
|
||||
"" | "V" | "VISIBILITY" => {
|
||||
self.choosing_type = false;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => CmdResult::ReportError(
|
||||
crate::t!("Only Visibility parameter placement is currently supported.")
|
||||
.into_owned(),
|
||||
crate::t!(
|
||||
"Only Point and Visibility parameter placement is currently supported."
|
||||
)
|
||||
.into_owned(),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
//! Shared native graph construction for Block Editor authoring objects.
|
||||
use codec::objects::{
|
||||
BlockEvalExpression, BlockEvalValue, DynamicBlockData, DynamicBlockObject, ObjectType,
|
||||
};
|
||||
use codec::xdata::{ExtendedDataRecord, XDataValue};
|
||||
use codec::{CadDocument, EntityType, Handle};
|
||||
|
||||
pub(super) const MAJOR: i32 = 33;
|
||||
pub(super) const MINOR: i32 = 329;
|
||||
pub(super) const NO_VALUE: i16 = -9999;
|
||||
pub(super) const ELEMENT_TAG: &str = "AcDbBlockRepETag";
|
||||
pub(super) const REPRESENTATION_TAG: &str = "AcDbBlockRepBTag";
|
||||
pub(super) const TRUE_NAME: &str = "AcDbDynamicBlockTrueName";
|
||||
pub(super) const GUID: &str = "AcDbDynamicBlockGUID";
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct Slots {
|
||||
pub node: i32,
|
||||
pub eval: i32,
|
||||
pub edge: i32,
|
||||
}
|
||||
|
||||
/// Resolve a graph without touching it. Reject malformed/opaque relationships
|
||||
/// before opening a history transaction or allocating any new native objects.
|
||||
pub(super) fn graph(
|
||||
document: &CadDocument,
|
||||
definition: Handle,
|
||||
) -> Result<Option<Handle>, &'static str> {
|
||||
if !document
|
||||
.block_records
|
||||
.iter()
|
||||
.any(|record| record.handle == definition)
|
||||
{
|
||||
return Err("The block definition is no longer available.");
|
||||
}
|
||||
let Some(dictionary) = document.extension_dictionary_handle(definition) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(ObjectType::Dictionary(record)) = document.objects.get(&dictionary) else {
|
||||
return Err("The block extension dictionary is unavailable.");
|
||||
};
|
||||
let Some(handle) = record.get("ACAD_ENHANCEDBLOCK") else {
|
||||
return Ok(None);
|
||||
};
|
||||
let Some(ObjectType::DynamicBlock(object)) = document.objects.get(&handle) else {
|
||||
return Err("The block authoring graph cannot be edited.");
|
||||
};
|
||||
let DynamicBlockData::EvaluationGraph(graph) = &object.data else {
|
||||
return Err("The block authoring graph cannot be edited.");
|
||||
};
|
||||
if object.owner != dictionary {
|
||||
return Err("The block authoring graph has an invalid owner.");
|
||||
}
|
||||
let nodes: rustc_hash::FxHashSet<_> = graph.nodes.iter().map(|node| node.id).collect();
|
||||
let edges: rustc_hash::FxHashSet<_> = graph.edges.iter().map(|edge| edge.id).collect();
|
||||
if nodes.len() != graph.nodes.len()
|
||||
|| edges.len() != graph.edges.len()
|
||||
|| graph
|
||||
.nodes
|
||||
.iter()
|
||||
.any(|node| node.id < 0 || document.objects.get(&node.expression).is_none())
|
||||
|| graph.edges.iter().any(|edge| {
|
||||
edge.id < 0
|
||||
|| !nodes.contains(&edge.source_node)
|
||||
|| !nodes.contains(&edge.destination_node)
|
||||
})
|
||||
{
|
||||
return Err("The block authoring graph has invalid node relationships.");
|
||||
}
|
||||
slots(Some(graph))?;
|
||||
Ok(Some(handle))
|
||||
}
|
||||
|
||||
fn slots(graph: Option<&codec::objects::BlockEvaluationGraph>) -> Result<Slots, &'static str> {
|
||||
let node = graph
|
||||
.and_then(|graph| graph.nodes.iter().map(|node| node.id).max())
|
||||
.unwrap_or(-1)
|
||||
.checked_add(1);
|
||||
let eval = graph
|
||||
.map(|graph| {
|
||||
graph.first_node_id.max(graph.first_node_id_copy).max(
|
||||
graph
|
||||
.nodes
|
||||
.iter()
|
||||
.map(|node| node.next_id)
|
||||
.max()
|
||||
.unwrap_or(0),
|
||||
)
|
||||
})
|
||||
.unwrap_or(0)
|
||||
.checked_add(1);
|
||||
let edge = graph
|
||||
.and_then(|graph| graph.edges.iter().map(|edge| edge.id).max())
|
||||
.unwrap_or(-1)
|
||||
.checked_add(1);
|
||||
match (node, eval, edge) {
|
||||
(Some(node), Some(eval), Some(edge))
|
||||
if node.checked_add(3).is_some()
|
||||
&& eval.checked_add(3).is_some()
|
||||
&& edge.checked_add(2).is_some() =>
|
||||
{
|
||||
Ok(Slots { node, eval, edge })
|
||||
}
|
||||
_ => Err("The block authoring graph has no free node IDs."),
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare an existing graph for appending, or create the native graph and
|
||||
/// definition metadata once. Existing nodes, edges, GUIDs and relationships stay.
|
||||
pub(super) fn prepare(
|
||||
document: &mut CadDocument,
|
||||
definition: Handle,
|
||||
) -> Result<(Handle, Slots), &'static str> {
|
||||
if let Some(handle) = graph(document, definition)? {
|
||||
let ObjectType::DynamicBlock(object) = &document.objects[&handle] else {
|
||||
unreachable!()
|
||||
};
|
||||
let DynamicBlockData::EvaluationGraph(graph) = &object.data else {
|
||||
unreachable!()
|
||||
};
|
||||
return Ok((handle, slots(Some(graph))?));
|
||||
}
|
||||
register_classes(
|
||||
document,
|
||||
&[
|
||||
("ACAD_EVALUATION_GRAPH", "AcDbEvalGraph", 55),
|
||||
(
|
||||
"ACDB_DYNAMICBLOCKPURGEPREVENTER_VERSION",
|
||||
"AcDbDynamicBlockPurgePreventer",
|
||||
61,
|
||||
),
|
||||
],
|
||||
);
|
||||
for app in [ELEMENT_TAG, REPRESENTATION_TAG, TRUE_NAME, GUID] {
|
||||
ensure_app_id(document, app);
|
||||
}
|
||||
let name = document
|
||||
.block_records
|
||||
.iter()
|
||||
.find(|record| record.handle == definition)
|
||||
.unwrap()
|
||||
.name
|
||||
.clone();
|
||||
let dictionary = document.ensure_extension_dictionary(definition);
|
||||
let handle = document.allocate_handle();
|
||||
let preventer = document.allocate_handle();
|
||||
let mut object = dynamic(
|
||||
handle,
|
||||
dictionary,
|
||||
"ACAD_EVALUATION_GRAPH",
|
||||
"AcDbEvalGraph",
|
||||
DynamicBlockData::EvaluationGraph(Default::default()),
|
||||
);
|
||||
object.reactors.push(dictionary);
|
||||
document
|
||||
.objects
|
||||
.insert(handle, ObjectType::DynamicBlock(object));
|
||||
let mut object = dynamic(
|
||||
preventer,
|
||||
dictionary,
|
||||
"ACDB_DYNAMICBLOCKPURGEPREVENTER_VERSION",
|
||||
"AcDbDynamicBlockPurgePreventer",
|
||||
DynamicBlockData::Representation(codec::objects::BlockRepresentationData {
|
||||
flags: 1,
|
||||
block: definition,
|
||||
}),
|
||||
);
|
||||
object.reactors.push(dictionary);
|
||||
document
|
||||
.objects
|
||||
.insert(preventer, ObjectType::DynamicBlock(object));
|
||||
if let Some(ObjectType::Dictionary(dictionary)) = document.objects.get_mut(&dictionary) {
|
||||
dictionary.add_entry("ACAD_ENHANCEDBLOCK", handle);
|
||||
dictionary.add_entry("AcDbDynamicBlockRoundTripPurgePreventer", preventer);
|
||||
}
|
||||
let mut name_record = ExtendedDataRecord::new(TRUE_NAME);
|
||||
name_record.add_value(XDataValue::String(name));
|
||||
document.set_object_xdata_record(definition, name_record);
|
||||
let mut guid = ExtendedDataRecord::new(GUID);
|
||||
guid.add_value(XDataValue::String(new_guid()));
|
||||
document.set_object_xdata_record(definition, guid);
|
||||
tag_members(document, definition);
|
||||
Ok((handle, slots(None)?))
|
||||
}
|
||||
|
||||
/// A one-point parameter, its XY/visibility grip and two position expressions.
|
||||
/// Graph node IDs, expression IDs and edge IDs are separate namespaces.
|
||||
pub(super) fn append(
|
||||
document: &mut CadDocument,
|
||||
handle: Handle,
|
||||
slots: Slots,
|
||||
expressions: [Handle; 4],
|
||||
) {
|
||||
use codec::objects::{BlockEvaluationEdge, BlockEvaluationNode};
|
||||
let ObjectType::DynamicBlock(object) = document.objects.get_mut(&handle).unwrap() else {
|
||||
unreachable!()
|
||||
};
|
||||
let DynamicBlockData::EvaluationGraph(graph) = &mut object.data else {
|
||||
unreachable!()
|
||||
};
|
||||
let n = slots.node;
|
||||
let e = slots.edge;
|
||||
let links = [
|
||||
[e, e, e + 1, e + 2],
|
||||
[-1, -1, e, e],
|
||||
[e + 1, e + 1, -1, -1],
|
||||
[e + 2, e + 2, -1, -1],
|
||||
];
|
||||
for (offset, expression) in expressions.into_iter().enumerate() {
|
||||
graph.nodes.push(BlockEvaluationNode {
|
||||
id: n + offset as i32,
|
||||
edge_flags: 32,
|
||||
next_id: slots.eval + offset as i32,
|
||||
expression,
|
||||
node_data: links[offset],
|
||||
active_cycles: None,
|
||||
});
|
||||
}
|
||||
for (offset, source_node, destination_node, outgoing_edges) in [
|
||||
(0, n + 1, n, [-1; 5]),
|
||||
(1, n, n + 2, [-1, -1, -1, e + 2, -1]),
|
||||
(2, n, n + 3, [-1, -1, e + 1, -1, -1]),
|
||||
] {
|
||||
graph.edges.push(BlockEvaluationEdge {
|
||||
id: e + offset,
|
||||
next_id: 0,
|
||||
incoming_edge: 1,
|
||||
source_node,
|
||||
destination_node,
|
||||
outgoing_edges,
|
||||
});
|
||||
}
|
||||
graph.first_node_id = slots.eval + 3;
|
||||
graph.first_node_id_copy = graph.first_node_id;
|
||||
}
|
||||
|
||||
pub(super) fn register_classes(document: &mut CadDocument, classes: &[(&str, &str, i32)]) {
|
||||
for &(dxf_name, cpp_class_name, maintenance_version) in classes {
|
||||
if let Some(existing) = document.classes.get_mut_by_name(dxf_name) {
|
||||
existing.proxy_flags = codec::classes::ProxyFlags(1153);
|
||||
existing.dwg_version = 25;
|
||||
existing.maintenance_version = maintenance_version;
|
||||
continue;
|
||||
}
|
||||
document.classes.add_or_update(codec::classes::DxfClass {
|
||||
dxf_name: dxf_name.to_string(),
|
||||
cpp_class_name: cpp_class_name.to_string(),
|
||||
application_name: "ObjectDBX Classes".to_string(),
|
||||
proxy_flags: codec::classes::ProxyFlags(1153),
|
||||
instance_count: 0,
|
||||
was_zombie: false,
|
||||
is_an_entity: false,
|
||||
class_number: 0,
|
||||
item_class_id: 0x1F3,
|
||||
dwg_version: 25,
|
||||
maintenance_version,
|
||||
unknown1: 0,
|
||||
unknown2: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn ensure_app_id(document: &mut CadDocument, name: &str) {
|
||||
if !document.app_ids.contains(name) {
|
||||
let mut app = codec::tables::AppId::new(name);
|
||||
app.handle = document.allocate_handle();
|
||||
let _ = document.app_ids.add(app);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn expression(
|
||||
node_id: i32,
|
||||
value_code: i16,
|
||||
value: BlockEvalValue,
|
||||
) -> BlockEvalExpression {
|
||||
BlockEvalExpression {
|
||||
parent_id: BlockEvalExpression::NO_PARENT,
|
||||
major: MAJOR,
|
||||
minor: MINOR,
|
||||
value_code,
|
||||
value,
|
||||
node_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn dynamic(
|
||||
handle: Handle,
|
||||
owner: Handle,
|
||||
dxf_name: &str,
|
||||
cpp_class_name: &str,
|
||||
data: DynamicBlockData,
|
||||
) -> DynamicBlockObject {
|
||||
let mut object = DynamicBlockObject::new(dxf_name, cpp_class_name);
|
||||
object.handle = handle;
|
||||
object.owner = owner;
|
||||
object.data = data;
|
||||
object
|
||||
}
|
||||
|
||||
pub(super) fn members(document: &CadDocument, definition: Handle) -> Vec<Handle> {
|
||||
document
|
||||
.block_records
|
||||
.iter()
|
||||
.find(|record| record.handle == definition)
|
||||
.map(|record| {
|
||||
record
|
||||
.entity_handles
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|handle| {
|
||||
!matches!(
|
||||
document.get_entity(*handle),
|
||||
None | Some(EntityType::Block(_) | EntityType::BlockEnd(_))
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(super) fn element_tag(id: i32, handle: Handle) -> ExtendedDataRecord {
|
||||
let mut tag = ExtendedDataRecord::new(ELEMENT_TAG);
|
||||
tag.add_value(XDataValue::Integer16(1));
|
||||
tag.add_value(XDataValue::Integer32(id));
|
||||
tag.add_value(XDataValue::Handle(handle));
|
||||
tag
|
||||
}
|
||||
|
||||
pub(super) fn element_id(entity: &EntityType) -> Option<i32> {
|
||||
entity
|
||||
.common()
|
||||
.extended_data
|
||||
.get_record(ELEMENT_TAG)?
|
||||
.values
|
||||
.iter()
|
||||
.find_map(|value| match value {
|
||||
XDataValue::Integer32(id) => Some(*id),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn set_entity_tag(
|
||||
document: &mut CadDocument,
|
||||
handle: Handle,
|
||||
record: ExtendedDataRecord,
|
||||
) {
|
||||
let app = document
|
||||
.app_ids
|
||||
.get(&record.application_name)
|
||||
.map(|app| app.handle.value());
|
||||
if let Some(entity) = document.get_entity_mut(handle) {
|
||||
let xdata = &mut entity.common_mut().extended_data;
|
||||
// The fresh record, not bytes kept from a read, is what gets written.
|
||||
if let Some(app) = app {
|
||||
xdata.raw_dwg_eed.retain(|(owner, _)| *owner != app);
|
||||
}
|
||||
xdata.upsert_record(record);
|
||||
}
|
||||
}
|
||||
|
||||
/// Give every object of the definition its element tag, numbering the new
|
||||
/// ones after the highest id in use, and record the next id on the block.
|
||||
pub(super) fn tag_members(document: &mut CadDocument, definition: Handle) {
|
||||
let members = members(document, definition);
|
||||
let mut next = members
|
||||
.iter()
|
||||
.filter_map(|handle| document.get_entity(*handle).and_then(element_id))
|
||||
.max()
|
||||
.map_or(0, |id| id + 1);
|
||||
for handle in members {
|
||||
let id = match document.get_entity(handle).and_then(element_id) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
next += 1;
|
||||
next - 1
|
||||
}
|
||||
};
|
||||
set_entity_tag(document, handle, element_tag(id, handle));
|
||||
}
|
||||
let mut tag = ExtendedDataRecord::new(ELEMENT_TAG);
|
||||
tag.add_value(XDataValue::Integer16(1));
|
||||
tag.add_value(XDataValue::Integer32(next));
|
||||
document.set_object_xdata_record(definition, tag);
|
||||
}
|
||||
|
||||
pub(super) fn new_guid() -> String {
|
||||
let mut bytes = [0u8; 16];
|
||||
if getrandom::fill(&mut bytes).is_err() {
|
||||
let seed = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_or(0, |elapsed| elapsed.as_nanos());
|
||||
bytes = seed.to_le_bytes();
|
||||
}
|
||||
let hex: String = bytes.iter().map(|byte| format!("{byte:02X}")).collect();
|
||||
format!(
|
||||
"{{{}-{}-{}-{}-{}}}",
|
||||
&hex[0..8],
|
||||
&hex[8..12],
|
||||
&hex[12..16],
|
||||
&hex[16..20],
|
||||
&hex[20..32]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,549 @@
|
||||
//! Native Point parameter authoring. Action evaluation is a separate service.
|
||||
use super::block_authoring::{self as authoring, *};
|
||||
use codec::objects::{
|
||||
BlockConnection, BlockElement, BlockEvalValue, BlockGrip, BlockGripExpression,
|
||||
BlockOnePointParameter, BlockParameter, BlockParameterProperty, BlockPointParameter,
|
||||
DynamicBlockData, ObjectType,
|
||||
};
|
||||
use codec::types::{Transform, Vector3};
|
||||
use codec::{CadDocument, Handle};
|
||||
|
||||
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<(Handle, &BlockPointParameter)> {
|
||||
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(graph_data) = &object.data else {
|
||||
return Vec::new();
|
||||
};
|
||||
graph_data
|
||||
.nodes
|
||||
.iter()
|
||||
.filter_map(|node| match document.objects.get(&node.expression) {
|
||||
Some(ObjectType::DynamicBlock(object)) if object.owner == graph => match &object.data {
|
||||
DynamicBlockData::PointParameter(parameter) => Some((object.handle, parameter)),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn placement_error(document: &CadDocument, definition: Handle) -> Option<&'static str> {
|
||||
authoring::graph(document, definition).err()
|
||||
}
|
||||
|
||||
pub(crate) fn create(
|
||||
document: &mut CadDocument,
|
||||
definition: Handle,
|
||||
location: Vector3,
|
||||
label: Vector3,
|
||||
) -> Result<Handle, &'static str> {
|
||||
if ![
|
||||
location.x, location.y, location.z, label.x, label.y, label.z,
|
||||
]
|
||||
.iter()
|
||||
.all(|value| value.is_finite())
|
||||
{
|
||||
return Err("Specify finite parameter and label locations.");
|
||||
}
|
||||
let (graph, slots) = authoring::prepare(document, definition)?;
|
||||
register_classes(
|
||||
document,
|
||||
&[
|
||||
("BLOCKPOINTPARAMETER", "AcDbBlockPointParameter", 55),
|
||||
("BLOCKXYGRIP", "AcDbBlockXYGrip", 55),
|
||||
("BLOCKGRIPLOCATIONCOMPONENT", "AcDbBlockGripExpr", 20),
|
||||
],
|
||||
);
|
||||
let existing = parameters(document, definition);
|
||||
let name = (1..)
|
||||
.map(|number| format!("Position{number}"))
|
||||
.find(|name| {
|
||||
existing
|
||||
.iter()
|
||||
.all(|(_, parameter)| !parameter.position_name.eq_ignore_ascii_case(name))
|
||||
})
|
||||
.unwrap();
|
||||
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.to_string(),
|
||||
major: MAJOR,
|
||||
minor: MINOR,
|
||||
eed_1071: 0,
|
||||
};
|
||||
let native = BlockPointParameter {
|
||||
parameter: BlockOnePointParameter {
|
||||
parameter: BlockParameter {
|
||||
element: element(0, "Point"),
|
||||
show_properties: true,
|
||||
chain_actions: false,
|
||||
},
|
||||
definition_point: location,
|
||||
properties: ["DisplacementX", "DisplacementY"].map(|name| BlockParameterProperty {
|
||||
connections: vec![BlockConnection {
|
||||
code: slots.eval + 1,
|
||||
name: name.into(),
|
||||
}],
|
||||
}),
|
||||
// Native 93 links the one-point parameter to its grip expression ID.
|
||||
property_count: slots.eval + 1,
|
||||
},
|
||||
position_name: name,
|
||||
position_description: String::new(),
|
||||
definition_label_point: label,
|
||||
};
|
||||
document.objects.insert(
|
||||
parameter,
|
||||
ObjectType::DynamicBlock(dynamic(
|
||||
parameter,
|
||||
graph,
|
||||
"BLOCKPOINTPARAMETER",
|
||||
"AcDbBlockPointParameter",
|
||||
DynamicBlockData::PointParameter(native),
|
||||
)),
|
||||
);
|
||||
document.objects.insert(
|
||||
grip,
|
||||
ObjectType::DynamicBlock(dynamic(
|
||||
grip,
|
||||
graph,
|
||||
"BLOCKXYGRIP",
|
||||
"AcDbBlockXYGrip",
|
||||
DynamicBlockData::XYGrip(BlockGrip {
|
||||
element: element(1, "Grip"),
|
||||
flags_91: slots.eval + 2,
|
||||
flags_92: slots.eval + 3,
|
||||
location,
|
||||
insert_cycling: true,
|
||||
insert_cycling_weight: -1,
|
||||
}),
|
||||
)),
|
||||
);
|
||||
for (handle, offset, axis) in [(x, 2, "UpdatedX"), (y, 3, "UpdatedY")] {
|
||||
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)
|
||||
}
|
||||
|
||||
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(|(handle, parameter)| (*handle, parameter.parameter.property_count))
|
||||
.collect::<Vec<_>>();
|
||||
let mut changed = 0;
|
||||
for (handle, grip_id) in owned {
|
||||
if let Some(ObjectType::DynamicBlock(object)) = document.objects.get_mut(&handle) {
|
||||
if let DynamicBlockData::PointParameter(parameter) = &mut object.data {
|
||||
parameter.parameter.definition_point =
|
||||
transform.apply(parameter.parameter.definition_point);
|
||||
parameter.definition_label_point =
|
||||
transform.apply(parameter.definition_label_point);
|
||||
changed += 1;
|
||||
}
|
||||
}
|
||||
for object in document.objects.values_mut() {
|
||||
if let ObjectType::DynamicBlock(object) = object {
|
||||
if object.owner == graph {
|
||||
if let DynamicBlockData::XYGrip(grip) = &mut object.data {
|
||||
if grip.element.eval.node_id == grip_id {
|
||||
grip.location = transform.apply(grip.location);
|
||||
changed += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::scene::{block_visibility, Scene};
|
||||
use codec::{entities::Line, EntityType};
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires OCS_DYNAMIC_BLOCK_FIXTURES with local DWG fixtures"]
|
||||
fn point_parameter_fixture_graph_append_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(|entry| entry.unwrap().path())
|
||||
.filter(|path| {
|
||||
path.extension()
|
||||
.is_some_and(|extension| extension.eq_ignore_ascii_case("dwg"))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
paths.sort();
|
||||
assert!(!paths.is_empty());
|
||||
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 scene = Scene::new();
|
||||
scene.document = crate::io::load_bytes(&filename, bytes).unwrap();
|
||||
let definitions = scene
|
||||
.document
|
||||
.block_records
|
||||
.iter()
|
||||
.filter(|record| graph_handle(&scene.document, record.handle).is_some())
|
||||
.map(|record| record.handle)
|
||||
.collect::<Vec<_>>();
|
||||
let existing = !definitions.is_empty();
|
||||
let definition = if existing {
|
||||
definitions
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|definition| authoring::graph(&scene.document, *definition).is_ok())
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"{filename}: no editable decoded graph: {:?}",
|
||||
definitions
|
||||
.iter()
|
||||
.map(|definition| authoring::graph(&scene.document, *definition))
|
||||
.collect::<Vec<_>>()
|
||||
)
|
||||
})
|
||||
} else {
|
||||
scene.define_block_raw(
|
||||
"POINT_PARAMETER_TEST",
|
||||
Vector3::ZERO,
|
||||
vec![EntityType::Line(Line::from_points(
|
||||
Vector3::ZERO,
|
||||
Vector3::UNIT_X,
|
||||
))],
|
||||
);
|
||||
scene
|
||||
.document
|
||||
.block_records
|
||||
.get("POINT_PARAMETER_TEST")
|
||||
.unwrap()
|
||||
.handle
|
||||
};
|
||||
let before = scene.document.clone();
|
||||
let graph = graph_handle(&before, definition);
|
||||
let old_nodes = existing.then(|| graph_data(&before, definition).nodes.clone());
|
||||
let location = Vector3::new(3.25, -4.5, 2.0);
|
||||
let label = Vector3::new(6.0, 7.0, 2.0);
|
||||
let handle = create(&mut scene.document, definition, location, label).unwrap();
|
||||
let name = parameters(&scene.document, definition)
|
||||
.iter()
|
||||
.find(|(found, _)| *found == handle)
|
||||
.unwrap()
|
||||
.1
|
||||
.position_name
|
||||
.clone();
|
||||
for (handle, object) in &before.objects {
|
||||
if Some(*handle) != graph {
|
||||
assert_eq!(
|
||||
scene.document.objects.get(handle),
|
||||
Some(object),
|
||||
"{filename}: preserved {handle}"
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(nodes) = old_nodes {
|
||||
assert_eq!(
|
||||
&graph_data(&scene.document, definition).nodes[..nodes.len()],
|
||||
&nodes
|
||||
);
|
||||
}
|
||||
for extension in ["dwg", "dxf"] {
|
||||
let bytes =
|
||||
crate::io::save_to_bytes(&scene.document, extension, scene.document.version)
|
||||
.unwrap();
|
||||
let read = crate::io::load_bytes(&format!("point.{extension}"), bytes).unwrap();
|
||||
let points = parameters(&read, definition);
|
||||
let point = points
|
||||
.iter()
|
||||
.find(|(_, point)| point.position_name == name)
|
||||
.unwrap_or_else(|| panic!("{filename}: missing {name} in {extension}"))
|
||||
.1;
|
||||
assert_eq!(
|
||||
point.parameter.definition_point, location,
|
||||
"{filename}: {extension}"
|
||||
);
|
||||
assert_eq!(
|
||||
point.definition_label_point, label,
|
||||
"{filename}: {extension}"
|
||||
);
|
||||
assert!(
|
||||
authoring::graph(&read, definition).is_ok(),
|
||||
"{filename}: graph in {extension}"
|
||||
);
|
||||
}
|
||||
assert_eq!(Sha256::digest(std::fs::read(&path).unwrap()), digest);
|
||||
eprintln!("{filename}: Point DWG/DXF roundtrips; appended to existing graph={existing}; original objects/checksum preserved");
|
||||
}
|
||||
}
|
||||
|
||||
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 graph_data(
|
||||
document: &CadDocument,
|
||||
definition: Handle,
|
||||
) -> &codec::objects::BlockEvaluationGraph {
|
||||
let ObjectType::DynamicBlock(object) =
|
||||
&document.objects[&graph_handle(document, definition).unwrap()]
|
||||
else {
|
||||
panic!("native graph");
|
||||
};
|
||||
let DynamicBlockData::EvaluationGraph(graph) = &object.data else {
|
||||
panic!("graph data");
|
||||
};
|
||||
graph
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_parameter_appends_to_visibility_and_keeps_existing_objects() {
|
||||
for visibility_first in [false, true] {
|
||||
let (mut scene, definition) = fixture();
|
||||
if visibility_first {
|
||||
block_visibility::create(&mut scene.document, definition, Vector3::ZERO).unwrap();
|
||||
} else {
|
||||
create(
|
||||
&mut scene.document,
|
||||
definition,
|
||||
Vector3::ZERO,
|
||||
Vector3::UNIT_X,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let before = scene.document.clone();
|
||||
let graph = graph_handle(&before, definition).unwrap();
|
||||
let nodes = graph_data(&before, definition).nodes.clone();
|
||||
let edges = graph_data(&before, definition).edges.clone();
|
||||
create(
|
||||
&mut scene.document,
|
||||
definition,
|
||||
Vector3::new(2.0, 3.0, 4.0),
|
||||
Vector3::new(5.0, 6.0, 4.0),
|
||||
)
|
||||
.unwrap();
|
||||
if !visibility_first {
|
||||
block_visibility::create(&mut scene.document, definition, Vector3::UNIT_Y).unwrap();
|
||||
}
|
||||
for (handle, object) in &before.objects {
|
||||
if *handle != graph {
|
||||
assert_eq!(
|
||||
scene.document.objects.get(handle),
|
||||
Some(object),
|
||||
"preserved {handle}"
|
||||
);
|
||||
}
|
||||
}
|
||||
let after = graph_data(&scene.document, definition);
|
||||
assert_eq!(&after.nodes[..nodes.len()], &nodes);
|
||||
assert_eq!(&after.edges[..edges.len()], &edges);
|
||||
assert!(block_visibility::parameter(&scene.document, definition).is_some());
|
||||
assert_eq!(
|
||||
parameters(&scene.document, definition).len(),
|
||||
if visibility_first { 1 } else { 2 }
|
||||
);
|
||||
assert!(authoring::graph(&scene.document, definition).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_parameter_native_fields_and_shared_graph_roundtrip() {
|
||||
let (mut scene, definition) = fixture();
|
||||
let location = Vector3::new(3.25, -4.5, 6.0);
|
||||
let label = Vector3::new(7.0, 8.0, 6.0);
|
||||
create(&mut scene.document, definition, location, label).unwrap();
|
||||
create(
|
||||
&mut scene.document,
|
||||
definition,
|
||||
Vector3::ZERO,
|
||||
Vector3::UNIT_X,
|
||||
)
|
||||
.unwrap();
|
||||
block_visibility::create(&mut scene.document, definition, Vector3::ZERO).unwrap();
|
||||
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();
|
||||
let found = parameters(&document, definition);
|
||||
assert_eq!(found.len(), 2, "{extension}");
|
||||
assert_eq!(found[0].1.position_name, "Position1");
|
||||
assert_eq!(found[1].1.position_name, "Position2");
|
||||
assert_eq!(found[0].1.parameter.definition_point, location);
|
||||
assert_eq!(found[0].1.definition_label_point, label);
|
||||
assert_eq!(
|
||||
found[0].1.parameter.properties[0].connections[0].name,
|
||||
"DisplacementX"
|
||||
);
|
||||
assert!(block_visibility::parameter(&document, definition).is_some());
|
||||
assert!(authoring::graph(&document, definition).is_ok());
|
||||
let point = found[0].1;
|
||||
let grip = document
|
||||
.objects
|
||||
.values()
|
||||
.find_map(|object| match object {
|
||||
ObjectType::DynamicBlock(object) => match &object.data {
|
||||
DynamicBlockData::XYGrip(grip)
|
||||
if grip.element.eval.node_id == point.parameter.property_count =>
|
||||
{
|
||||
Some(grip)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
.expect("native point grip");
|
||||
assert_eq!(grip.location, location);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_parameter_reframe_moves_position_label_and_own_grip() {
|
||||
let (mut scene, definition) = fixture();
|
||||
create(
|
||||
&mut scene.document,
|
||||
definition,
|
||||
Vector3::new(3.0, 4.0, 5.0),
|
||||
Vector3::new(6.0, 7.0, 5.0),
|
||||
)
|
||||
.unwrap();
|
||||
let transform = Transform::from_translation(Vector3::new(-1.0, -2.0, -3.0));
|
||||
assert_eq!(reframe(&mut scene.document, definition, &transform), 2);
|
||||
let point = parameters(&scene.document, definition)[0].1;
|
||||
assert_eq!(
|
||||
point.parameter.definition_point,
|
||||
Vector3::new(2.0, 2.0, 2.0)
|
||||
);
|
||||
assert_eq!(point.definition_label_point, Vector3::new(5.0, 5.0, 2.0));
|
||||
let grip = scene
|
||||
.document
|
||||
.objects
|
||||
.values()
|
||||
.find_map(|object| match object {
|
||||
ObjectType::DynamicBlock(object) => match &object.data {
|
||||
DynamicBlockData::XYGrip(grip) => Some(grip),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(grip.location, point.parameter.definition_point);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_parameter_positions_contribute_to_editor_extents_only() {
|
||||
let (mut scene, definition) = fixture();
|
||||
create(
|
||||
&mut scene.document,
|
||||
definition,
|
||||
Vector3::new(30.0, 40.0, 0.0),
|
||||
Vector3::new(60.0, 70.0, 0.0),
|
||||
)
|
||||
.unwrap();
|
||||
scene.block_edit_block = Some(definition);
|
||||
scene.bump_geometry();
|
||||
let (_, max) = scene.visible_block_bounds(definition).unwrap();
|
||||
assert!(max.x >= 60.0 && max.y >= 70.0);
|
||||
scene.block_edit_block = None;
|
||||
scene.bump_geometry();
|
||||
let (_, max) = scene.visible_block_bounds(definition).unwrap();
|
||||
assert!(
|
||||
max.x <= 1.0 && max.y < 1.0,
|
||||
"authoring labels are not block reference geometry"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_parameter_rejects_invalid_graph_and_locations_without_mutation() {
|
||||
let (mut scene, definition) = fixture();
|
||||
let before = scene.document.clone();
|
||||
assert!(create(
|
||||
&mut scene.document,
|
||||
definition,
|
||||
Vector3::new(f64::NAN, 0.0, 0.0),
|
||||
Vector3::ZERO
|
||||
)
|
||||
.is_err());
|
||||
assert_eq!(scene.document, before);
|
||||
create(
|
||||
&mut scene.document,
|
||||
definition,
|
||||
Vector3::ZERO,
|
||||
Vector3::UNIT_X,
|
||||
)
|
||||
.unwrap();
|
||||
let handle = graph_handle(&scene.document, definition).unwrap();
|
||||
if let Some(ObjectType::DynamicBlock(object)) = scene.document.objects.get_mut(&handle) {
|
||||
if let DynamicBlockData::EvaluationGraph(graph) = &mut object.data {
|
||||
graph.first_node_id = i32::MAX;
|
||||
}
|
||||
}
|
||||
let before = scene.document.clone();
|
||||
assert!(create(
|
||||
&mut scene.document,
|
||||
definition,
|
||||
Vector3::ZERO,
|
||||
Vector3::ZERO
|
||||
)
|
||||
.is_err());
|
||||
assert_eq!(scene.document, before);
|
||||
assert!(block_visibility::create(&mut scene.document, definition, Vector3::ZERO).is_none());
|
||||
assert_eq!(scene.document, before);
|
||||
}
|
||||
}
|
||||
+19
-287
@@ -10,10 +10,9 @@
|
||||
//! no copy until another state is picked for it.
|
||||
|
||||
use codec::objects::{
|
||||
BlockElement, BlockEvalExpression, BlockEvalValue, BlockEvaluationEdge, BlockEvaluationGraph,
|
||||
BlockEvaluationNode, BlockGrip, BlockGripExpression, BlockParameterPropertyInfo,
|
||||
BlockRepresentationData, BlockVisibilityParameter, BlockVisibilityState, DynamicBlockData,
|
||||
DynamicBlockObject, ObjectType,
|
||||
BlockElement, BlockEvalExpression, BlockEvalValue, BlockGrip, BlockGripExpression,
|
||||
BlockParameterPropertyInfo, BlockRepresentationData, BlockVisibilityParameter,
|
||||
BlockVisibilityState, DynamicBlockData, ObjectType,
|
||||
};
|
||||
use codec::types::Vector3;
|
||||
use codec::xdata::{ExtendedDataRecord, XDataValue};
|
||||
@@ -21,16 +20,7 @@ use codec::{CadDocument, EntityType, Handle};
|
||||
|
||||
use super::Scene;
|
||||
|
||||
const ELEMENT_TAG: &str = "AcDbBlockRepETag";
|
||||
const REPRESENTATION_TAG: &str = "AcDbBlockRepBTag";
|
||||
const TRUE_NAME: &str = "AcDbDynamicBlockTrueName";
|
||||
const GUID: &str = "AcDbDynamicBlockGUID";
|
||||
|
||||
/// Object version the parameter, grip and expressions carry.
|
||||
const MAJOR: i32 = 33;
|
||||
const MINOR: i32 = 329;
|
||||
/// Value code of an expression that holds no value.
|
||||
const NO_VALUE: i16 = -9999;
|
||||
use super::block_authoring::*;
|
||||
|
||||
impl Scene {
|
||||
/// BVMODE and BVSHOW picking reveal only state-controlled members of BEDIT.
|
||||
@@ -94,67 +84,6 @@ const CLASSES: &[(&str, &str, i32)] = &[
|
||||
),
|
||||
];
|
||||
|
||||
/// Register the classes with the flags and versions the reference writes
|
||||
/// for them; a reader takes a class registered otherwise for a proxy.
|
||||
fn register_classes(document: &mut CadDocument) {
|
||||
for &(dxf_name, cpp_class_name, maintenance_version) in CLASSES {
|
||||
if let Some(existing) = document.classes.get_mut_by_name(dxf_name) {
|
||||
existing.proxy_flags = codec::classes::ProxyFlags(1153);
|
||||
existing.dwg_version = 25;
|
||||
existing.maintenance_version = maintenance_version;
|
||||
continue;
|
||||
}
|
||||
document.classes.add_or_update(codec::classes::DxfClass {
|
||||
dxf_name: dxf_name.to_string(),
|
||||
cpp_class_name: cpp_class_name.to_string(),
|
||||
application_name: "ObjectDBX Classes".to_string(),
|
||||
proxy_flags: codec::classes::ProxyFlags(1153),
|
||||
instance_count: 0,
|
||||
was_zombie: false,
|
||||
is_an_entity: false,
|
||||
class_number: 0,
|
||||
item_class_id: 0x1F3,
|
||||
dwg_version: 25,
|
||||
maintenance_version,
|
||||
unknown1: 0,
|
||||
unknown2: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_app_id(document: &mut CadDocument, name: &str) {
|
||||
if !document.app_ids.contains(name) {
|
||||
let mut app = codec::tables::AppId::new(name);
|
||||
app.handle = document.allocate_handle();
|
||||
let _ = document.app_ids.add(app);
|
||||
}
|
||||
}
|
||||
|
||||
fn expression(node_id: i32, value_code: i16, value: BlockEvalValue) -> BlockEvalExpression {
|
||||
BlockEvalExpression {
|
||||
parent_id: BlockEvalExpression::NO_PARENT,
|
||||
major: MAJOR,
|
||||
minor: MINOR,
|
||||
value_code,
|
||||
value,
|
||||
node_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn dynamic(
|
||||
handle: Handle,
|
||||
owner: Handle,
|
||||
dxf_name: &str,
|
||||
cpp_class_name: &str,
|
||||
data: DynamicBlockData,
|
||||
) -> DynamicBlockObject {
|
||||
let mut object = DynamicBlockObject::new(dxf_name, cpp_class_name);
|
||||
object.handle = handle;
|
||||
object.owner = owner;
|
||||
object.data = data;
|
||||
object
|
||||
}
|
||||
|
||||
/// The visibility parameter of block definition `definition`.
|
||||
pub fn parameter(document: &CadDocument, definition: Handle) -> Option<&BlockVisibilityParameter> {
|
||||
document.block_visibility_param_for_def(definition)
|
||||
@@ -185,135 +114,13 @@ pub(crate) fn replace_states(
|
||||
tag_members(document, definition);
|
||||
}
|
||||
|
||||
/// The objects of a definition, block begin and end left out.
|
||||
fn members(document: &CadDocument, definition: Handle) -> Vec<Handle> {
|
||||
document
|
||||
.block_records
|
||||
.iter()
|
||||
.find(|record| record.handle == definition)
|
||||
.map(|record| {
|
||||
record
|
||||
.entity_handles
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|handle| {
|
||||
!matches!(
|
||||
document.get_entity(*handle),
|
||||
None | Some(EntityType::Block(_) | EntityType::BlockEnd(_))
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn element_tag(id: i32, handle: Handle) -> ExtendedDataRecord {
|
||||
let mut tag = ExtendedDataRecord::new(ELEMENT_TAG);
|
||||
tag.add_value(XDataValue::Integer16(1));
|
||||
tag.add_value(XDataValue::Integer32(id));
|
||||
tag.add_value(XDataValue::Handle(handle));
|
||||
tag
|
||||
}
|
||||
|
||||
fn element_id(entity: &EntityType) -> Option<i32> {
|
||||
entity
|
||||
.common()
|
||||
.extended_data
|
||||
.get_record(ELEMENT_TAG)?
|
||||
.values
|
||||
.iter()
|
||||
.find_map(|value| match value {
|
||||
XDataValue::Integer32(id) => Some(*id),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn set_entity_tag(document: &mut CadDocument, handle: Handle, record: ExtendedDataRecord) {
|
||||
let app = document
|
||||
.app_ids
|
||||
.get(&record.application_name)
|
||||
.map(|app| app.handle.value());
|
||||
if let Some(entity) = document.get_entity_mut(handle) {
|
||||
let xdata = &mut entity.common_mut().extended_data;
|
||||
// The fresh record, not bytes kept from a read, is what gets written.
|
||||
if let Some(app) = app {
|
||||
xdata.raw_dwg_eed.retain(|(owner, _)| *owner != app);
|
||||
}
|
||||
xdata.upsert_record(record);
|
||||
}
|
||||
}
|
||||
|
||||
/// Give every object of the definition its element tag, numbering the new
|
||||
/// ones after the highest id in use, and record the next id on the block.
|
||||
fn tag_members(document: &mut CadDocument, definition: Handle) {
|
||||
let members = members(document, definition);
|
||||
let mut next = members
|
||||
.iter()
|
||||
.filter_map(|handle| document.get_entity(*handle).and_then(element_id))
|
||||
.max()
|
||||
.map_or(0, |id| id + 1);
|
||||
for handle in members {
|
||||
let id = match document.get_entity(handle).and_then(element_id) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
next += 1;
|
||||
next - 1
|
||||
}
|
||||
};
|
||||
set_entity_tag(document, handle, element_tag(id, handle));
|
||||
}
|
||||
let mut tag = ExtendedDataRecord::new(ELEMENT_TAG);
|
||||
tag.add_value(XDataValue::Integer16(1));
|
||||
tag.add_value(XDataValue::Integer32(next));
|
||||
document.set_object_xdata_record(definition, tag);
|
||||
}
|
||||
|
||||
/// A registry-format GUID for a new dynamic block.
|
||||
fn new_guid() -> String {
|
||||
let mut bytes = [0u8; 16];
|
||||
if getrandom::fill(&mut bytes).is_err() {
|
||||
let seed = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_or(0, |elapsed| elapsed.as_nanos());
|
||||
bytes = seed.to_le_bytes();
|
||||
}
|
||||
let hex: String = bytes.iter().map(|byte| format!("{byte:02X}")).collect();
|
||||
format!(
|
||||
"{{{}-{}-{}-{}-{}}}",
|
||||
&hex[0..8],
|
||||
&hex[8..12],
|
||||
&hex[12..16],
|
||||
&hex[16..20],
|
||||
&hex[20..32]
|
||||
)
|
||||
}
|
||||
|
||||
/// Why another visibility parameter cannot be placed in this definition.
|
||||
/// Existing evaluation graphs must be composed, never replaced by a new graph.
|
||||
pub fn placement_error(document: &CadDocument, definition: Handle) -> Option<&'static str> {
|
||||
if parameter(document, definition).is_some() {
|
||||
return Some("This block already has a visibility parameter.");
|
||||
}
|
||||
if !document
|
||||
.block_records
|
||||
.iter()
|
||||
.any(|record| record.handle == definition)
|
||||
{
|
||||
return Some("The block definition is no longer available.");
|
||||
}
|
||||
if let Some(handle) = document.extension_dictionary_handle(definition) {
|
||||
match document.objects.get(&handle) {
|
||||
Some(ObjectType::Dictionary(dictionary)) => {
|
||||
if dictionary.get("ACAD_ENHANCEDBLOCK").is_some() {
|
||||
return Some(
|
||||
"Adding Visibility to an existing authoring graph is not available yet.",
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => return Some("The block extension dictionary is unavailable."),
|
||||
}
|
||||
}
|
||||
None
|
||||
super::block_authoring::graph(document, definition).err()
|
||||
}
|
||||
|
||||
/// Make block definition `definition` dynamic with a visibility parameter
|
||||
@@ -330,18 +137,9 @@ pub fn create(document: &mut CadDocument, definition: Handle, location: Vector3)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let name = document
|
||||
.block_records
|
||||
.iter()
|
||||
.find(|record| record.handle == definition)?
|
||||
.name
|
||||
.clone();
|
||||
register_classes(document);
|
||||
for app in [ELEMENT_TAG, REPRESENTATION_TAG, TRUE_NAME, GUID] {
|
||||
ensure_app_id(document, app);
|
||||
}
|
||||
let dictionary = document.ensure_extension_dictionary(definition);
|
||||
let [graph, parameter, grip, x, y, preventer] = [(); 6].map(|_| document.allocate_handle());
|
||||
let (graph, slots) = super::block_authoring::prepare(document, definition).ok()?;
|
||||
register_classes(document, CLASSES);
|
||||
let [parameter, grip, x, y] = [(); 4].map(|_| document.allocate_handle());
|
||||
|
||||
store(
|
||||
document,
|
||||
@@ -353,7 +151,7 @@ pub fn create(document: &mut CadDocument, definition: Handle, location: Vector3)
|
||||
eval_minor: MINOR,
|
||||
eval_value_code: NO_VALUE,
|
||||
eval_value: BlockEvalValue::None,
|
||||
eval_node_id: 1,
|
||||
eval_node_id: slots.eval,
|
||||
element_name: "Visibility State".to_string(),
|
||||
element_major: MAJOR,
|
||||
element_minor: MINOR,
|
||||
@@ -367,7 +165,7 @@ pub fn create(document: &mut CadDocument, definition: Handle, location: Vector3)
|
||||
BlockParameterPropertyInfo::default(),
|
||||
BlockParameterPropertyInfo::default(),
|
||||
],
|
||||
property_info_count: 2,
|
||||
property_info_count: slots.eval + 1,
|
||||
is_initialized: true,
|
||||
unknown_bool: false,
|
||||
all_blocks: Vec::new(),
|
||||
@@ -384,14 +182,14 @@ pub fn create(document: &mut CadDocument, definition: Handle, location: Vector3)
|
||||
"AcDbBlockVisibilityGrip",
|
||||
DynamicBlockData::VisibilityGrip(BlockGrip {
|
||||
element: BlockElement {
|
||||
eval: expression(2, NO_VALUE, BlockEvalValue::None),
|
||||
eval: expression(slots.eval + 1, NO_VALUE, BlockEvalValue::None),
|
||||
name: "Grip".to_string(),
|
||||
major: MAJOR,
|
||||
minor: MINOR,
|
||||
eed_1071: 0,
|
||||
},
|
||||
flags_91: 3,
|
||||
flags_92: 4,
|
||||
flags_91: slots.eval + 2,
|
||||
flags_92: slots.eval + 3,
|
||||
location,
|
||||
insert_cycling: false,
|
||||
insert_cycling_weight: -1,
|
||||
@@ -400,7 +198,10 @@ pub fn create(document: &mut CadDocument, definition: Handle, location: Vector3)
|
||||
document
|
||||
.objects
|
||||
.insert(grip, ObjectType::DynamicBlock(grip_object));
|
||||
for (handle, node, axis) in [(x, 3, "UpdatedX"), (y, 4, "UpdatedY")] {
|
||||
for (handle, node, axis) in [
|
||||
(x, slots.eval + 2, "UpdatedX"),
|
||||
(y, slots.eval + 3, "UpdatedY"),
|
||||
] {
|
||||
let component = dynamic(
|
||||
handle,
|
||||
graph,
|
||||
@@ -408,7 +209,7 @@ pub fn create(document: &mut CadDocument, definition: Handle, location: Vector3)
|
||||
"AcDbBlockGripExpr",
|
||||
DynamicBlockData::GripLocationComponent(BlockGripExpression {
|
||||
eval: expression(node, 40, BlockEvalValue::Real(0.0)),
|
||||
grip_type: 1,
|
||||
grip_type: slots.eval,
|
||||
expression: axis.to_string(),
|
||||
}),
|
||||
);
|
||||
@@ -416,76 +217,7 @@ pub fn create(document: &mut CadDocument, definition: Handle, location: Vector3)
|
||||
.objects
|
||||
.insert(handle, ObjectType::DynamicBlock(component));
|
||||
}
|
||||
// The grip drives the parameter, whose position the two expressions
|
||||
// read: node 0 the parameter, 1 the grip, 2 and 3 the expressions.
|
||||
let node = |id: i32, expression: Handle, node_data: [i32; 4]| BlockEvaluationNode {
|
||||
id,
|
||||
edge_flags: 32,
|
||||
next_id: id + 1,
|
||||
expression,
|
||||
node_data,
|
||||
active_cycles: None,
|
||||
};
|
||||
let edge =
|
||||
|id: i32, source_node: i32, destination_node: i32, outgoing_edges| BlockEvaluationEdge {
|
||||
id,
|
||||
next_id: 0,
|
||||
incoming_edge: 1,
|
||||
source_node,
|
||||
destination_node,
|
||||
outgoing_edges,
|
||||
};
|
||||
let mut graph_object = dynamic(
|
||||
graph,
|
||||
dictionary,
|
||||
"ACAD_EVALUATION_GRAPH",
|
||||
"AcDbEvalGraph",
|
||||
DynamicBlockData::EvaluationGraph(BlockEvaluationGraph {
|
||||
first_node_id: 4,
|
||||
first_node_id_copy: 4,
|
||||
nodes: vec![
|
||||
node(0, parameter, [0, 0, 1, 2]),
|
||||
node(1, grip, [-1, -1, 0, 0]),
|
||||
node(2, x, [1, 1, -1, -1]),
|
||||
node(3, y, [2, 2, -1, -1]),
|
||||
],
|
||||
edges: vec![
|
||||
edge(0, 1, 0, [-1; 5]),
|
||||
edge(1, 0, 2, [-1, -1, -1, 2, -1]),
|
||||
edge(2, 0, 3, [-1, -1, 1, -1, -1]),
|
||||
],
|
||||
}),
|
||||
);
|
||||
graph_object.reactors.push(dictionary);
|
||||
document
|
||||
.objects
|
||||
.insert(graph, ObjectType::DynamicBlock(graph_object));
|
||||
let mut preventer_object = dynamic(
|
||||
preventer,
|
||||
dictionary,
|
||||
"ACDB_DYNAMICBLOCKPURGEPREVENTER_VERSION",
|
||||
"AcDbDynamicBlockPurgePreventer",
|
||||
DynamicBlockData::Representation(BlockRepresentationData {
|
||||
flags: 1,
|
||||
block: definition,
|
||||
}),
|
||||
);
|
||||
preventer_object.reactors.push(dictionary);
|
||||
document
|
||||
.objects
|
||||
.insert(preventer, ObjectType::DynamicBlock(preventer_object));
|
||||
if let Some(ObjectType::Dictionary(dictionary)) = document.objects.get_mut(&dictionary) {
|
||||
dictionary.add_entry("ACAD_ENHANCEDBLOCK", graph);
|
||||
dictionary.add_entry("AcDbDynamicBlockRoundTripPurgePreventer", preventer);
|
||||
}
|
||||
|
||||
let mut true_name = ExtendedDataRecord::new(TRUE_NAME);
|
||||
true_name.add_value(XDataValue::String(name));
|
||||
document.set_object_xdata_record(definition, true_name);
|
||||
let mut guid = ExtendedDataRecord::new(GUID);
|
||||
guid.add_value(XDataValue::String(new_guid()));
|
||||
document.set_object_xdata_record(definition, guid);
|
||||
tag_members(document, definition);
|
||||
super::block_authoring::append(document, graph, slots, [parameter, grip, x, y]);
|
||||
Some(parameter)
|
||||
}
|
||||
|
||||
|
||||
@@ -1664,6 +1664,25 @@ impl Scene {
|
||||
// (`camera_for_viewport`), so reading the camera here recurses. A wire
|
||||
// with no precise position falls back to the z = 0 plane.
|
||||
let fallback_z = 0.0;
|
||||
// Authoring graphics belong to the editor view, never INSERT geometry.
|
||||
// Include them when framing/reopening an edited definition, even if
|
||||
// the block contains parameters only.
|
||||
if self.block_edit_block == Some(block) {
|
||||
for (_, parameter) in super::block_point_parameter::parameters(&self.document, block) {
|
||||
for point in [
|
||||
parameter.parameter.definition_point,
|
||||
parameter.definition_label_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);
|
||||
fold!(Some((point, point)));
|
||||
}
|
||||
}
|
||||
let scale = crate::scene::annotative::scale_handle_by_name(
|
||||
&self.document,
|
||||
&self.document.header.current_annotation_scale,
|
||||
|
||||
@@ -10,6 +10,8 @@ pub mod assoc_array;
|
||||
pub(crate) mod block_metadata;
|
||||
pub mod block_service;
|
||||
pub mod block_visibility;
|
||||
pub(crate) mod block_authoring;
|
||||
pub(crate) mod block_point_parameter;
|
||||
pub mod cache;
|
||||
pub mod compare;
|
||||
pub mod convert;
|
||||
|
||||
+6
-1
@@ -553,7 +553,12 @@ impl Scene {
|
||||
let _ = self.sync_displayed_annotation_context(handle);
|
||||
}
|
||||
let parameter_changes =
|
||||
super::block_visibility::reframe(&mut self.document, block_record, local_from_old);
|
||||
super::block_visibility::reframe(&mut self.document, block_record, local_from_old)
|
||||
+ super::block_point_parameter::reframe(
|
||||
&mut self.document,
|
||||
block_record,
|
||||
local_from_old,
|
||||
);
|
||||
if !changed.is_empty() || parameter_changes > 0 {
|
||||
self.rebuild_derived_caches();
|
||||
}
|
||||
|
||||
@@ -978,6 +978,7 @@ pub fn selection_overlay<'a>(
|
||||
constraint_glyph_tooltip: Option<String>,
|
||||
constraint_cursor_badge: Option<String>,
|
||||
authoring_visibility: Option<(Point, String, iced::Rectangle)>,
|
||||
authoring_points: Vec<(Point, Point, String, iced::Rectangle)>,
|
||||
) -> Element<'a, Message> {
|
||||
canvas(SelectionCanvas {
|
||||
selection,
|
||||
@@ -1007,6 +1008,7 @@ pub fn selection_overlay<'a>(
|
||||
constraint_glyph_tooltip,
|
||||
constraint_cursor_badge,
|
||||
authoring_visibility,
|
||||
authoring_points,
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
@@ -1084,6 +1086,7 @@ struct SelectionCanvas {
|
||||
/// participates in an enabled geometric constraint.
|
||||
constraint_cursor_badge: Option<String>,
|
||||
authoring_visibility: Option<(Point, String, iced::Rectangle)>,
|
||||
authoring_points: Vec<(Point, Point, String, iced::Rectangle)>,
|
||||
}
|
||||
|
||||
/// The move gizmo: an arrow per axis (X red, Y green, Z blue) from the
|
||||
@@ -2325,6 +2328,29 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||
});
|
||||
});
|
||||
}
|
||||
for (point, label_point, label, clip) in &self.authoring_points {
|
||||
frame.with_clip(*clip, |frame| {
|
||||
let color = theme.palette().primary.base.color;
|
||||
frame.stroke(
|
||||
&canvas::Path::line(*point, *label_point),
|
||||
canvas::Stroke::default().with_color(color),
|
||||
);
|
||||
frame.fill(
|
||||
&canvas::Path::rectangle(
|
||||
Point::new(point.x - 3.0, point.y - 3.0),
|
||||
iced::Size::new(6.0, 6.0),
|
||||
),
|
||||
color,
|
||||
);
|
||||
frame.fill_text(canvas::Text {
|
||||
content: label.clone(),
|
||||
position: Point::new(label_point.x + 8.0, label_point.y - 9.0),
|
||||
color,
|
||||
size: iced::Pixels(13.0),
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
}
|
||||
// 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.
|
||||
|
||||
@@ -188,7 +188,11 @@ const PARAMETER_SETS: &[(&str, &[u8])] = &[
|
||||
];
|
||||
|
||||
/// Catalog rows remain inert until their authoring commands exist.
|
||||
fn catalog(tab: Tab, visibility_error: Option<&'static str>) -> Element<'static, Message> {
|
||||
fn catalog(
|
||||
tab: Tab,
|
||||
visibility_error: Option<&'static str>,
|
||||
point_error: Option<&'static str>,
|
||||
) -> Element<'static, Message> {
|
||||
let (entries, unavailable) = match tab {
|
||||
Tab::Parameters => (
|
||||
PARAMETERS,
|
||||
@@ -203,7 +207,9 @@ fn catalog(tab: Tab, visibility_error: Option<&'static str>) -> Element<'static,
|
||||
};
|
||||
column(entries.iter().map(|(label, icon)| {
|
||||
let visibility = tab == Tab::Parameters && *label == "Visibility";
|
||||
let enabled = visibility && visibility_error.is_none();
|
||||
let point = tab == Tab::Parameters && *label == "Point";
|
||||
let enabled =
|
||||
(visibility && visibility_error.is_none()) || (point && point_error.is_none());
|
||||
let base = if enabled {
|
||||
icons::semantic(icon, 28.0)
|
||||
} else {
|
||||
@@ -226,19 +232,36 @@ fn catalog(tab: Tab, visibility_error: Option<&'static str>) -> Element<'static,
|
||||
base
|
||||
};
|
||||
let message = enabled.then(|| Message::RibbonToolClick {
|
||||
tool_id: "BPARAMETER_VISIBILITY".into(),
|
||||
event: crate::modules::ModuleEvent::Command("BPARAMETER VISIBILITY".into()),
|
||||
tool_id: if point {
|
||||
"BPARAMETER_POINT"
|
||||
} else {
|
||||
"BPARAMETER_VISIBILITY"
|
||||
}
|
||||
.into(),
|
||||
event: crate::modules::ModuleEvent::Command(
|
||||
if point {
|
||||
"BPARAMETER POINT"
|
||||
} else {
|
||||
"BPARAMETER VISIBILITY"
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
});
|
||||
let description = if visibility {
|
||||
crate::t!(visibility_error
|
||||
.unwrap_or("Place a visibility parameter, then use BVSTATE to manage its states."))
|
||||
.into_owned()
|
||||
} else if point {
|
||||
crate::t!(point_error.unwrap_or("Place a point parameter and its position label."))
|
||||
.into_owned()
|
||||
} else {
|
||||
unavailable.to_string()
|
||||
};
|
||||
let entry = tool_row(label, icon, message, &description);
|
||||
if visibility {
|
||||
container(entry).id("authoring-tool-visibility").into()
|
||||
} else if point {
|
||||
container(entry).id("authoring-tool-point").into()
|
||||
} else {
|
||||
entry
|
||||
}
|
||||
@@ -350,6 +373,7 @@ pub fn view(
|
||||
width: f32,
|
||||
chrome: Chrome,
|
||||
visibility_error: Option<&'static str>,
|
||||
point_error: Option<&'static str>,
|
||||
) -> Element<'static, Message> {
|
||||
let tabs = column(Tab::ALL.into_iter().map(|category| {
|
||||
let label = category.label();
|
||||
@@ -399,7 +423,7 @@ pub fn view(
|
||||
chrome,
|
||||
),
|
||||
row![
|
||||
scrollable(catalog(selected_tab, visibility_error))
|
||||
scrollable(catalog(selected_tab, visibility_error, point_error))
|
||||
.width(Fill)
|
||||
.height(Fill),
|
||||
tabs
|
||||
|
||||
Reference in new issue
Block a user