From 86c0e05878b3d8b4da77a286024a3547d6949433 Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Sat, 17 Oct 2026 23:57:58 +0100 Subject: [PATCH] feat: apply Lookup rows to block references Expose writable Lookup outputs as reference choices and materialize each selected row's parameter geometry. --- src/app/properties.rs | 19 ++- src/app/update/command.rs | 26 ++++ src/scene/block_actions/instance.rs | 4 +- src/scene/block_lookup.rs | 189 +++++++++++++++++++++++++++- src/scene/block_lookup/tests.rs | 66 ++++++++++ 5 files changed, 296 insertions(+), 8 deletions(-) diff --git a/src/app/properties.rs b/src/app/properties.rs index a33679064..8f2dda280 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -915,10 +915,15 @@ impl OpenCADStudio { xref_rows(&mut sections, ins, &br.xref_path, factor, overrides); } // A dynamic block's visibility states, picked like its grip. - if let Some(row) = Self::visibility_property(doc, handle) { + let mut custom_properties = Vec::new(); + custom_properties.extend(Self::visibility_property(doc, handle)); + if let Some(row) = crate::scene::block_lookup::reference_property(doc, handle) { + custom_properties.push(row); + } + if !custom_properties.is_empty() { sections.push(crate::scene::model::object::PropSection { title: t!("Custom").into_owned(), - props: vec![row], + props: custom_properties, }); } // An associative array's counts and spacings. @@ -1601,8 +1606,9 @@ impl OpenCADStudio { // text derives its paper height from the two endpoints, // while annotative text exposes that paper height and a // separate calculated model height. - if let Some(factor) = - self.tabs[i].scene.annotative_text_height_factor(entity.common().handle) + if let Some(factor) = self.tabs[i] + .scene + .annotative_text_height_factor(entity.common().handle) { // Annotative text stores the model height of its default scale; // the Height row edits the paper height at the displayed scale. @@ -1633,7 +1639,10 @@ impl OpenCADStudio { for section in sections.iter_mut() { if let Some(row) = section.props.iter_mut().find(|row| row.field == "height") { let editable = !aligned - && !matches!(row.value, crate::scene::model::object::PropValue::ReadOnly(_)); + && !matches!( + row.value, + crate::scene::model::object::PropValue::ReadOnly(_) + ); *row = crate::entities::common::num_prop( t!("Paper text height").as_ref(), "height", diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 7eba0c6f9..5a9ac2adf 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -3408,6 +3408,32 @@ impl OpenCADStudio { self.apply_visibility_state(handle, state); } } + } else if field == "dyn_lookup" { + self.tabs[i].properties.edit_choice_open = false; + let mut changed = false; + for &handle in &handles { + if self.tabs[i].scene.is_layer_locked(handle) { + continue; + } + let result = crate::scene::block_lookup::evaluate_reference_choice( + &self.tabs[i].scene.document, + handle, + &value, + ); + match result { + Ok(Some(staged)) => { + self.apply_action_document(i, staged); + self.tabs[i].scene.bump_geometry(); + self.tabs[i].dirty = true; + changed = true; + } + Ok(None) => {} + Err(error) => self.command_line.push_error(crate::t!(error).as_ref()), + } + } + if changed { + self.finish_pending_history(i); + } } else if field == "block" { // Name dropdown on a block reference: re-point the // selected inserts to the picked definition. A stale diff --git a/src/scene/block_actions/instance.rs b/src/scene/block_actions/instance.rs index 31decd0fb..91f26b1eb 100644 --- a/src/scene/block_actions/instance.rs +++ b/src/scene/block_actions/instance.rs @@ -148,7 +148,7 @@ fn ensure_dictionary( Ok(h) } -fn save( +pub(crate) fn save( doc: &mut CadDocument, insert: Handle, p: &Parameter, @@ -352,7 +352,7 @@ pub(crate) fn refresh_after_edit( Ok(staged) } -fn materialize( +pub(crate) fn materialize( doc: &CadDocument, insert: Handle, def: Handle, diff --git a/src/scene/block_lookup.rs b/src/scene/block_lookup.rs index fa8c8fa1f..b9b11acf2 100644 --- a/src/scene/block_lookup.rs +++ b/src/scene/block_lookup.rs @@ -1,4 +1,5 @@ //! 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, @@ -9,7 +10,6 @@ use codec::{ types::{Transform, Vector3}, CadDocument, Handle, }; -use super::block_authoring::graph; #[cfg(test)] mod tests; @@ -210,6 +210,193 @@ pub(crate) fn table( .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, diff --git a/src/scene/block_lookup/tests.rs b/src/scene/block_lookup/tests.rs index f2585c7c5..b8b777350 100644 --- a/src/scene/block_lookup/tests.rs +++ b/src/scene/block_lookup/tests.rs @@ -227,6 +227,72 @@ fn lookup_malformed_dependencies_are_rejected_without_mutation() { assert_eq!(document, before); } +#[test] +fn lookup_reverse_selection_evaluates_saved_reference_geometry() { + let (mut document, definition) = fixture(); + let line = document.block_records.get("LOOKUP").unwrap().entity_handles[0]; + let distance = distance::create( + &mut document, + definition, + distance::Kind::Linear, + Vector3::ZERO, + Vector3::new(2., 0., 0.), + Vector3::UNIT_Y, + ) + .unwrap(); + crate::scene::block_actions::create( + &mut document, + definition, + crate::scene::block_actions::Kind::Move, + distance, + &[line], + glam::DVec3::ZERO, + ) + .unwrap(); + let lookup = create(&mut document, definition, Vector3::ZERO).unwrap(); + let lookup_table = draft(&document, definition, lookup); + apply_table(&mut document, definition, lookup, lookup_table).unwrap(); + let original_line = document.get_entity(line).unwrap().clone(); + let insert = document + .add_entity(EntityType::Insert(Insert::new("LOOKUP", Vector3::ZERO))) + .unwrap(); + + let initial = reference_property(&document, insert).unwrap(); + let crate::scene::model::object::PropValue::Choice { selected, options } = initial.value else { + panic!("Lookup property should be a choice.") + }; + assert_eq!(selected, "Small"); + assert_eq!(options, vec!["Small", "Large"]); + + let evaluated = evaluate_reference_choice(&document, insert, "Large") + .unwrap() + .unwrap(); + assert_eq!(document.get_entity(line), Some(&original_line)); + let EntityType::Insert(reference) = evaluated.get_entity(insert).unwrap() else { + panic!() + }; + let member = evaluated + .block_records + .get(&reference.block_name) + .unwrap() + .entity_handles[0]; + let EntityType::Line(line) = evaluated.get_entity(member).unwrap() else { + panic!() + }; + assert_eq!(line.start, Vector3::new(2., 0., 0.)); + assert_eq!(line.end, Vector3::new(3., 0., 0.)); + let selected = reference_property(&evaluated, insert).unwrap(); + let crate::scene::model::object::PropValue::Choice { selected, .. } = selected.value else { + panic!() + }; + assert_eq!(selected, "Large"); + assert!( + evaluate_reference_value(&evaluated, insert, lookup, "Large") + .unwrap() + .is_none() + ); +} + #[test] fn lookup_existing_expressions_survive_row_reordering_and_output_edits() { let (mut document, definition) = fixture();