//! Native Move/Scale/Stretch/Rotate/Flip/Array authoring and deterministic block evaluation. use super::block_authoring::{self as authoring, *}; use codec::objects::{ BlockAction, BlockActionOffsets, BlockActionWithBasePoint, BlockArrayAction, BlockBasePointAction, BlockConnection, BlockElement, BlockEvalValue, BlockFlipAction, BlockMoveAction, BlockStretchAction, DynamicBlockData, ObjectType, }; use codec::types::{Transform, Vector3}; use codec::{CadDocument, EntityType, Handle}; use glam::DVec3; pub(crate) mod instance; mod stretch; mod representation; #[cfg(test)] mod tests; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Kind { Move, Scale, Rotate, Flip, Stretch, Array, } impl Kind { pub(crate) fn parse(s: &str) -> Option { match s.to_ascii_uppercase().as_str() { "MOVE" | "M" => Some(Self::Move), "SCALE" | "S" => Some(Self::Scale), "ROTATE" | "R" => Some(Self::Rotate), "FLIP" | "F" => Some(Self::Flip), "STRETCH" | "ST" => Some(Self::Stretch), "ARRAY" | "A" => Some(Self::Array), _ => None, } } pub(crate) fn name(self) -> &'static str { match self { Self::Move => "Move", Self::Scale => "Scale", Self::Rotate => "Rotate", Self::Flip => "Flip", Self::Stretch => "Stretch", Self::Array => "Array", } } } #[derive(Clone, Debug)] pub(crate) struct Parameter { pub handle: Handle, pub id: i32, pub grip: Handle, pub name: String, pub base: DVec3, pub end: DVec3, pub linear: bool, pub point: bool, pub xy: bool, pub rotation: bool, pub flip: bool, pub reference: DVec3, } pub(crate) fn v(p: Vector3) -> DVec3 { DVec3::new(p.x, p.y, p.z) } pub(crate) fn native(p: DVec3) -> Vector3 { Vector3::new(p.x, p.y, p.z) } #[derive(Clone, Copy, Debug, PartialEq)] pub enum Value { Point(DVec3), Flip(bool), } pub(crate) fn all_parameters(doc: &CadDocument, definition: Handle) -> Vec { let mut result = parameters(doc, definition, Kind::Move); result.extend(parameters(doc, definition, Kind::Rotate)); result.extend(parameters(doc, definition, Kind::Flip)); result.sort_by_key(|p| p.id); result } pub(crate) fn compatible(p: &Parameter, kind: Kind) -> bool { match kind { Kind::Move => !p.rotation && !p.flip, Kind::Stretch => !p.rotation && !p.flip && !p.point, Kind::Scale => !p.rotation && !p.flip && !p.point && !p.xy, Kind::Rotate => p.rotation, Kind::Flip => p.flip, Kind::Array => !p.rotation && !p.flip && !p.point, } } fn flip_component(doc: &CadDocument, p: &Parameter) -> Result { let Some(ObjectType::DynamicBlock(o)) = doc.objects.get(&p.grip) else { return Err("Missing Flip grip."); }; let DynamicBlockData::FlipGrip(g) = &o.data else { return Err("Missing Flip grip."); }; let candidates = doc .objects .iter() .filter_map(|(h, x)| match x { ObjectType::DynamicBlock(x) if x.owner == o.owner => match &x.data { DynamicBlockData::GripLocationComponent(c) if c.eval.node_id == g.combined_state && c.expression == "UpdatedFlip" => { Some(*h) } _ => None, }, _ => None, }) .collect::>(); if candidates.len() != 1 { return Err("The Flip state binding is unavailable."); } Ok(candidates[0]) } pub(crate) fn value(doc: &CadDocument, p: &Parameter) -> Result { if !p.flip { return Ok(Value::Point(current(doc, p))); } let h = flip_component(doc, p)?; let ObjectType::DynamicBlock(o) = &doc.objects[&h] else { unreachable!() }; let DynamicBlockData::GripLocationComponent(c) = &o.data else { unreachable!() }; match c.eval.value { BlockEvalValue::Short(0) => Ok(Value::Flip(false)), BlockEvalValue::Short(1) => Ok(Value::Flip(true)), _ => Err("The Flip state is not zero or one."), } } pub(crate) fn set_value( doc: &mut CadDocument, p: &Parameter, value: Value, ) -> Result<(), &'static str> { if let Value::Flip(state) = value { if !p.flip { return Err("Select a Flip parameter."); } let h = flip_component(doc, p)?; let Some(ObjectType::DynamicBlock(o)) = doc.objects.get_mut(&h) else { unreachable!() }; let DynamicBlockData::GripLocationComponent(c) = &mut o.data else { unreachable!() }; c.eval.value = BlockEvalValue::Short(i16::from(state)); return Ok(()); } let Value::Point(position) = value else { unreachable!() }; let Some(ObjectType::DynamicBlock(o)) = doc.objects.get_mut(&p.grip) else { return Err("Missing parameter grip."); }; match &mut o.data { DynamicBlockData::XYGrip(g) | DynamicBlockData::PolarGrip(g) | DynamicBlockData::RotationGrip(g) => g.location = native(position), DynamicBlockData::LinearGrip(g) => g.grip.location = native(position), _ => return Err("Unsupported parameter grip."), } Ok(()) } pub(crate) fn grip_position(p: &Parameter, value: Value) -> DVec3 { match value { Value::Point(at) => at, Value::Flip(_) => p.reference, } } /// A Flip arrow points across its reflection line and reverses with its state. pub(crate) fn grip_direction(p: &Parameter, value: Value) -> Option { let Value::Flip(flipped) = value else { return None; }; let across = DVec3::Z.cross(p.end - p.base).try_normalize()?; Some(if flipped { -across } else { across }) } pub(crate) fn parameters(doc: &CadDocument, definition: Handle, kind: Kind) -> Vec { let Ok(Some(graph)) = authoring::graph(doc, definition) else { return vec![]; }; let mut result = vec![]; for (handle, p) in super::block_point_parameter::parameters(doc, definition) { if kind == Kind::Move { result.push(Parameter { handle, id: p.parameter.parameter.element.eval.node_id, grip: Handle::NULL, name: p.position_name.clone(), base: v(p.parameter.definition_point), end: v(p.parameter.definition_point), linear: false, point: true, xy: false, rotation: false, flip: false, reference: DVec3::ZERO, }); } } for p in super::block_distance_parameter::parameters(doc, definition) { if matches!(kind, Kind::Rotate | Kind::Flip) { continue; } let xy = p.kind == super::block_distance_parameter::Kind::XY; if kind == Kind::Scale && xy { continue; } result.push(Parameter { handle: p.handle, id: p.native.parameter.element.eval.node_id, grip: Handle::NULL, name: p.labels.first().cloned().unwrap_or_default(), base: v(p.native.definition_base_point), end: v(p.native.definition_end_point), linear: p.kind == super::block_distance_parameter::Kind::Linear, point: false, xy, rotation: false, flip: false, reference: DVec3::ZERO, }); } for p in super::block_orientation_parameter::parameters(doc, definition) { use super::block_orientation_parameter::Kind as O; if !matches!( (kind, p.kind), (Kind::Rotate, O::Rotation) | (Kind::Flip, O::Flip) ) { continue; } result.push(Parameter { handle: p.handle, id: p.native.parameter.element.eval.node_id, grip: Handle::NULL, name: p.label, base: v(p.native.definition_base_point), end: v(p.native.definition_end_point), linear: false, point: false, xy: false, rotation: kind == Kind::Rotate, flip: kind == Kind::Flip, reference: v(p.reference), }); } // Resolve the actual linked grip, rather than assuming expression IDs are contiguous. result.retain_mut(|p| { let grip_id = if p.rotation || p.flip { let Some(ObjectType::DynamicBlock(o)) = doc.objects.get(&p.handle) else { return false; }; match &o.data { DynamicBlockData::RotationParameter(p) => p.parameter.property_states[1], DynamicBlockData::FlipParameter(p) => p.flags_96, _ => return false, } } else if p.point { super::block_point_parameter::parameters(doc, definition) .into_iter() .find(|(h, _)| *h == p.handle) .unwrap() .1 .parameter .property_count } else { super::block_distance_parameter::parameters(doc, definition) .into_iter() .find(|q| q.handle == p.handle) .unwrap() .native .properties[2] .connections .first() .map(|c| c.code) .unwrap_or(-1) }; let grips = doc .objects .iter() .filter_map(|(h, o)| { let ObjectType::DynamicBlock(o) = o else { return None; }; if o.owner != graph { return None; } let grip = match &o.data { DynamicBlockData::XYGrip(g) | DynamicBlockData::PolarGrip(g) | DynamicBlockData::RotationGrip(g) => g, DynamicBlockData::FlipGrip(g) => &g.grip, DynamicBlockData::LinearGrip(g) => &g.grip, _ => return None, }; (grip.element.eval.node_id == grip_id).then_some(*h) }) .collect::>(); if grips.len() != 1 || !p.base.is_finite() || !p.end.is_finite() || (!p.point && (p.end.distance(p.base) < 1e-9 || !p.end.distance(p.base).is_finite())) { return false; } p.grip = grips[0]; if p.flip { if let Some(ObjectType::DynamicBlock(o)) = doc.objects.get(&p.grip) { if let DynamicBlockData::FlipGrip(g) = &o.data { let at = v(g.grip.location); if at.distance(p.base) > 1e-8 { p.reference = at; } } } if p.reference.distance(p.base) < 1e-8 { p.reference = p.end; } } true }); result.sort_by_key(|p| p.id); result } pub(crate) fn supported(e: &EntityType) -> bool { matches!( e, EntityType::Line(_) | EntityType::Circle(_) | EntityType::Arc(_) | EntityType::LwPolyline(_) | EntityType::Ellipse(_) | EntityType::Point(_) | EntityType::Text(_) | EntityType::MText(_) ) } #[cfg(test)] pub(crate) fn create( doc: &mut CadDocument, definition: Handle, kind: Kind, parameter: Handle, targets: &[Handle], location: DVec3, ) -> Result { create_with_options(doc, definition, kind, parameter, targets, location, None, None) } #[cfg(test)] pub(crate) fn create_array( doc: &mut CadDocument, definition: Handle, parameter: Handle, targets: &[Handle], location: DVec3, column_offset: f64, row_offset: f64, ) -> Result { create_with_options( doc, definition, Kind::Array, parameter, targets, location, None, Some((column_offset, row_offset)), ) } #[allow(dead_code)] pub(crate) fn create_with_frame( doc: &mut CadDocument, definition: Handle, kind: Kind, parameter: Handle, targets: &[Handle], location: DVec3, frame: Option<[DVec3; 2]>, ) -> Result { create_with_options(doc, definition, kind, parameter, targets, location, frame, None) } pub(crate) fn create_with_options( doc: &mut CadDocument, definition: Handle, kind: Kind, parameter: Handle, targets: &[Handle], location: DVec3, frame: Option<[DVec3; 2]>, array_offsets: Option<(f64, f64)>, ) -> Result { if !location.is_finite() { return Err("Specify a finite action location."); } let p = parameters(doc, definition, kind) .into_iter() .find(|p| p.handle == parameter) .ok_or("Select a compatible parameter belonging to this block.")?; if targets.is_empty() { return Err("Select at least one object for the action."); } let members = &doc .block_records .iter() .find(|b| b.handle == definition) .ok_or("The block definition is unavailable.")? .entity_handles; if targets.iter().any(|h| { !members.contains(h) || doc .get_entity(*h) .is_none_or(|e| !supported(e) || e.common().owner_handle != definition) }) { return Err("This milestone supports lines, polylines, arcs, circles, ellipses, points and text belonging to this block."); } let (column_offset, row_offset) = if kind == Kind::Array { let (col, row) = array_offsets.unwrap_or((1.0, 1.0)); if col <= 1e-6 || !col.is_finite() { return Err("Specify a positive column offset."); } if p.xy && (row <= 1e-6 || !row.is_finite()) { return Err("Specify a positive row offset."); } (col, row) } else { (0.0, 0.0) }; let stretch_binding = if kind == Kind::Stretch { if (location.z - p.end.z).abs() > 1e-8 { return Err("Place the Stretch marker in the parameter's XY plane."); } Some(stretch::bind( doc, targets, frame.ok_or("Specify a stretch frame before selecting objects.")?, p.end.z, )?) } else { None }; let mut staged = doc.clone(); let (graph, slots) = authoring::prepare(&mut staged, definition)?; let (dxf, cpp) = match kind { Kind::Move => ("BLOCKMOVEACTION", "AcDbBlockMoveAction"), Kind::Scale => ("BLOCKSCALEACTION", "AcDbBlockScaleAction"), Kind::Rotate => ("BLOCKROTATEACTION", "AcDbBlockRotationAction"), Kind::Flip => ("BLOCKFLIPACTION", "AcDbBlockFlipAction"), Kind::Stretch => ("BLOCKSTRETCHACTION", "AcDbBlockStretchAction"), Kind::Array => ("BLOCKARRAYACTION", "AcDbBlockArrayAction"), }; register_classes(&mut staged, &[(dxf, cpp, 55)]); let handle = staged.allocate_handle(); let mut dependencies = targets.to_vec(); dependencies.sort(); dependencies.dedup(); let action = BlockAction { element: BlockElement { eval: expression(slots.eval, NO_VALUE, BlockEvalValue::None), name: format!("{}{}", kind.name(), slots.eval), major: MAJOR, minor: MINOR, eed_1071: 0, }, display_location: native(location), dependencies, action_ids: vec![], }; let connection = |name: &str| BlockConnection { code: p.id, name: name.into(), }; let data = match kind { Kind::Array => DynamicBlockData::ArrayAction(BlockArrayAction { action, connections: [ connection("Base"), connection("End"), connection("UpdatedBase"), connection("UpdatedEnd"), ], column_offset, row_offset, }), Kind::Stretch => { let (points, handles) = stretch_binding.unwrap(); DynamicBlockData::StretchAction(BlockStretchAction { action, connections: [connection("EndXDelta"), connection("EndYDelta")], points, handles, codes: vec![], offsets: BlockActionOffsets { distance_multiplier: 1., angle_offset: 0., flags: 0, }, }) } Kind::Move => DynamicBlockData::MoveAction(BlockMoveAction { action, connections: if p.point { [connection("XDelta"), connection("YDelta")] } else { [connection("EndXDelta"), connection("EndYDelta")] }, offsets: BlockActionOffsets { distance_multiplier: 1., angle_offset: 0., flags: 0, }, }), Kind::Flip => DynamicBlockData::FlipAction(BlockFlipAction { action, connections: [ connection("Flip"), connection("UpdatedFlip"), connection("UpdatedBase"), connection("UpdatedEnd"), ], }), Kind::Rotate => DynamicBlockData::RotateAction(BlockBasePointAction { action: BlockActionWithBasePoint { action, offset: Vector3::ZERO, connections: vec![connection("UpdatedBaseX"), connection("UpdatedBaseY")], dependent: true, base_point: Vector3::ZERO, }, connections: vec![connection("AngleDelta")], dwg_tail_byte: 0, }), Kind::Scale => DynamicBlockData::ScaleAction(BlockBasePointAction { action: BlockActionWithBasePoint { action, offset: Vector3::ZERO, connections: vec![connection("UpdatedBaseX"), connection("UpdatedBaseY")], dependent: true, base_point: Vector3::ZERO, }, connections: vec![ connection("Scale"), connection("XScale"), connection("YScale"), ], dwg_tail_byte: 0, }), }; staged.objects.insert( handle, ObjectType::DynamicBlock(dynamic(handle, graph, dxf, cpp, data)), ); authoring::append_single(&mut staged, graph, slots, handle); authoring::graph::bind_ids(&mut staged, graph, slots.eval, &[p.id])?; *doc = staged; Ok(handle) } /// Retarget an existing native action, retaining identity, graph and options. pub(crate) fn retarget( doc: &mut CadDocument, definition: Handle, handle: Handle, targets: &[Handle], frame: Option<[DVec3; 2]>, ) -> Result { let (_, action, kind, id) = actions(doc, definition) .into_iter() .find(|(h, _, _, _)| *h == handle) .ok_or("Select an action belonging to this block.")?; let mut targets = targets.to_vec(); targets.sort(); targets.dedup(); if targets.is_empty() { return Err("An action needs at least one object."); } let members = &doc .block_records .iter() .find(|b| b.handle == definition) .ok_or("The definition is unavailable.")? .entity_handles; if targets.iter().any(|h| { !members.contains(h) || doc .get_entity(*h) .is_none_or(|e| e.common().owner_handle != definition || !supported(e)) }) { return Err("Select supported drawing objects belonging to this block."); } let old_targets = action.dependencies.clone(); let mut staged = doc.clone(); let ObjectType::DynamicBlock(o) = staged.objects.get_mut(&handle).unwrap() else { unreachable!() }; match &mut o.data { DynamicBlockData::StretchAction(a) => { let plane = parameters(doc, definition, Kind::Stretch) .into_iter() .find(|p| p.id == id) .ok_or("The Stretch parameter is unavailable.")? .end .z; stretch::validate_options(a)?; if let Some(corners) = frame { let (points, handles) = stretch::bind(doc, &targets, corners, plane)?; a.points = points; a.handles = handles; } else { a.handles.retain(|h| targets.contains(&h.handle)); let mut retained = a.clone(); retained.action.dependencies.retain(|h| targets.contains(h)); stretch::validate(doc, &retained)?; let added = targets .iter() .copied() .filter(|h| !old_targets.contains(h)) .collect::>(); a.handles .extend(stretch::bind_existing(doc, &added, &a.points, plane)?); a.handles.sort_by_key(|h| h.handle); } a.action.dependencies = targets; } DynamicBlockData::MoveAction(a) => a.action.dependencies = targets, DynamicBlockData::FlipAction(a) => a.action.dependencies = targets, DynamicBlockData::ScaleAction(a) | DynamicBlockData::RotateAction(a) => { a.action.action.dependencies = targets } DynamicBlockData::ArrayAction(a) => a.action.dependencies = targets, _ => return Err("This action's selection cannot be edited yet."), } if frame.is_some() && kind != Kind::Stretch { return Err("Only Stretch actions have a stretch frame."); } if staged == *doc { return Ok(false); } *doc = staged; Ok(true) } pub(crate) fn actions( doc: &CadDocument, definition: Handle, ) -> Vec<(Handle, &BlockAction, Kind, i32)> { let Ok(Some(graph)) = authoring::graph(doc, definition) else { return vec![]; }; let ObjectType::DynamicBlock(o) = &doc.objects[&graph] else { return vec![]; }; let DynamicBlockData::EvaluationGraph(g) = &o.data else { return vec![]; }; g.nodes .iter() .filter_map(|n| { let ObjectType::DynamicBlock(o) = doc.objects.get(&n.expression)? else { return None; }; if o.owner != graph { return None; } match &o.data { DynamicBlockData::MoveAction(a) => { Some((o.handle, &a.action, Kind::Move, a.connections[0].code)) } DynamicBlockData::StretchAction(a) => { Some((o.handle, &a.action, Kind::Stretch, a.connections[0].code)) } DynamicBlockData::FlipAction(a) => { Some((o.handle, &a.action, Kind::Flip, a.connections[0].code)) } DynamicBlockData::RotateAction(a) => a .connections .first() .map(|c| (o.handle, &a.action.action, Kind::Rotate, c.code)), DynamicBlockData::ScaleAction(a) => a .connections .first() .map(|c| (o.handle, &a.action.action, Kind::Scale, c.code)), DynamicBlockData::ArrayAction(a) => a .connections .first() .map(|c| (o.handle, &a.action, Kind::Array, c.code)), _ => None, } }) .collect() } /// Drop erased objects from action targets without replacing graph nodes. pub(crate) fn prune_erased_targets(doc: &mut CadDocument, definition: Handle) { let live = authoring::members(doc, definition) .into_iter() .collect::>(); let handles = actions(doc, definition) .into_iter() .map(|(h, _, _, _)| h) .collect::>(); for h in handles { let Some(ObjectType::DynamicBlock(o)) = doc.objects.get_mut(&h) else { continue; }; let action = match &mut o.data { DynamicBlockData::MoveAction(a) => &mut a.action, DynamicBlockData::FlipAction(a) => &mut a.action, DynamicBlockData::ArrayAction(a) => &mut a.action, DynamicBlockData::StretchAction(a) => { a.handles.retain(|b| live.contains(&b.handle)); &mut a.action } DynamicBlockData::ScaleAction(a) | DynamicBlockData::RotateAction(a) => { &mut a.action.action } _ => continue, }; action.dependencies.retain(|h| live.contains(h)); } } pub(crate) fn frame_outline(doc: &CadDocument, handle: Handle) -> Vec { let Some(ObjectType::DynamicBlock(o)) = doc.objects.get(&handle) else { return vec![]; }; let DynamicBlockData::StretchAction(a) = &o.data else { return vec![]; }; stretch::outline(&a.points, a.action.display_location.z) } pub(crate) fn reframe(doc: &mut CadDocument, definition: Handle, t: &Transform) -> usize { let handles = actions(doc, definition) .iter() .map(|(h, _, _, _)| *h) .collect::>(); for h in &handles { let ObjectType::DynamicBlock(o) = doc.objects.get_mut(h).unwrap() else { unreachable!() }; match &mut o.data { DynamicBlockData::MoveAction(a) => { a.action.display_location = t.apply(a.action.display_location) } DynamicBlockData::ArrayAction(a) => { a.action.display_location = t.apply(a.action.display_location) } DynamicBlockData::StretchAction(a) => { let plane = a.action.display_location.z; a.action.display_location = t.apply(a.action.display_location); a.points = stretch::outline(&a.points, plane) .into_iter() .map(|p| { let p = t.apply(p); codec::types::Vector2::new(p.x, p.y) }) .collect(); } DynamicBlockData::FlipAction(a) => { a.action.display_location = t.apply(a.action.display_location) } DynamicBlockData::ScaleAction(a) | DynamicBlockData::RotateAction(a) => { a.action.action.display_location = t.apply(a.action.action.display_location); a.action.base_point = t.apply(a.action.base_point); a.action.offset = t.apply_rotation(a.action.offset); } _ => {} } } handles.len() } pub(crate) fn current(doc: &CadDocument, p: &Parameter) -> DVec3 { let Some(ObjectType::DynamicBlock(o)) = doc.objects.get(&p.grip) else { return p.end; }; match &o.data { DynamicBlockData::XYGrip(g) | DynamicBlockData::PolarGrip(g) | DynamicBlockData::RotationGrip(g) => v(g.location), DynamicBlockData::LinearGrip(g) => v(g.grip.location), _ => p.end, } } /// Visibility can give the test INSERT an anonymous representation. Keep its /// parallel members in sync, retaining their own handles and visibility flags. pub(crate) fn sync_representation( doc: &mut CadDocument, definition: Handle, name: &str, ) -> Result<(), &'static str> { let original = doc .block_records .iter() .find(|b| b.handle == definition) .ok_or("The test definition is unavailable.")?; if original.name == name { return Ok(()); } let source = original.entity_handles.clone(); let target = doc .block_records .get(name) .ok_or("The test representation is unavailable.")? .entity_handles .clone(); let has_array = actions(doc, definition) .iter() .any(|(_, _, k, _)| *k == Kind::Array); if source.len() == target.len() && !has_array { let affected = actions(doc, definition) .iter() .flat_map(|(_, a, _, _)| a.dependencies.iter().copied()) .collect::>(); for (from, to) in source.into_iter().zip(target) { if !affected.contains(&from) { continue; } let mut entity = doc .get_entity(from) .cloned() .ok_or("An action target is unavailable.")?; let current = doc .get_entity_mut(to) .ok_or("An action representation target is unavailable.")?; *entity.common_mut() = current.common().clone(); *current = entity; } return Ok(()); } let evaluated = doc.clone(); let mut scene = crate::scene::Scene::new(); scene.document = doc.clone(); representation::reconcile(&mut scene, definition, name, &evaluated)?; *doc = scene.document; Ok(()) } /// Re-evaluate from the scratch document's immutable starting geometry. This /// avoids incremental drift and makes repeated changes/order independent. #[cfg(test)] pub(crate) fn evaluate( doc: &mut CadDocument, baseline: &CadDocument, definition: Handle, parameter: Handle, position: DVec3, ) -> Result { evaluate_value(doc, baseline, definition, parameter, Value::Point(position)) } pub(crate) fn evaluate_value( doc: &mut CadDocument, baseline: &CadDocument, definition: Handle, parameter: Handle, input: Value, ) -> Result { evaluate_impl(doc, baseline, definition, parameter, input, false) } pub(crate) fn rebuild( doc: &mut CadDocument, baseline: &CadDocument, definition: Handle, ) -> Result<(), &'static str> { let Some(p) = all_parameters(baseline, definition).into_iter().next() else { return Ok(()); }; let input = value(doc, &p)?; evaluate_impl(doc, baseline, definition, p.handle, input, true)?; Ok(()) } fn evaluate_impl( doc: &mut CadDocument, baseline: &CadDocument, definition: Handle, parameter: Handle, input: Value, force: bool, ) -> Result { let graph = authoring::graph(baseline, definition)? .ok_or("The block authoring graph is unavailable.")?; if baseline.objects.values().any(|o| { matches!(o, ObjectType::DynamicBlock(o) if o.owner == graph && matches!(&o.data, DynamicBlockData::PolarStretchAction(_))) }) { return Err("This block contains action types not supported yet."); } let p = all_parameters(baseline, definition) .into_iter() .find(|p| p.handle == parameter) .ok_or("The block parameter is unavailable.")?; let input = match input { Value::Flip(_) if !p.flip => return Err("Select a Flip parameter."), Value::Point(_) if p.flip => return Err("Use the Flip grip to toggle its state."), Value::Point(mut position) => { if !position.is_finite() { return Err("Specify a finite parameter position."); } position.z = p.end.z; if p.linear { let axis = (p.end - p.base).normalize(); let length = (position - p.base).dot(axis); if length < 1e-9 { return Err("Specify a positive parameter length."); } position = p.base + axis * length; } if p.rotation { let delta = position - p.base; if delta.length() < 1e-9 { return Err("Specify a rotation direction away from the parameter base."); } position = p.base + delta.normalize() * p.end.distance(p.base); } Value::Point(position) } value => value, }; let previous = value(doc, &p)?; if !force && match (previous, input) { (Value::Flip(a), Value::Flip(b)) => a == b, (Value::Point(a), Value::Point(b)) => a.distance(b) < 1e-10, _ => false, } { return Ok(false); } let mut staged = doc.clone(); set_value(&mut staged, &p, input)?; let baseline_members = authoring::members(baseline, definition) .into_iter() .collect::>(); let extra_handles = staged .entities() .filter(|e| { e.common().owner_handle == definition && !baseline_members.contains(&e.common().handle) }) .map(|e| e.common().handle) .collect::>(); for h in extra_handles { staged.remove_entity(h); } if let Some(record) = staged.block_records.iter_mut().find(|b| b.handle == definition) { record.entity_handles.retain(|h| { baseline_members.contains(h) || *h == record.block_entity_handle || *h == record.block_end_handle }); } let all = actions(baseline, definition); let mut affected = std::collections::BTreeSet::new(); for (h, a, kind, _) in &all { if *kind == Kind::Stretch { let ObjectType::DynamicBlock(o) = &baseline.objects[h] else { unreachable!() }; let DynamicBlockData::StretchAction(s) = &o.data else { unreachable!() }; stretch::validate(baseline, s)?; } affected.extend(a.dependencies.iter().copied()); } for h in affected { let mut entity = baseline .get_entity(h) .cloned() .ok_or("An action target is unavailable.")?; if !supported(&entity) { return Err("This action contains an unsupported object type."); } // State flags may have changed through the existing visibility grip. if let Some(e) = staged.get_entity(h) { entity.common_mut().invisible = e.common().invisible; } for (action_handle, a, kind, id) in &all { if !a.dependencies.contains(&h) || *kind == Kind::Array { continue; } let q = parameters(baseline, definition, *kind) .into_iter() .find(|p| p.id == *id) .ok_or("An action has an unsupported parameter binding.")?; if !supported_parameter(baseline, q.handle) { return Err("This parameter uses value constraints or chaining not supported yet."); } let end = current(&staged, &q); let transform = match kind { Kind::Move | Kind::Stretch => { crate::command::EntityTransform::Translate(end - q.end) } Kind::Rotate => { let a = end - q.base; let b = q.end - q.base; crate::command::EntityTransform::Rotate { center: q.base, axis: DVec3::Z, angle_rad: a.y.atan2(a.x) - b.y.atan2(b.x), } } Kind::Flip => { if value(&staged, &q)? == Value::Flip(false) { crate::command::EntityTransform::Translate(DVec3::ZERO) } else { crate::command::EntityTransform::Mirror { p1: q.base, p2: q.end, working_normal: DVec3::Z, } } } Kind::Scale => { let factor = end.distance(q.base) / q.end.distance(q.base); if !factor.is_finite() || factor < 1e-9 { return Err("Specify a positive scale distance."); } crate::command::EntityTransform::Scale { center: q.base, factor, } } Kind::Array => unreachable!(), }; // Imported actions with custom multipliers/offsets/chaining must // not silently behave like the simple actions authored here. if !a.action_ids.is_empty() { return Err("Chained actions are not supported yet."); } let ObjectType::DynamicBlock(o) = &baseline.objects[action_handle] else { unreachable!() }; match &o.data { DynamicBlockData::MoveAction(m) if m.offsets.distance_multiplier != 1. || m.offsets.angle_offset != 0. || m.offsets.flags != 0 || m.connections != if q.point { [ BlockConnection { code: q.id, name: "XDelta".into(), }, BlockConnection { code: q.id, name: "YDelta".into(), }, ] } else { [ BlockConnection { code: q.id, name: "EndXDelta".into(), }, BlockConnection { code: q.id, name: "EndYDelta".into(), }, ] } => { return Err("This Move action uses options not supported yet.") } DynamicBlockData::FlipAction(f) if f.connections != ["Flip", "UpdatedFlip", "UpdatedBase", "UpdatedEnd"].map(|name| { BlockConnection { code: q.id, name: name.into(), } }) => { return Err("This Flip action uses options not supported yet.") } DynamicBlockData::RotateAction(r) if !r.action.dependent || r.action.offset != Vector3::ZERO || r.connections != vec![BlockConnection { code: q.id, name: "AngleDelta".into(), }] || r.action.connections != ["UpdatedBaseX", "UpdatedBaseY"].map(|name| BlockConnection { code: q.id, name: name.into(), }) => { return Err("This Rotate action uses options not supported yet.") } DynamicBlockData::ScaleAction(s) if !s.action.dependent || s.action.offset != Vector3::ZERO || s.connections != ["Scale", "XScale", "YScale"].map(|name| BlockConnection { code: q.id, name: name.into(), }) || s.action.connections != ["UpdatedBaseX", "UpdatedBaseY"].map(|name| BlockConnection { code: q.id, name: name.into(), }) => { return Err("This Scale action uses options not supported yet.") } _ => {} } if let DynamicBlockData::StretchAction(s) = &o.data { stretch::apply(&mut entity, s, end - q.end)?; } else { super::view::dispatch::apply_transform(&mut entity, &transform); } let bounds = entity.as_entity().bounding_box(); if !v(bounds.min).is_finite() || !v(bounds.max).is_finite() { return Err("The parameter value produces non-finite geometry."); } } *staged .get_entity_mut(h) .ok_or("An action target was removed.")? = entity; } for (action_handle, a, kind, id) in &all { if *kind != Kind::Array { continue; } let ObjectType::DynamicBlock(o) = &baseline.objects[action_handle] else { unreachable!() }; let DynamicBlockData::ArrayAction(arr) = &o.data else { unreachable!() }; let q = parameters(baseline, definition, Kind::Array) .into_iter() .find(|p| p.id == *id) .ok_or("An action has an unsupported parameter binding.")?; if !supported_parameter(baseline, q.handle) { return Err("This parameter uses value constraints or chaining not supported yet."); } if arr.column_offset <= 1e-6 || !arr.column_offset.is_finite() || (q.xy && (arr.row_offset <= 1e-6 || !arr.row_offset.is_finite())) || arr.connections != ["Base", "End", "UpdatedBase", "UpdatedEnd"].map(|name| BlockConnection { code: q.id, name: name.into(), }) { return Err("This Array action uses options not supported yet."); } if !a.action_ids.is_empty() { return Err("Chained actions are not supported yet."); } let end = current(&staged, &q); let displacements: Vec<(usize, DVec3)> = if q.xy { let dx = (end.x - q.base.x).abs(); let dy = (end.y - q.base.y).abs(); let nx = ((dx + 1e-9) / arr.column_offset).floor() as usize + 1; let ny = ((dy + 1e-9) / arr.row_offset).floor() as usize + 1; let dir_x = if end.x >= q.base.x { 1.0 } else { -1.0 }; let dir_y = if end.y >= q.base.y { 1.0 } else { -1.0 }; let mut list = vec![]; let mut idx = 1; for row in 0..ny { for col in 0..nx { if col == 0 && row == 0 { continue; } let disp = DVec3::new( (col as f64) * arr.column_offset * dir_x, (row as f64) * arr.row_offset * dir_y, 0.0, ); list.push((idx, disp)); idx += 1; } } list } else if q.linear { let axis = (q.end - q.base).normalize(); let length = (end - q.base).dot(axis); let n = ((length.max(0.0) + 1e-9) / arr.column_offset).floor() as usize + 1; (1..n) .map(|k| (k, (k as f64) * arr.column_offset * axis)) .collect() } else { let delta = end - q.base; let dist = delta.length(); let axis = if dist > 1e-9 { delta / dist } else { (q.end - q.base).normalize() }; let n = ((dist + 1e-9) / arr.column_offset).floor() as usize + 1; (1..n) .map(|k| (k, (k as f64) * arr.column_offset * axis)) .collect() }; for (k, disp) in displacements { for target_h in &a.dependencies { let mut clone = staged .get_entity(*target_h) .cloned() .ok_or("An action target is unavailable.")?; super::view::dispatch::apply_transform( &mut clone, &crate::command::EntityTransform::Translate(disp), ); let bounds = clone.as_entity().bounding_box(); if !v(bounds.min).is_finite() || !v(bounds.max).is_finite() { return Err("The parameter value produces non-finite geometry."); } let new_h = staged.allocate_handle(); clone.common_mut().handle = new_h; clone.common_mut().owner_handle = definition; clone.common_mut().raw_record = None; clone.common_mut().xdictionary_handle = None; clone.common_mut().reactors.clear(); crate::scene::Scene::reset_clone_subhandles(&mut staged, &mut clone); let base_id = authoring::element_id(&clone); staged .add_entity(clone) .map_err(|_| "Could not add array item.")?; if let Some(base_id) = base_id { let clone_id = base_id.wrapping_add((k as i32) * 100_000); authoring::set_entity_tag( &mut staged, new_h, authoring::element_tag(clone_id, new_h), ); } } } } *doc = staged; Ok(true) } fn supported_parameter(doc: &CadDocument, handle: Handle) -> bool { let Some(ObjectType::DynamicBlock(o)) = doc.objects.get(&handle) else { return false; }; let distance = |s: &codec::objects::BlockParameterValueSet| { s.flags == 1 && s.minimum == 0. && s.maximum == 0. && s.increment == 0. && s.values.is_empty() }; match &o.data { DynamicBlockData::FlipParameter(p) => !p.parameter.parameter.chain_actions, DynamicBlockData::RotationParameter(p) => { !p.parameter.parameter.chain_actions && p.value_set.flags == 3 && p.value_set.minimum == 0. && p.value_set.maximum == std::f64::consts::TAU && p.value_set.increment == 0. && p.value_set.values.is_empty() } DynamicBlockData::PointParameter(p) => !p.parameter.parameter.chain_actions, DynamicBlockData::LinearParameter(p) => { !p.parameter.parameter.chain_actions && distance(&p.value_set) } DynamicBlockData::XYParameter(p) => { !p.parameter.parameter.chain_actions && distance(&p.x_value_set) && distance(&p.y_value_set) } DynamicBlockData::PolarParameter(p) => { !p.parameter.parameter.chain_actions && distance(&p.distance_value_set) && p.angle_value_set.flags == 3 && p.angle_value_set.minimum == 0. && p.angle_value_set.maximum == std::f64::consts::TAU && p.angle_value_set.increment == 0. && p.angle_value_set.values.is_empty() } _ => false, } }