forked from 1b-agent/opencadstudio-upstream
Merge pull request #1413 from ramox81/codex/linear-constraint-parity
Linear dimensional constraints: point flow, dynamic dimension, driving parameter, persistence # Conflicts: # src/scene/parametric_constraints.rs
This commit is contained in:
commit
0ee8b15511
19 files changed
+2204
-61
No files matched your search
+511
-3
@@ -3,6 +3,28 @@ use crate::command::{CmdResult, SelectionEntity, StepInput};
|
||||
use acadrust::Handle;
|
||||
use iced::Task;
|
||||
|
||||
/// `old` as a whole identifier in `source` becomes `new`.
|
||||
fn replace_identifier(source: &str, old: &str, new: &str) -> String {
|
||||
let mut out = String::with_capacity(source.len());
|
||||
let mut token = String::new();
|
||||
let flush = |token: &mut String, out: &mut String| {
|
||||
if !token.is_empty() {
|
||||
out.push_str(if token == old { new } else { token.as_str() });
|
||||
token.clear();
|
||||
}
|
||||
};
|
||||
for c in source.chars() {
|
||||
if c.is_alphanumeric() || c == '_' {
|
||||
token.push(c);
|
||||
} else {
|
||||
flush(&mut token, &mut out);
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
flush(&mut token, &mut out);
|
||||
out
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
pub(super) fn reject_locked_edit(&mut self, i: usize, handle: Handle) -> bool {
|
||||
let Some(layer) = self.tabs[i].scene.locked_layer_name(handle) else {
|
||||
@@ -1103,6 +1125,257 @@ impl OpenCADStudio {
|
||||
TableCellEditStart::Started
|
||||
}
|
||||
|
||||
/// Reports a refused command-line value and shows the prompt again.
|
||||
fn reprompt_active_command(&mut self, i: usize, message: &str) {
|
||||
self.command_line.push_error(message);
|
||||
if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|command| command.prompt()) {
|
||||
self.command_line.push_info(&prompt);
|
||||
}
|
||||
}
|
||||
|
||||
/// Drops what removed dimensional constraints leave behind: their dynamic
|
||||
/// dimensions, and their parameters when no other constraint reads them.
|
||||
pub(crate) fn purge_dimensional_extras(
|
||||
&mut self,
|
||||
i: usize,
|
||||
dimensions: Vec<Handle>,
|
||||
parameters: Vec<String>,
|
||||
) {
|
||||
if !dimensions.is_empty() {
|
||||
self.tabs[i].scene.erase_entities(&dimensions);
|
||||
}
|
||||
let unused: Vec<String> = parameters
|
||||
.into_iter()
|
||||
.filter(|name| {
|
||||
!self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraints
|
||||
.iter()
|
||||
.flat_map(|set| set.constraints.iter())
|
||||
.any(|constraint| {
|
||||
matches!(
|
||||
&constraint.driving_param,
|
||||
Some(crate::scene::named_parameters::DrivingValue::Named(used))
|
||||
if used == name
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
if !unused.is_empty() {
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
for name in &unused {
|
||||
self.tabs[i].scene.named_parameters_mut().remove(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `Dynamic` or `Annotational`, the DCFORM setting.
|
||||
pub(crate) fn constraint_form_name(&self) -> &'static str {
|
||||
if self.constraint_form_annotational {
|
||||
"Annotational"
|
||||
} else {
|
||||
"Dynamic"
|
||||
}
|
||||
}
|
||||
|
||||
/// The value prompt a double-clicked constraint dimension opens.
|
||||
pub(crate) fn dynamic_dimension_value_command(
|
||||
&self,
|
||||
i: usize,
|
||||
handle: Handle,
|
||||
) -> Option<crate::modules::parametric::DimensionValueCommand> {
|
||||
use crate::scene::named_parameters::DrivingValue;
|
||||
let scene = &self.tabs[i].scene;
|
||||
for set in &scene.parametric_constraints {
|
||||
let Some((id, _)) = set
|
||||
.dimensions
|
||||
.iter()
|
||||
.find(|(_, dimension)| **dimension == handle)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let constraint = set.get(*id)?;
|
||||
let Some(DrivingValue::Named(name)) = &constraint.driving_param else {
|
||||
return None;
|
||||
};
|
||||
let table = if set.local_parameters.is_empty() {
|
||||
scene.named_parameters()
|
||||
} else {
|
||||
&set.local_parameters
|
||||
};
|
||||
let current = table
|
||||
.get(name)
|
||||
.map(|parameter| parameter.source.trim().to_string())
|
||||
.unwrap_or_default();
|
||||
return Some(crate::modules::parametric::DimensionValueCommand::new(
|
||||
name.clone(),
|
||||
current,
|
||||
));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Applies `expression` or `newname=expression` to parameter `name`:
|
||||
/// the dimension value prompt and -PARAMETERS Edit.
|
||||
pub(crate) fn apply_parameter_input(&mut self, i: usize, name: &str, input: &str) {
|
||||
let input = input.trim();
|
||||
let (target, expression) = match input.split_once('=') {
|
||||
Some((new_name, expression)) => {
|
||||
(new_name.trim().to_string(), expression.trim().to_string())
|
||||
}
|
||||
None => (name.to_string(), input.to_string()),
|
||||
};
|
||||
if expression.is_empty() {
|
||||
return;
|
||||
}
|
||||
if target != name {
|
||||
if let Err(error) = self.rename_parameter(i, name, &target) {
|
||||
self.command_line.push_error(&error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let mut table = self.tabs[i].scene.named_parameters().clone();
|
||||
if let Err(error) = table.set(&target, &expression) {
|
||||
self.command_line.push_error(&error.to_string());
|
||||
return;
|
||||
}
|
||||
if !table
|
||||
.resolve(&target)
|
||||
.is_ok_and(|value| value.is_finite())
|
||||
{
|
||||
self.command_line.push_error("Invalid expression.");
|
||||
return;
|
||||
}
|
||||
let pending = self.begin_undo(i, "Edit parameter", 0, true);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
self.tabs[i].scene.named_parameters = table;
|
||||
self.resolve_named_parameter_edit(i, &target);
|
||||
self.tabs[i].scene.refresh_dynamic_dimension_texts();
|
||||
self.tabs[i].dirty = true;
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
|
||||
/// -PARAMETERS New.
|
||||
pub(crate) fn create_parameter(&mut self, i: usize, name: &str, expression: &str) {
|
||||
if self.tabs[i].scene.named_parameters().contains(name) {
|
||||
self.command_line
|
||||
.push_error(&format!("Parameter {name} already exists."));
|
||||
return;
|
||||
}
|
||||
let mut table = self.tabs[i].scene.named_parameters().clone();
|
||||
if let Err(error) = table.set(name, expression) {
|
||||
self.command_line.push_error(&error.to_string());
|
||||
return;
|
||||
}
|
||||
let pending = self.begin_undo(i, "New parameter", 0, true);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
self.tabs[i].scene.named_parameters = table;
|
||||
self.tabs[i].scene.sync_native_parametric_graph();
|
||||
self.tabs[i].dirty = true;
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
|
||||
/// Renames a parameter everywhere: the table, the expressions that use
|
||||
/// it and the constraints it drives.
|
||||
pub(crate) fn rename_parameter(&mut self, i: usize, old: &str, new: &str) -> Result<(), String> {
|
||||
use crate::scene::named_parameters::{is_reserved_name, is_valid_name, DrivingValue};
|
||||
let table = self.tabs[i].scene.named_parameters().clone();
|
||||
let Some(source) = table.get(old).map(|parameter| parameter.source.clone()) else {
|
||||
return Err(format!("Parameter {old} not found."));
|
||||
};
|
||||
if !is_valid_name(new) || is_reserved_name(new) || table.contains(new) {
|
||||
return Err(format!("Invalid parameter name {new}."));
|
||||
}
|
||||
let mut renamed = table.clone();
|
||||
renamed.set(new, &source).map_err(|error| error.to_string())?;
|
||||
for parameter in table.iter() {
|
||||
if parameter.name == old {
|
||||
continue;
|
||||
}
|
||||
let rewritten = replace_identifier(¶meter.source, old, new);
|
||||
if rewritten != parameter.source {
|
||||
renamed
|
||||
.set(¶meter.name, &rewritten)
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
}
|
||||
renamed.remove(old);
|
||||
let pending = self.begin_undo(i, "Rename parameter", 0, true);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
self.tabs[i].scene.named_parameters = renamed;
|
||||
let scopes: Vec<_> = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraints
|
||||
.iter()
|
||||
.map(|set| set.scope)
|
||||
.collect();
|
||||
for scope in scopes {
|
||||
let before = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(scope)
|
||||
.filter(|set| {
|
||||
set.constraints.iter().any(|constraint| {
|
||||
matches!(&constraint.driving_param, Some(DrivingValue::Named(name)) if name == old)
|
||||
})
|
||||
})
|
||||
.cloned();
|
||||
let Some(before) = before else {
|
||||
continue;
|
||||
};
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_parametric_constraints_before(scope, before);
|
||||
let set = self.tabs[i].scene.parametric_constraint_set_mut(scope);
|
||||
for constraint in &mut set.constraints {
|
||||
if matches!(&constraint.driving_param, Some(DrivingValue::Named(name)) if name == old) {
|
||||
constraint.driving_param = Some(DrivingValue::Named(new.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.tabs[i].scene.refresh_dynamic_dimension_texts();
|
||||
self.tabs[i].scene.sync_native_parametric_graph();
|
||||
self.tabs[i].dirty = true;
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// -PARAMETERS Delete: a parameter no constraint drives with.
|
||||
pub(crate) fn delete_parameter(&mut self, i: usize, name: &str) {
|
||||
use crate::scene::named_parameters::DrivingValue;
|
||||
if !self.tabs[i].scene.named_parameters().contains(name) {
|
||||
self.command_line
|
||||
.push_error(&format!("Parameter {name} not found."));
|
||||
return;
|
||||
}
|
||||
let in_use = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraints
|
||||
.iter()
|
||||
.flat_map(|set| set.constraints.iter())
|
||||
.any(|constraint| {
|
||||
matches!(&constraint.driving_param, Some(DrivingValue::Named(used)) if used == name)
|
||||
});
|
||||
if in_use {
|
||||
self.command_line
|
||||
.push_error(&format!("Parameter {name} is used by a constraint."));
|
||||
return;
|
||||
}
|
||||
let pending = self.begin_undo(i, "Delete parameter", 0, true);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
self.tabs[i].scene.named_parameters_mut().remove(name);
|
||||
self.tabs[i].scene.sync_native_parametric_graph();
|
||||
self.tabs[i].dirty = true;
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_parametric_constraint(
|
||||
&mut self,
|
||||
id: crate::scene::parametric_constraints::ConstraintId,
|
||||
@@ -1117,8 +1390,13 @@ impl OpenCADStudio {
|
||||
return false;
|
||||
};
|
||||
let touched: Vec<Handle> = constraint.refs.iter().map(|r| r.entity).collect();
|
||||
let dimension = set.dimensions.get(&id).copied();
|
||||
let parameter = match &constraint.driving_param {
|
||||
Some(crate::scene::named_parameters::DrivingValue::Named(name)) => Some(name.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let constraints_before = set.clone();
|
||||
let pending = self.begin_undo(i, label, touched.len(), true);
|
||||
let pending = self.begin_undo(i, label, touched.len(), dimension.is_none());
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_parametric_constraints_before(scope, constraints_before);
|
||||
@@ -1126,6 +1404,11 @@ impl OpenCADStudio {
|
||||
.scene
|
||||
.parametric_constraint_set_mut(scope)
|
||||
.remove(id);
|
||||
self.purge_dimensional_extras(
|
||||
i,
|
||||
dimension.into_iter().collect(),
|
||||
parameter.into_iter().collect(),
|
||||
);
|
||||
if self.tabs[i].scene.selected_constraint == Some(id) {
|
||||
self.tabs[i].scene.selected_constraint = None;
|
||||
}
|
||||
@@ -1219,6 +1502,23 @@ impl OpenCADStudio {
|
||||
.flat_map(|c| c.refs.iter().map(|r| r.entity))
|
||||
.collect();
|
||||
|
||||
// A changed value moves a dimensional constraint's second point; its
|
||||
// first point stays, as in the reference.
|
||||
let anchors: Vec<crate::scene::parametric_constraints::ParametricRef> = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraints
|
||||
.iter()
|
||||
.flat_map(|set| set.constraints.iter())
|
||||
.filter(|c| {
|
||||
c.enabled
|
||||
&& matches!(
|
||||
c.driving_param,
|
||||
Some(crate::scene::named_parameters::DrivingValue::Named(_))
|
||||
)
|
||||
})
|
||||
.filter_map(|c| c.refs.first().copied())
|
||||
.filter(|reference| reference.marker.is_some())
|
||||
.collect();
|
||||
let pending = self.begin_undo(i, "Apply named parameters", touched.len(), true);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
self.tabs[i].scene.named_parameters = table;
|
||||
@@ -1228,10 +1528,15 @@ impl OpenCADStudio {
|
||||
.into_iter()
|
||||
.map(|h| (h, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
self.tabs[i].scene.bump_entities_with_parametric_policy(
|
||||
&changes,
|
||||
&anchors,
|
||||
self.constraint_solve_mode,
|
||||
);
|
||||
} else {
|
||||
self.tabs[i].scene.sync_native_parametric_graph();
|
||||
}
|
||||
self.tabs[i].scene.refresh_dynamic_dimension_texts();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
@@ -1268,7 +1573,25 @@ impl OpenCADStudio {
|
||||
.into_iter()
|
||||
.map(|h| (h, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
// A changed value moves a dimensional constraint's second point; its
|
||||
// first point stays, as in the reference.
|
||||
let anchors: Vec<crate::scene::parametric_constraints::ParametricRef> = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraints
|
||||
.iter()
|
||||
.flat_map(|set| set.constraints.iter())
|
||||
.filter(|c| {
|
||||
c.enabled
|
||||
&& matches!(&c.driving_param, Some(crate::scene::named_parameters::DrivingValue::Named(n)) if n == name)
|
||||
})
|
||||
.filter_map(|c| c.refs.first().copied())
|
||||
.filter(|reference| reference.marker.is_some())
|
||||
.collect();
|
||||
self.tabs[i].scene.bump_entities_with_parametric_policy(
|
||||
&changes,
|
||||
&anchors,
|
||||
self.constraint_solve_mode,
|
||||
);
|
||||
}
|
||||
|
||||
/// Commits one field of one Parameters-section row (Properties panel) —
|
||||
@@ -3458,6 +3781,191 @@ impl OpenCADStudio {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
CmdResult::CheckConstraintPoint(pick) => {
|
||||
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 =
|
||||
acadrust::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));
|
||||
return match result {
|
||||
Some(result) => self.apply_cmd_result(result),
|
||||
None => Task::none(),
|
||||
};
|
||||
}
|
||||
CmdResult::AddDimensionalConstraint {
|
||||
kind,
|
||||
first,
|
||||
second,
|
||||
first_point,
|
||||
second_point,
|
||||
location,
|
||||
axis,
|
||||
name,
|
||||
expression,
|
||||
label,
|
||||
} => {
|
||||
use crate::modules::annotate::{aligned_dim, linear_dim};
|
||||
use crate::scene::named_parameters::{is_valid_name, DrivingValue};
|
||||
use crate::scene::parametric_constraints::{dynamic_dimension_text, ConstraintKind};
|
||||
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
let refs = vec![first, second];
|
||||
// A refused value keeps the prompt open, as the reference's
|
||||
// editor does.
|
||||
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 Task::none();
|
||||
}
|
||||
let name = name.trim().to_string();
|
||||
if !is_valid_name(&name) {
|
||||
self.reprompt_active_command(i, "Invalid parameter name.");
|
||||
return 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 Task::none();
|
||||
}
|
||||
let value = match table.resolve(&name) {
|
||||
Ok(value) if value.is_finite() => value,
|
||||
_ => {
|
||||
self.reprompt_active_command(i, "Invalid expression.");
|
||||
return Task::none();
|
||||
}
|
||||
};
|
||||
// DCFORM Annotational: an ordinary plotted dimension that shows
|
||||
// the value at dimension precision.
|
||||
let annotational = self.constraint_form_annotational;
|
||||
let source = if annotational {
|
||||
format!("{value:.4}")
|
||||
} else {
|
||||
expression.clone()
|
||||
};
|
||||
let text = dynamic_dimension_text(
|
||||
&name,
|
||||
&source,
|
||||
value,
|
||||
self.tabs[i].scene.constraint_name_format,
|
||||
);
|
||||
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,
|
||||
);
|
||||
let layer = self.tabs[i].active_layer.clone();
|
||||
if layer != "0" {
|
||||
entity.as_entity_mut().set_layer(layer);
|
||||
}
|
||||
// A dynamic dimension draws in the constraint gray, not the
|
||||
// current color.
|
||||
if !annotational {
|
||||
entity.common_mut().color = acadrust::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];
|
||||
if second.entity != first.entity {
|
||||
touched.push(second.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);
|
||||
if annotational {
|
||||
self.tabs[i].scene.mark_dimension_annotational(dimension);
|
||||
}
|
||||
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(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 stays; a value other than the measured one
|
||||
// moves the second, as in the reference.
|
||||
self.tabs[i].scene.bump_entities_with_parametric_policy(
|
||||
&changes,
|
||||
&[first],
|
||||
self.constraint_solve_mode,
|
||||
);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
CmdResult::AddFixedConstraint(pick) => {
|
||||
use crate::modules::parametric::FixConstraintCommand;
|
||||
use crate::scene::parametric_constraints::{
|
||||
|
||||
+179
-3
@@ -1131,7 +1131,26 @@ impl OpenCADStudio {
|
||||
self.command_line.push_output("No constraints found.");
|
||||
return None;
|
||||
}
|
||||
let pending = self.begin_undo(i, "Delete constraints", handles.len(), true);
|
||||
let dimensions: Vec<acadrust::Handle> = ids
|
||||
.iter()
|
||||
.filter_map(|id| before.dimensions.get(id).copied())
|
||||
.collect();
|
||||
let parameters: Vec<String> = ids
|
||||
.iter()
|
||||
.filter_map(|id| before.get(*id))
|
||||
.filter_map(|constraint| match &constraint.driving_param {
|
||||
Some(crate::scene::named_parameters::DrivingValue::Named(name)) => {
|
||||
Some(name.clone())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let pending = self.begin_undo(
|
||||
i,
|
||||
"Delete constraints",
|
||||
handles.len(),
|
||||
dimensions.is_empty(),
|
||||
);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_parametric_constraints_before(scope, before);
|
||||
@@ -1139,6 +1158,7 @@ impl OpenCADStudio {
|
||||
for id in &ids {
|
||||
set.remove(*id);
|
||||
}
|
||||
self.purge_dimensional_extras(i, dimensions, parameters);
|
||||
let changes: Vec<_> = handles
|
||||
.iter()
|
||||
.copied()
|
||||
@@ -1290,6 +1310,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
"DCCONVERT" => {
|
||||
self.dim_constraint_last = "Convert";
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
if handles.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
@@ -1486,6 +1507,131 @@ impl OpenCADStudio {
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
"DIMCONSTRAINT" => {
|
||||
use crate::modules::parametric::DimConstraintMenuCommand;
|
||||
self.command_line.push_output(&format!(
|
||||
"Current settings: Constraint form = {}",
|
||||
self.constraint_form_name()
|
||||
));
|
||||
let new_cmd = DimConstraintMenuCommand::new(self.dim_constraint_last);
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
"DCFORM" => {
|
||||
use crate::modules::parametric::ConstraintFormCommand;
|
||||
let new_cmd = ConstraintFormCommand::new(self.constraint_form_annotational);
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
// DCFORM's answer, then on into DIMCONSTRAINT's options as the
|
||||
// reference does.
|
||||
cmd if cmd.starts_with("DCFORM_SET ") => {
|
||||
use crate::modules::parametric::DimConstraintMenuCommand;
|
||||
self.constraint_form_annotational = cmd
|
||||
.trim_start_matches("DCFORM_SET ")
|
||||
.trim()
|
||||
.eq_ignore_ascii_case("Annotational");
|
||||
self.command_line.push_output(&format!(
|
||||
"Current settings: Constraint form = {}",
|
||||
self.constraint_form_name()
|
||||
));
|
||||
let new_cmd = DimConstraintMenuCommand::new(self.dim_constraint_last);
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
// A dynamic dimension's value prompt (double-click).
|
||||
cmd if cmd.starts_with("DCVALUE ") => {
|
||||
let rest = cmd.trim_start_matches("DCVALUE ").trim();
|
||||
if let Some((name, input)) = rest.split_once(' ') {
|
||||
self.apply_parameter_input(i, name.trim(), input);
|
||||
}
|
||||
}
|
||||
|
||||
"-PARAMETERS" => {
|
||||
use crate::modules::parametric::ParametersCliCommand;
|
||||
let new_cmd = ParametersCliCommand::new();
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
cmd if cmd.starts_with("-PARAMETERS ") => {
|
||||
let rest = cmd.trim_start_matches("-PARAMETERS ").trim();
|
||||
let (op, args) = rest.split_once(' ').unwrap_or((rest, ""));
|
||||
match op {
|
||||
"LIST" => {
|
||||
let rule = "-".repeat(74);
|
||||
let rows: Vec<String> = {
|
||||
let table = self.tabs[i].scene.named_parameters();
|
||||
table
|
||||
.iter()
|
||||
.map(|parameter| {
|
||||
let value = table
|
||||
.resolve(¶meter.name)
|
||||
.map(|value| format!("{value:.4}"))
|
||||
.unwrap_or_else(|_| "**".to_string());
|
||||
format!(
|
||||
"Parameter: {:<12} Expression: {:<21} Value: {value}",
|
||||
parameter.name, parameter.source
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
self.command_line.push_output(&rule);
|
||||
for row in rows {
|
||||
self.command_line.push_output(&row);
|
||||
}
|
||||
self.command_line.push_output(&rule);
|
||||
}
|
||||
"NEW" => {
|
||||
if let Some((name, expression)) = args.split_once(' ') {
|
||||
self.create_parameter(i, name.trim(), expression.trim());
|
||||
}
|
||||
}
|
||||
"EDIT" => {
|
||||
use crate::modules::parametric::ParametersCliCommand;
|
||||
let name = args.trim().to_string();
|
||||
let old = {
|
||||
let table = self.tabs[i].scene.named_parameters();
|
||||
table.get(&name).map(|parameter| {
|
||||
let value = table
|
||||
.resolve(&name)
|
||||
.map(|value| format!("{value:.4}"))
|
||||
.unwrap_or_else(|_| "**".to_string());
|
||||
format!("Old Expression = {}, Value = {value}", parameter.source)
|
||||
})
|
||||
};
|
||||
match old {
|
||||
Some(line) => {
|
||||
self.command_line.push_output(&line);
|
||||
let new_cmd = ParametersCliCommand::edit_expression(name);
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
None => self
|
||||
.command_line
|
||||
.push_error(&format!("Parameter {name} not found.")),
|
||||
}
|
||||
}
|
||||
"SET" => {
|
||||
if let Some((name, expression)) = args.split_once(' ') {
|
||||
self.apply_parameter_input(i, name.trim(), expression.trim());
|
||||
}
|
||||
}
|
||||
"RENAME" => {
|
||||
if let Some((old, new)) = args.split_once(' ') {
|
||||
if let Err(error) = self.rename_parameter(i, old.trim(), new.trim()) {
|
||||
self.command_line.push_error(&error);
|
||||
}
|
||||
}
|
||||
}
|
||||
"DELETE" => self.delete_parameter(i, args.trim()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
"EDCONSTRAINT" => {
|
||||
use crate::modules::parametric::EqualDistanceConstraintCommand;
|
||||
let new_cmd = EqualDistanceConstraintCommand::new();
|
||||
@@ -1726,8 +1872,35 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
"DCONSTRAINT" | "DCLINEAR" | "DCHORIZONTAL" | "DCVERTICAL" | "DCALIGNED"
|
||||
| "DCRADIUS" | "DCDIAMETER" => {
|
||||
"DCLINEAR" | "DCHORIZONTAL" | "DCVERTICAL" | "DCALIGNED" => {
|
||||
use crate::modules::parametric::{DimConstraintAxis, DimConstraintCommand};
|
||||
use crate::scene::parametric_constraints::next_dimensional_parameter_name;
|
||||
let axis = match cmd {
|
||||
"DCHORIZONTAL" => DimConstraintAxis::Horizontal,
|
||||
"DCVERTICAL" => DimConstraintAxis::Vertical,
|
||||
"DCALIGNED" => DimConstraintAxis::Aligned,
|
||||
_ => DimConstraintAxis::Linear,
|
||||
};
|
||||
self.dim_constraint_last = match cmd {
|
||||
"DCHORIZONTAL" => "Horizontal",
|
||||
"DCVERTICAL" => "Vertical",
|
||||
"DCALIGNED" => "Aligned",
|
||||
_ => "Linear",
|
||||
};
|
||||
let name = next_dimensional_parameter_name(self.tabs[i].scene.named_parameters());
|
||||
// Both constraint points are picked inside the command.
|
||||
self.tabs[i].scene.deselect_all();
|
||||
let command = DimConstraintCommand::new(axis, name);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
|
||||
"DCONSTRAINT" | "DCRADIUS" | "DCDIAMETER" => {
|
||||
if cmd == "DCRADIUS" {
|
||||
self.dim_constraint_last = "Radius";
|
||||
} else if cmd == "DCDIAMETER" {
|
||||
self.dim_constraint_last = "Diameter";
|
||||
}
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
if handles.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
@@ -1767,6 +1940,9 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
"ACONSTRAINT" | "DCANGULAR" => {
|
||||
if cmd == "DCANGULAR" {
|
||||
self.dim_constraint_last = "ANgular";
|
||||
}
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
if handles.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
|
||||
@@ -1052,6 +1052,8 @@ impl OpenCADStudio {
|
||||
| "CONSTRAINTINFER"
|
||||
| "CONSTRAINTBARDISPLAY"
|
||||
| "CONSTRAINTBARMODE"
|
||||
| "CONSTRAINTNAMEFORMAT"
|
||||
| "DYNCONSTRAINTDISPLAY"
|
||||
) =>
|
||||
{
|
||||
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
|
||||
@@ -1075,7 +1077,7 @@ impl OpenCADStudio {
|
||||
if name.is_empty() || name == "?" {
|
||||
self.command_line.push_info(&super::plotvars::setvar_listing());
|
||||
self.command_line.push_info(
|
||||
crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY CONSTRAINTBARMODE ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(),
|
||||
crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY CONSTRAINTBARMODE CONSTRAINTNAMEFORMAT DYNCONSTRAINTDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(),
|
||||
);
|
||||
} else {
|
||||
if name == "CETRANSPARENCY" {
|
||||
@@ -1275,17 +1277,26 @@ impl OpenCADStudio {
|
||||
| "CONSTRAINTINFER"
|
||||
| "CONSTRAINTBARDISPLAY"
|
||||
| "CONSTRAINTBARMODE"
|
||||
| "CONSTRAINTNAMEFORMAT"
|
||||
| "DYNCONSTRAINTDISPLAY"
|
||||
) {
|
||||
let current = match name.as_str() {
|
||||
"CONSTRAINTSOLVEMODE" => i16::from(self.constraint_solve_mode),
|
||||
"CONSTRAINTINFER" => i16::from(self.constraint_infer),
|
||||
"CONSTRAINTBARDISPLAY" => self.constraint_bar_display,
|
||||
"CONSTRAINTBARMODE" => self.constraint_bar_mode,
|
||||
"CONSTRAINTNAMEFORMAT" => {
|
||||
i16::from(self.tabs[i].scene.constraint_name_format)
|
||||
}
|
||||
"DYNCONSTRAINTDISPLAY" => {
|
||||
i16::from(self.tabs[i].scene.dynamic_constraint_display)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let maximum = match name.as_str() {
|
||||
"CONSTRAINTBARDISPLAY" => 3,
|
||||
"CONSTRAINTBARMODE" => 4095,
|
||||
"CONSTRAINTNAMEFORMAT" => 2,
|
||||
_ => 1,
|
||||
};
|
||||
if let Some(value) = &value {
|
||||
@@ -1304,6 +1315,16 @@ impl OpenCADStudio {
|
||||
self.constraint_bar_display = mode
|
||||
}
|
||||
"CONSTRAINTBARMODE" => self.constraint_bar_mode = mode,
|
||||
"CONSTRAINTNAMEFORMAT" => {
|
||||
self.tabs[i].scene.constraint_name_format = mode as u8;
|
||||
self.tabs[i].scene.refresh_dynamic_dimension_texts();
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
"DYNCONSTRAINTDISPLAY" => {
|
||||
self.tabs[i].scene.dynamic_constraint_display =
|
||||
mode != 0;
|
||||
self.tabs[i].scene.refresh_hidden_dynamic_dimensions();
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
self.persist_settings_if_changed();
|
||||
|
||||
@@ -612,6 +612,10 @@ pub(super) struct OpenCADStudio {
|
||||
pub constraint_solve_mode: bool,
|
||||
pub constraint_infer: bool,
|
||||
pub constraint_bar_display: i16,
|
||||
/// DCFORM: new dimensional constraints use the annotational form.
|
||||
pub constraint_form_annotational: bool,
|
||||
/// The dimensional constraint DIMCONSTRAINT offers by default: the last one used.
|
||||
pub dim_constraint_last: &'static str,
|
||||
pub constraint_bar_mode: i16,
|
||||
/// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command);
|
||||
/// 0 disables autosave.
|
||||
@@ -3949,6 +3953,8 @@ impl OpenCADStudio {
|
||||
constraint_solve_mode: true,
|
||||
constraint_infer: false,
|
||||
constraint_bar_display: 3,
|
||||
constraint_form_annotational: false,
|
||||
dim_constraint_last: "Aligned",
|
||||
constraint_bar_mode: 4095,
|
||||
savetime_min: 10,
|
||||
default_bg_color: None,
|
||||
|
||||
@@ -1464,7 +1464,7 @@ impl OpenCADStudio {
|
||||
|
||||
self.prepare_native_save(i);
|
||||
let version = self.tabs[i].scene.document.version;
|
||||
let snapshot = self.tabs[i].scene.document.clone();
|
||||
let snapshot = self.tabs[i].scene.document_for_save();
|
||||
crate::io::save_owned_as_version_atomic(
|
||||
snapshot,
|
||||
&path,
|
||||
@@ -1893,7 +1893,7 @@ impl OpenCADStudio {
|
||||
path: std::path::PathBuf,
|
||||
) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let document = self.tabs[i].scene.document.clone();
|
||||
let document = self.tabs[i].scene.document_for_save();
|
||||
let handles: Vec<_> = self.tabs[i].scene.selected.iter().copied().collect();
|
||||
let worker_name = block_name.clone();
|
||||
let worker_path = path.clone();
|
||||
@@ -2298,7 +2298,7 @@ impl OpenCADStudio {
|
||||
crate::ui::wrap_bar::dropdown_bounds(crate::app::view::VIEWPORT_CAPTURE_BOUNDS_ID)
|
||||
});
|
||||
let clone_started = iced::time::Instant::now();
|
||||
let mut snapshot = self.tabs[i].scene.document.clone();
|
||||
let mut snapshot = self.tabs[i].scene.document_for_save();
|
||||
// Save-As across folders: rebase relative reference paths onto the
|
||||
// new base dir inside the snapshot only (live strings are untouched).
|
||||
if purpose == crate::app::SavePurpose::SaveAs {
|
||||
|
||||
@@ -785,6 +785,8 @@ impl OpenCADStudio {
|
||||
pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].scene.update(t - self.start);
|
||||
// Dynamic dimensions keep their screen size across zoom bands.
|
||||
self.tabs[i].scene.refresh_dynamic_dimension_scales(false);
|
||||
|
||||
// If the camera moved since we last synced, write it back to
|
||||
// the document and mark the file dirty.
|
||||
@@ -5058,6 +5060,15 @@ properties={:.1}ms picked={}",
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
// A constraint dimension edits its parameter from the
|
||||
// command line, standing in for the reference's in-place
|
||||
// value editor.
|
||||
if let Some(prompt) = self.dynamic_dimension_value_command(i, handle) {
|
||||
use crate::command::CadCommand;
|
||||
self.command_line.push_info(&prompt.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(prompt));
|
||||
return Task::none();
|
||||
}
|
||||
// Any text-bearing entity opens its in-place editor
|
||||
// (plain box or rich MText editor, per type). A
|
||||
// Leader resolves to the entity it annotates.
|
||||
|
||||
Reference in new issue
Block a user