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:
Hakan Seven committed 2026-09-21 22:36:20 +03:00
commit 0ee8b15511
19 files changed
+2204 -61

No files matched your search

+511 -3
View File
@@ -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(&parameter.source, old, new);
if rewritten != parameter.source {
renamed
.set(&parameter.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
View File
@@ -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(&parameter.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;
+22 -1
View File
@@ -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();
+6
View File
@@ -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,
+3 -3
View File
@@ -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 {
+11
View File
@@ -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.
+29
View File
@@ -1610,6 +1610,25 @@ pub enum CmdResult {
multiple: bool,
label: &'static str,
},
/// Resolves a constraint-point pick against the live document and hands
/// it back through `CadCommand::accept_constraint_point`; a miss reports
/// `No valid constraint point found.` and asks again.
CheckConstraintPoint(CoincidentPick),
/// Adds a dimensional constraint between two constraint points together
/// with the dynamic dimension that shows it and the parameter
/// (`name` = `expression`) that drives it.
AddDimensionalConstraint {
kind: crate::scene::parametric_constraints::ConstraintKind,
first: crate::scene::parametric_constraints::ParametricRef,
second: crate::scene::parametric_constraints::ParametricRef,
first_point: DVec3,
second_point: DVec3,
location: DVec3,
axis: DVec3,
name: String,
expression: String,
label: &'static str,
},
/// Adds a point or object symmetry relation around a picked line. The
/// first reference and axis remain fixed during initial placement.
AddSymmetricConstraint {
@@ -2453,6 +2472,16 @@ pub trait CadCommand: Send {
false
}
/// A constraint point the host resolved for a `CheckConstraintPoint`
/// pick, with its world position.
fn accept_constraint_point(
&mut self,
_reference: crate::scene::parametric_constraints::ParametricRef,
_point: DVec3,
) -> CmdResult {
CmdResult::NeedPoint
}
/// Include filled hatch / DXF SOLID regions in the entity hit-test.
///
/// Most entity-pick commands operate on curve geometry and intentionally
+33
View File
@@ -6295,6 +6295,39 @@ fn dimension_text_is_outside(dim: &Dimension, style: Option<&DimStyle>) -> bool
}
}
/// Where a dynamic dimension's lock mark sits: just after the (horizontal)
/// text, with the direction that points away from the text.
pub(crate) fn dynamic_dimension_lock_anchor(
document: &CadDocument,
dim: &Dimension,
anno_scale: f64,
) -> Option<(Vector3, Vector3)> {
let style_name = &dim.base().style_name;
let source_style = document.dim_styles.iter().find(|s| {
s.name.eq_ignore_ascii_case(style_name)
|| (style_name.trim().is_empty() && s.name.eq_ignore_ascii_case("Standard"))
});
let effective_style = source_style.map(|style| resolved_dimension_style(style, dim, document));
let style = effective_style.as_ref();
let dim_scale = style
.map(|s| if s.dimscale > 1e-6 { s.dimscale } else { anno_scale })
.unwrap_or(1.0);
let text_height = style.map(|s| s.dimtxt * dim_scale).unwrap_or(2.5);
let value = dimension_text_value(dim, style)?;
let stack_scale = style.map(dimtfac_or_one).unwrap_or(1.0);
let half_width = text_cells(&value, stack_scale) * text_height * CELL_WIDTH * 0.5;
let pos = dimension_text_pos_f64(dim, style, text_height, dim_scale);
let outward = Vector3::new(1.0, 0.0, 0.0);
Some((
Vector3::new(
pos.x + outward.x * half_width,
pos.y + outward.y * half_width,
pos.z,
),
outward,
))
}
fn dimension_text_natural_rotation(dim: &Dimension) -> f64 {
let angle = match dim {
Dimension::Linear(d) => d.rotation,
+25 -15
View File
@@ -67,26 +67,36 @@ impl AlignedDimensionCommand {
let first = self.plane.to_local(first);
let second = self.plane.to_local(second);
let point = self.plane.to_local(point);
let mut dim = DimensionAligned::new(v3(first), v3(second));
// The placement point is stored so commit matches the preview.
dim.definition_point = v3(point);
dim.base.definition_point = v3(point);
let (d1, d2) = dim_line_endpoints(first, second, point);
dim.base.text_middle_point = v3((d1 + d2) * 0.5);
dim.base.insertion_point = dim.base.text_middle_point;
dim.base.actual_measurement = dim.measurement();
crate::entities::dimension::set_dimension_text_override(
&mut dim.base,
self.text_override.clone(),
);
let mut entity = aligned_dimension_entity(first, second, point, self.text_override.clone());
// An explicit text angle overrides the default rotation.
if let Some(angle) = self.text_angle {
dim.base.text_rotation = angle;
if let (Some(angle), EntityType::Dimension(dimension)) = (self.text_angle, &mut entity) {
dimension.base_mut().text_rotation = angle;
}
self.plane.place_entity(EntityType::Dimension(Dimension::Aligned(dim)))
self.plane.place_entity(entity)
}
}
/// An aligned dimension between two points of the working plane, its
/// dimension line through `point` — what DIMALIGNED places and what an
/// aligned dimensional constraint draws.
pub(crate) fn aligned_dimension_entity(
first: DVec3,
second: DVec3,
point: DVec3,
text_override: Option<String>,
) -> EntityType {
let mut dim = DimensionAligned::new(v3(first), v3(second));
// The placement point is stored so commit matches the preview.
dim.definition_point = v3(point);
dim.base.definition_point = v3(point);
let (d1, d2) = dim_line_endpoints(first, second, point);
dim.base.text_middle_point = v3((d1 + d2) * 0.5);
dim.base.insertion_point = dim.base.text_middle_point;
dim.base.actual_measurement = dim.measurement();
crate::entities::dimension::set_dimension_text_override(&mut dim.base, text_override);
EntityType::Dimension(Dimension::Aligned(dim))
}
impl CadCommand for AlignedDimensionCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
+27 -15
View File
@@ -15,7 +15,7 @@ use glam::DVec3;
use crate::t;
/// Select the measured axis from where the dimension line clears the points.
fn measure_axis(first: DVec3, second: DVec3, def: DVec3) -> DVec3 {
pub(crate) fn measure_axis(first: DVec3, second: DVec3, def: DVec3) -> DVec3 {
let outside = |value: f64, a: f64, b: f64| {
let (low, high) = if a <= b { (a, b) } else { (b, a) };
(low - value).max(value - high).max(0.0)
@@ -113,26 +113,38 @@ impl LinearDimensionCommand {
let first = self.plane.to_local(first);
let second = self.plane.to_local(second);
let point = self.plane.to_local(point);
let mut dim = DimensionLinear::new(v3(first), v3(second));
let axis = self.axis_mode.axis(first, second, point);
dim.rotation = axis.y.atan2(axis.x);
dim.set_offset(dimension_line_offset(second, point, axis));
dim.base.definition_point = dim.definition_point;
dim.base.text_middle_point = v3(linear_text_pos(first, second, point, axis));
dim.base.insertion_point = dim.base.text_middle_point;
dim.base.actual_measurement = dim.measurement();
crate::entities::dimension::set_dimension_text_override(
&mut dim.base,
self.text_override.clone(),
);
let mut entity =
linear_dimension_entity(first, second, point, axis, self.text_override.clone());
// An explicit text angle overrides the UCS-derived rotation.
if let Some(angle) = self.text_angle {
dim.base.text_rotation = angle;
if let (Some(angle), EntityType::Dimension(dimension)) = (self.text_angle, &mut entity) {
dimension.base_mut().text_rotation = angle;
}
self.plane.place_entity(EntityType::Dimension(Dimension::Linear(dim)))
self.plane.place_entity(entity)
}
}
/// A linear dimension between two points of the working plane, its
/// dimension line through `point` along `axis` — what DIMLINEAR places and
/// what a dynamic dimensional constraint draws.
pub(crate) fn linear_dimension_entity(
first: DVec3,
second: DVec3,
point: DVec3,
axis: DVec3,
text_override: Option<String>,
) -> EntityType {
let mut dim = DimensionLinear::new(v3(first), v3(second));
dim.rotation = axis.y.atan2(axis.x);
dim.set_offset(dimension_line_offset(second, point, axis));
dim.base.definition_point = dim.definition_point;
dim.base.text_middle_point = v3(linear_text_pos(first, second, point, axis));
dim.base.insertion_point = dim.base.text_middle_point;
dim.base.actual_measurement = dim.measurement();
crate::entities::dimension::set_dimension_text_override(&mut dim.base, text_override);
EntityType::Dimension(Dimension::Linear(dim))
}
impl CadCommand for LinearDimensionCommand {
fn name(&self) -> &'static str {
"DIMLINEAR"
+656
View File
@@ -0,0 +1,656 @@
use acadrust::{EntityType, Handle};
use glam::DVec3;
use crate::command::{CadCommand, CmdOption, CmdResult, CoincidentPick};
use crate::scene::parametric_constraints::{
measured_expression, resolve_point, ConstraintKind, ParametricRef,
};
/// Which distance a dimensional constraint measures.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum DimConstraintAxis {
/// Horizontal or vertical, decided by where the dimension line goes.
Linear,
Horizontal,
Vertical,
Aligned,
}
#[derive(Clone, Copy)]
enum Step {
/// `Specify first constraint point or [Object] <Object>:`
First,
/// `Select object:`
Object,
/// `Specify second constraint point:`
Second {
first: ParametricRef,
first_point: DVec3,
},
/// `Specify dimension line location:`
Location {
first: ParametricRef,
second: ParametricRef,
first_point: DVec3,
second_point: DVec3,
},
/// `Enter value or name and value <d1=100>:`
Value {
first: ParametricRef,
second: ParametricRef,
first_point: DVec3,
second_point: DVec3,
location: DVec3,
axis: DVec3,
kind: ConstraintKind,
measured: f64,
},
}
/// The reference's Linear/Horizontal/Vertical/Aligned dimensional constraint:
/// two constraint points (or one object's ends), a dimension line location,
/// then the parameter name and expression the dynamic dimension carries.
pub struct DimConstraintCommand {
axis: DimConstraintAxis,
step: Step,
picked_entity: Option<EntityType>,
/// The next free `dN` the host reserved for this constraint.
default_name: String,
}
impl DimConstraintCommand {
pub const NO_OBJECT: &'static str = "No object found.";
pub const NO_POINT: &'static str = "No valid constraint point found.";
pub const SAME_POINT: &'static str =
"The object or point is already selected. Select a different object or constraint point.";
pub fn new(axis: DimConstraintAxis, default_name: String) -> Self {
Self {
axis,
step: Step::First,
picked_entity: None,
default_name,
}
}
fn command_name(&self) -> &'static str {
match self.axis {
DimConstraintAxis::Linear => "DCLINEAR",
DimConstraintAxis::Horizontal => "DCHORIZONTAL",
DimConstraintAxis::Vertical => "DCVERTICAL",
DimConstraintAxis::Aligned => "DCALIGNED",
}
}
fn noun(&self) -> &'static str {
match self.axis {
DimConstraintAxis::Linear => "Linear",
DimConstraintAxis::Horizontal => "Horizontal",
DimConstraintAxis::Vertical => "Vertical",
DimConstraintAxis::Aligned => "Aligned",
}
}
fn report(message: &str) -> CmdResult {
CmdResult::ReportError(message.to_string())
}
/// The two end points an object pick constrains: a line's or an arc's
/// ends, the picked polyline segment's vertices.
fn object_ends(
entity: &EntityType,
handle: Handle,
point: DVec3,
) -> Option<(ParametricRef, ParametricRef)> {
match entity {
EntityType::Line(_) | EntityType::Arc(_) => {
Some((ParametricRef::point(handle, 0), ParametricRef::point(handle, 1)))
}
EntityType::LwPolyline(_) | EntityType::Polyline2D(_) => {
let (source, _, _) =
crate::scene::centerline::picked_source(entity, handle, point)?;
let index = source.segment_index;
// A closed polyline's last segment ends at the first vertex.
let end = if resolve_point(entity, index + 1).is_some() {
index + 1
} else {
0
};
Some((ParametricRef::point(handle, index), ParametricRef::point(handle, end)))
}
_ => None,
}
}
fn world(entity: &EntityType, reference: ParametricRef) -> Option<DVec3> {
let point = resolve_point(entity, reference.marker?)?;
Some(DVec3::new(point.x, point.y, point.z))
}
/// The measured kind and axis: Linear reads the dimension line location
/// the way a linear dimension does, the others are fixed.
fn decide(&self, first: DVec3, second: DVec3, location: DVec3) -> (ConstraintKind, DVec3) {
match self.axis {
DimConstraintAxis::Linear => {
let axis =
crate::modules::annotate::linear_dim::measure_axis(first, second, location);
if axis.x.abs() > 0.5 {
(ConstraintKind::DistanceX, DVec3::X)
} else {
(ConstraintKind::DistanceY, DVec3::Y)
}
}
DimConstraintAxis::Horizontal => (ConstraintKind::DistanceX, DVec3::X),
DimConstraintAxis::Vertical => (ConstraintKind::DistanceY, DVec3::Y),
DimConstraintAxis::Aligned => (
ConstraintKind::Distance,
(second - first).normalize_or(DVec3::X),
),
}
}
fn measured(kind: ConstraintKind, first: DVec3, second: DVec3) -> f64 {
match kind {
ConstraintKind::DistanceX => (second.x - first.x).abs(),
ConstraintKind::DistanceY => (second.y - first.y).abs(),
_ => (second - first).length(),
}
}
fn build(&self, name: String, expression: String) -> Option<CmdResult> {
let Step::Value {
first,
second,
first_point,
second_point,
location,
axis,
kind,
..
} = self.step
else {
return None;
};
if expression.trim().is_empty() {
return None;
}
Some(CmdResult::AddDimensionalConstraint {
kind,
first,
second,
first_point,
second_point,
location,
axis,
name,
expression,
label: match self.axis {
DimConstraintAxis::Linear => "Linear constraint",
DimConstraintAxis::Horizontal => "Horizontal distance constraint",
DimConstraintAxis::Vertical => "Vertical distance constraint",
DimConstraintAxis::Aligned => "Aligned constraint",
},
})
}
}
impl CadCommand for DimConstraintCommand {
fn name(&self) -> &'static str {
self.command_name()
}
fn prompt(&self) -> String {
let name = self.command_name();
match self.step {
Step::First => {
format!("{name} Specify first constraint point or [Object] <Object>:")
}
Step::Object => format!("{name} Select object:"),
Step::Second { .. } => format!("{name} Specify second constraint point:"),
Step::Location { .. } => format!("{name} Specify dimension line location:"),
Step::Value { measured, .. } => format!(
"{name} Enter value or name and value <{}={}>:",
self.default_name,
measured_expression(measured)
),
}
}
fn options(&self) -> Vec<CmdOption> {
matches!(self.step, Step::First)
.then(|| vec![CmdOption::new("Object", "O")])
.unwrap_or_default()
}
fn wants_text_input(&self) -> bool {
true
}
fn point_step_accepts_keywords(&self) -> bool {
matches!(self.step, Step::First | Step::Value { .. })
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
match self.step {
Step::First => {
let keyword = text.trim().trim_start_matches('_').to_ascii_uppercase();
if matches!(keyword.as_str(), "O" | "OBJECT") {
self.step = Step::Object;
return Some(CmdResult::NeedPoint);
}
None
}
Step::Value { .. } => {
let text = text.trim();
let (name, expression) = match text.split_once('=') {
Some((name, expression)) => (name.trim().to_string(), expression.trim().to_string()),
None => (self.default_name.clone(), text.to_string()),
};
self.build(name, expression)
}
_ => None,
}
}
fn needs_entity_pick(&self) -> bool {
matches!(self.step, Step::First | Step::Object | Step::Second { .. })
}
fn entity_pick_accepts_points(&self) -> bool {
true
}
fn typed_point_picks_entity(&self) -> bool {
true
}
fn entity_pick_highlights_hover(&self) -> bool {
true
}
fn inject_before_entity_pick(&self) -> bool {
true
}
fn inject_picked_entity(&mut self, entity: EntityType) {
self.picked_entity = Some(entity);
}
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
match self.step {
// The host resolves the constraint point and hands it back
// through `accept_constraint_point`.
Step::First | Step::Second { .. } => CmdResult::CheckConstraintPoint(CoincidentPick {
handle: (!handle.is_null()).then_some(handle),
point,
whole_curve: false,
}),
Step::Object => {
if handle.is_null() {
return Self::report(Self::NO_OBJECT);
}
let Some(entity) = self.picked_entity.take() else {
return CmdResult::NeedPoint;
};
let Some((first, second)) = Self::object_ends(&entity, handle, point) else {
return Self::report(&format!(
"Invalid selection for {}. Select a line, polyline segment or arc.",
self.noun()
));
};
let (Some(first_point), Some(second_point)) =
(Self::world(&entity, first), Self::world(&entity, second))
else {
return Self::report(Self::NO_POINT);
};
self.step = Step::Location {
first,
second,
first_point,
second_point,
};
CmdResult::NeedPoint
}
Step::Location { .. } | Step::Value { .. } => self.on_point(point),
}
}
fn accept_constraint_point(&mut self, reference: ParametricRef, point: DVec3) -> CmdResult {
match self.step {
Step::First => {
self.step = Step::Second {
first: reference,
first_point: point,
};
CmdResult::NeedPoint
}
Step::Second { first, first_point } => {
if first == reference {
return Self::report(Self::SAME_POINT);
}
self.step = Step::Location {
first,
second: reference,
first_point,
second_point: point,
};
CmdResult::NeedPoint
}
_ => CmdResult::NeedPoint,
}
}
fn on_point(&mut self, point: DVec3) -> CmdResult {
match self.step {
Step::First | Step::Second { .. } => CmdResult::CheckConstraintPoint(CoincidentPick {
handle: None,
point,
whole_curve: false,
}),
Step::Object => Self::report(Self::NO_OBJECT),
Step::Location {
first,
second,
first_point,
second_point,
} => {
let (kind, axis) = self.decide(first_point, second_point, point);
let measured = Self::measured(kind, first_point, second_point);
self.step = Step::Value {
first,
second,
first_point,
second_point,
location: point,
axis,
kind,
measured,
};
CmdResult::ReportMeasurement(format!("Dimension text = {measured:.4}"))
}
Step::Value { .. } => CmdResult::NeedPoint,
}
}
fn on_enter(&mut self) -> CmdResult {
match self.step {
Step::First => {
self.step = Step::Object;
CmdResult::NeedPoint
}
Step::Value { measured, .. } => self
.build(self.default_name.clone(), measured_expression(measured))
.unwrap_or(CmdResult::Cancel),
_ => CmdResult::Cancel,
}
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
/// `DIMCONSTRAINT`: the reference's option front end for the dimensional
/// constraint family; each choice runs the focused command.
pub struct DimConstraintMenuCommand {
/// The option Enter takes: the dimensional constraint used last.
default: &'static str,
}
impl DimConstraintMenuCommand {
pub fn new(default: &'static str) -> Self {
Self { default }
}
fn dispatch(keyword: &str) -> Option<&'static str> {
Some(match keyword {
"L" | "LINEAR" => "DCLINEAR",
"H" | "HORIZONTAL" => "DCHORIZONTAL",
"V" | "VERTICAL" => "DCVERTICAL",
"A" | "ALIGNED" => "DCALIGNED",
"AN" | "ANGULAR" => "DCANGULAR",
"R" | "RADIAL" | "RADIUS" => "DCRADIUS",
"D" | "DIAMETER" => "DCDIAMETER",
"F" | "FORM" => "DCFORM",
"C" | "CONVERT" => "DCCONVERT",
_ => return None,
})
}
fn command_for_label(label: &str) -> &'static str {
match label {
"Linear" => "DCLINEAR",
"Horizontal" => "DCHORIZONTAL",
"Vertical" => "DCVERTICAL",
"ANgular" => "DCANGULAR",
"Radius" => "DCRADIUS",
"Diameter" => "DCDIAMETER",
"Convert" => "DCCONVERT",
_ => "DCALIGNED",
}
}
}
impl CadCommand for DimConstraintMenuCommand {
fn name(&self) -> &'static str {
"DIMCONSTRAINT"
}
fn prompt(&self) -> String {
format!(
"DIMCONSTRAINT Enter a dimensional constraint option [Linear/Horizontal/Vertical/Aligned/ANgular/Radius/Diameter/Form/Convert] <{}>:",
self.default
)
}
fn options(&self) -> Vec<CmdOption> {
vec![
CmdOption::new("Linear", "L"),
CmdOption::new("Horizontal", "H"),
CmdOption::new("Vertical", "V"),
CmdOption::new("Aligned", "A"),
CmdOption::new("ANgular", "AN"),
CmdOption::new("Radius", "R"),
CmdOption::new("Diameter", "D"),
CmdOption::new("Form", "F"),
CmdOption::new("Convert", "C"),
]
}
fn wants_text_input(&self) -> bool {
true
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let keyword = text.trim().trim_start_matches('_').to_ascii_uppercase();
Self::dispatch(&keyword).map(|command| CmdResult::Dispatch(command.to_string()))
}
fn on_point(&mut self, _point: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Dispatch(Self::command_for_label(self.default).to_string())
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
/// `DCFORM`: `Enter constraint form [Annotational/Dynamic] <current>:`;
/// the host records the answer and continues into DIMCONSTRAINT's option
/// prompt, as the reference does.
pub struct ConstraintFormCommand {
annotational: bool,
}
impl ConstraintFormCommand {
pub fn new(annotational: bool) -> Self {
Self { annotational }
}
fn current(&self) -> &'static str {
if self.annotational {
"Annotational"
} else {
"Dynamic"
}
}
}
impl CadCommand for ConstraintFormCommand {
fn name(&self) -> &'static str {
"DCFORM"
}
fn prompt(&self) -> String {
format!(
"DCFORM Enter constraint form [Annotational/Dynamic] <{}>:",
self.current()
)
}
fn options(&self) -> Vec<CmdOption> {
vec![
CmdOption::new("Annotational", "A"),
CmdOption::new("Dynamic", "D"),
]
}
fn wants_text_input(&self) -> bool {
true
}
fn point_step_accepts_keywords(&self) -> bool {
true
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let keyword = text.trim().trim_start_matches('_').to_ascii_uppercase();
let form = match keyword.as_str() {
"A" | "ANNOTATIONAL" => "Annotational",
"D" | "DYNAMIC" => "Dynamic",
_ => return None,
};
Some(CmdResult::Dispatch(format!("DCFORM_SET {form}")))
}
fn on_point(&mut self, _point: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Dispatch(format!("DCFORM_SET {}", self.current()))
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
/// The value prompt a double-clicked dynamic dimension opens, standing in
/// for the reference's in-place editor; `name=expression` also renames.
pub struct DimensionValueCommand {
name: String,
current: String,
}
impl DimensionValueCommand {
pub fn new(name: String, current: String) -> Self {
Self { name, current }
}
}
impl CadCommand for DimensionValueCommand {
fn name(&self) -> &'static str {
"DCVALUE"
}
fn prompt(&self) -> String {
format!(
"DCVALUE Enter value or name and value <{}={}>:",
self.name, self.current
)
}
fn wants_text_input(&self) -> bool {
true
}
fn point_step_accepts_keywords(&self) -> bool {
true
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let text = text.trim();
(!text.is_empty()).then(|| CmdResult::Dispatch(format!("DCVALUE {} {text}", self.name)))
}
fn on_point(&mut self, _point: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
#[cfg(test)]
mod tests {
use super::*;
use acadrust::entities::Line;
use acadrust::types::Vector3;
#[test]
fn object_pick_takes_the_line_ends_then_asks_for_the_location() {
let mut command = DimConstraintCommand::new(DimConstraintAxis::Linear, "d1".into());
assert!(matches!(command.on_enter(), CmdResult::NeedPoint));
command.inject_picked_entity(EntityType::Line(Line::from_points(
Vector3::ZERO,
Vector3::new(100.0, 50.0, 0.0),
)));
assert!(matches!(
command.on_entity_pick(Handle::new(7), DVec3::new(50.0, 25.0, 0.0)),
CmdResult::NeedPoint
));
// Below the pair: horizontal, so the measured value is the X span.
let CmdResult::ReportMeasurement(text) = command.on_point(DVec3::new(50.0, -30.0, 0.0))
else {
panic!("the location must report the dimension text");
};
assert_eq!(text, "Dimension text = 100.0000");
assert_eq!(
command.prompt(),
"DCLINEAR Enter value or name and value <d1=100>:"
);
let CmdResult::AddDimensionalConstraint {
kind, name, expression, ..
} = command.on_text_input("w=d1*2").expect("a value builds the constraint")
else {
panic!("the value must build the constraint");
};
assert_eq!(kind, ConstraintKind::DistanceX);
assert_eq!(name, "w");
assert_eq!(expression, "d1*2");
}
#[test]
fn a_location_beside_the_pair_measures_vertically() {
let mut command = DimConstraintCommand::new(DimConstraintAxis::Linear, "d1".into());
let first = ParametricRef::point(Handle::new(7), 0);
let second = ParametricRef::point(Handle::new(7), 1);
command.accept_constraint_point(first, DVec3::ZERO);
command.accept_constraint_point(second, DVec3::new(100.0, 50.0, 0.0));
let CmdResult::ReportMeasurement(text) = command.on_point(DVec3::new(140.0, 25.0, 0.0))
else {
panic!("the location must report the dimension text");
};
assert_eq!(text, "Dimension text = 50.0000");
}
}
+8 -1
View File
@@ -4,6 +4,8 @@
mod coincident;
mod concentric;
mod dim_constraint;
mod parameters_cli;
mod constraint_bar;
mod equal_distance;
#[path = "equal.rs"]
@@ -25,6 +27,11 @@ mod tools;
mod value;
pub use coincident::{coincident_tool, CoincidentConstraintCommand};
pub use concentric::ConcentricConstraintCommand;
pub use dim_constraint::{
ConstraintFormCommand, DimConstraintAxis, DimConstraintCommand, DimConstraintMenuCommand,
DimensionValueCommand,
};
pub use parameters_cli::ParametersCliCommand;
pub use constraint_bar::ConstraintBarOptionCommand;
pub use equal_command::EqualConstraintCommand;
pub use equal_distance::{equal_distance_tool, EqualDistanceConstraintCommand};
@@ -156,7 +163,7 @@ inventory::submit!(crate::command::CommandRegistration {
names: &[
"AUTOCONSTRAIN", "CONSTRAINTSETTINGS", "GCSMOOTH", "GCSHOW", "GCHIDE", "GCRESET",
"GCSHOWALL", "GCHIDEALL", "DCSHOW", "DCHIDE", "DCSHOWALL", "DCHIDEALL",
"DCCONVERT", "DELCONSTRAINT",
"DCCONVERT", "DELCONSTRAINT", "DIMCONSTRAINT", "DCFORM", "-PARAMETERS",
]
});
+146
View File
@@ -0,0 +1,146 @@
use glam::DVec3;
use crate::command::{CadCommand, CmdOption, CmdResult};
/// `-PARAMETERS`: the reference's command-line parameter manager. One run
/// performs one operation; the host answers the `LIST`, `NEW`, `EDIT`,
/// `SET`, `RENAME` and `DELETE` dispatches.
pub struct ParametersCliCommand {
step: Step,
}
#[derive(Clone)]
enum Step {
/// `Enter a parameter option [New/Edit/Rename/Delete/?]:`
Option,
/// `Enter name for new user parameter:`
NewName,
/// `Enter expression:`
NewExpression { name: String },
/// `Enter parameter name:`
EditName,
/// `Enter expression:` after the host printed the old expression.
EditExpression { name: String },
/// `Enter old parameter name:`
RenameOld,
/// `Enter new parameter name:`
RenameNew { old: String },
/// `Enter parameter name to delete:`
DeleteName,
}
impl ParametersCliCommand {
pub fn new() -> Self {
Self { step: Step::Option }
}
/// The expression prompt the host opens after printing the old expression.
pub fn edit_expression(name: String) -> Self {
Self {
step: Step::EditExpression { name },
}
}
}
impl Default for ParametersCliCommand {
fn default() -> Self {
Self::new()
}
}
impl CadCommand for ParametersCliCommand {
fn name(&self) -> &'static str {
"-PARAMETERS"
}
fn prompt(&self) -> String {
let text = match &self.step {
Step::Option => "Enter a parameter option [New/Edit/Rename/Delete/?]:",
Step::NewName => "Enter name for new user parameter:",
Step::NewExpression { .. } | Step::EditExpression { .. } => "Enter expression:",
Step::EditName => "Enter parameter name:",
Step::RenameOld => "Enter old parameter name:",
Step::RenameNew { .. } => "Enter new parameter name:",
Step::DeleteName => "Enter parameter name to delete:",
};
format!("-PARAMETERS {text}")
}
fn options(&self) -> Vec<CmdOption> {
match self.step {
Step::Option => vec![
CmdOption::new("New", "N"),
CmdOption::new("Edit", "E"),
CmdOption::new("Rename", "R"),
CmdOption::new("Delete", "D"),
CmdOption::new("?", "?"),
],
_ => Vec::new(),
}
}
fn wants_text_input(&self) -> bool {
true
}
fn point_step_accepts_keywords(&self) -> bool {
true
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let text = text.trim();
if text.is_empty() {
return None;
}
match self.step.clone() {
Step::Option => {
let keyword = text.trim_start_matches('_').to_ascii_uppercase();
let next = match keyword.as_str() {
"N" | "NEW" => Step::NewName,
"E" | "EDIT" => Step::EditName,
"R" | "RENAME" => Step::RenameOld,
"D" | "DELETE" => Step::DeleteName,
"?" => return Some(CmdResult::Dispatch("-PARAMETERS LIST".to_string())),
_ => return None,
};
self.step = next;
Some(CmdResult::NeedPoint)
}
Step::NewName => {
self.step = Step::NewExpression {
name: text.to_string(),
};
Some(CmdResult::NeedPoint)
}
Step::NewExpression { name } => {
Some(CmdResult::Dispatch(format!("-PARAMETERS NEW {name} {text}")))
}
Step::EditName => Some(CmdResult::Dispatch(format!("-PARAMETERS EDIT {text}"))),
Step::EditExpression { name } => {
Some(CmdResult::Dispatch(format!("-PARAMETERS SET {name} {text}")))
}
Step::RenameOld => {
self.step = Step::RenameNew {
old: text.to_string(),
};
Some(CmdResult::NeedPoint)
}
Step::RenameNew { old } => {
Some(CmdResult::Dispatch(format!("-PARAMETERS RENAME {old} {text}")))
}
Step::DeleteName => Some(CmdResult::Dispatch(format!("-PARAMETERS DELETE {text}"))),
}
}
fn on_point(&mut self, _point: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
+142 -3
View File
@@ -2,7 +2,8 @@
use acadrust::objects::{
Assoc2dConstraintGroup, AssocAction, AssocActionDependency, AssocConstraintNode,
AssocConstraintNodeData, AssocDependency, AssocEvalValue, AssocEvalVariant,
AssocConstraintNodeData, AssocDependency, AssocDimDependencyBody, AssocEvalValue,
AssocEvalVariant,
AssocGeomDependency, AssocNetwork, AssocPersistentSubentId, AssocValueDependency,
AssocVariable, AssociativeData, AssociativeObject, ObjectType,
};
@@ -1291,6 +1292,67 @@ impl Allocator<'_> {
}
handle
}
/// The `AcDbAssocDependency` + `AcDbAssocDimDependencyBody` pair that
/// ties a dimensional constraint to the dynamic dimension showing it.
fn dimension_dependency(
&mut self,
group_handle: Handle,
dimension: Handle,
name: &str,
dependency_id: i32,
) -> Handle {
let handle = self.document.allocate_handle();
let body = self.insert_associative(
handle,
"ACDBASSOCDIMDEPENDENCYBODY",
"AcDbAssocDimDependencyBody",
AssociativeData::DimDependencyBody(AssocDimDependencyBody {
dependency_body_version: 1,
base_version: 1,
name: name.to_string(),
class_version: 1,
}),
);
self.insert_associative_at(
handle,
group_handle,
"ACDBASSOCDEPENDENCY",
"AcDbAssocDependency",
AssociativeData::Dependency(AssocDependency {
class_version: 2,
is_read_dependency: true,
is_write_dependency: true,
is_attached_to_object: true,
is_delegating_to_owning_action: true,
dependent_on: dimension,
dependency_body: body,
dependency_body_id: dependency_id,
..Default::default()
}),
);
// Earlier materializations' dependencies are gone by now; drop
// their reactor entries before adding this one.
let live: Vec<Handle> = self
.document
.get_entity(dimension)
.map(|entity| {
entity
.common()
.reactors
.iter()
.copied()
.filter(|reactor| self.document.objects.contains_key(reactor))
.collect()
})
.unwrap_or_default();
if let Some(entity) = self.document.get_entity_mut(dimension) {
let reactors = &mut entity.common_mut().reactors;
*reactors = live;
reactors.push(handle);
}
handle
}
}
/// Sets the dependency on whole-geometry nodes. Implicit points retain a
@@ -1355,6 +1417,27 @@ fn set_value_dependency(data: &mut AssocConstraintNodeData, handle: Handle) {
}
}
/// Patches a dimensional-constraint node's `dimension_dependency` field.
fn set_dimension_dependency(data: &mut AssocConstraintNodeData, handle: Handle) {
match data {
AssocConstraintNodeData::Distance {
dimension_dependency,
..
}
| AssocConstraintNodeData::Angle {
dimension_dependency,
..
}
| AssocConstraintNodeData::RadiusDiameter {
dimension_dependency,
..
} => {
*dimension_dependency = handle;
}
_ => {}
}
}
/// Associative classes that must have real class numbers in DWG/DXF.
const ASSOC_CLASSES: &[(&str, &str, i32)] = &[
(
@@ -1366,6 +1449,8 @@ const ASSOC_CLASSES: &[(&str, &str, i32)] = &[
("ACDBASSOCVARIABLE", "AcDbAssocVariable", 45),
("ACDBASSOCGEOMDEPENDENCY", "AcDbAssocGeomDependency", 29),
("ACDBASSOCVALUEDEPENDENCY", "AcDbAssocValueDependency", 29),
("ACDBASSOCDEPENDENCY", "AcDbAssocDependency", 29),
("ACDBASSOCDIMDEPENDENCYBODY", "AcDbAssocDimDependencyBody", 45),
];
fn ensure_associative_classes_registered(document: &mut CadDocument) {
@@ -1806,6 +1891,19 @@ fn driving_value(
}
}
/// The dynamic dimension an `AcDbAssocDependency` ties to a dimensional
/// constraint node.
fn dependency_dimension(document: &CadDocument, dependency: Handle) -> Option<Handle> {
let ObjectType::Associative(object) = document.objects.get(&dependency)? else {
return None;
};
let AssociativeData::Dependency(dependency) = &object.data else {
return None;
};
let target = dependency.dependent_on;
matches!(document.get_entity(target)?, EntityType::Dimension(_)).then_some(target)
}
fn owning_composite(data: &AssocConstraintNodeData) -> Option<i32> {
match data {
AssocConstraintNodeData::Geometrical { owner_id, .. }
@@ -2250,7 +2348,25 @@ pub(super) fn native_constraint_set(
{
continue;
}
set.add(kind, targets, driving);
let dimension = match &node.data {
AssocConstraintNodeData::Angle {
dimension_dependency,
..
}
| AssocConstraintNodeData::Distance {
dimension_dependency,
..
}
| AssocConstraintNodeData::RadiusDiameter {
dimension_dependency,
..
} => dependency_dimension(document, *dimension_dependency),
_ => None,
};
let id = set.add(kind, targets, driving);
if let Some(dimension) = dimension {
set.dimensions.insert(id, dimension);
}
if let Some(constraint) = set.constraints.last_mut() {
constraint.enabled = enabled;
constraint.rigid_points = rigid_points;
@@ -2429,15 +2545,21 @@ fn materialize_scope(
// Pass 1 (read-only): constraint-node shapes.
let mut needs_value_dependency = Vec::new();
let mut dimension_links = Vec::new();
let (mut nodes, entities, referenced_entities) = {
let mut builder = GroupBuilder::new(document);
for constraint in &set.constraints {
constraint_node(
let node_id = constraint_node(
&mut builder,
document,
constraint,
&mut needs_value_dependency,
);
if let Some((node_id, dimension)) = node_id.zip(set.dimensions.get(&constraint.id)) {
if document.get_entity(*dimension).is_some() {
dimension_links.push((node_id, *dimension));
}
}
}
let GroupBuilder {
nodes,
@@ -2527,6 +2649,7 @@ fn materialize_scope(
}
let mut group_dependencies = geometry_dependencies;
let mut node_names: FxHashMap<i32, String> = FxHashMap::default();
for (node_id, driving) in needs_value_dependency {
let Some(driving) = driving else { continue };
let Ok(resolved) = driving.resolve(parameters) else {
@@ -2542,6 +2665,7 @@ fn materialize_scope(
}
DrivingValue::Literal(_) => (format!("d{node_id}"), resolved.to_string()),
};
node_names.insert(node_id, name.clone());
let variable_handle = allocator.variable(network_handle, &name, &formula, resolved);
let dependency_id = group_dependencies.len() as i32 + 1;
let dep_handle =
@@ -2551,6 +2675,18 @@ fn materialize_scope(
set_value_dependency(&mut node.data, dep_handle);
}
}
for (node_id, dimension) in dimension_links {
let Some(name) = node_names.get(&node_id) else {
continue;
};
let dependency_id = group_dependencies.len() as i32 + 1;
let dep_handle =
allocator.dimension_dependency(group_handle?, dimension, name, dependency_id);
group_dependencies.push(dep_handle);
if let Some(node) = nodes.iter_mut().find(|n| n.node_id == node_id) {
set_dimension_dependency(&mut node.data, dep_handle);
}
}
if let Some(group_handle) = group_handle {
let group = Assoc2dConstraintGroup {
@@ -2674,6 +2810,9 @@ impl Scene {
pub(crate) fn load_parametric_constraints_from_document(&mut self) {
self.parametric_constraints.clear();
// Dynamic dimensions read from the file get their screen size on the
// first tick after the view is known.
self.dynamic_dimension_camera_gen = None;
let owners: Vec<_> = self
.document
.block_records
+3
View File
@@ -1402,6 +1402,9 @@ impl Scene {
}
if self.object_isolation.hides(common.handle)
|| (!include_preview_hidden && self.preview_hidden.contains(&common.handle))
|| self.hidden_dynamic_dimensions.contains(&common.handle)
// A dynamic dimension never plots.
|| (plot_only && self.is_dynamic_dimension(common.handle))
{
return false;
}
+15
View File
@@ -2295,6 +2295,15 @@ pub struct Scene {
has_associative_centers: std::cell::Cell<Option<bool>>,
/// Runtime parametric constraint sets decoded from standard graph scopes.
pub(crate) parametric_constraints: Vec<parametric_constraints::ParametricConstraintSet>,
/// CONSTRAINTNAMEFORMAT: what a dynamic dimension's text shows —
/// 0 the parameter name, 1 the value, 2 `name=value`.
pub constraint_name_format: u8,
/// DYNCONSTRAINTDISPLAY: whether dynamic dimensions draw at all.
pub dynamic_constraint_display: bool,
/// Dynamic dimensions DCHIDE or DYNCONSTRAINTDISPLAY 0 keep off screen.
hidden_dynamic_dimensions: HashSet<Handle>,
/// The camera generation the dynamic dimensions were last scaled for.
dynamic_dimension_camera_gen: Option<u64>,
/// Session-only visibility overrides for constraint glyphs.
hidden_parametric_constraints: HashSet<(
parametric_constraints::ParametricScope,
@@ -2625,6 +2634,10 @@ impl Scene {
dependency_index_cache: RefCell::new(None),
associative_hatch_source_cache: RefCell::new(None),
parametric_constraints: Vec::new(),
constraint_name_format: 2,
dynamic_constraint_display: true,
hidden_dynamic_dimensions: HashSet::default(),
dynamic_dimension_camera_gen: None,
hidden_parametric_constraints: HashSet::default(),
shown_parametric_constraints: HashSet::default(),
named_parameters: named_parameters::ParameterTable::new(),
@@ -3319,6 +3332,7 @@ impl Scene {
}
}
if !self.parametric_constraints.is_empty() || !self.named_parameters.is_empty() {
self.refresh_dynamic_dimension_texts();
self.sync_native_parametric_graph();
}
if !changes.is_empty() {
@@ -5533,6 +5547,7 @@ impl Scene {
self.object_isolation.hides(handle)
|| self.preview_hidden.contains(&handle)
|| self.command_preview_hidden.contains(&handle)
|| self.hidden_dynamic_dimensions.contains(&handle)
}
/// Replace the command-owned source hide set and refresh only handles whose
+321 -1
View File
@@ -351,6 +351,57 @@ pub(crate) fn equal_size_follower(
Some(entity)
}
/// The first free `d1`, `d2`, … name a new dimensional constraint takes.
pub(crate) fn next_dimensional_parameter_name(
table: &super::named_parameters::ParameterTable,
) -> String {
(1..)
.map(|n| format!("d{n}"))
.find(|name| !table.contains(name))
.unwrap_or_else(|| "d1".to_string())
}
/// The constraint-bar label that draws a dynamic dimension's lock mark.
pub const DYNAMIC_DIMENSION_GLYPH: &str = "\u{1F512}";
/// The on-screen text height of a dynamic dimension, in pixels.
pub const DYNAMIC_DIMENSION_TEXT_PX: f32 = 12.0;
/// The xdata application that marks an annotational constraint dimension.
pub const ANNOTATIONAL_DIMENSION_APP: &str = "OCS_CONSTRAINT_FORM";
/// The text a dynamic dimension shows: CONSTRAINTNAMEFORMAT 0 = name,
/// 1 = value, 2 = name=value. A plain number shows as typed (`d1=100`); an
/// expression shows the expression under the reference's `fx:` prefix
/// (`fx: d2=d1*2`).
pub(crate) fn dynamic_dimension_text(name: &str, source: &str, value: f64, format: u8) -> String {
let source = source.trim();
let literal = source.parse::<f64>().is_ok();
let shown = if literal {
source.to_string()
} else {
measured_expression(value)
};
match format {
0 => name.to_string(),
1 if literal => shown,
1 => format!("fx: {shown}"),
_ if literal => format!("{name}={shown}"),
_ => format!("fx: {name}={source}"),
}
}
/// The expression a measured value seeds a parameter with (`100`, `111.8034`).
pub(crate) fn measured_expression(value: f64) -> String {
let text = format!("{value:.4}");
let text = text.trim_end_matches('0').trim_end_matches('.');
if text.is_empty() || text == "-" {
"0".to_string()
} else {
text.to_string()
}
}
/// Grabbed points are exact kernel inputs; the solver anchors the remaining
/// endpoint coordinates according to the line's directional constraints.
pub(crate) fn grip_solve_anchor_refs(
@@ -585,6 +636,8 @@ pub struct ParametricConstraintSet {
/// parameter names may legitimately occur in different blocks.
pub(crate) local_parameters: super::named_parameters::ParameterTable,
pub(crate) retained_standard_groups: Vec<Handle>,
/// The dynamic dimension that shows a dimensional constraint, by id.
pub(crate) dimensions: std::collections::HashMap<ConstraintId, Handle>,
/// Cached total remaining degrees of freedom, summed across every
/// independent solve partition in this scope — updated by
/// `parametric_solve::solve_scope` each time this set is resolved. `None`
@@ -611,6 +664,7 @@ impl ParametricConstraintSet {
next_id: 0,
local_parameters: super::named_parameters::ParameterTable::new(),
retained_standard_groups: Vec::new(),
dimensions: std::collections::HashMap::new(),
dof: None,
conflicts: Vec::new(),
}
@@ -702,6 +756,7 @@ impl ParametricConstraintSet {
/// Removes a constraint by id. Returns whether one was actually removed.
pub fn remove(&mut self, id: ConstraintId) -> bool {
self.dimensions.remove(&id);
let before = self.constraints.len();
self.constraints.retain(|c| c.id != id);
self.constraints.len() != before
@@ -1420,6 +1475,66 @@ impl super::Scene {
!self.hidden_parametric_constraints.contains(&(scope, id))
}
/// Rewrites every dynamic dimension's text from its constraint's
/// parameter name and current value, in the CONSTRAINTNAMEFORMAT
/// reading `constraint_name_format` selects.
pub(crate) fn refresh_dynamic_dimension_texts(&mut self) {
let format = self.constraint_name_format;
let mut updates: Vec<(Handle, String)> = Vec::new();
for set in &self.parametric_constraints {
let table = if set.local_parameters.is_empty() {
&self.named_parameters
} else {
&set.local_parameters
};
for (id, dimension) in &set.dimensions {
let Some(constraint) = set.get(*id) else {
continue;
};
let (name, source, value) = match &constraint.driving_param {
Some(DrivingValue::Named(name)) => (
name.clone(),
table
.get(name)
.map(|parameter| parameter.source.clone())
.unwrap_or_default(),
table.resolve(name).unwrap_or(f64::NAN),
),
Some(DrivingValue::Literal(value)) => {
(String::new(), measured_expression(*value), *value)
}
None => continue,
};
// An annotational dimension shows the value at dimension
// precision, as the reference does.
let source = if self.dimension_is_annotational(*dimension) {
format!("{value:.4}")
} else {
source
};
updates.push((
*dimension,
dynamic_dimension_text(&name, &source, value, format),
));
}
}
for (handle, text) in updates {
let Some(acadrust::EntityType::Dimension(mut dimension)) =
self.document.get_entity(handle).cloned()
else {
continue;
};
if dimension.base().user_text.as_deref() == Some(text.as_str()) {
continue;
}
crate::entities::dimension::set_dimension_text_override(
dimension.base_mut(),
Some(text),
);
self.update_entity(acadrust::EntityType::Dimension(dimension));
}
}
pub fn should_display_parametric_constraint(
&self,
scope: ParametricScope,
@@ -1483,9 +1598,162 @@ impl super::Scene {
self.shown_parametric_constraints.remove(&(scope, *id));
}
}
self.refresh_hidden_dynamic_dimensions();
ids.len()
}
/// True for a dynamic dimension of a dimensional constraint (not an
/// annotational one, which is an ordinary plotted dimension).
pub(crate) fn is_dynamic_dimension(&self, handle: Handle) -> bool {
self.parametric_constraints
.iter()
.any(|set| set.dimensions.values().any(|dimension| *dimension == handle))
&& !self.dimension_is_annotational(handle)
}
/// True when a dimensional constraint's dimension uses the annotational
/// form: drawn with its style, plotted, never rescaled to the screen.
pub(crate) fn dimension_is_annotational(&self, handle: Handle) -> bool {
self.document.get_entity(handle).is_some_and(|entity| {
entity
.common()
.extended_data
.get_record(ANNOTATIONAL_DIMENSION_APP)
.is_some()
})
}
/// Marks a constraint dimension as annotational (DCFORM Annotational).
pub(crate) fn mark_dimension_annotational(&mut self, handle: Handle) {
crate::scene::view::dispatch::set_entity_xdata(
&mut self.document,
handle,
ANNOTATIONAL_DIMENSION_APP,
Some(vec![acadrust::xdata::XDataValue::String(
"ANNOTATIONAL".to_string(),
)]),
);
}
/// The document as written to a file: a dynamic dimension's screen-size
/// overrides are a display matter and stay out of the file.
pub(crate) fn document_for_save(&self) -> acadrust::CadDocument {
use crate::entities::dim_override as ov;
let mut document = self.document.clone();
for set in &self.parametric_constraints {
for handle in set.dimensions.values().copied() {
if self.dimension_is_annotational(handle) {
continue;
}
for code in [ov::DIMSCALE, ov::DIMTIH, ov::DIMTOH] {
ov::set(&mut document, handle, code, None);
}
}
}
document
}
/// Gives every dynamic dimension a DIMSCALE override that keeps its
/// text, arrows and offsets at a screen size (the reference draws them
/// at a constant pixel size whatever the zoom). Runs whenever the camera
/// changed; `force` re-applies after a dimension was created or loaded.
pub fn refresh_dynamic_dimension_scales(&mut self, force: bool) {
let Some(wpp) = self.world_per_pixel() else {
return;
};
if !force && self.dynamic_dimension_camera_gen == Some(self.camera_generation) {
return;
}
self.dynamic_dimension_camera_gen = Some(self.camera_generation);
let handles: Vec<Handle> = self
.parametric_constraints
.iter()
.flat_map(|set| set.dimensions.values().copied())
.collect();
let mut changes = Vec::new();
for handle in handles {
let Some(acadrust::EntityType::Dimension(dimension)) = self.document.get_entity(handle)
else {
continue;
};
// An annotational dimension keeps its style's size.
if self.dimension_is_annotational(handle) {
continue;
}
let style_name = dimension.base().style_name.clone();
let text_height = self
.document
.dim_styles
.iter()
.find(|style| {
style.name.eq_ignore_ascii_case(&style_name)
|| (style_name.trim().is_empty()
&& style.name.eq_ignore_ascii_case("Standard"))
})
.map(|style| style.dimtxt)
.filter(|height| *height > 1e-9)
.unwrap_or(0.18);
let scale = f64::from(wpp) * f64::from(DYNAMIC_DIMENSION_TEXT_PX) / text_height;
use crate::entities::dim_override as ov;
let xdata = &dimension.base().common.extended_data;
let current = ov::real(xdata, ov::DIMSCALE);
// The reference draws dynamic dimension text horizontally.
let horizontal = ov::int(xdata, ov::DIMTIH) == Some(1) && ov::int(xdata, ov::DIMTOH) == Some(1);
if current.is_some_and(|value| (value - scale).abs() < 1e-9) && horizontal {
continue;
}
ov::set(
&mut self.document,
handle,
ov::DIMSCALE,
Some(acadrust::xdata::XDataValue::Real(scale)),
);
if !horizontal {
for code in [ov::DIMTIH, ov::DIMTOH] {
ov::set(
&mut self.document,
handle,
code,
Some(acadrust::xdata::XDataValue::Integer16(1)),
);
}
}
changes.push((handle, super::ChangeKind::Modified));
}
if !changes.is_empty() {
self.bump_entities(&changes);
}
}
/// Re-derives which dynamic dimensions stay off screen (DCHIDE, or
/// DYNCONSTRAINTDISPLAY 0 for all of them) and redraws the ones that
/// changed.
pub(crate) fn refresh_hidden_dynamic_dimensions(&mut self) {
let desired: rustc_hash::FxHashSet<Handle> = self
.parametric_constraints
.iter()
.flat_map(|set| {
set.dimensions.iter().filter_map(|(id, dimension)| {
(!self.dimension_is_annotational(*dimension)
&& (!self.dynamic_constraint_display
|| !self.is_parametric_constraint_visible(set.scope, *id)))
.then_some(*dimension)
})
})
.collect();
if desired == self.hidden_dynamic_dimensions {
return;
}
let changes: Vec<_> = self
.hidden_dynamic_dimensions
.symmetric_difference(&desired)
.copied()
.map(|handle| (handle, super::ChangeKind::Modified))
.collect();
self.hidden_dynamic_dimensions = desired;
self.bump_entities(&changes);
}
/// Infers relations already present in the selected geometry.
pub fn inferred_parametric_constraints(
&self,
@@ -1862,7 +2130,8 @@ impl super::Scene {
let mut placements = set
.constraints
.iter()
.filter(|c| c.enabled)
// A dynamic dimension is its constraint's whole display.
.filter(|c| c.enabled && !set.dimensions.contains_key(&c.id))
.filter(|c| {
let selected = c
.refs
@@ -1971,6 +2240,57 @@ impl super::Scene {
equal_anchors.push(*point);
true
});
// A dynamic dimension carries the reference's lock mark at the
// start of its text instead of a constraint bar.
for (id, dimension) in &set.dimensions {
let Some(constraint) = set.get(*id) else {
continue;
};
if !constraint.enabled || self.entity_temporarily_hidden(*dimension) {
continue;
}
let Some(acadrust::EntityType::Dimension(entity)) = self.document.get_entity(*dimension)
else {
continue;
};
let Some((anchor, outward)) = crate::entities::dimension::dynamic_dimension_lock_anchor(
&self.document,
entity,
self.annotation_scale as f64,
) else {
continue;
};
let project = |point: Vector3| {
crate::scene::pick::grip::project_rte(
glam::DVec3::new(point.x, point.y, point.z),
view_rot,
eye,
bounds,
)
};
let (Some(screen), Some(outward_screen)) = (
project(anchor),
project(Vector3::new(
anchor.x + outward.x,
anchor.y + outward.y,
anchor.z + outward.z,
)),
) else {
continue;
};
let direction = (outward_screen - screen).normalize_or(glam::Vec2::NEG_X);
let point = iced::Point::new(bounds.x + screen.x, bounds.y + screen.y);
if point.x.is_finite() && point.y.is_finite() {
placements.push((
*id,
point,
direction.to_array(),
DYNAMIC_DIMENSION_GLYPH.to_string(),
false,
Vec::new(),
));
}
}
placements
}
+41 -1
View File
@@ -2443,7 +2443,32 @@ fn solve_scope(
})
})
.flatten();
let pinned = axis_pin.unwrap_or(*reference);
// A transformed entity whose own end points a driving dimension
// measures keeps only its second constraint point where the transform
// put it; the dimension pulls the first end back to its value (a line
// scaled 2x about its start keeps its d1 by moving that start), as in
// the reference.
let dimension_pin = (reference.marker.is_none() && axis_pin.is_none())
.then(|| {
constraints.iter().find_map(|c| {
(c.enabled
&& c.driving_param.is_some()
&& matches!(
c.kind,
ConstraintKind::Distance
| ConstraintKind::DistanceX
| ConstraintKind::DistanceY
| ConstraintKind::DistanceDirected
)
&& c.refs.len() >= 2
&& c.refs
.iter()
.all(|r| r.entity == reference.entity && r.marker.is_some()))
.then(|| c.refs[1])
})
})
.flatten();
let pinned = axis_pin.or(dimension_pin).unwrap_or(*reference);
// The re-aligned entity keeps the length the transform gave it (a
// scaled vertical line stays scaled), not its pre-edit length.
if axis_pin.is_some() {
@@ -2743,6 +2768,21 @@ fn solve_scope(
constraint.refs.get(2).is_some_and(|axis| axis.entity == *handle)
})
|| equal_followers.iter().any(|follower| follower.entity == *handle)
// A driving dimension between an entity's own ends is its size;
// a retained length would contradict a new value.
|| constraints.iter().any(|c| {
c.enabled
&& c.driving_param.is_some()
&& matches!(
c.kind,
ConstraintKind::Distance
| ConstraintKind::DistanceX
| ConstraintKind::DistanceY
| ConstraintKind::DistanceDirected
)
&& c.refs.len() >= 2
&& c.refs.iter().all(|r| r.entity == *handle && r.marker.is_some())
})
{
continue;
}
+26 -15
View File
@@ -39,6 +39,11 @@ fn is_fixed_glyph(label: &str) -> bool {
label == "F" || label == crate::scene::parametric_constraints::FIXED_POINT_GLYPH
}
/// The lock mark a dynamic dimension carries (`DYNAMIC_DIMENSION_GLYPH`).
fn is_dynamic_dimension_glyph(label: &str) -> bool {
label == crate::scene::parametric_constraints::DYNAMIC_DIMENSION_GLYPH
}
/// Vertical's two axis-mark labels (`parametric_constraints::vertical_glyph_label`).
fn is_vertical_glyph(label: &str) -> bool {
label == "│" || label == crate::scene::parametric_constraints::VERTICAL_POINTS_GLYPH
@@ -48,7 +53,11 @@ fn constraint_glyph_size(label: &str) -> Size {
if is_compact_coincident_glyph(label) {
return Size::new(COINCIDENT_GLYPH_SIZE, COINCIDENT_GLYPH_SIZE);
}
if label == "G²" || is_fixed_glyph(label) || is_vertical_glyph(label) {
if label == "G²"
|| is_fixed_glyph(label)
|| is_vertical_glyph(label)
|| is_dynamic_dimension_glyph(label)
{
let side = CONSTRAINT_GLYPH_SIZE + CONSTRAINT_GLYPH_PAD_Y * 2.0;
return Size::new(side, side);
}
@@ -109,15 +118,10 @@ fn draw_concentric_constraint_glyph(
fn draw_fixed_constraint_glyph(
frame: &mut canvas::Frame,
center: Point,
text: Color,
lock: Color,
badge: Color,
point_marker: bool,
) {
let lock = if point_marker {
text
} else {
Color::from_rgb8(214, 76, 76)
};
let shackle = canvas::Path::new(|builder| {
let shackle_center = Point::new(center.x, center.y - 1.6);
for step in 0..=12 {
@@ -2114,7 +2118,10 @@ impl canvas::Program<Message> for SelectionCanvas {
})
.into(),
);
frame.fill(&pill, bg);
// A dynamic dimension's lock sits bare on the drawing.
if !is_dynamic_dimension_glyph(label) {
frame.fill(&pill, bg);
}
if *is_selected {
frame.stroke(
&pill,
@@ -2132,14 +2139,18 @@ impl canvas::Program<Message> for SelectionCanvas {
draw_smooth_constraint_glyph(&mut frame, glyph_center, fg);
} else if label == "◎" {
draw_concentric_constraint_glyph(&mut frame, glyph_center, fg);
} else if is_dynamic_dimension_glyph(label) {
draw_fixed_constraint_glyph(&mut frame, glyph_center, fg, bg, false);
} else if is_fixed_glyph(label) {
draw_fixed_constraint_glyph(
&mut frame,
glyph_center,
fg,
bg,
label == crate::scene::parametric_constraints::FIXED_POINT_GLYPH,
);
let point =
label == crate::scene::parametric_constraints::FIXED_POINT_GLYPH;
// The object-mode lock reads red, the point-mode one white.
let lock = if point {
fg
} else {
Color::from_rgb8(214, 76, 76)
};
draw_fixed_constraint_glyph(&mut frame, glyph_center, lock, bg, point);
} else if is_vertical_glyph(label) {
draw_vertical_constraint_glyph(
&mut frame,