forked from 1b-agent/opencadstudio-upstream
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:
1 parent
ba457d693c
commit
86c0e05878
5 files changed
+296
-8
No files matched your search
+14
-5
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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, ¶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<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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user