use super::*; impl OpenCADStudio { pub(super) fn handle_open_auto_constrain_settings(&mut self) { self.auto_constrain_saved = Some(self.auto_constrain_settings.clone()); self.auto_constrain_selected_row = 0; self.auto_constrain_distance_input = format!("{}", self.auto_constrain_settings.distance_tolerance); self.auto_constrain_angle_input = format!("{}", self.auto_constrain_settings.angle_tolerance_deg); self.active_modal = Some(super::super::ModalKind::AutoConstrainSettings); } pub(super) fn handle_add_parametric_constraint(&mut self, result: CmdResult) -> Option> { let i = self.active_tab; let CmdResult::AddParametricConstraint { kind, refs, driving_param, label, } = result else { unreachable!("router only routes the matching variant"); }; use crate::scene::parametric_constraints::ConstraintKind; // GCSMOOTH and FXCONSTRAINT re-prompt after a rejected pick. let keep_command = self.tabs[i].active_cmd.as_ref().is_some_and(|command| { (kind == ConstraintKind::Smooth && command.name() == "GCSMOOTH") || (kind == ConstraintKind::Fixed && command.name() == "FXCONSTRAINT") }); if let Err(message) = self.tabs[i].scene.validate_parametric_constraint( kind, &refs, driving_param.as_ref(), ) { if !keep_command { self.tabs[i].active_cmd = None; } self.tabs[i].snap_result = None; self.command_line.push_error(message); if keep_command { if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|command| command.prompt()) { self.command_line.push_info(&prompt); } } return Some(Task::none()); } let scope = self.tabs[i].current_parametric_scope(); if kind == ConstraintKind::Fixed && self.tabs[i] .scene .parametric_constraint_set(scope) .is_some_and(|set| { set.constraints.iter().any(|existing| { existing.enabled && existing.kind == kind && existing.refs == refs }) }) { self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.command_line .push_error("The constraint already exists on the selected objects."); return Some(Task::none()); } let constraints_before = self.tabs[i] .scene .parametric_constraint_set(scope) .cloned() .unwrap_or_else(|| { crate::scene::parametric_constraints::ParametricConstraintSet::new(scope) }); let touched: Vec = refs.iter().map(|r| r.entity).collect(); let pending = self.begin_undo(i, label, touched.len(), true); self.tabs[i] .scene .record_undo_parametric_constraints_before(scope, constraints_before); let retain_size = self.constraint_solve_mode && driving_param.is_none(); let id = self.tabs[i].scene.parametric_constraint_set_mut(scope).add( kind, refs, driving_param, ); self.tabs[i].scene.note_parametric_constraint_applied( scope, id, self.constraint_bar_display, ); let changes: Vec<(Handle, crate::scene::ChangeKind)> = touched .into_iter() .map(|h| (h, crate::scene::ChangeKind::Modified)) .collect(); self.tabs[i].scene.bump_entities_with_parametric_policy( &changes, &[], retain_size, ); self.tabs[i].dirty = true; self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.command_line.push_output("Constraint applied."); self.refresh_properties(); if let Some(pd) = pending { self.commit_undo_delta(i, pd); } None } pub(super) fn handle_add_equal_constraint(&mut self, result: CmdResult) -> Option> { let i = self.active_tab; let CmdResult::AddEqualConstraint { first, others, multiple, label, } = result else { unreachable!("router only routes the matching variant"); }; use crate::modules::parametric::EqualConstraintCommand; use crate::scene::parametric_constraints::{ equal_size, equal_size_follower, ConstraintKind, EqualSize, ParametricRef, }; let scope = self.tabs[i].current_parametric_scope(); // Enter ends a Multiple flow with the reference's summary line. let finishing = multiple && others.is_empty(); let mut followers: Vec = Vec::new(); for other in others { let refs = [first, other]; if other == first || self .tabs[i] .scene .validate_parametric_constraint(ConstraintKind::Equal, &refs, None) .is_err() || equal_size_follower(&self.tabs[i].scene.document, first, other) .is_none() { // The reference names the kind the first object // asks for. let message = match equal_size(&self.tabs[i].scene.document, first) { Some(EqualSize::Radius(_)) => EqualConstraintCommand::INVALID_RADIUS_OBJECT, _ => EqualConstraintCommand::INVALID_LENGTH_OBJECT, }; self.command_line.push_error(message); continue; } let exists = self.tabs[i] .scene .parametric_constraint_set(scope) .is_some_and(|set| { set.constraints.iter().any(|existing| { existing.enabled && existing.kind == ConstraintKind::Equal && (existing.refs == refs || existing.refs == [other, first]) }) }); if exists { self.command_line .push_error("The constraint already exists on the selected objects."); continue; } followers.push(other); } // A refused second object is asked for again, as in the // reference; Multiple keeps asking anyway. let keep = (multiple && !finishing) || (!multiple && followers.is_empty()); if keep { if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|command| command.prompt()) { self.command_line.push_info(&prompt); } } else { self.tabs[i].active_cmd = None; } self.tabs[i].snap_result = None; if finishing { let summary = match equal_size(&self.tabs[i].scene.document, first) { Some(EqualSize::Radius(_)) => "Radius of objects made equal", _ => "Length of objects made equal", }; self.command_line.push_output(summary); } if followers.is_empty() { return Some(Task::none()); } let constraints_before = self.tabs[i] .scene .parametric_constraint_set(scope) .cloned() .unwrap_or_else(|| { crate::scene::parametric_constraints::ParametricConstraintSet::new(scope) }); let mut touched = vec![first.entity]; for follower in &followers { if !touched.contains(&follower.entity) { touched.push(follower.entity); } } let pending = self.begin_undo(i, label, touched.len(), true); self.tabs[i] .scene .record_undo_parametric_constraints_before(scope, constraints_before); // The reference resizes the follower in place — its start (a // circle its center) and direction stay, only its length or // radius takes the first object's — so do that first and let // the relation then hold what already fits. for follower in &followers { if let Some(resized) = equal_size_follower(&self.tabs[i].scene.document, first, *follower) { self.tabs[i].scene.update_entity(resized); } let id = self.tabs[i] .scene .parametric_constraint_set_mut(scope) .add(ConstraintKind::Equal, vec![first, *follower], None); self.tabs[i].scene.note_parametric_constraint_applied( scope, id, self.constraint_bar_display, ); } let changes = touched .into_iter() .map(|handle| (handle, crate::scene::ChangeKind::Modified)) .collect::>(); self.tabs[i].scene.bump_entities_with_parametric_policy( &changes, &[], self.constraint_solve_mode, ); self.tabs[i].dirty = true; self.refresh_properties(); if let Some(pd) = pending { self.commit_undo_delta(i, pd); } None } pub(super) fn handle_check_constraint_point( &mut self, pick: crate::command::CoincidentPick, ) -> Task { let i = self.active_tab; use crate::scene::parametric_constraints::{ nearest_parametric_point, nearest_parametric_point_on_entity, resolve_point, }; let scope = self.tabs[i].current_parametric_scope(); let resolved = { let document = &self.tabs[i].scene.document; let world = codec::types::Vector3::new(pick.point.x, pick.point.y, pick.point.z); let reference = match pick.handle { Some(handle) => { nearest_parametric_point_on_entity(document, scope, handle, world) } None => nearest_parametric_point(document, scope, world, None), }; reference.map(|reference| { let point = document .get_entity(reference.entity) .zip(reference.marker) .and_then(|(entity, marker)| resolve_point(entity, marker)) .map_or(pick.point, |p| glam::DVec3::new(p.x, p.y, p.z)); (reference, point) }) }; let Some((reference, point)) = resolved else { return self.apply_cmd_result(CmdResult::ReportError( crate::modules::parametric::DimConstraintCommand::NO_POINT.to_string(), )); }; let result = self.tabs[i] .active_cmd .as_mut() .map(|command| command.accept_constraint_point(reference, point)); match result { Some(result) => self.apply_cmd_result(result), None => Task::none(), } } pub(super) fn handle_add_dimensional_constraint(&mut self, result: CmdResult) -> Option> { let i = self.active_tab; let CmdResult::AddDimensionalConstraint { kind, first, second, first_point, second_point, location, axis, direction, name, expression, renamed, label, } = result else { unreachable!("router only routes the matching variant"); }; use crate::modules::annotate::{aligned_dim, linear_dim}; use crate::scene::named_parameters::{is_valid_name, DrivingValue}; use crate::scene::parametric_constraints::{ distance_direction_type, dynamic_dimension_text, ConstraintKind, }; let scope = self.tabs[i].current_parametric_scope(); let mut refs = vec![first, second]; refs.extend(direction); // The same measurement between the same references is // refused after the value, and the command ends. let duplicate = self.tabs[i] .scene .parametric_constraint_set(scope) .is_some_and(|set| { set.constraints.iter().any(|c| { c.enabled && c.kind == kind && c.refs.len() == refs.len() && c.refs[..2].contains(&first) && c.refs[..2].contains(&second) && c.refs.get(2) == refs.get(2) }) }); if duplicate { self.command_line .push_output("The constraint already exists on the selected objects."); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; return Some(Task::none()); } // A refused value keeps the prompt open, as the reference's // editor does. A directed distance validates as the plain // two-point distance it also is. let (validate_kind, validate_refs) = if kind == ConstraintKind::DistanceDirected { (ConstraintKind::Distance, &refs[..2]) } else { (kind, &refs[..]) }; if let Err(message) = self.tabs[i].scene.validate_parametric_constraint( validate_kind, validate_refs, Some(&DrivingValue::Literal(1.0)), ) { self.reprompt_active_command(i, message); return Some(Task::none()); } let anchors = crate::scene::parametric_constraints::dimensional_anchor_refs( &self.tabs[i].scene.document, &refs, ); let name = name.trim().to_string(); if !is_valid_name(&name) { self.command_line.push_error( "Only alphanumeric names, starting with alpha characters, are allowed.", ); self.reprompt_active_command(i, &format!("Invalid parameter name: {name}.")); return Some(Task::none()); } if renamed && self.tabs[i].scene.named_parameters().contains(&name) { self.reprompt_active_command(i, "Parameter with this name already exists."); return Some(Task::none()); } let mut table = self.tabs[i].scene.named_parameters().clone(); if let Err(error) = table.set(&name, &expression) { self.reprompt_active_command(i, &error.to_string()); return Some(Task::none()); } let value = match table.resolve(&name) { Ok(value) if value.is_finite() => value, _ => { self.command_line.push_error("Invalid expression."); self.reprompt_active_command( i, "The parameter is used in an expression which results in an invalid value for a dimensional constraint.", ); return Some(Task::none()); } }; // DCFORM Annotational: an ordinary plotted dimension on the // current layer that shows the value at dimension precision. let annotational = self.constraint_form_annotational; let text = dynamic_dimension_text( &name, value, self.tabs[i].scene.constraint_name_format, false, Some(&expression), annotational, None, ); let mut entity = if kind == ConstraintKind::Distance { aligned_dim::aligned_dimension_entity( first_point, second_point, location, Some(text), ) } else { linear_dim::linear_dimension_entity( first_point, second_point, location, axis, Some(text), ) }; crate::scene::creation_style::apply_current_creation_styles( &self.tabs[i].scene.document, &mut entity, ); if annotational { entity .as_entity_mut() .set_layer(self.tabs[i].active_layer.clone()); } else { // A dynamic dimension lives on the reference's constraints // layer, hidden from the layer lists and never plotted, and // draws in the constraint gray, not the current color. self.tabs[i].scene.ensure_dynamic_dimension_layer(); entity.as_entity_mut().set_layer( crate::scene::parametric_constraints::DYNAMIC_DIMENSION_LAYER.to_string(), ); entity.common_mut().color = codec::types::Color::Rgb { r: 103, g: 109, b: 118, }; } let constraints_before = self.tabs[i] .scene .parametric_constraint_set(scope) .cloned() .unwrap_or_else(|| { crate::scene::parametric_constraints::ParametricConstraintSet::new(scope) }); let mut touched = vec![first.entity]; for reference in [Some(second), direction].into_iter().flatten() { if !touched.contains(&reference.entity) { touched.push(reference.entity); } } let pending = self.begin_undo(i, label, touched.len() + 1, false); self.tabs[i] .scene .record_undo_parametric_constraints_before(scope, constraints_before); self.tabs[i].scene.record_undo_named_parameters_before(); self.tabs[i].scene.named_parameters = table; let dimension = self.tabs[i].scene.add_entity(entity); let set = self.tabs[i].scene.parametric_constraint_set_mut(scope); let id = set.add(kind, refs, Some(DrivingValue::Named(name))); if direction.is_some() { if let Some(constraint) = set.constraints.iter_mut().find(|c| c.id == id) { constraint.distance_direction_type = distance_direction_type::PERPENDICULAR_TO_LINE; } } set.dimensions.insert(id, dimension); self.tabs[i].scene.note_parametric_constraint_applied( scope, id, self.constraint_bar_display, ); self.tabs[i].scene.attach_dimension_association( dimension, vec![Some(first.entity), Some(second.entity)], ); let changes: Vec<(Handle, crate::scene::ChangeKind)> = touched .into_iter() .map(|handle| (handle, crate::scene::ChangeKind::Modified)) .collect(); // The first point (and a line it belongs to that the distance // runs perpendicular to) stays; a value other than the // measured one moves the second, as in the reference. Not // `retain_size`: the typed value is the new length. self.tabs[i] .scene .bump_entities_with_parametric_policy(&changes, &anchors, false); // DYNCONSTRAINTDISPLAY 0 also keeps a new dynamic dimension off screen. self.tabs[i].scene.refresh_hidden_dynamic_dimensions(); self.tabs[i].scene.refresh_dynamic_dimension_scales(true); self.tabs[i].dirty = true; self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.refresh_properties(); if let Some(pd) = pending { self.commit_undo_delta(i, pd); } None } pub(super) fn handle_add_radial_constraint(&mut self, result: CmdResult) -> Option> { let i = self.active_tab; let CmdResult::AddRadialConstraint { circle, center, radius, location, diameter, name, expression, renamed, } = result else { unreachable!("router only routes the matching variant"); }; use crate::modules::annotate::{diameter_dim, radius_dim}; use crate::scene::named_parameters::{is_valid_name, DrivingValue}; use crate::scene::parametric_constraints::{ dynamic_dimension_text, ConstraintKind, ParametricRef, }; let scope = self.tabs[i].current_parametric_scope(); let kind = if diameter { ConstraintKind::Diameter } else { ConstraintKind::Radius }; let refs = vec![circle]; // A circle already carrying a radius or diameter constraint // is refused after the value, as the reference does. let duplicate = self.tabs[i] .scene .parametric_constraint_set(scope) .is_some_and(|set| { set.constraints.iter().any(|c| { c.enabled && matches!( c.kind, ConstraintKind::Radius | ConstraintKind::Diameter ) && c.refs.iter().any(|r| r.entity == circle.entity) }) }); if duplicate { self.command_line .push_output("The constraint already exists on the selected objects."); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; return Some(Task::none()); } if let Err(message) = self.tabs[i].scene.validate_parametric_constraint( kind, &refs, Some(&DrivingValue::Literal(1.0)), ) { self.reprompt_active_command(i, message); return Some(Task::none()); } let name = name.trim().to_string(); if !is_valid_name(&name) { self.command_line.push_error( "Only alphanumeric names, starting with alpha characters, are allowed.", ); self.reprompt_active_command(i, &format!("Invalid parameter name: {name}.")); return Some(Task::none()); } if renamed && self.tabs[i].scene.named_parameters().contains(&name) { self.reprompt_active_command(i, "Parameter with this name already exists."); return Some(Task::none()); } let mut table = self.tabs[i].scene.named_parameters().clone(); if let Err(error) = table.set(&name, &expression) { self.reprompt_active_command(i, &error.to_string()); return Some(Task::none()); } // A circle has no zero or negative size: the value is // refused and the prompt comes back. let value = match table.resolve(&name) { Ok(value) if value.is_finite() && value > 0.0 => value, _ => { self.command_line.push_error("Invalid expression."); self.reprompt_active_command( i, "The parameter is used in an expression which results in an invalid value for a dimensional constraint.", ); return Some(Task::none()); } }; let annotational = self.constraint_form_annotational; let text = dynamic_dimension_text( &name, value, self.tabs[i].scene.constraint_name_format, false, Some(&expression), annotational, None, ); let entity = if diameter { diameter_dim::diameter_constraint_entity(center, radius, location, Some(text)) } else { radius_dim::radius_constraint_entity(center, radius, location, Some(text)) }; let Some(mut entity) = entity else { self.reprompt_active_command(i, "No valid constraint point found."); return Some(Task::none()); }; crate::scene::creation_style::apply_current_creation_styles( &self.tabs[i].scene.document, &mut entity, ); if annotational { entity .as_entity_mut() .set_layer(self.tabs[i].active_layer.clone()); } else { self.tabs[i].scene.ensure_dynamic_dimension_layer(); entity.as_entity_mut().set_layer( crate::scene::parametric_constraints::DYNAMIC_DIMENSION_LAYER.to_string(), ); entity.common_mut().color = codec::types::Color::Rgb { r: 103, g: 109, b: 118, }; } let constraints_before = self.tabs[i] .scene .parametric_constraint_set(scope) .cloned() .unwrap_or_else(|| { crate::scene::parametric_constraints::ParametricConstraintSet::new(scope) }); // The centre stays where it is; the value moves the rim. let anchors = vec![ParametricRef::center(circle.entity)]; let pending = self.begin_undo( i, if diameter { "Diameter constraint" } else { "Radius constraint" }, 2, false, ); self.tabs[i] .scene .record_undo_parametric_constraints_before(scope, constraints_before); self.tabs[i].scene.record_undo_named_parameters_before(); self.tabs[i].scene.named_parameters = table; let dimension = self.tabs[i].scene.add_entity(entity); let set = self.tabs[i].scene.parametric_constraint_set_mut(scope); let id = set.add(kind, refs, Some(DrivingValue::Named(name))); set.dimensions.insert(id, dimension); self.tabs[i].scene.note_parametric_constraint_applied( scope, id, self.constraint_bar_display, ); self.tabs[i] .scene .attach_dimension_association(dimension, vec![Some(circle.entity)]); let changes = vec![(circle.entity, crate::scene::ChangeKind::Modified)]; self.tabs[i] .scene .bump_entities_with_parametric_policy(&changes, &anchors, false); self.tabs[i].scene.refresh_hidden_dynamic_dimensions(); self.tabs[i].scene.refresh_dynamic_dimension_scales(true); self.tabs[i].dirty = true; self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.refresh_properties(); if let Some(pd) = pending { self.commit_undo_delta(i, pd); } None } pub(super) fn handle_add_angular_constraint(&mut self, result: CmdResult) -> Option> { let i = self.active_tab; let CmdResult::AddAngularConstraint { refs, points, location, sector, name, expression, renamed, } = result else { unreachable!("router only routes the matching variant"); }; use crate::modules::annotate::angular_dim; use crate::scene::named_parameters::{is_valid_name, DrivingValue}; use crate::scene::parametric_constraints::{ angle_decimals, dynamic_dimension_text, ConstraintKind, ParametricRef, }; let scope = self.tabs[i].current_parametric_scope(); let kind = if refs.len() == 2 { ConstraintKind::Angle } else { ConstraintKind::Angle3Point }; // The same angle between the same sides is refused after the // value (whichever sector), and the command ends. let duplicate = self.tabs[i] .scene .parametric_constraint_set(scope) .is_some_and(|set| { set.constraints.iter().any(|c| { c.enabled && c.kind == kind && c.refs.len() == refs.len() && if kind == ConstraintKind::Angle { c.refs.contains(&refs[0]) && c.refs.contains(&refs[1]) } else { c.refs == refs } }) }); if duplicate { self.command_line .push_output("The constraint already exists on the selected objects."); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; return Some(Task::none()); } if let Err(message) = self.tabs[i].scene.validate_parametric_constraint( kind, &refs, Some(&DrivingValue::Literal(1.0)), ) { self.reprompt_active_command(i, message); return Some(Task::none()); } let name = name.trim().to_string(); if !is_valid_name(&name) { self.command_line.push_error( "Only alphanumeric names, starting with alpha characters, are allowed.", ); self.reprompt_active_command(i, &format!("Invalid parameter name: {name}.")); return Some(Task::none()); } if renamed && self.tabs[i].scene.named_parameters().contains(&name) { self.reprompt_active_command(i, "Parameter with this name already exists."); return Some(Task::none()); } let mut table = self.tabs[i].scene.named_parameters().clone(); if let Err(error) = table.set(&name, &expression) { self.reprompt_active_command(i, &error.to_string()); return Some(Task::none()); } let value = match table.resolve(&name) { Ok(value) if value.is_finite() => value, _ => { self.command_line.push_error("Invalid expression."); self.reprompt_active_command( i, "The parameter is used in an expression which results in an invalid value for a dimensional constraint.", ); return Some(Task::none()); } }; let annotational = self.constraint_form_annotational; let decimals = angle_decimals(&self.tabs[i].scene.document, None); let text = dynamic_dimension_text( &name, value, self.tabs[i].scene.constraint_name_format, false, Some(&expression), annotational, Some(decimals), ); let entity = if kind == ConstraintKind::Angle { angular_dim::angular_two_line_entity( points[0], points[1], points[2], points[3], location, Some(text), ) } else { angular_dim::angular_three_point_entity( points[0], points[1], points[2], location, Some(text), ) }; let Some(mut entity) = entity else { self.reprompt_active_command(i, "No valid constraint point found."); return Some(Task::none()); }; crate::scene::creation_style::apply_current_creation_styles( &self.tabs[i].scene.document, &mut entity, ); if annotational { entity .as_entity_mut() .set_layer(self.tabs[i].active_layer.clone()); } else { self.tabs[i].scene.ensure_dynamic_dimension_layer(); entity.as_entity_mut().set_layer( crate::scene::parametric_constraints::DYNAMIC_DIMENSION_LAYER.to_string(), ); entity.common_mut().color = codec::types::Color::Rgb { r: 103, g: 109, b: 118, }; } let constraints_before = self.tabs[i] .scene .parametric_constraint_set(scope) .cloned() .unwrap_or_else(|| { crate::scene::parametric_constraints::ParametricConstraintSet::new(scope) }); let mut touched: Vec = Vec::new(); for reference in &refs { if !touched.contains(&reference.entity) { touched.push(reference.entity); } } // The first side (or the vertex and first point) stays; a // value other than the measured one turns the other side. let anchors: Vec = crate::scene::parametric_constraints::dimensional_anchor_refs( &self.tabs[i].scene.document, &refs, ); let association = if kind == ConstraintKind::Angle { vec![ Some(refs[0].entity), Some(refs[0].entity), Some(refs[1].entity), Some(refs[1].entity), ] } else { vec![ Some(refs[1].entity), Some(refs[0].entity), Some(refs[2].entity), ] }; let pending = self.begin_undo(i, "Angular constraint", touched.len() + 1, false); self.tabs[i] .scene .record_undo_parametric_constraints_before(scope, constraints_before); self.tabs[i].scene.record_undo_named_parameters_before(); self.tabs[i].scene.named_parameters = table; let dimension = self.tabs[i].scene.add_entity(entity); let set = self.tabs[i].scene.parametric_constraint_set_mut(scope); let id = set.add(kind, refs, Some(DrivingValue::Named(name))); if let Some(constraint) = set.constraints.iter_mut().find(|c| c.id == id) { constraint.angle_sector = sector; } set.dimensions.insert(id, dimension); self.tabs[i].scene.note_parametric_constraint_applied( scope, id, self.constraint_bar_display, ); self.tabs[i] .scene .attach_dimension_association(dimension, association); let changes: Vec<(Handle, crate::scene::ChangeKind)> = touched .into_iter() .map(|handle| (handle, crate::scene::ChangeKind::Modified)) .collect(); self.tabs[i] .scene .bump_entities_with_parametric_policy(&changes, &anchors, false); self.tabs[i].scene.refresh_hidden_dynamic_dimensions(); self.tabs[i].scene.refresh_dynamic_dimension_scales(true); self.tabs[i].dirty = true; self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.refresh_properties(); if let Some(pd) = pending { self.commit_undo_delta(i, pd); } None } pub(super) fn handle_make_parallel(&mut self, result: CmdResult) -> Task { let i = self.active_tab; let CmdResult::MakeParallel { first_line, first_ends, first_pick, second_line, second_ends, } = result else { unreachable!("router only routes the matching variant"); }; use crate::scene::parametric_constraints::{ resolve_point, two_lines_placed_entity, ConstraintKind, }; let scope = self.tabs[i].current_parametric_scope(); let refs = [first_line, second_line]; if let Err(message) = self.tabs[i] .scene .validate_parametric_constraint(ConstraintKind::Parallel, &refs, None) { return self.apply_cmd_result(CmdResult::ReportError(message.to_string())); } // The reference's placement of the second line, written // before the Parallel constraint holds it there. Built out of // line and boxed: this arm's locals would otherwise sit in // `apply_cmd_result`'s frame, which is on every click's stack. let placed_entity = two_lines_placed_entity( &self.tabs[i].scene.document, first_ends, first_pick, second_line, second_ends, ); let already = self.tabs[i] .scene .parametric_constraint_set(scope) .is_some_and(|set| { set.constraints.iter().any(|c| { c.enabled && c.kind == ConstraintKind::Parallel && c.refs.contains(&first_line) && c.refs.contains(&second_line) }) }); if !already || placed_entity.is_some() { let constraints_before = self.tabs[i] .scene .parametric_constraint_set(scope) .cloned() .unwrap_or_else(|| { crate::scene::parametric_constraints::ParametricConstraintSet::new( scope, ) }); let pending = self.begin_undo(i, "Parallel constraint", 1, true); if let Some(entity) = placed_entity { self.tabs[i].scene.replace_entity_recorded(entity); } self.tabs[i] .scene .record_undo_parametric_constraints_before(scope, constraints_before); if !already { let set = self.tabs[i].scene.parametric_constraint_set_mut(scope); let id = set.add(ConstraintKind::Parallel, refs.to_vec(), None); self.tabs[i].scene.note_parametric_constraint_applied( scope, id, self.constraint_bar_display, ); } // Both lines' ends hold the placement; the solve only // settles whatever else is tied to them. let mut pins = first_ends.to_vec(); pins.extend(second_ends); self.tabs[i].scene.bump_entities_with_parametric_policy( &[(second_line.entity, crate::scene::ChangeKind::Modified)], &pins, self.constraint_solve_mode, ); self.tabs[i].dirty = true; if let Some(pd) = pending { self.commit_undo_delta(i, pd); } } let ends = { let document = &self.tabs[i].scene.document; document.get_entity(second_line.entity).and_then(|entity| { let world = |reference: crate::scene::parametric_constraints::ParametricRef| { let point = resolve_point(entity, reference.marker?)?; Some((reference, glam::DVec3::new(point.x, point.y, point.z))) }; Some([world(second_ends[0])?, world(second_ends[1])?]) }) }; let Some(ends) = ends else { return self.apply_cmd_result(CmdResult::ReportError( crate::modules::parametric::DimConstraintCommand::NO_OBJECT.to_string(), )); }; let result = self.tabs[i] .active_cmd .as_mut() .map(|command| command.accept_parallel_line(ends)); match result { Some(result) => self.apply_cmd_result(result), None => Task::none(), } } pub(super) fn handle_add_fixed_constraint( &mut self, pick: crate::command::CoincidentPick, ) -> Task { let i = self.active_tab; use crate::modules::parametric::FixConstraintCommand; use crate::scene::parametric_constraints::{ nearest_parametric_point, nearest_parametric_point_on_entity, parametric_curve_ref_for_pick, ConstraintKind, }; let scope = self.tabs[i].current_parametric_scope(); let document = &self.tabs[i].scene.document; let world = codec::types::Vector3::new(pick.point.x, pick.point.y, pick.point.z); let resolved = match (pick.whole_curve, pick.handle) { (true, Some(handle)) => { parametric_curve_ref_for_pick(document, scope, handle, world) .ok_or(FixConstraintCommand::INVALID_OBJECT) } (true, None) => Err(FixConstraintCommand::NO_OBJECT), (false, Some(handle)) => { nearest_parametric_point_on_entity(document, scope, handle, world) .ok_or(FixConstraintCommand::INVALID_OBJECT) } (false, None) => nearest_parametric_point(document, scope, world, None) .ok_or(FixConstraintCommand::NO_POINT), }; // An off-plane or 3D curve is "not a valid object" to the // reference, not a solver limitation. let resolved = resolved.and_then(|reference| { let supported = !pick.whole_curve || self.tabs[i] .scene .validate_parametric_constraint( ConstraintKind::Fixed, &[reference], None, ) .is_ok(); supported .then_some(reference) .ok_or(FixConstraintCommand::INVALID_OBJECT) }); match resolved { Ok(reference) => self.apply_cmd_result(CmdResult::AddParametricConstraint { kind: ConstraintKind::Fixed, refs: vec![reference], driving_param: None, label: "Fixed constraint", }), // A miss re-prompts: `ReportError` keeps the command. Err(message) => { self.apply_cmd_result(CmdResult::ReportError(message.to_string())) } } } pub(super) fn handle_check_horizontal_point( &mut self, kind: crate::scene::parametric_constraints::ConstraintKind, pick: crate::command::CoincidentPick, ) -> Task { let i = self.active_tab; use crate::modules::parametric::HorizontalConstraintCommand; use crate::scene::parametric_constraints::nearest_parametric_point; // The reference rejects a missed first point right away and // asks for it again; a hit moves on to the second point. let scope = self.tabs[i].current_parametric_scope(); let found = nearest_parametric_point( &self.tabs[i].scene.document, scope, codec::types::Vector3::new(pick.point.x, pick.point.y, pick.point.z), None, ) .is_some(); if !found { self.command_line .push_error("No valid constraint point found."); } let command = HorizontalConstraintCommand::resume(kind, found.then_some(pick)); self.command_line .push_info(&crate::command::CadCommand::prompt(&command)); self.tabs[i].active_cmd = Some(Box::new(command)); self.tabs[i].snap_result = None; Task::none() } pub(super) fn handle_add_horizontal_constraint(&mut self, result: CmdResult) -> Option> { let i = self.active_tab; let CmdResult::AddHorizontalConstraint { kind, selection, direction, label, } = result else { unreachable!("router only routes the matching variant"); }; use crate::command::{EntityTransform, HorizontalConstraintSelection}; use crate::modules::parametric::HorizontalConstraintCommand; use crate::scene::parametric_constraints::{ nearest_parametric_point, nearest_parametric_point_on_entity, resolve_point, ConstraintKind, DirectionalAxis, ParametricRef, }; let vertical = kind == ConstraintKind::Vertical; let axis = if vertical { "Vertical" } else { "Horizontal" }; let scope = self.tabs[i].current_parametric_scope(); let to_world = |point: glam::DVec3| { codec::types::Vector3::new(point.x, point.y, point.z) }; let (refs, initial_fixed) = match selection { HorizontalConstraintSelection::Reference(reference) => { // The reference turns the object about its first // vertex: a line's start, the picked polyline // segment's first vertex, a text's insertion point, // an ellipse's center. let initial_fixed = match reference.directional_axis() { Some(DirectionalAxis::EllipseMajor | DirectionalAxis::EllipseMinor) => { vec![ParametricRef::center(reference.entity)] } Some(DirectionalAxis::TextBaseline) => { vec![ParametricRef::point(reference.entity, 0)] } None => vec![ParametricRef::point( reference.entity, reference.segment_index().map_or(0, |index| index as i32), )], }; (vec![reference], initial_fixed) } HorizontalConstraintSelection::Points(first, second) => { let resolve = |pick: crate::command::CoincidentPick| { if let Some(handle) = pick.handle { nearest_parametric_point_on_entity( &self.tabs[i].scene.document, scope, handle, to_world(pick.point), ) } else { nearest_parametric_point( &self.tabs[i].scene.document, scope, to_world(pick.point), None, ) } }; // A miss asks for that point again, keeping a good // first pick; the same point twice asks for another // second point — the reference's own re-prompts. let (first_ref, second_ref) = match (resolve(first), resolve(second)) { (Some(first_ref), Some(second_ref)) if first_ref != second_ref => { (first_ref, second_ref) } (first_ref, second_ref) => { let (message, keep_first) = if first_ref.is_none() { ("No valid constraint point found.", None) } else if second_ref.is_none() { ("No valid constraint point found.", Some(first)) } else { ( "The object or point is already selected. Select a different object or constraint point.", Some(first), ) }; let command = HorizontalConstraintCommand::resume(kind, keep_first); self.command_line.push_error(message); self.command_line .push_info(&crate::command::CadCommand::prompt(&command)); self.tabs[i].active_cmd = Some(Box::new(command)); self.tabs[i].snap_result = None; return Some(Task::none()); } }; (vec![first_ref, second_ref], vec![first_ref]) } }; let axis_length = direction.x.hypot(direction.y); if axis_length <= 1.0e-12 { self.command_line.push_error(&format!( "{axis}: the current UCS {} axis is not supported.", if vertical { "Y" } else { "X" } )); return Some(Task::none()); } let direction = codec::types::Vector3::new( direction.x / axis_length, direction.y / axis_length, 0.0, ); if let Err(message) = self.tabs[i].scene.validate_parametric_constraint(kind, &refs, None) { self.command_line.push_error(message); return Some(Task::none()); } if self .tabs[i] .scene .parametric_constraint_set(scope) .is_some_and(|set| set.contains_axis_constraint(kind, &refs, direction)) { self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.command_line .push_error("The constraint already exists on the selected objects."); return Some(Task::none()); } let constraints_before = self.tabs[i] .scene .parametric_constraint_set(scope) .cloned() .unwrap_or_else(|| { crate::scene::parametric_constraints::ParametricConstraintSet::new(scope) }); let mut touched = Vec::new(); for reference in &refs { if !touched.contains(&reference.entity) { touched.push(reference.entity); } } let pending = self.begin_undo(i, label, touched.len(), true); self.tabs[i] .scene .record_undo_parametric_constraints_before(scope, constraints_before); // Two points on different objects: the reference slides the // second object rigidly onto the axis through the first // point instead of re-solving its shape, so move it first // and let the constraint then hold what already fits. if let [first_ref, second_ref] = refs.as_slice() { if first_ref.entity != second_ref.entity { let position = |reference: &ParametricRef| { self.tabs[i] .scene .document .get_entity(reference.entity) .zip(reference.marker) .and_then(|(entity, marker)| resolve_point(entity, marker)) }; if let (Some(first_point), Some(second_point)) = (position(first_ref), position(second_ref)) { let normal = (-direction.y, direction.x); let offset = (second_point.x - first_point.x) * normal.0 + (second_point.y - first_point.y) * normal.1; if offset.abs() > 1.0e-9 { self.tabs[i].scene.transform_entities( &[second_ref.entity], &EntityTransform::Translate(glam::DVec3::new( -offset * normal.0, -offset * normal.1, 0.0, )), ); } } } } // The solver cannot start from an axis lying exactly across // the datum (its equations are singular there), so turn the // object onto the axis about its anchor first — the // reference turns it about that anchor too — and let the // constraint hold what already fits. if let Some((handle, anchor, end, vertex)) = crate::scene::parametric_constraints::axis_alignment_target( &self.tabs[i].scene.document, &refs, ) { let axis = (end.x - anchor.x, end.y - anchor.y); let length = axis.0.hypot(axis.1); let target = if axis.0 * direction.x + axis.1 * direction.y >= 0.0 { (direction.x, direction.y) } else { (-direction.x, -direction.y) }; let angle = (axis.0 * target.1 - axis.1 * target.0) .atan2(axis.0 * target.0 + axis.1 * target.1); if length > 1.0e-9 && angle.abs() > 1.0e-9 { match vertex { Some(index) => { let moved = self.tabs[i].scene.document.get_entity(handle).cloned(); if let Some(mut entity) = moved { if crate::scene::parametric_constraints::set_polyline_vertex( &mut entity, index, anchor.x + length * target.0, anchor.y + length * target.1, ) { self.tabs[i].scene.update_entity(entity); } } } None => self.tabs[i].scene.transform_entities( &[handle], &EntityTransform::Rotate { center: glam::DVec3::new(anchor.x, anchor.y, anchor.z), axis: glam::DVec3::Z, angle_rad: angle, }, ), } } } let id = self.tabs[i] .scene .parametric_constraint_set_mut(scope) .add_axis_constraint(kind, refs, direction); self.tabs[i].scene.note_parametric_constraint_applied( scope, id, self.constraint_bar_display, ); let changes = touched .into_iter() .map(|handle| (handle, crate::scene::ChangeKind::Modified)) .collect::>(); self.tabs[i].scene.bump_entities_with_parametric_policy( &changes, &initial_fixed, self.constraint_solve_mode, ); self.tabs[i].dirty = true; self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.command_line .push_output(&format!("{axis} constraint applied.")); self.refresh_properties(); if let Some(pd) = pending { self.commit_undo_delta(i, pd); } None } pub(super) fn handle_add_symmetric_constraint( &mut self, selection: crate::command::SymmetricConstraintSelection, axis: crate::scene::parametric_constraints::ParametricRef, label: &'static str, ) -> Option> { let i = self.active_tab; use crate::command::SymmetricConstraintSelection; use crate::scene::parametric_constraints::ConstraintKind; let scope = self.tabs[i].current_parametric_scope(); let to_world = |point: glam::DVec3| { codec::types::Vector3::new(point.x, point.y, point.z) }; let resolve_point = |pick: crate::command::CoincidentPick| { if let Some(handle) = pick.handle { crate::scene::parametric_constraints::nearest_parametric_point_on_entity( &self.tabs[i].scene.document, scope, handle, to_world(pick.point), ) } else { crate::scene::parametric_constraints::nearest_parametric_point( &self.tabs[i].scene.document, scope, to_world(pick.point), None, ) } }; let (first, second) = match selection { SymmetricConstraintSelection::Objects(first, second) => (first, second), SymmetricConstraintSelection::Points(first, second) => { let (Some(first), Some(second)) = (resolve_point(first), resolve_point(second)) else { self.command_line.push_error( "Symmetric: select supported endpoints, centers, midpoints, or vertices.", ); return Some(Task::none()); }; (first, second) } }; if first == second || axis.entity == first.entity || axis.entity == second.entity { self.command_line .push_error("Symmetric: select two different references and a separate line axis."); return Some(Task::none()); } let refs = vec![first, second, axis]; if let Err(message) = self.tabs[i].scene.validate_parametric_constraint( ConstraintKind::Symmetric, &refs, None, ) { self.command_line.push_error(message); return Some(Task::none()); } let duplicate = self.tabs[i] .scene .parametric_constraint_set(scope) .is_some_and(|set| { set.constraints.iter().any(|constraint| { constraint.enabled && constraint.kind == ConstraintKind::Symmetric && matches!(constraint.refs.as_slice(), [a, b, m] if *m == axis && ((*a == first && *b == second) || (*a == second && *b == first))) }) }); if duplicate { self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.command_line .push_error("The constraint already exists on the selected objects."); return Some(Task::none()); } let constraints_before = self.tabs[i] .scene .parametric_constraint_set(scope) .cloned() .unwrap_or_else(|| { crate::scene::parametric_constraints::ParametricConstraintSet::new(scope) }); let mut touched = Vec::new(); for reference in &refs { if !touched.contains(&reference.entity) { touched.push(reference.entity); } } let pending = self.begin_undo(i, label, touched.len(), true); self.tabs[i] .scene .record_undo_parametric_constraints_before(scope, constraints_before); let id = self.tabs[i].scene.parametric_constraint_set_mut(scope).add( ConstraintKind::Symmetric, refs, None, ); self.tabs[i].scene.note_parametric_constraint_applied( scope, id, self.constraint_bar_display, ); let changes = touched .into_iter() .map(|handle| (handle, crate::scene::ChangeKind::Modified)) .collect::>(); self.tabs[i].scene.bump_entities_with_initial_parametric_policy( &changes, &[first, axis], false, ); self.tabs[i].dirty = true; self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.command_line .push_output("Symmetric constraint applied."); self.refresh_properties(); if let Some(pd) = pending { self.commit_undo_delta(i, pd); } None } pub(super) fn handle_add_perpendicular_constraint(&mut self, result: CmdResult) -> Option> { let i = self.active_tab; let CmdResult::AddPerpendicularConstraint { first, second, first_fixed, second_start, label, } = result else { unreachable!("router only routes the matching variant"); }; use crate::scene::parametric_constraints::ConstraintKind; let refs = vec![first, second]; if let Err(message) = self.tabs[i].scene.validate_parametric_constraint( ConstraintKind::Perpendicular, &refs, None, ) { self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.command_line.push_error(message); return Some(Task::none()); } let scope = self.tabs[i].current_parametric_scope(); let constraints_before = self.tabs[i] .scene .parametric_constraint_set(scope) .cloned() .unwrap_or_else(|| { crate::scene::parametric_constraints::ParametricConstraintSet::new(scope) }); let touched = if first.entity == second.entity { vec![first.entity] } else { vec![first.entity, second.entity] }; let pending = self.begin_undo(i, label, touched.len(), true); self.tabs[i] .scene .record_undo_parametric_constraints_before(scope, constraints_before); let id = self.tabs[i].scene.parametric_constraint_set_mut(scope).add( ConstraintKind::Perpendicular, refs, None, ); self.tabs[i].scene.note_parametric_constraint_applied( scope, id, self.constraint_bar_display, ); let changes = touched .into_iter() .map(|handle| (handle, crate::scene::ChangeKind::Modified)) .collect::>(); self.tabs[i].scene.bump_entities_with_initial_parametric_policy( &changes, &[first_fixed, second_start], true, ); self.tabs[i].dirty = true; self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.command_line.push_output("Constraint applied."); self.refresh_properties(); if let Some(pd) = pending { self.commit_undo_delta(i, pd); } None } pub(super) fn handle_add_tangent_constraint( &mut self, first: crate::scene::parametric_constraints::ParametricRef, second: crate::scene::parametric_constraints::ParametricRef, label: &'static str, ) -> Option> { let i = self.active_tab; use crate::scene::parametric_constraints::ConstraintKind; let refs = vec![first, second]; if let Err(message) = self.tabs[i].scene.validate_parametric_constraint( ConstraintKind::Tangent, &refs, None, ) { self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.command_line.push_error(message); return Some(Task::none()); } let scope = self.tabs[i].current_parametric_scope(); let constraints_before = self.tabs[i] .scene .parametric_constraint_set(scope) .cloned() .unwrap_or_else(|| { crate::scene::parametric_constraints::ParametricConstraintSet::new(scope) }); let touched = if first.entity == second.entity { vec![first.entity] } else { vec![first.entity, second.entity] }; let pending = self.begin_undo(i, label, touched.len(), true); self.tabs[i] .scene .record_undo_parametric_constraints_before(scope, constraints_before); let id = self.tabs[i].scene.parametric_constraint_set_mut(scope).add( ConstraintKind::Tangent, refs, None, ); self.tabs[i].scene.note_parametric_constraint_applied( scope, id, self.constraint_bar_display, ); let changes = touched .into_iter() .map(|handle| (handle, crate::scene::ChangeKind::Modified)) .collect::>(); self.tabs[i].scene.bump_entities_with_initial_parametric_policy( &changes, &[first], true, ); self.tabs[i].dirty = true; self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.command_line.push_output("Constraint applied."); self.refresh_properties(); if let Some(pd) = pending { self.commit_undo_delta(i, pd); } None } pub(super) fn handle_add_concentric_constraint( &mut self, first: crate::scene::parametric_constraints::ParametricRef, second: crate::scene::parametric_constraints::ParametricRef, label: &'static str, ) -> Option> { let i = self.active_tab; use crate::scene::parametric_constraints::ConstraintKind; let refs = vec![first, second]; if let Err(message) = self.tabs[i].scene.validate_parametric_constraint( ConstraintKind::Concentric, &refs, None, ) { self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.command_line.push_error(message); return Some(Task::none()); } let scope = self.tabs[i].current_parametric_scope(); let duplicate = self.tabs[i] .scene .parametric_constraint_set(scope) .is_some_and(|set| { set.constraints.iter().any(|constraint| { constraint.enabled && constraint.kind == ConstraintKind::Concentric && matches!(constraint.refs.as_slice(), [a, b] if (*a == first && *b == second) || (*a == second && *b == first)) }) }); if duplicate { self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.command_line .push_output("The Concentric constraint already exists."); return Some(Task::none()); } let constraints_before = self.tabs[i] .scene .parametric_constraint_set(scope) .cloned() .unwrap_or_else(|| { crate::scene::parametric_constraints::ParametricConstraintSet::new(scope) }); let touched = if first.entity == second.entity { vec![first.entity] } else { vec![first.entity, second.entity] }; let pending = self.begin_undo(i, label, touched.len(), true); self.tabs[i] .scene .record_undo_parametric_constraints_before(scope, constraints_before); let id = self.tabs[i].scene.parametric_constraint_set_mut(scope).add( ConstraintKind::Concentric, refs, None, ); self.tabs[i].scene.note_parametric_constraint_applied( scope, id, self.constraint_bar_display, ); let changes = touched .into_iter() .map(|handle| (handle, crate::scene::ChangeKind::Modified)) .collect::>(); self.tabs[i].scene.bump_entities_with_initial_parametric_policy( &changes, &[first], true, ); self.tabs[i].dirty = true; self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.command_line.push_output("Constraint applied."); self.refresh_properties(); if let Some(pd) = pending { self.commit_undo_delta(i, pd); } None } pub(super) fn handle_add_coincident_constraint(&mut self, result: CmdResult) -> Option> { let i = self.active_tab; let CmdResult::AddCoincidentConstraint { first, second, multiple, label, } = result else { unreachable!("router only routes the matching variant"); }; let scope = self.tabs[i].current_parametric_scope(); let to_world = |p: glam::DVec3| codec::types::Vector3::new(p.x, p.y, p.z); let resolve = |pick: crate::command::CoincidentPick, exclude: Option| { if pick.whole_curve { pick.handle.and_then(|handle| { crate::scene::parametric_constraints::parametric_curve_ref_for_pick( &self.tabs[i].scene.document, scope, handle, to_world(pick.point), ) }) } else if let Some(handle) = pick.handle { crate::scene::parametric_constraints::nearest_parametric_point_on_entity( &self.tabs[i].scene.document, scope, handle, to_world(pick.point), ) } else { crate::scene::parametric_constraints::nearest_parametric_point( &self.tabs[i].scene.document, scope, to_world(pick.point), exclude, ) } }; let resolved_first = resolve(first, None); let resolved_second = resolved_first .and_then(|reference| resolve(second, Some(reference.entity))); let (Some(first_ref), Some(second_ref)) = (resolved_first, resolved_second) else { self.command_line.push_error( "Coincident: select a supported endpoint, center, midpoint, vertex, or curve.", ); return Some(Task::none()); }; let (kind, refs) = match (first.whole_curve, second.whole_curve) { (false, false) if first_ref != second_ref => ( crate::scene::parametric_constraints::ConstraintKind::Coincident, vec![first_ref, second_ref], ), (true, false) => ( crate::scene::parametric_constraints::ConstraintKind::PointOnCurve, vec![second_ref, first_ref], ), (false, true) => ( crate::scene::parametric_constraints::ConstraintKind::PointOnCurve, vec![first_ref, second_ref], ), _ => { self.command_line .push_error("Coincident: select one point and one curve, or two points."); return Some(Task::none()); } }; if let Err(error) = self.tabs[i] .scene .validate_parametric_constraint(kind, &refs, None) { self.command_line.push_error(error); return Some(Task::none()); } let touched: Vec<_> = refs.iter().map(|reference| reference.entity).collect(); let constraints_before = self.tabs[i] .scene .parametric_constraint_set(scope) .cloned() .unwrap_or_else(|| { crate::scene::parametric_constraints::ParametricConstraintSet::new(scope) }); let pending = self.begin_undo(i, label, touched.len(), true); self.tabs[i] .scene .record_undo_parametric_constraints_before(scope, constraints_before); let id = self.tabs[i] .scene .parametric_constraint_set_mut(scope) .add(kind, refs, None); self.tabs[i].scene.note_parametric_constraint_applied( scope, id, self.constraint_bar_display, ); let changes: Vec<_> = touched .iter() .copied() .map(|handle| (handle, crate::scene::ChangeKind::Modified)) .collect(); self.tabs[i].scene.bump_entities_with_parametric_policy( &changes, &[first_ref], self.constraint_solve_mode, ); self.tabs[i].dirty = true; self.tabs[i].snap_result = None; if !multiple { self.tabs[i].active_cmd = None; } self.command_line.push_output("Coincident constraint applied."); if multiple { if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|cmd| cmd.prompt()) { self.command_line.push_info(&prompt); } self.command_line.set_step_options( self.tabs[i] .active_cmd .as_ref() .map(|cmd| cmd.options()) .unwrap_or_default(), ); } self.refresh_properties(); if let Some(pd) = pending { self.commit_undo_delta(i, pd); } None } pub(super) fn handle_add_auto_coincident_constraints(&mut self, handles: Vec) { let i = self.active_tab; use crate::scene::parametric_constraints::ConstraintKind; let scope = self.tabs[i].current_parametric_scope(); let inferred = self.tabs[i] .scene .inferred_coincident_constraints(scope, &handles); let count = inferred.len(); if count > 0 { let constraints_before = self.tabs[i] .scene .parametric_constraint_set(scope) .cloned() .unwrap_or_else(|| { crate::scene::parametric_constraints::ParametricConstraintSet::new( scope, ) }); let pending = self.begin_undo(i, "Coincident auto constrain", handles.len(), true); self.tabs[i] .scene .record_undo_parametric_constraints_before(scope, constraints_before); for refs in inferred { let id = self.tabs[i] .scene .parametric_constraint_set_mut(scope) .add(ConstraintKind::Coincident, refs, None); self.tabs[i].scene.note_parametric_constraint_applied( scope, id, self.constraint_bar_display, ); } let changes: Vec<_> = handles .iter() .copied() .map(|handle| (handle, crate::scene::ChangeKind::Modified)) .collect(); self.tabs[i].scene.bump_entities(&changes); self.tabs[i].dirty = true; self.refresh_properties(); if let Some(pd) = pending { self.commit_undo_delta(i, pd); } } self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; self.command_line .push_output(format!("{} coincident constraint(s) applied.", count).as_str()); } pub(super) fn handle_add_point_on_entity_constraint( &mut self, result: CmdResult, ) -> Option> { let i = self.active_tab; let CmdResult::AddPointOnEntityConstraint { point, target, kind, label, } = result else { unreachable!("router only routes the matching variant"); }; let scope = self.tabs[i].current_parametric_scope(); let to_world = |p: glam::DVec3| codec::types::Vector3::new(p.x, p.y, p.z); let resolved = crate::scene::parametric_constraints::nearest_parametric_point( &self.tabs[i].scene.document, scope, to_world(point), Some(target), ); match resolved { Some(point_ref) => { use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef}; // `CenterPoint` addresses its whole-entity side via // the center marker (same solve as Coincident / // Concentric — `ConstraintKind::CenterPoint`'s own // doc comment); `Midpoint`/`PointOnCurve` address it // as a whole entity. let target_ref = if kind == ConstraintKind::CenterPoint { ParametricRef::center(target) } else { ParametricRef::whole(target) }; return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint { kind, refs: vec![point_ref, target_ref], driving_param: None, label, })); } None => { self.command_line.push_error( "Pick didn't land on a point (endpoint, center, or vertex) — enable an Endpoint/Center object snap and try again.", ); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; } } None } pub(super) fn handle_add_equal_distance_constraint( &mut self, points: [glam::DVec3; 4], label: &'static str, ) -> Option> { let i = self.active_tab; let scope = self.tabs[i].current_parametric_scope(); let to_world = |p: glam::DVec3| codec::types::Vector3::new(p.x, p.y, p.z); let mut refs = Vec::with_capacity(4); for p in points { let Some(r) = crate::scene::parametric_constraints::nearest_parametric_point( &self.tabs[i].scene.document, scope, to_world(p), None, ) else { refs.clear(); break; }; refs.push(r); } if refs.len() == 4 { return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint { kind: crate::scene::parametric_constraints::ConstraintKind::EqualDistance, refs, driving_param: None, label, })); } self.command_line.push_error( "Equal Distance: a pick didn't land on a point (endpoint, center, or vertex) — enable an Endpoint/Center object snap and try again.", ); self.tabs[i].active_cmd = None; self.tabs[i].snap_result = None; None } }