From 5c931e926bafc5a3f6f4ffb58cbd030fd2df4dc2 Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Sun, 18 Oct 2026 00:13:33 +0100 Subject: [PATCH] feat: evaluate polar Lookup angle inputs Map UpdatedAngle columns to polar parameter endpoints and verify row-driven geometry materialization. --- src/scene/block_lookup.rs | 141 ++++++++++++++++++++++---------- src/scene/block_lookup/tests.rs | 70 ++++++++++++++++ 2 files changed, 166 insertions(+), 45 deletions(-) diff --git a/src/scene/block_lookup.rs b/src/scene/block_lookup.rs index d4f192b47..05dec8630 100644 --- a/src/scene/block_lookup.rs +++ b/src/scene/block_lookup.rs @@ -154,30 +154,44 @@ pub(crate) struct Property { pub column: BlockLookupColumn, pub value: String, } -/// These distance bindings are verified against the supplied native tables. +/// These distance and angle bindings match the native Lookup connection names. 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) + .flat_map(|p| { + let node_id = p.native.parameter.element.eval.node_id; + let distance = (p.native.definition_end_point - p.native.definition_base_point) .length() - .to_string(), + .to_string(); + let mut properties = vec![Property { + label: p.labels[0].clone(), + column: BlockLookupColumn { + node_id, + value_type: 40, + property_type: 2, + connection_name: "UpdatedDistance".into(), + unmatched_name: "Custom".into(), + ..Default::default() + }, + value: distance, + }]; + if p.kind == super::block_distance_parameter::Kind::Polar { + let delta = p.native.definition_end_point - p.native.definition_base_point; + let angle = delta.y.atan2(delta.x).rem_euclid(std::f64::consts::TAU); + properties.push(Property { + label: p.labels[1].clone(), + column: BlockLookupColumn { + node_id, + value_type: 40, + property_type: 2, + connection_name: "UpdatedAngle".into(), + unmatched_name: "Custom".into(), + ..Default::default() + }, + value: angle.to_string(), + }); + } + properties }) .collect() } @@ -226,17 +240,21 @@ fn reference_row( if property.lookup_property { return true; } - if property.value_type != 40 - || !property - .connection_name - .eq_ignore_ascii_case("UpdatedDistance") - { + if property.value_type != 40 { return false; } let Some(parameter) = params.iter().find(|p| p.id == property.node_id) else { return false; }; - if !(parameter.linear || parameter.polar) { + let is_distance = property + .connection_name + .eq_ignore_ascii_case("UpdatedDistance"); + let is_angle = property + .connection_name + .eq_ignore_ascii_case("UpdatedAngle"); + if !(is_distance && (parameter.linear || parameter.polar) + || is_angle && parameter.polar) + { return false; } let Ok(endpoint) = super::block_actions::instance::current(document, insert, parameter) @@ -250,10 +268,22 @@ fn reference_row( else { return false; }; - let actual = endpoint.distance(parameter.base); + let actual = if is_angle { + (endpoint.y - parameter.base.y) + .atan2(endpoint.x - parameter.base.x) + .rem_euclid(std::f64::consts::TAU) + } else { + endpoint.distance(parameter.base) + }; + let difference = if is_angle { + (actual - expected + std::f64::consts::PI).rem_euclid(std::f64::consts::TAU) + - std::f64::consts::PI + } else { + actual - expected + }; actual.is_finite() && expected.is_finite() - && (actual - expected).abs() <= 1e-8 * actual.abs().max(expected.abs()).max(1.0) + && difference.abs() <= 1e-8 * actual.abs().max(expected.abs()).max(1.0) }) }) } @@ -334,41 +364,62 @@ pub(crate) fn evaluate_reference_value( let parameters = super::block_actions::all_parameters(document, definition); let mut evaluated = document.clone(); + let mut endpoints = std::collections::BTreeMap::new(); for (column, property) in table.columns.iter().enumerate() { if property.lookup_property { continue; } - if property.value_type != 40 - || !property - .connection_name - .eq_ignore_ascii_case("UpdatedDistance") - { + if property.value_type != 40 { return Err("This Lookup input property is unsupported."); } let parameter = parameters .iter() .find(|parameter| parameter.id == property.node_id) .ok_or("A Lookup input property is unavailable.")?; - if !(parameter.linear || parameter.polar) { + let is_distance = property + .connection_name + .eq_ignore_ascii_case("UpdatedDistance"); + let is_angle = property + .connection_name + .eq_ignore_ascii_case("UpdatedAngle"); + if !(is_distance && (parameter.linear || parameter.polar) || is_angle && parameter.polar) { return Err("This Lookup input property is unsupported."); } - let distance = table + let value = 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), - )?; + .filter(|value| value.is_finite() && (!is_distance || *value > 1e-9)) + .ok_or("The selected Lookup row has an invalid parameter value.")?; + let endpoint = if let Some(endpoint) = endpoints.get_mut(¶meter.id) { + endpoint + } else { + let current = super::block_actions::instance::current(document, insert, parameter)?; + endpoints.entry(parameter.id).or_insert(current) + }; + let delta = *endpoint - parameter.base; + if is_distance { + let axis = delta + .try_normalize() + .or_else(|| (parameter.end - parameter.base).try_normalize()) + .ok_or("A Lookup input parameter has no direction.")?; + *endpoint = parameter.base + axis * value; + } else { + let radius = delta.length(); + *endpoint = parameter.base + glam::DVec3::new(value.cos(), value.sin(), 0.0) * radius; + } + } + for parameter in ¶meters { + if let Some(endpoint) = endpoints.get(¶meter.id) { + super::block_actions::set_value( + &mut evaluated, + parameter, + super::block_actions::Value::Point(*endpoint), + )?; + } } super::block_actions::rebuild(&mut evaluated, document, definition)?; let mut result = diff --git a/src/scene/block_lookup/tests.rs b/src/scene/block_lookup/tests.rs index 730232a82..3e7da88e1 100644 --- a/src/scene/block_lookup/tests.rs +++ b/src/scene/block_lookup/tests.rs @@ -301,6 +301,76 @@ fn lookup_reverse_selection_evaluates_saved_reference_geometry() { assert_eq!(unsupported, before); } +#[test] +fn lookup_updated_angle_rotates_polar_parameter_and_materializes_action_geometry() { + let (mut document, definition) = fixture(); + let line = document.block_records.get("LOOKUP").unwrap().entity_handles[0]; + let parameter = distance::create( + &mut document, + definition, + distance::Kind::Polar, + Vector3::ZERO, + Vector3::new(2.0, 0.0, 0.0), + Vector3::UNIT_Y, + ) + .unwrap(); + crate::scene::block_actions::create( + &mut document, + definition, + crate::scene::block_actions::Kind::Move, + parameter, + &[line], + glam::DVec3::ZERO, + ) + .unwrap(); + let lookup = create(&mut document, definition, Vector3::ZERO).unwrap(); + let lookup_parameter = parameters(&document, definition) + .into_iter() + .find(|(handle, _)| *handle == lookup) + .unwrap() + .1; + let polar_properties = properties(&document, definition); + assert_eq!(polar_properties.len(), 2); + assert_eq!(polar_properties[1].column.connection_name, "UpdatedAngle"); + let mut table = empty_table(lookup_parameter); + table.columns.insert(0, polar_properties[0].column.clone()); + table.columns.insert(1, polar_properties[1].column.clone()); + table.column_count = 3; + table.row_count = 2; + table.expressions = vec![ + "2".into(), + "0".into(), + "Small".into(), + "4".into(), + std::f64::consts::FRAC_PI_2.to_string(), + "Large".into(), + ]; + apply_table(&mut document, definition, lookup, table).unwrap(); + let insert = document + .add_entity(EntityType::Insert(Insert::new("LOOKUP", Vector3::ZERO))) + .unwrap(); + + let evaluated = evaluate_reference_choice(&document, insert, "Large") + .unwrap() + .unwrap(); + 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!((line.start - Vector3::new(-2.0, 4.0, 0.0)).length() < 1e-9); + assert!((line.end - Vector3::new(-1.0, 4.0, 0.0)).length() < 1e-9); + assert!(evaluate_reference_choice(&evaluated, insert, "Large") + .unwrap() + .is_none()); +} + #[test] fn lookup_existing_expressions_survive_row_reordering_and_output_edits() { let (mut document, definition) = fixture();