feat: apply Lookup rows to block references

Expose writable Lookup outputs as reference choices and materialize each selected row's parameter geometry.
This commit is contained in:
Karim Jerbi committed 2026-10-17 23:57:58 +01:00
1 parent ba457d693c
commit 86c0e05878
5 files changed
+296 -8

No files matched your search

+14 -5
View File
@@ -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",
+26
View File
@@ -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
+2 -2
View File
@@ -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,
+188 -1
View File
@@ -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<usize> {
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::<f64>().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<crate::scene::model::object::Property> {
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::<Vec<_>>();
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<Option<CadDocument>, &'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::<f64>()
.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, &parameter)?;
super::block_actions::instance::save(&mut result, insert, &parameter, value)?;
}
Ok(Some(result))
}
pub(crate) fn evaluate_reference_choice(
document: &CadDocument,
insert: Handle,
selected: &str,
) -> Result<Option<CadDocument>, &'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,
+66
View File
@@ -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();