diff --git a/src/app/block_editor_tests.rs b/src/app/block_editor_tests.rs index 3fab2b3ce..72297c88b 100644 --- a/src/app/block_editor_tests.rs +++ b/src/app/block_editor_tests.rs @@ -59,6 +59,145 @@ fn feed(app: &mut OpenCADStudio, text: &str) { let _ = app.update(Message::CommandSubmit); } +fn authored_points(app: &OpenCADStudio) -> Vec { + 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) { diff --git a/src/app/command_driver/mod.rs b/src/app/command_driver/mod.rs index bc3596097..0f3dfe0cd 100644 --- a/src/app/command_driver/mod.rs +++ b/src/app/command_driver/mod.rs @@ -575,6 +575,9 @@ impl OpenCADStudio { return task; } } + result @ CmdResult::PlaceBlockPointParameter { .. } => { + return self.handle_place_block_point_parameter(result); + } CmdResult::PlaceBlockVisibilityParameter { document_id, definition, diff --git a/src/app/commands/bvisibility.rs b/src/app/commands/bvisibility.rs index b8d56d268..775422ebd 100644 --- a/src/app/commands/bvisibility.rs +++ b/src/app/commands/bvisibility.rs @@ -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 { + 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()); } diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index 274ecad0c..f5d861b32 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -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(); } diff --git a/src/app/dock/tests.rs b/src/app/dock/tests.rs index eeae91bcf..37d3b11f3 100644 --- a/src/app/dock/tests.rs +++ b/src/app/dock/tests.rs @@ -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}, diff --git a/src/app/dock/view.rs b/src/app/dock/view.rs index 1aa05e381..49f271e1c 100644 --- a/src/app/dock/view.rs +++ b/src/app/dock/view.rs @@ -407,6 +407,7 @@ impl OpenCADStudio { width, chrome, self.visibility_placement_error(), + self.point_parameter_placement_error(), ) } crate::ui::dock::PanelId::BlockPalette => { diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 0ea5b1e30..b2b03600e 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -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, ) }; diff --git a/src/command.rs b/src/command.rs index 7f08ee1ed..f75018b5f 100644 --- a/src/command.rs +++ b/src/command.rs @@ -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, diff --git a/src/modules/block_editor/mod.rs b/src/modules/block_editor/mod.rs index c45fcf76f..4dba9ac87 100644 --- a/src/modules/block_editor/mod.rs +++ b/src/modules/block_editor/mod.rs @@ -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; diff --git a/src/modules/block_editor/point_parameter.rs b/src/modules/block_editor/point_parameter.rs new file mode 100644 index 000000000..c17b88be5 --- /dev/null +++ b/src/modules/block_editor/point_parameter.rs @@ -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, +} + +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 { + 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 + } +} diff --git a/src/modules/block_editor/visibility_parameter.rs b/src/modules/block_editor/visibility_parameter.rs index abb162afc..ab6a44bca 100644 --- a/src/modules/block_editor/visibility_parameter.rs +++ b/src/modules/block_editor/visibility_parameter.rs @@ -27,7 +27,8 @@ impl CadCommand for VisibilityParameterCommand { fn prompt(&self) -> String { if self.choosing_type { - crate::t!("BPARAMETER Enter parameter type [Visibility] :").into_owned() + crate::t!("BPARAMETER Enter parameter type [Point/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 { 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(), ), }, ) diff --git a/src/scene/block_authoring.rs b/src/scene/block_authoring.rs new file mode 100644 index 000000000..16f499f9e --- /dev/null +++ b/src/scene/block_authoring.rs @@ -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, &'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 { + 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 { + 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 { + 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] + ) +} diff --git a/src/scene/block_point_parameter.rs b/src/scene/block_point_parameter.rs new file mode 100644 index 000000000..5c9b546b4 --- /dev/null +++ b/src/scene/block_point_parameter.rs @@ -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 { + 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 { + 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::>(); + 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::>(); + 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::>(); + 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::>() + ) + }) + } 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); + } +} diff --git a/src/scene/block_visibility.rs b/src/scene/block_visibility.rs index 321237760..abcbdd27e 100644 --- a/src/scene/block_visibility.rs +++ b/src/scene/block_visibility.rs @@ -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 { - 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 { - 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) } diff --git a/src/scene/camera_ops.rs b/src/scene/camera_ops.rs index 82e78a364..0cf06db9b 100644 --- a/src/scene/camera_ops.rs +++ b/src/scene/camera_ops.rs @@ -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, diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 1d2e3c1d6..a34e7713c 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -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; diff --git a/src/scene/modify.rs b/src/scene/modify.rs index e58e8a401..2e30b3dcc 100644 --- a/src/scene/modify.rs +++ b/src/scene/modify.rs @@ -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(); } diff --git a/src/ui/overlay.rs b/src/ui/overlay.rs index 7f311e5a4..87966498b 100644 --- a/src/ui/overlay.rs +++ b/src/ui/overlay.rs @@ -978,6 +978,7 @@ pub fn selection_overlay<'a>( constraint_glyph_tooltip: Option, constraint_cursor_badge: Option, 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, 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 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. diff --git a/src/ui/window/authoring_palettes.rs b/src/ui/window/authoring_palettes.rs index 461043c40..00d28ff2a 100644 --- a/src/ui/window/authoring_palettes.rs +++ b/src/ui/window/authoring_palettes.rs @@ -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