//! Native Lookup parameter and property-table authoring. use super::block_authoring::graph; use super::block_authoring::{self as authoring, *}; use codec::objects::{ BlockAction, BlockElement, BlockEvalValue, BlockGrip, BlockGripExpression, BlockLookupAction, BlockLookupColumn, BlockLookupParameter, BlockOnePointParameter, BlockParameter, DynamicBlockData, ObjectType, }; use codec::{ types::{Transform, Vector3}, CadDocument, Handle, }; #[cfg(test)] mod tests; fn graph_handle(document: &CadDocument, definition: Handle) -> Option { let ObjectType::Dictionary(d) = document .objects .get(&document.extension_dictionary_handle(definition)?)? else { return None; }; d.get("ACAD_ENHANCEDBLOCK") } pub(crate) fn parameters( document: &CadDocument, definition: Handle, ) -> Vec<(Handle, &BlockLookupParameter)> { let Some(graph) = graph_handle(document, definition) else { return vec![]; }; let Some(ObjectType::DynamicBlock(o)) = document.objects.get(&graph) else { return vec![]; }; let DynamicBlockData::EvaluationGraph(g) = &o.data else { return vec![]; }; g.nodes .iter() .filter_map(|n| match document.objects.get(&n.expression) { Some(ObjectType::DynamicBlock(o)) if o.owner == graph => match &o.data { DynamicBlockData::LookupParameter(p) => Some((o.handle, p)), _ => 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, ) -> Result { if ![location.x, location.y, location.z] .iter() .all(|v| v.is_finite()) { return Err("Specify a finite parameter location."); } let names = parameters(document, definition) .iter() .map(|(_, p)| p.lookup_name.clone()) .collect::>(); let name = (1..) .map(|n| format!("Lookup{n}")) .find(|s| names.iter().all(|n| !n.eq_ignore_ascii_case(s))) .unwrap(); let (graph, slots) = authoring::prepare(document, definition)?; register_classes( document, &[ ("BLOCKLOOKUPPARAMETER", "AcDbBlockLookUpParameter", 55), ("BLOCKLOOKUPGRIP", "AcDbBlockLookUpGrip", 55), ("BLOCKGRIPLOCATIONCOMPONENT", "AcDbBlockGripExpr", 20), ], ); let handles = [(); 4].map(|_| document.allocate_handle()); let element = |offset, name: &str| BlockElement { eval: expression(slots.eval + offset, NO_VALUE, BlockEvalValue::None), name: name.into(), major: MAJOR, minor: MINOR, eed_1071: 0, }; let parameter = BlockLookupParameter { parameter: BlockOnePointParameter { parameter: BlockParameter { element: element(0, "Lookup"), show_properties: true, chain_actions: false, }, definition_point: location, property_count: slots.eval + 1, ..Default::default() }, lookup_name: name, ..Default::default() }; document.objects.insert( handles[0], ObjectType::DynamicBlock(dynamic( handles[0], graph, "BLOCKLOOKUPPARAMETER", "AcDbBlockLookUpParameter", DynamicBlockData::LookupParameter(parameter), )), ); let grip = BlockGrip { element: element(1, "Grip"), flags_91: slots.eval + 2, flags_92: slots.eval + 3, location, insert_cycling: false, insert_cycling_weight: -1, }; document.objects.insert( handles[1], ObjectType::DynamicBlock(dynamic( handles[1], graph, "BLOCKLOOKUPGRIP", "AcDbBlockLookUpGrip", DynamicBlockData::LookupGrip(grip), )), ); for (offset, axis) in [(2, "UpdatedX"), (3, "UpdatedY")] { let h = handles[offset as usize]; document.objects.insert( h, ObjectType::DynamicBlock(dynamic( h, 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, handles); Ok(handles[0]) } #[derive(Clone, Debug, PartialEq)] pub(crate) struct Property { pub label: String, pub column: BlockLookupColumn, pub value: String, } /// These distance bindings are verified against the supplied native tables. pub(crate) fn properties(document: &CadDocument, definition: Handle) -> Vec { super::block_distance_parameter::parameters(document, definition) .iter() .filter(|p| { matches!( p.kind, super::block_distance_parameter::Kind::Linear | super::block_distance_parameter::Kind::Polar ) }) .map(|p| Property { label: p.labels[0].clone(), column: BlockLookupColumn { node_id: p.native.parameter.element.eval.node_id, value_type: 40, property_type: 2, connection_name: "UpdatedDistance".into(), unmatched_name: "Custom".into(), ..Default::default() }, value: (p.native.definition_end_point - p.native.definition_base_point) .length() .to_string(), }) .collect() } pub(crate) fn table( document: &CadDocument, definition: Handle, parameter: Handle, ) -> Result, &'static str> { let p = parameters(document, definition) .into_iter() .find(|(h, _)| *h == parameter) .ok_or("The Lookup parameter is no longer available.")? .1; if p.index == 0 { return Ok(None); } let graph = graph_handle(document, definition).ok_or("The authoring graph is unavailable.")?; document .objects .values() .find_map(|o| match o { ObjectType::DynamicBlock(o) if o.owner == graph => match &o.data { DynamicBlockData::LookupAction(a) if a.action.element.eval.node_id == p.index => { Some((o.handle, a.clone())) } _ => None, }, _ => None, }) .map(Some) .ok_or("The Lookup action cannot be edited.") } /// Resolve the active row for a saved reference from its numeric input values. fn reference_row( document: &CadDocument, insert: Handle, definition: Handle, table: &BlockLookupAction, ) -> Option { let params = super::block_actions::all_parameters(document, definition); let rows = usize::try_from(table.row_count).ok()?; let columns = table.columns.len(); (0..rows).find(|row| { table.columns.iter().enumerate().all(|(column, property)| { if property.lookup_property { return true; } let Some(parameter) = params.iter().find(|p| p.id == property.node_id) else { return false; }; if !(parameter.linear || parameter.polar) { return false; } let Ok(endpoint) = super::block_actions::instance::current(document, insert, parameter) else { return false; }; let Some(expected) = table .expressions .get(row * columns + column) .and_then(|value| value.parse::().ok()) else { return false; }; let actual = endpoint.distance(parameter.base); actual.is_finite() && expected.is_finite() && (actual - expected).abs() <= 1e-8 * actual.abs().max(expected.abs()).max(1.0) }) }) } /// A writable Lookup output as a per-reference property choice. pub(crate) fn reference_property( document: &CadDocument, insert: Handle, ) -> Option { let definition = super::block_actions::instance::definition(document, insert)?; for (parameter, lookup) in parameters(document, definition) { let Some((_, table)) = table(document, definition, parameter).ok().flatten() else { continue; }; let Some((column, _)) = table .columns .iter() .enumerate() .find(|(_, column)| column.lookup_property && column.writable) else { continue; }; let rows = usize::try_from(table.row_count).ok()?; let options = (0..rows) .filter_map(|row| table.expressions.get(row * table.columns.len() + column)) .map(|value| value.trim().to_string()) .collect::>(); if options.is_empty() { continue; } let selected = reference_row(document, insert, definition, &table) .and_then(|row| table.expressions.get(row * table.columns.len() + column)) .cloned() .unwrap_or_default(); return Some(crate::scene::model::object::Property { label: lookup.lookup_name.clone(), field: "dyn_lookup", value: crate::scene::model::object::PropValue::Choice { selected, options }, }); } None } /// Apply a writable Lookup row to a saved reference and materialize its geometry. pub(crate) fn evaluate_reference_value( document: &CadDocument, insert: Handle, lookup_parameter: Handle, selected: &str, ) -> Result, &'static str> { let definition = super::block_actions::instance::definition(document, insert) .ok_or("Select a uniformly scaled dynamic block reference.")?; let _lookup = parameters(document, definition) .into_iter() .find(|(handle, _)| *handle == lookup_parameter) .ok_or("The Lookup parameter is unavailable.")?; let (_, table) = table(document, definition, lookup_parameter)?.ok_or("The Lookup table is unavailable.")?; let (output_column, _) = table .columns .iter() .enumerate() .find(|(_, column)| column.lookup_property && column.writable) .ok_or("The Lookup table has no writable output.")?; let rows = usize::try_from(table.row_count).map_err(|_| "The Lookup table is invalid.")?; let columns = table.columns.len(); let row = (0..rows) .find(|row| { table .expressions .get(row * columns + output_column) .is_some_and(|value| value.trim().eq_ignore_ascii_case(selected.trim())) }) .ok_or("Choose a value from the Lookup table.")?; if reference_row(document, insert, definition, &table) == Some(row) { return Ok(None); } let parameters = super::block_actions::all_parameters(document, definition); let mut evaluated = document.clone(); for (column, property) in table.columns.iter().enumerate() { if property.lookup_property { continue; } let parameter = parameters .iter() .find(|parameter| parameter.id == property.node_id) .ok_or("A Lookup input property is unavailable.")?; if !(parameter.linear || parameter.polar) { return Err("This Lookup input property is unsupported."); } let distance = table .expressions .get(row * columns + column) .ok_or("The selected Lookup row is incomplete.")? .trim() .parse::() .ok() .filter(|value| value.is_finite() && *value > 1e-9) .ok_or("The selected Lookup row has an invalid distance.")?; let axis = (parameter.end - parameter.base) .try_normalize() .ok_or("A Lookup input parameter has no direction.")?; super::block_actions::set_value( &mut evaluated, parameter, super::block_actions::Value::Point(parameter.base + axis * distance), )?; } super::block_actions::rebuild(&mut evaluated, document, definition)?; let mut result = super::block_actions::instance::materialize(document, insert, definition, &evaluated)?; for parameter in parameters { let value = super::block_actions::value(&evaluated, ¶meter)?; super::block_actions::instance::save(&mut result, insert, ¶meter, value)?; } Ok(Some(result)) } pub(crate) fn evaluate_reference_choice( document: &CadDocument, insert: Handle, selected: &str, ) -> Result, &'static str> { let definition = super::block_actions::instance::definition(document, insert) .ok_or("Select a uniformly scaled dynamic block reference.")?; for (handle, _) in parameters(document, definition) { let Ok(Some((_, table))) = table(document, definition, handle) else { continue; }; let Some((column, _)) = table .columns .iter() .enumerate() .find(|(_, column)| column.lookup_property && column.writable) else { continue; }; let rows = usize::try_from(table.row_count).unwrap_or_default(); if (0..rows).any(|row| { table .expressions .get(row * table.columns.len() + column) .is_some_and(|value| value.trim().eq_ignore_ascii_case(selected.trim())) }) { return evaluate_reference_value(document, insert, handle, selected); } } Err("Choose a value from the Lookup table.") } pub(crate) fn empty_table(parameter: &BlockLookupParameter) -> BlockLookupAction { BlockLookupAction { column_count: 1, columns: vec![BlockLookupColumn { node_id: parameter.parameter.parameter.element.eval.node_id, value_type: 1, property_type: 0, lookup_property: true, writable: true, connection_name: "lookupString".into(), unmatched_name: "Custom".into(), }], flag_280: true, ..Default::default() } } pub(crate) fn validate( table: &BlockLookupAction, original: Option<&BlockLookupAction>, ) -> Result<(), String> { let rows = usize::try_from(table.row_count).map_err(|_| "Invalid table size.")?; let cols = table.columns.len(); if rows == 0 || rows > 1000 || cols < 2 || cols > 64 || table.column_count != cols as i32 || table.expressions.len() != rows * cols { return Err("Add at least one input property and one complete row.".into()); } if !table.columns.iter().any(|c| !c.lookup_property) || !table.columns.iter().any(|c| c.lookup_property) { return Err("A table needs input and lookup properties.".into()); } let mut inputs = std::collections::BTreeSet::new(); let mut outputs = vec![std::collections::BTreeSet::new(); cols]; for row in 0..rows { let mut key = vec![]; for (col, property) in table.columns.iter().enumerate() { let cell = &table.expressions[row * cols + col]; if cell.trim().is_empty() || cell.chars().any(char::is_control) { return Err("Complete each cell without control characters.".into()); } if property.lookup_property { if property.writable && !outputs[col].insert(cell.trim().to_lowercase()) { return Err("Reverse lookup requires unique lookup values.".into()); } } else { let numeric = cell.trim().parse::().ok().filter(|v| v.is_finite()); if property.value_type == 40 && numeric.is_none() && !original.is_some_and(|old| { old.columns.iter().enumerate().any(|(index, column)| { column == property && old .expressions .iter() .skip(index) .step_by(old.columns.len()) .any(|value| value == cell) }) }) { return Err("Enter finite decimal input values. Existing expressions can be retained unchanged.".into()); } key.push( numeric .map(|v| if v == 0.0 { "0".into() } else { v.to_string() }) .unwrap_or_else(|| cell.trim().to_lowercase()), ); } } if !inputs.insert(key) { return Err("Each input-property row must be unique.".into()); } } Ok(()) } pub(crate) fn apply_table( document: &mut CadDocument, definition: Handle, parameter: Handle, edited: BlockLookupAction, ) -> Result { let mut working = document.clone(); let handle = apply_table_inner(&mut working, definition, parameter, edited)?; *document = working; Ok(handle) } fn apply_table_inner( document: &mut CadDocument, definition: Handle, parameter: Handle, mut edited: BlockLookupAction, ) -> Result { let previous = table(document, definition, parameter).map_err(str::to_string)?; validate(&edited, previous.as_ref().map(|(_, t)| t))?; let (graph, slots) = authoring::prepare(document, definition).map_err(str::to_string)?; let handle = if let Some((handle, original)) = &previous { edited.action = original.action.clone(); *handle } else { register_classes( document, &[("BLOCKLOOKUPACTION", "AcDbBlockLookupAction", 55)], ); let h = document.allocate_handle(); let p = parameters(document, definition) .into_iter() .find(|(h, _)| *h == parameter) .unwrap() .1; edited.action = BlockAction { element: BlockElement { eval: expression(slots.eval, NO_VALUE, BlockEvalValue::None), name: p.lookup_name.clone(), major: MAJOR, minor: MINOR, eed_1071: 0, }, display_location: p.parameter.definition_point, ..Default::default() }; authoring::append_single(document, graph, slots, h); document.objects.insert( h, ObjectType::DynamicBlock(dynamic( h, graph, "BLOCKLOOKUPACTION", "AcDbBlockLookupAction", DynamicBlockData::LookupAction(edited.clone()), )), ); h }; let node_id = edited.action.element.eval.node_id; graph::bind_ids( document, graph, node_id, &edited.columns.iter().map(|c| c.node_id).collect::>(), ) .map_err(str::to_string)?; if let Some(ObjectType::DynamicBlock(o)) = document.objects.get_mut(&handle) { o.data = DynamicBlockData::LookupAction(edited); } if let Some(ObjectType::DynamicBlock(o)) = document.objects.get_mut(¶meter) { if let DynamicBlockData::LookupParameter(p) = &mut o.data { p.index = node_id; } } Ok(handle) } 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(|(h, p)| (*h, p.parameter.property_count, p.index)) .collect::>(); let mut changed = 0; for (handle, grip_id, action_id) in owned { if let Some(ObjectType::DynamicBlock(o)) = document.objects.get_mut(&handle) { if let DynamicBlockData::LookupParameter(p) = &mut o.data { p.parameter.definition_point = transform.apply(p.parameter.definition_point); changed += 1; } } for o in document.objects.values_mut() { if let ObjectType::DynamicBlock(o) = o { if o.owner != graph { continue; } match &mut o.data { DynamicBlockData::LookupGrip(g) if g.element.eval.node_id == grip_id => { g.location = transform.apply(g.location); changed += 1; } DynamicBlockData::LookupAction(a) if action_id > 0 && a.action.element.eval.node_id == action_id => { a.action.display_location = transform.apply(a.action.display_location); changed += 1; } _ => {} } } } } changed }