forked from 1b-agent/opencadstudio-upstream
1948 lines
76 KiB
Rust
1948 lines
76 KiB
Rust
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<Task<Message>> {
|
|
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<Handle> = 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<Task<Message>> {
|
|
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<ParametricRef> = 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::<Vec<_>>();
|
|
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<Message> {
|
|
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<Task<Message>> {
|
|
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<Task<Message>> {
|
|
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<Task<Message>> {
|
|
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<Handle> = 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<ParametricRef> =
|
|
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<Message> {
|
|
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<Message> {
|
|
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<Message> {
|
|
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<Task<Message>> {
|
|
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::<Vec<_>>();
|
|
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<Task<Message>> {
|
|
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::<Vec<_>>();
|
|
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<Task<Message>> {
|
|
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::<Vec<_>>();
|
|
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<Task<Message>> {
|
|
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::<Vec<_>>();
|
|
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<Task<Message>> {
|
|
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::<Vec<_>>();
|
|
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<Task<Message>> {
|
|
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<Handle>| {
|
|
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<Handle>) {
|
|
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<Task<Message>> {
|
|
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<Task<Message>> {
|
|
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
|
|
}
|
|
}
|