forked from 1b-agent/opencadstudio-upstream
Merge pull request #1431 from ramox81:codex/angular-constraint-parity
This commit is contained in:
commit
d37ce4d617
12 files changed
+1278
-205
No files matched your search
+225
-5
@@ -1527,8 +1527,12 @@ impl OpenCADStudio {
|
||||
Some(crate::scene::named_parameters::DrivingValue::Named(_))
|
||||
)
|
||||
})
|
||||
.filter_map(|c| c.refs.first().copied())
|
||||
.filter(|reference| reference.marker.is_some())
|
||||
.flat_map(|c| {
|
||||
crate::scene::parametric_constraints::dimensional_anchor_refs(
|
||||
&self.tabs[i].scene.document,
|
||||
&c.refs,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let pending = self.begin_undo(i, "Apply named parameters", touched.len(), true);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
@@ -1590,7 +1594,6 @@ impl OpenCADStudio {
|
||||
&c.refs,
|
||||
)
|
||||
})
|
||||
.filter(|reference| reference.marker.is_some())
|
||||
.collect();
|
||||
self.tabs[i].dirty = true;
|
||||
if touched.is_empty() {
|
||||
@@ -1845,7 +1848,15 @@ impl OpenCADStudio {
|
||||
entity.common_mut().color = acadrust::types::Color::ByLayer;
|
||||
// The dynamic form's screen-size and horizontal-text
|
||||
// overrides go; the style draws it.
|
||||
for code in [dov::DIMSCALE, dov::DIMTIH, dov::DIMTOH] {
|
||||
for code in [
|
||||
dov::DIMSCALE,
|
||||
dov::DIMGAP,
|
||||
dov::DIMEXO,
|
||||
dov::DIMEXE,
|
||||
dov::DIMASZ,
|
||||
dov::DIMTIH,
|
||||
dov::DIMTOH,
|
||||
] {
|
||||
dov::set_on_entity(&mut entity, code, None);
|
||||
}
|
||||
} else {
|
||||
@@ -4049,6 +4060,7 @@ impl OpenCADStudio {
|
||||
direction,
|
||||
name,
|
||||
expression,
|
||||
renamed,
|
||||
label,
|
||||
} => {
|
||||
use crate::modules::annotate::{aligned_dim, linear_dim};
|
||||
@@ -4104,7 +4116,14 @@ impl OpenCADStudio {
|
||||
);
|
||||
let name = name.trim().to_string();
|
||||
if !is_valid_name(&name) {
|
||||
self.reprompt_active_command(i, "Invalid parameter name.");
|
||||
self.command_line.push_error(
|
||||
"Only alphanumeric names, starting with alpha characters, are allowed.",
|
||||
);
|
||||
self.reprompt_active_command(i, &format!("Invalid parameter name: {name}."));
|
||||
return Task::none();
|
||||
}
|
||||
if renamed && self.tabs[i].scene.named_parameters().contains(&name) {
|
||||
self.reprompt_active_command(i, "Parameter with this name already exists.");
|
||||
return Task::none();
|
||||
}
|
||||
let mut table = self.tabs[i].scene.named_parameters().clone();
|
||||
@@ -4133,6 +4152,7 @@ impl OpenCADStudio {
|
||||
false,
|
||||
Some(&expression),
|
||||
annotational,
|
||||
None,
|
||||
);
|
||||
let mut entity = if kind == ConstraintKind::Distance {
|
||||
aligned_dim::aligned_dimension_entity(
|
||||
@@ -4232,6 +4252,206 @@ impl OpenCADStudio {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
CmdResult::CancelWithMessage(message) => {
|
||||
self.command_line.push_error(&message);
|
||||
return self.apply_cmd_result(CmdResult::Cancel);
|
||||
}
|
||||
CmdResult::AddAngularConstraint {
|
||||
refs,
|
||||
points,
|
||||
location,
|
||||
sector,
|
||||
name,
|
||||
expression,
|
||||
renamed,
|
||||
} => {
|
||||
use crate::modules::annotate::angular_dim;
|
||||
use crate::scene::named_parameters::{is_valid_name, DrivingValue};
|
||||
use crate::scene::parametric_constraints::{
|
||||
angle_decimals, dynamic_dimension_text, ConstraintKind, ParametricRef,
|
||||
};
|
||||
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
let kind = if refs.len() == 2 {
|
||||
ConstraintKind::Angle
|
||||
} else {
|
||||
ConstraintKind::Angle3Point
|
||||
};
|
||||
// The same angle between the same sides is refused after the
|
||||
// value (whichever sector), and the command ends.
|
||||
let duplicate = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(scope)
|
||||
.is_some_and(|set| {
|
||||
set.constraints.iter().any(|c| {
|
||||
c.enabled
|
||||
&& c.kind == kind
|
||||
&& c.refs.len() == refs.len()
|
||||
&& if kind == ConstraintKind::Angle {
|
||||
c.refs.contains(&refs[0]) && c.refs.contains(&refs[1])
|
||||
} else {
|
||||
c.refs == refs
|
||||
}
|
||||
})
|
||||
});
|
||||
if duplicate {
|
||||
self.command_line
|
||||
.push_output("The constraint already exists on the selected objects.");
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
return Task::none();
|
||||
}
|
||||
if let Err(message) = self.tabs[i].scene.validate_parametric_constraint(
|
||||
kind,
|
||||
&refs,
|
||||
Some(&DrivingValue::Literal(1.0)),
|
||||
) {
|
||||
self.reprompt_active_command(i, message);
|
||||
return Task::none();
|
||||
}
|
||||
let name = name.trim().to_string();
|
||||
if !is_valid_name(&name) {
|
||||
self.command_line.push_error(
|
||||
"Only alphanumeric names, starting with alpha characters, are allowed.",
|
||||
);
|
||||
self.reprompt_active_command(i, &format!("Invalid parameter name: {name}."));
|
||||
return Task::none();
|
||||
}
|
||||
if renamed && self.tabs[i].scene.named_parameters().contains(&name) {
|
||||
self.reprompt_active_command(i, "Parameter with this name already exists.");
|
||||
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.command_line.push_error("Invalid expression.");
|
||||
self.reprompt_active_command(
|
||||
i,
|
||||
"The parameter is used in an expression which results in an invalid value for a dimensional constraint.",
|
||||
);
|
||||
return Task::none();
|
||||
}
|
||||
};
|
||||
let annotational = self.constraint_form_annotational;
|
||||
let decimals = angle_decimals(&self.tabs[i].scene.document, None);
|
||||
let text = dynamic_dimension_text(
|
||||
&name,
|
||||
value,
|
||||
self.tabs[i].scene.constraint_name_format,
|
||||
false,
|
||||
Some(&expression),
|
||||
annotational,
|
||||
Some(decimals),
|
||||
);
|
||||
let entity = if kind == ConstraintKind::Angle {
|
||||
angular_dim::angular_two_line_entity(
|
||||
points[0], points[1], points[2], points[3], location, Some(text),
|
||||
)
|
||||
} else {
|
||||
angular_dim::angular_three_point_entity(
|
||||
points[0], points[1], points[2], location, Some(text),
|
||||
)
|
||||
};
|
||||
let Some(mut entity) = entity else {
|
||||
self.reprompt_active_command(i, "No valid constraint point found.");
|
||||
return Task::none();
|
||||
};
|
||||
crate::scene::creation_style::apply_current_creation_styles(
|
||||
&self.tabs[i].scene.document,
|
||||
&mut entity,
|
||||
);
|
||||
if annotational {
|
||||
entity
|
||||
.as_entity_mut()
|
||||
.set_layer(self.tabs[i].active_layer.clone());
|
||||
} else {
|
||||
self.tabs[i].scene.ensure_dynamic_dimension_layer();
|
||||
entity.as_entity_mut().set_layer(
|
||||
crate::scene::parametric_constraints::DYNAMIC_DIMENSION_LAYER.to_string(),
|
||||
);
|
||||
entity.common_mut().color = 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<Handle> = Vec::new();
|
||||
for reference in &refs {
|
||||
if !touched.contains(&reference.entity) {
|
||||
touched.push(reference.entity);
|
||||
}
|
||||
}
|
||||
// The first side (or the vertex and first point) stays; a
|
||||
// value other than the measured one turns the other side.
|
||||
let anchors: Vec<ParametricRef> =
|
||||
crate::scene::parametric_constraints::dimensional_anchor_refs(
|
||||
&self.tabs[i].scene.document,
|
||||
&refs,
|
||||
);
|
||||
let association = if kind == ConstraintKind::Angle {
|
||||
vec![
|
||||
Some(refs[0].entity),
|
||||
Some(refs[0].entity),
|
||||
Some(refs[1].entity),
|
||||
Some(refs[1].entity),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
Some(refs[1].entity),
|
||||
Some(refs[0].entity),
|
||||
Some(refs[2].entity),
|
||||
]
|
||||
};
|
||||
let pending = self.begin_undo(i, "Angular constraint", touched.len() + 1, false);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_parametric_constraints_before(scope, constraints_before);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
self.tabs[i].scene.named_parameters = table;
|
||||
let dimension = self.tabs[i].scene.add_entity(entity);
|
||||
let set = self.tabs[i].scene.parametric_constraint_set_mut(scope);
|
||||
let id = set.add(kind, refs, Some(DrivingValue::Named(name)));
|
||||
if let Some(constraint) = set.constraints.iter_mut().find(|c| c.id == id) {
|
||||
constraint.angle_sector = sector;
|
||||
}
|
||||
set.dimensions.insert(id, dimension);
|
||||
self.tabs[i].scene.note_parametric_constraint_applied(
|
||||
scope,
|
||||
id,
|
||||
self.constraint_bar_display,
|
||||
);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.attach_dimension_association(dimension, association);
|
||||
let changes: Vec<(Handle, crate::scene::ChangeKind)> = touched
|
||||
.into_iter()
|
||||
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.bump_entities_with_parametric_policy(&changes, &anchors, false);
|
||||
self.tabs[i].scene.refresh_hidden_dynamic_dimensions();
|
||||
self.tabs[i].scene.refresh_dynamic_dimension_scales(true);
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.refresh_properties();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
CmdResult::MakeParallel {
|
||||
first_line,
|
||||
first_ends,
|
||||
|
||||
+16
-43
@@ -1569,10 +1569,8 @@ impl OpenCADStudio {
|
||||
table
|
||||
.iter()
|
||||
.map(|parameter| {
|
||||
let value = table
|
||||
.resolve(¶meter.name)
|
||||
.map(|value| format!("{value:.4}"))
|
||||
.unwrap_or_else(|_| "**".to_string());
|
||||
let value =
|
||||
self.tabs[i].scene.parameter_value_text(¶meter.name);
|
||||
format!(
|
||||
"Parameter: {:<12} Expression: {:<21} Value: {value}",
|
||||
parameter.name, parameter.source
|
||||
@@ -1597,10 +1595,7 @@ impl OpenCADStudio {
|
||||
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());
|
||||
let value = self.tabs[i].scene.parameter_value_text(&name);
|
||||
format!("Old Expression = {}, Value = {value}", parameter.source)
|
||||
})
|
||||
};
|
||||
@@ -1941,41 +1936,19 @@ 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;
|
||||
let sel = SelectObjectsCommand::new(cmd);
|
||||
self.command_line.push_info(&sel.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(sel));
|
||||
} else if handles.len() != 2 {
|
||||
self.command_line.push_output(
|
||||
"Select exactly two lines (first = reference, second = the one that rotates), then run this constraint again.",
|
||||
);
|
||||
} else {
|
||||
use crate::modules::parametric::AngleConstraintCommand;
|
||||
let command_name = if cmd == "DCANGULAR" {
|
||||
"DCANGULAR"
|
||||
} else {
|
||||
"ACONSTRAINT"
|
||||
};
|
||||
match AngleConstraintCommand::with_name(
|
||||
&self.tabs[i].scene,
|
||||
handles[0],
|
||||
handles[1],
|
||||
command_name,
|
||||
) {
|
||||
Some(new_cmd) => {
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
None => self
|
||||
.command_line
|
||||
.push_output("Select two lines for an angle constraint."),
|
||||
}
|
||||
}
|
||||
use crate::modules::parametric::{DimConstraintAxis, DimConstraintCommand};
|
||||
use crate::scene::parametric_constraints::{
|
||||
angle_decimals, next_angular_parameter_name,
|
||||
};
|
||||
self.dim_constraint_last = "ANgular";
|
||||
let name = next_angular_parameter_name(self.tabs[i].scene.named_parameters());
|
||||
let decimals = angle_decimals(&self.tabs[i].scene.document, None);
|
||||
// Both sides are picked inside the command.
|
||||
self.tabs[i].scene.deselect_all();
|
||||
let command = DimConstraintCommand::new(DimConstraintAxis::Angular, name)
|
||||
.with_angle_decimals(decimals);
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
|
||||
// ── Model commands (3D primitives) ─────────────────────────────
|
||||
|
||||
@@ -1054,6 +1054,7 @@ impl OpenCADStudio {
|
||||
| "CONSTRAINTBARMODE"
|
||||
| "CONSTRAINTNAMEFORMAT"
|
||||
| "DYNCONSTRAINTDISPLAY"
|
||||
| "CCONSTRAINTFORM"
|
||||
) =>
|
||||
{
|
||||
return self.dispatch_styleprops(&format!("SETVAR {cmd}"), i);
|
||||
@@ -1279,6 +1280,7 @@ impl OpenCADStudio {
|
||||
| "CONSTRAINTBARMODE"
|
||||
| "CONSTRAINTNAMEFORMAT"
|
||||
| "DYNCONSTRAINTDISPLAY"
|
||||
| "CCONSTRAINTFORM"
|
||||
) {
|
||||
let current = match name.as_str() {
|
||||
"CONSTRAINTSOLVEMODE" => i16::from(self.constraint_solve_mode),
|
||||
@@ -1291,6 +1293,7 @@ impl OpenCADStudio {
|
||||
"DYNCONSTRAINTDISPLAY" => {
|
||||
i16::from(self.tabs[i].scene.dynamic_constraint_display)
|
||||
}
|
||||
"CCONSTRAINTFORM" => i16::from(self.constraint_form_annotational),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let maximum = match name.as_str() {
|
||||
@@ -1325,6 +1328,11 @@ impl OpenCADStudio {
|
||||
mode != 0;
|
||||
self.tabs[i].scene.refresh_hidden_dynamic_dimensions();
|
||||
}
|
||||
// The form new dimensional constraints take
|
||||
// (0 dynamic, 1 annotational), as DCFORM sets.
|
||||
"CCONSTRAINTFORM" => {
|
||||
self.constraint_form_annotational = mode != 0
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
self.persist_settings_if_changed();
|
||||
|
||||
+89
-8
@@ -2342,6 +2342,8 @@ impl OpenCADStudio {
|
||||
handle,
|
||||
) {
|
||||
Some((set, constraint)) => {
|
||||
let annotational =
|
||||
self.tabs[i].scene.dimension_is_annotational(handle);
|
||||
sections = dynamic_dimension_sections(
|
||||
&self.tabs[i].scene,
|
||||
handle,
|
||||
@@ -2349,11 +2351,23 @@ impl OpenCADStudio {
|
||||
constraint,
|
||||
sections,
|
||||
);
|
||||
match entity {
|
||||
acadrust::EntityType::Dimension(
|
||||
acadrust::entities::Dimension::Aligned(_),
|
||||
) => t!("Aligned Dimensional Constraint").into_owned(),
|
||||
_ => t!("Linear Dimensional Constraint").into_owned(),
|
||||
// An annotational constraint is an ordinary
|
||||
// dimension to the panel; the dynamic form is
|
||||
// named after its constraint.
|
||||
if annotational {
|
||||
title
|
||||
} else {
|
||||
use acadrust::entities::Dimension;
|
||||
match entity {
|
||||
acadrust::EntityType::Dimension(Dimension::Aligned(_)) => {
|
||||
t!("Aligned Dimensional Constraint")
|
||||
}
|
||||
acadrust::EntityType::Dimension(
|
||||
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_),
|
||||
) => t!("Angular Dimensional Constraint"),
|
||||
_ => t!("Linear Dimensional Constraint"),
|
||||
}
|
||||
.into_owned()
|
||||
}
|
||||
}
|
||||
None => title,
|
||||
@@ -2805,7 +2819,35 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={}
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.tabs[i].scene.is_dynamic_dimension(handle) {
|
||||
let dynamic_angle = match contextual.as_ref() {
|
||||
acadrust::EntityType::Dimension(
|
||||
dim @ (acadrust::entities::Dimension::Angular2Ln(_)
|
||||
| acadrust::entities::Dimension::Angular3Pt(_)),
|
||||
) if self.tabs[i].scene.is_dynamic_dimension(handle) => Some(dim),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(dim) = dynamic_angle {
|
||||
// A dynamic angle shows a triangle at each end of its arc
|
||||
// pointing away from it, and squares at the arc point and
|
||||
// the text — nothing on the sides.
|
||||
let (arc_id, text_id) =
|
||||
if matches!(dim, acadrust::entities::Dimension::Angular2Ln(_)) {
|
||||
(4, 5)
|
||||
} else {
|
||||
(3, 4)
|
||||
};
|
||||
if let Some(ends) = crate::entities::dimension::angular_arc_ends(dim) {
|
||||
entity_grips.retain(|grip| grip.id == arc_id || grip.id == text_id);
|
||||
for (point, away) in ends {
|
||||
entity_grips.insert(
|
||||
0,
|
||||
crate::entities::common::oriented_triangle_grip(
|
||||
arc_id, point, away,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if self.tabs[i].scene.is_dynamic_dimension(handle) {
|
||||
// A dynamic dimension shows the reference's grips: a
|
||||
// triangle at each constraint point aimed at the other
|
||||
// one, and the text square — no dimension line grip.
|
||||
@@ -3738,7 +3780,14 @@ fn dynamic_dimension_sections(
|
||||
row(
|
||||
t!("Value").as_ref(),
|
||||
"dyn_constraint_value",
|
||||
PropValue::ReadOnly(value.map(|v| format!("{v:.4}")).unwrap_or_default()),
|
||||
// An angle's value reads at the angular precision, as on the
|
||||
// dimension and in -PARAMETERS.
|
||||
PropValue::ReadOnly(match &constraint.driving_param {
|
||||
Some(DrivingValue::Named(name)) if set.local_parameters.is_empty() => {
|
||||
scene.parameter_value_text(name)
|
||||
}
|
||||
_ => value.map(|v| format!("{v:.4}")).unwrap_or_default(),
|
||||
}),
|
||||
),
|
||||
row(
|
||||
t!("Description").as_ref(),
|
||||
@@ -3748,7 +3797,39 @@ fn dynamic_dimension_sections(
|
||||
],
|
||||
}];
|
||||
if annotational {
|
||||
result.extend(sections);
|
||||
// The reference lists General first, then the constraint, then the
|
||||
// dimension's own groups; the constraint's dimension is not an
|
||||
// associative dimension to it, and its text is the constraint's.
|
||||
let mut sections = sections;
|
||||
sections.retain(|section| {
|
||||
section.title != t!("3D Visualization").as_ref()
|
||||
&& section.title != "Associative Data"
|
||||
});
|
||||
for section in &mut sections {
|
||||
section
|
||||
.props
|
||||
.retain(|property| property.field != "association_status");
|
||||
for property in &mut section.props {
|
||||
match property.field {
|
||||
"associative" => property.value = PropValue::ReadOnly("No".to_string()),
|
||||
"text_override" => {
|
||||
if let PropValue::PlainText(text) | PropValue::EditText(text) =
|
||||
&property.value
|
||||
{
|
||||
property.value = PropValue::ReadOnly(text.clone());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
let constraint_section = result.remove(0);
|
||||
let general = (!sections.is_empty()).then(|| sections.remove(0));
|
||||
result = general
|
||||
.into_iter()
|
||||
.chain(std::iter::once(constraint_section))
|
||||
.chain(sections)
|
||||
.collect();
|
||||
} else if let Some(text_rotation) = text_rotation {
|
||||
result.push(PropSection {
|
||||
title: t!("Text").into_owned(),
|
||||
|
||||
@@ -1630,8 +1630,26 @@ pub enum CmdResult {
|
||||
direction: Option<crate::scene::parametric_constraints::ParametricRef>,
|
||||
name: String,
|
||||
expression: String,
|
||||
/// The user named the parameter (`name=expression`); an existing
|
||||
/// name is then refused.
|
||||
renamed: bool,
|
||||
label: &'static str,
|
||||
},
|
||||
/// Adds an angular constraint with its dynamic angular dimension and
|
||||
/// the `angN` parameter that drives it. Two lines: `refs` =
|
||||
/// `[first_line, second_line]`, `points` = both lines' ends; three
|
||||
/// points: `refs` = `[first, vertex, second]`, `points` =
|
||||
/// `[vertex, first, second]`.
|
||||
AddAngularConstraint {
|
||||
refs: Vec<crate::scene::parametric_constraints::ParametricRef>,
|
||||
points: Vec<DVec3>,
|
||||
location: DVec3,
|
||||
/// Which of the four angles the dimension line location picked.
|
||||
sector: u8,
|
||||
name: String,
|
||||
expression: String,
|
||||
renamed: bool,
|
||||
},
|
||||
/// Aligned's 2Lines: makes `second_line` parallel to `first_line` (whose
|
||||
/// ends stay put), then hands the second line's solved ends back through
|
||||
/// `CadCommand::accept_parallel_line`.
|
||||
@@ -1812,6 +1830,8 @@ pub enum CmdResult {
|
||||
ReportMeasurement(String),
|
||||
/// Print an input error and keep the command active.
|
||||
ReportError(String),
|
||||
/// Reports an error and ends the command (`Lines are parallel.`).
|
||||
CancelWithMessage(String),
|
||||
/// Print a measurement result, clear the current selection, and keep the command active.
|
||||
ReportMeasurementAndDeselect(String),
|
||||
/// Clear the current selection and keep the command active at its updated step.
|
||||
|
||||
@@ -702,12 +702,14 @@ pub fn set_property(
|
||||
| "dim_alt_precision"
|
||||
| "dim_tolerance_precision"
|
||||
| "dim_alt_tolerance_precision" => parse_precision_label(trimmed),
|
||||
// "On" is text aligned with its dimension line: the horizontal
|
||||
// flag (DIMTIH/DIMTOH) off.
|
||||
"dim_text_outside_align" | "dim_text_inside_align" | "dim_line_inside" => {
|
||||
yes(trimmed).map(|value| i16::from(value == 0))
|
||||
}
|
||||
"dim_ext_line_fixed"
|
||||
| "dim_text_outside_align"
|
||||
| "dim_text_inside_align"
|
||||
| "dim_text_inside"
|
||||
| "dim_line_forced"
|
||||
| "dim_line_inside"
|
||||
| "dim_alt_enabled" => yes(trimmed),
|
||||
"dim_decimal_separator" => trimmed
|
||||
.chars()
|
||||
|
||||
+206
-29
@@ -2143,7 +2143,7 @@ pub fn style_sections(
|
||||
choice(
|
||||
t!("Text outside align").as_ref(),
|
||||
"dim_text_outside_align",
|
||||
on(int(ov::DIMTOH, s.dimtoh as i16) != 0),
|
||||
on(int(ov::DIMTOH, s.dimtoh as i16) == 0),
|
||||
&["On", "Off"],
|
||||
true,
|
||||
),
|
||||
@@ -2178,9 +2178,15 @@ pub fn style_sections(
|
||||
choice(
|
||||
t!("Text inside align").as_ref(),
|
||||
"dim_text_inside_align",
|
||||
on(int(ov::DIMTIH, s.dimtih as i16) != 0),
|
||||
on(int(ov::DIMTIH, s.dimtih as i16) == 0),
|
||||
&["On", "Off"],
|
||||
dimtix || matches!(dimension, Dimension::LargeRadial(_)),
|
||||
dimtix
|
||||
|| matches!(
|
||||
dimension,
|
||||
Dimension::LargeRadial(_)
|
||||
| Dimension::Angular2Ln(_)
|
||||
| Dimension::Angular3Pt(_)
|
||||
),
|
||||
),
|
||||
property(
|
||||
t!("Text position X").as_ref(),
|
||||
@@ -2283,7 +2289,7 @@ pub fn style_sections(
|
||||
choice(
|
||||
t!("Dim line inside").as_ref(),
|
||||
"dim_line_inside",
|
||||
on(int(ov::DIMSOXD, s.dimsoxd as i16) != 0),
|
||||
on(int(ov::DIMSOXD, s.dimsoxd as i16) == 0),
|
||||
&["On", "Off"],
|
||||
true,
|
||||
),
|
||||
@@ -2999,13 +3005,105 @@ pub fn style_sections(
|
||||
];
|
||||
}
|
||||
sections.retain(|section| section.title != t!("Alternate Units").as_ref());
|
||||
let by_order = |order: &'static [&'static str]| {
|
||||
move |property: &Property| {
|
||||
order
|
||||
.iter()
|
||||
.position(|field| *field == property.field)
|
||||
.unwrap_or(order.len())
|
||||
}
|
||||
};
|
||||
if let Some(tolerances) = sections
|
||||
.iter_mut()
|
||||
.find(|section| section.title == t!("Tolerances").as_ref())
|
||||
{
|
||||
tolerances
|
||||
.props
|
||||
.retain(|property| !property.field.starts_with("dim_alt_tolerance_"));
|
||||
tolerances.props.retain(|property| {
|
||||
!property.field.starts_with("dim_alt_tolerance_")
|
||||
&& !matches!(
|
||||
property.field,
|
||||
"dim_tolerance_suppress_zero_feet" | "dim_tolerance_suppress_zero_inches"
|
||||
)
|
||||
});
|
||||
tolerances.props.sort_by_key(by_order(&[
|
||||
"dim_tolerance_alignment",
|
||||
"dim_tolerance_display",
|
||||
"dim_tolerance_limit_lower",
|
||||
"dim_tolerance_limit_upper",
|
||||
"dim_tolerance_pos_vert",
|
||||
"dim_tolerance_precision",
|
||||
"dim_tolerance_suppress_leading_zeros",
|
||||
"dim_tolerance_suppress_trailing_zeros",
|
||||
"dim_tolerance_text_height",
|
||||
]));
|
||||
}
|
||||
// An arc has no dimension line extension.
|
||||
if let Some(lines) = sections
|
||||
.iter_mut()
|
||||
.find(|section| section.title == t!("Lines & Arrows").as_ref())
|
||||
{
|
||||
lines.props.retain(|property| property.field != "dim_line_ext");
|
||||
}
|
||||
if let Some(fit) = sections
|
||||
.iter_mut()
|
||||
.find(|section| section.title == t!("Fit").as_ref())
|
||||
{
|
||||
fit.props.sort_by_key(by_order(&[
|
||||
"dim_line_forced",
|
||||
"dim_line_inside",
|
||||
"dim_scale_overall",
|
||||
"dim_fit",
|
||||
"dim_text_inside",
|
||||
"dim_text_movement",
|
||||
]));
|
||||
}
|
||||
if let Some(units) = sections
|
||||
.iter_mut()
|
||||
.find(|section| section.title == t!("Primary Units").as_ref())
|
||||
{
|
||||
units.props.sort_by_key(by_order(&[
|
||||
"dim_decimal_separator",
|
||||
"dim_prefix",
|
||||
"dim_suffix",
|
||||
"dim_angle_suppress_leading_zeros",
|
||||
"dim_angle_suppress_trailing_zeros",
|
||||
"dim_angle_precision",
|
||||
"dim_angle_units",
|
||||
]));
|
||||
}
|
||||
// The measurement reads at the angular precision, and automatic text
|
||||
// reports where it is drawn rather than an unset point.
|
||||
if let Some(text_section) = sections
|
||||
.iter_mut()
|
||||
.find(|section| section.title == t!("Text").as_ref())
|
||||
{
|
||||
let automatic = stored_text_point(dimension)
|
||||
.is_none()
|
||||
.then(|| styled_dimension_text_position(dimension, s, 1.0));
|
||||
for property in &mut text_section.props {
|
||||
match property.field {
|
||||
"measurement" => {
|
||||
// The row shows the number at the angular precision,
|
||||
// without the text's degree sign.
|
||||
let value = format_angular_value(
|
||||
displayed_measurement(dimension, Some(s)),
|
||||
Some(s),
|
||||
);
|
||||
property.value =
|
||||
PropValue::ReadOnly(value.trim_end_matches('°').to_string());
|
||||
}
|
||||
"text_x" => {
|
||||
if let Some(position) = automatic {
|
||||
property.value = PropValue::EditText(format!("{:.4}", position.x));
|
||||
}
|
||||
}
|
||||
"text_y" => {
|
||||
if let Some(position) = automatic {
|
||||
property.value = PropValue::EditText(format!("{:.4}", position.y));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4724,18 +4822,51 @@ fn dimension_geometry(
|
||||
let (p3, p4) = (lv(d.angle_vertex), lv(d.definition_point));
|
||||
let arc_point = lv(d.dimension_arc);
|
||||
match two_line_angle_frame(p1, p2, p3, p4, arc_point) {
|
||||
Some((vertex, start, end)) => append_angular_dimension(
|
||||
&mut g,
|
||||
vertex,
|
||||
vertex,
|
||||
vertex,
|
||||
arc_point,
|
||||
arrow1,
|
||||
arrow2,
|
||||
Some((start, end)),
|
||||
params,
|
||||
suppress,
|
||||
),
|
||||
Some((vertex, start, end)) => {
|
||||
// An extension line runs from a side's nearer end out to
|
||||
// the arc only where the arc lies beyond that side; where
|
||||
// the arc crosses the side itself there is nothing to add.
|
||||
let radius = vertex.distance(arc_point);
|
||||
let side = |angle: f32| -> (Vec3, bool) {
|
||||
let dir = Vec3::new(angle.cos(), angle.sin(), 0.0);
|
||||
let deviation =
|
||||
|a: Vec3, b: Vec3| normalized_or(b - a, dir).cross(dir).length();
|
||||
let (a, b) = if deviation(p1, p2) <= deviation(p3, p4) {
|
||||
(p1, p2)
|
||||
} else {
|
||||
(p3, p4)
|
||||
};
|
||||
let along = |p: Vec3| (p - vertex).dot(dir);
|
||||
let (lo, hi) = (along(a).min(along(b)), along(a).max(along(b)));
|
||||
let arc_end = vertex + dir * radius;
|
||||
let near = if a.distance(arc_end) <= b.distance(arc_end) {
|
||||
a
|
||||
} else {
|
||||
b
|
||||
};
|
||||
(near, radius >= lo - 1e-6 && radius <= hi + 1e-6)
|
||||
};
|
||||
let (first, first_covered) = side(start);
|
||||
let (second, second_covered) = side(end);
|
||||
let suppress = SuppressFlags {
|
||||
ext1: suppress.ext1 || first_covered,
|
||||
ext2: suppress.ext2 || second_covered,
|
||||
dim1: suppress.dim1,
|
||||
dim2: suppress.dim2,
|
||||
};
|
||||
append_angular_dimension(
|
||||
&mut g,
|
||||
vertex,
|
||||
first,
|
||||
second,
|
||||
arc_point,
|
||||
arrow1,
|
||||
arrow2,
|
||||
Some((start, end)),
|
||||
params,
|
||||
suppress,
|
||||
)
|
||||
}
|
||||
// Parallel lines have no vertex and so no angle to draw; the
|
||||
// extension lines alone say where the dimension was.
|
||||
None => {
|
||||
@@ -4749,14 +4880,14 @@ fn dimension_geometry(
|
||||
let first = lv(d.first_point);
|
||||
let second = lv(d.second_point);
|
||||
let arc_point = lv(d.definition_point);
|
||||
let explicit_sweep = two_line_angle_frame(
|
||||
vertex,
|
||||
first,
|
||||
vertex,
|
||||
second,
|
||||
arc_point,
|
||||
)
|
||||
.map(|(_, start, end)| (start, end));
|
||||
// The sweep starts at whichever ray the arc point says; the
|
||||
// extension lines swap with it, or they would cross the angle.
|
||||
let (first, second, explicit_sweep) =
|
||||
match three_point_frame(vertex, first, second, arc_point) {
|
||||
Some((start, end, true)) => (second, first, Some((start, end))),
|
||||
Some((start, end, false)) => (first, second, Some((start, end))),
|
||||
None => (first, second, None),
|
||||
};
|
||||
append_angular_dimension(
|
||||
&mut g,
|
||||
vertex,
|
||||
@@ -5398,6 +5529,35 @@ fn two_line_angle_frame(
|
||||
Some((vertex, start, end))
|
||||
}
|
||||
|
||||
/// The sweep of a three-point angular dimension: counter-clockwise from the
|
||||
/// first ray to the second when the arc point lies in that sweep, otherwise
|
||||
/// the other way round (`swapped`). Rays need no crossing, so a straight
|
||||
/// angle draws, and a reflex one is kept as picked.
|
||||
fn three_point_frame(
|
||||
vertex: Vec3,
|
||||
first: Vec3,
|
||||
second: Vec3,
|
||||
arc_point: Vec3,
|
||||
) -> Option<(f32, f32, bool)> {
|
||||
let angle_of = |d: Vec3| d.y.atan2(d.x);
|
||||
let (r1, r2, at) = (first - vertex, second - vertex, arc_point - vertex);
|
||||
if r1.length_squared() <= 1e-12 || r2.length_squared() <= 1e-12 || at.length_squared() <= 1e-12
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let tau = std::f32::consts::TAU;
|
||||
let (a1, a2) = (angle_of(r1), angle_of(r2));
|
||||
let sweep = (a2 - a1).rem_euclid(tau);
|
||||
if sweep <= 1e-6 {
|
||||
return None;
|
||||
}
|
||||
if (angle_of(at) - a1).rem_euclid(tau) <= sweep {
|
||||
Some((a1, a1 + sweep, false))
|
||||
} else {
|
||||
Some((a2, a2 + (tau - sweep), true))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn arc_dimension_angles(dimension: &DimensionArc) -> Option<(f32, f32)> {
|
||||
let raw = dimension.arc_end_parameter - dimension.arc_start_parameter;
|
||||
let mut sweep = raw.rem_euclid(std::f64::consts::TAU);
|
||||
@@ -5668,8 +5828,7 @@ fn angular_dimension_frame(dim: &Dimension) -> Option<(Vec3, f32, f32, f32)> {
|
||||
let first = vec3_local(value.first_point);
|
||||
let second = vec3_local(value.second_point);
|
||||
let arc_point = vec3_local(value.definition_point);
|
||||
let (vertex, start, end) =
|
||||
two_line_angle_frame(vertex, first, vertex, second, arc_point)?;
|
||||
let (start, end, _) = three_point_frame(vertex, first, second, arc_point)?;
|
||||
(vertex, start, end, arc_point)
|
||||
}
|
||||
Dimension::Arc(value) => {
|
||||
@@ -6329,6 +6488,24 @@ pub(crate) fn dynamic_dimension_lock_anchor(
|
||||
))
|
||||
}
|
||||
|
||||
/// The two ends of an angular dimension's arc, each with the direction
|
||||
/// pointing away from the arc: where a dynamic angle's triangle grips sit.
|
||||
pub(crate) fn angular_arc_ends(dim: &Dimension) -> Option<[(glam::DVec3, glam::DVec3); 2]> {
|
||||
let (vertex, start, end, radius) = angular_dimension_frame(dim)?;
|
||||
let point = |angle: f32| {
|
||||
let p = vertex + Vec3::new(angle.cos(), angle.sin(), 0.0) * radius;
|
||||
glam::DVec3::new(p.x as f64, p.y as f64, p.z as f64)
|
||||
};
|
||||
let away = |angle: f32, sign: f32| {
|
||||
glam::DVec3::new(
|
||||
(-angle.sin() * sign) as f64,
|
||||
(angle.cos() * sign) as f64,
|
||||
0.0,
|
||||
)
|
||||
};
|
||||
Some([(point(start), away(start, -1.0)), (point(end), away(end, 1.0))])
|
||||
}
|
||||
|
||||
fn dimension_text_natural_rotation(dim: &Dimension) -> f64 {
|
||||
let angle = match dim {
|
||||
Dimension::Linear(d) => d.rotation,
|
||||
|
||||
@@ -1093,7 +1093,7 @@ fn point_angle_in_frame(vertex: DVec3, point: DVec3, frame: (f64, f64)) -> bool
|
||||
(angle - frame.0).rem_euclid(std::f64::consts::TAU) <= sweep + 1.0e-9
|
||||
}
|
||||
|
||||
fn two_line_frame(
|
||||
pub(crate) fn two_line_frame(
|
||||
first_start: DVec3,
|
||||
first_end: DVec3,
|
||||
second_start: DVec3,
|
||||
@@ -1186,6 +1186,52 @@ fn angular_preview_with_frame(
|
||||
points
|
||||
}
|
||||
|
||||
/// A two-line angular dimension whose arc sits at `arc_point`, for a
|
||||
/// dimensional constraint; `None` when the lines are parallel or the arc
|
||||
/// point picks no sector.
|
||||
pub(crate) fn angular_two_line_entity(
|
||||
first_start: DVec3,
|
||||
first_end: DVec3,
|
||||
second_start: DVec3,
|
||||
second_end: DVec3,
|
||||
arc_point: DVec3,
|
||||
text: Option<String>,
|
||||
) -> Option<EntityType> {
|
||||
two_line_frame(first_start, first_end, second_start, second_end, arc_point)?;
|
||||
let mut dim = DimensionAngular2Ln::default();
|
||||
dim.first_point = v3(first_start);
|
||||
dim.second_point = v3(first_end);
|
||||
dim.angle_vertex = v3(second_start);
|
||||
dim.definition_point = v3(second_end);
|
||||
dim.dimension_arc = v3(arc_point);
|
||||
dim.base.definition_point = dim.definition_point;
|
||||
// No stored text point: the style places the text (DIMTAD) beside the
|
||||
// arc at render time, as the reference draws a dynamic dimension.
|
||||
dim.base.insertion_point = dim.dimension_arc;
|
||||
dim.base.actual_measurement = dim.measurement_degrees();
|
||||
crate::entities::dimension::set_dimension_text_override(&mut dim.base, text);
|
||||
Some(EntityType::Dimension(Dimension::Angular2Ln(dim)))
|
||||
}
|
||||
|
||||
/// A three-point angular dimension (vertex, two points, arc point), for a
|
||||
/// dimensional constraint.
|
||||
pub(crate) fn angular_three_point_entity(
|
||||
vertex: DVec3,
|
||||
first: DVec3,
|
||||
second: DVec3,
|
||||
arc_point: DVec3,
|
||||
text: Option<String>,
|
||||
) -> Option<EntityType> {
|
||||
let mut dim = DimensionAngular3Pt::new(v3(vertex), v3(first), v3(second));
|
||||
dim.definition_point = v3(arc_point);
|
||||
dim.base.definition_point = dim.definition_point;
|
||||
dim.base.text_middle_point = Vector3::new(0.0, 0.0, 0.0);
|
||||
dim.base.insertion_point = dim.definition_point;
|
||||
dim.base.actual_measurement = dim.measurement_degrees();
|
||||
crate::entities::dimension::set_dimension_text_override(&mut dim.base, text);
|
||||
Some(EntityType::Dimension(Dimension::Angular3Pt(dim)))
|
||||
}
|
||||
|
||||
fn v3(point: DVec3) -> Vector3 {
|
||||
Vector3::new(point.x, point.y, point.z)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ pub enum DimConstraintAxis {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
Aligned,
|
||||
/// The angle between two lines, or at a vertex between two points.
|
||||
Angular,
|
||||
}
|
||||
|
||||
/// A picked line (a line entity or one polyline segment) an Aligned
|
||||
@@ -82,6 +84,28 @@ enum Step {
|
||||
/// The line the distance is measured perpendicular to.
|
||||
direction: Option<LineTarget>,
|
||||
},
|
||||
/// Angular: `Select first line or arc or [3Point] <3Point>:`
|
||||
AngularFirst,
|
||||
/// `Select second line:`
|
||||
AngularSecond { first: LineTarget },
|
||||
/// 3Point: `Specify angle vertex:`
|
||||
AngularVertex,
|
||||
/// `Specify first angle constraint point:`
|
||||
AngularPoint1 { vertex: (ParametricRef, DVec3) },
|
||||
/// `Specify second angle constraint point:`
|
||||
AngularPoint2 {
|
||||
vertex: (ParametricRef, DVec3),
|
||||
first: (ParametricRef, DVec3),
|
||||
},
|
||||
/// `Specify dimension line location:` of an angle.
|
||||
AngularLocation { data: AngularData },
|
||||
/// `Enter value or name and value <ang1=27>:`
|
||||
AngularValue {
|
||||
data: AngularData,
|
||||
location: DVec3,
|
||||
sector: u8,
|
||||
measured: f64,
|
||||
},
|
||||
/// `Enter value or name and value <d1=100>:`
|
||||
Value {
|
||||
first: ParametricRef,
|
||||
@@ -96,6 +120,21 @@ enum Step {
|
||||
},
|
||||
}
|
||||
|
||||
/// Angular's picks: two lines, or a vertex and two points (an arc gives
|
||||
/// its center and its ends).
|
||||
#[derive(Clone, Copy)]
|
||||
enum AngularData {
|
||||
Lines {
|
||||
first: LineTarget,
|
||||
second: LineTarget,
|
||||
},
|
||||
Points {
|
||||
vertex: (ParametricRef, DVec3),
|
||||
first: (ParametricRef, DVec3),
|
||||
second: (ParametricRef, DVec3),
|
||||
},
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -105,6 +144,8 @@ pub struct DimConstraintCommand {
|
||||
picked_entity: Option<EntityType>,
|
||||
/// The next free `dN` the host reserved for this constraint.
|
||||
default_name: String,
|
||||
/// Angular precision (DIMADEC) for `Dimension text = 27`.
|
||||
angle_decimals: usize,
|
||||
}
|
||||
|
||||
impl DimConstraintCommand {
|
||||
@@ -113,22 +154,163 @@ impl DimConstraintCommand {
|
||||
pub const SAME_POINT: &'static str =
|
||||
"The object or point is already selected. Select a different object or constraint point.";
|
||||
pub const INVALID_LINE: &'static str = "Invalid selection for Aligned. Select a line segment, polyline segment, text, MText, major or minor axis of ellipse or elliptical arc.";
|
||||
pub const PARALLEL_LINES: &'static str = "Lines are parallel.";
|
||||
|
||||
pub fn new(axis: DimConstraintAxis, default_name: String) -> Self {
|
||||
Self {
|
||||
axis,
|
||||
step: Step::First,
|
||||
step: if axis == DimConstraintAxis::Angular {
|
||||
Step::AngularFirst
|
||||
} else {
|
||||
Step::First
|
||||
},
|
||||
picked_entity: None,
|
||||
default_name,
|
||||
angle_decimals: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// The angular precision the measured angle is reported with.
|
||||
pub fn with_angle_decimals(mut self, decimals: usize) -> Self {
|
||||
self.angle_decimals = decimals;
|
||||
self
|
||||
}
|
||||
|
||||
fn angle_display(&self, degrees: f64) -> String {
|
||||
let decimals = self.angle_decimals;
|
||||
format!(
|
||||
"{:.decimals$}",
|
||||
crate::scene::parametric_constraints::normalize_angle_display(degrees)
|
||||
)
|
||||
}
|
||||
|
||||
/// An Angular pick: a line or a polyline segment (text baselines and
|
||||
/// ellipse axes are not angle sides).
|
||||
fn angular_line(entity: &EntityType, handle: Handle, point: DVec3) -> Option<LineTarget> {
|
||||
matches!(
|
||||
entity,
|
||||
EntityType::Line(_) | EntityType::LwPolyline(_) | EntityType::Polyline2D(_)
|
||||
)
|
||||
.then(|| Self::line_target(entity, handle, point))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
/// The angle (degrees) the dimension line location picks between the
|
||||
/// directed sides `p1a→p1b` and `p2a→p2b`, and which of the four
|
||||
/// sectors it is in the solver's terms (which signed sides bound it).
|
||||
fn angle_frame(
|
||||
p1a: DVec3,
|
||||
p1b: DVec3,
|
||||
p2a: DVec3,
|
||||
p2b: DVec3,
|
||||
location: DVec3,
|
||||
) -> Option<(f64, u8)> {
|
||||
use crate::scene::parametric_constraints::angle_sector;
|
||||
let tau = std::f64::consts::TAU;
|
||||
let (_, start, end) =
|
||||
crate::modules::annotate::angular_dim::two_line_frame(p1a, p1b, p2a, p2b, location)?;
|
||||
let sweep = (end - start).rem_euclid(tau);
|
||||
let same = |a: f64, b: f64| {
|
||||
let d = (a - b).rem_euclid(tau);
|
||||
d < 1.0e-6 || d > tau - 1.0e-6
|
||||
};
|
||||
let d1 = p1b - p1a;
|
||||
let d2 = p2b - p2a;
|
||||
let a1 = d1.y.atan2(d1.x);
|
||||
let a2 = d2.y.atan2(d2.x);
|
||||
let pi = std::f64::consts::PI;
|
||||
let sector = if same(start, a1) || same(start, a1 + pi) {
|
||||
// Swept from a side of the first line to a side of the second.
|
||||
if same(start, a1) == same(end, a2) {
|
||||
angle_sector::PARALLEL_COUNTERCLOCKWISE
|
||||
} else {
|
||||
angle_sector::ANTIPARALLEL_COUNTERCLOCKWISE
|
||||
}
|
||||
} else if same(start, a2) == same(end, a1) {
|
||||
angle_sector::PARALLEL_CLOCKWISE
|
||||
} else {
|
||||
angle_sector::ANTIPARALLEL_CLOCKWISE
|
||||
};
|
||||
Some((sweep.to_degrees(), sector))
|
||||
}
|
||||
|
||||
/// The angle (degrees) the dimension line location picks at `vertex`
|
||||
/// between the rays to `first` and `second`: the counterclockwise sweep
|
||||
/// from the first ray when the location lies in it, else the other way
|
||||
/// round (a semicircle measures 180, a reflex angle is allowed).
|
||||
fn ray_frame(vertex: DVec3, first: DVec3, second: DVec3, location: DVec3) -> Option<(f64, u8)> {
|
||||
use crate::scene::parametric_constraints::angle_sector;
|
||||
let tau = std::f64::consts::TAU;
|
||||
let r1 = first - vertex;
|
||||
let r2 = second - vertex;
|
||||
let at = location - vertex;
|
||||
if r1.length_squared() < 1.0e-18 || r2.length_squared() < 1.0e-18 || at.length_squared() < 1.0e-18 {
|
||||
return None;
|
||||
}
|
||||
let a1 = r1.y.atan2(r1.x);
|
||||
let a2 = r2.y.atan2(r2.x);
|
||||
let t = at.y.atan2(at.x);
|
||||
let sweep = (a2 - a1).rem_euclid(tau);
|
||||
if sweep < 1.0e-9 {
|
||||
return None;
|
||||
}
|
||||
if (t - a1).rem_euclid(tau) <= sweep + 1.0e-9 {
|
||||
Some((sweep.to_degrees(), angle_sector::PARALLEL_COUNTERCLOCKWISE))
|
||||
} else {
|
||||
Some(((tau - sweep).to_degrees(), angle_sector::PARALLEL_CLOCKWISE))
|
||||
}
|
||||
}
|
||||
|
||||
fn build_angular(&self, name: String, expression: String) -> Option<CmdResult> {
|
||||
let Step::AngularValue {
|
||||
data,
|
||||
location,
|
||||
sector,
|
||||
..
|
||||
} = self.step
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
if expression.trim().is_empty() {
|
||||
return None;
|
||||
}
|
||||
let (refs, points) = match data {
|
||||
AngularData::Lines { first, second } => (
|
||||
vec![first.line, second.line],
|
||||
vec![
|
||||
first.ends[0].1,
|
||||
first.ends[1].1,
|
||||
second.ends[0].1,
|
||||
second.ends[1].1,
|
||||
],
|
||||
),
|
||||
AngularData::Points {
|
||||
vertex,
|
||||
first,
|
||||
second,
|
||||
} => (
|
||||
vec![first.0, vertex.0, second.0],
|
||||
vec![vertex.1, first.1, second.1],
|
||||
),
|
||||
};
|
||||
Some(CmdResult::AddAngularConstraint {
|
||||
refs,
|
||||
points,
|
||||
location,
|
||||
sector,
|
||||
renamed: name != self.default_name,
|
||||
name,
|
||||
expression,
|
||||
})
|
||||
}
|
||||
|
||||
fn command_name(&self) -> &'static str {
|
||||
match self.axis {
|
||||
DimConstraintAxis::Linear => "DCLINEAR",
|
||||
DimConstraintAxis::Horizontal => "DCHORIZONTAL",
|
||||
DimConstraintAxis::Vertical => "DCVERTICAL",
|
||||
DimConstraintAxis::Aligned => "DCALIGNED",
|
||||
DimConstraintAxis::Angular => "DCANGULAR",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +320,7 @@ impl DimConstraintCommand {
|
||||
DimConstraintAxis::Horizontal => "Horizontal",
|
||||
DimConstraintAxis::Vertical => "Vertical",
|
||||
DimConstraintAxis::Aligned => "Aligned",
|
||||
DimConstraintAxis::Angular => "Angular",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,7 +474,8 @@ impl DimConstraintCommand {
|
||||
}
|
||||
DimConstraintAxis::Horizontal => (ConstraintKind::DistanceX, DVec3::X),
|
||||
DimConstraintAxis::Vertical => (ConstraintKind::DistanceY, DVec3::Y),
|
||||
DimConstraintAxis::Aligned => (
|
||||
// Angular never measures here; its own steps decide the sector.
|
||||
DimConstraintAxis::Aligned | DimConstraintAxis::Angular => (
|
||||
ConstraintKind::Distance,
|
||||
(second - first).normalize_or(DVec3::X),
|
||||
),
|
||||
@@ -334,13 +518,14 @@ impl DimConstraintCommand {
|
||||
location,
|
||||
axis,
|
||||
direction: direction.map(|line| line.line),
|
||||
renamed: name != self.default_name,
|
||||
name,
|
||||
expression,
|
||||
label: match self.axis {
|
||||
DimConstraintAxis::Linear => "Linear constraint",
|
||||
DimConstraintAxis::Horizontal => "Horizontal distance constraint",
|
||||
DimConstraintAxis::Vertical => "Vertical distance constraint",
|
||||
DimConstraintAxis::Aligned => "Aligned constraint",
|
||||
DimConstraintAxis::Aligned | DimConstraintAxis::Angular => "Aligned constraint",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -371,7 +556,25 @@ impl CadCommand for DimConstraintCommand {
|
||||
Step::TwoLinesSecond { .. } | Step::TwoLinesParallel { .. } => {
|
||||
format!("{name} Select second line to make parallel:")
|
||||
}
|
||||
Step::Location { .. } => format!("{name} Specify dimension line location:"),
|
||||
Step::AngularFirst => {
|
||||
format!("{name} Select first line or arc or [3Point] <3Point>:")
|
||||
}
|
||||
Step::AngularSecond { .. } => format!("{name} Select second line:"),
|
||||
Step::AngularVertex => format!("{name} Specify angle vertex:"),
|
||||
Step::AngularPoint1 { .. } => {
|
||||
format!("{name} Specify first angle constraint point:")
|
||||
}
|
||||
Step::AngularPoint2 { .. } => {
|
||||
format!("{name} Specify second angle constraint point:")
|
||||
}
|
||||
Step::AngularLocation { .. } | Step::Location { .. } => {
|
||||
format!("{name} Specify dimension line location:")
|
||||
}
|
||||
Step::AngularValue { measured, .. } => format!(
|
||||
"{name} Enter value or name and value <{}={}>:",
|
||||
self.default_name,
|
||||
measured_expression(measured)
|
||||
),
|
||||
Step::Value { measured, .. } => format!(
|
||||
"{name} Enter value or name and value <{}={}>:",
|
||||
self.default_name,
|
||||
@@ -388,6 +591,7 @@ impl CadCommand for DimConstraintCommand {
|
||||
CmdOption::new("2Lines", "2L"),
|
||||
],
|
||||
Step::First => vec![CmdOption::new("Object", "O")],
|
||||
Step::AngularFirst => vec![CmdOption::new("3Point", "3P")],
|
||||
Step::PointLinePoint => vec![CmdOption::new("Line", "L")],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
@@ -400,7 +604,11 @@ impl CadCommand for DimConstraintCommand {
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
matches!(
|
||||
self.step,
|
||||
Step::First | Step::PointLinePoint | Step::Value { .. }
|
||||
Step::First
|
||||
| Step::PointLinePoint
|
||||
| Step::Value { .. }
|
||||
| Step::AngularFirst
|
||||
| Step::AngularValue { .. }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -427,13 +635,25 @@ impl CadCommand for DimConstraintCommand {
|
||||
}
|
||||
None
|
||||
}
|
||||
Step::Value { .. } => {
|
||||
Step::AngularFirst => {
|
||||
let keyword = text.trim().trim_start_matches('_').to_ascii_uppercase();
|
||||
if matches!(keyword.as_str(), "3P" | "3POINT") {
|
||||
self.step = Step::AngularVertex;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
None
|
||||
}
|
||||
Step::Value { .. } | Step::AngularValue { .. } => {
|
||||
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)
|
||||
if matches!(self.step, Step::AngularValue { .. }) {
|
||||
self.build_angular(name, expression)
|
||||
} else {
|
||||
self.build(name, expression)
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
@@ -451,6 +671,11 @@ impl CadCommand for DimConstraintCommand {
|
||||
| Step::LinePoint { .. }
|
||||
| Step::TwoLinesFirst
|
||||
| Step::TwoLinesSecond { .. }
|
||||
| Step::AngularFirst
|
||||
| Step::AngularSecond { .. }
|
||||
| Step::AngularVertex
|
||||
| Step::AngularPoint1 { .. }
|
||||
| Step::AngularPoint2 { .. }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -571,7 +796,81 @@ impl CadCommand for DimConstraintCommand {
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Location { .. } | Step::Value { .. } => self.on_point(point),
|
||||
Step::AngularFirst => {
|
||||
if handle.is_null() {
|
||||
return Self::report(Self::NO_OBJECT);
|
||||
}
|
||||
let Some(entity) = self.picked_entity.take() else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
if matches!(entity, EntityType::Arc(_)) {
|
||||
// An arc's included angle: its center and its ends.
|
||||
let center = ParametricRef::center(handle);
|
||||
let start = ParametricRef::point(handle, 0);
|
||||
let end = ParametricRef::point(handle, 1);
|
||||
let (Some(c), Some(s), Some(e)) = (
|
||||
Self::world(&entity, center),
|
||||
Self::world(&entity, start),
|
||||
Self::world(&entity, end),
|
||||
) else {
|
||||
return Self::report(Self::NO_POINT);
|
||||
};
|
||||
self.step = Step::AngularLocation {
|
||||
data: AngularData::Points {
|
||||
vertex: (center, c),
|
||||
first: (start, s),
|
||||
second: (end, e),
|
||||
},
|
||||
};
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
match Self::angular_line(&entity, handle, point) {
|
||||
Some(line) => {
|
||||
self.step = Step::AngularSecond { first: line };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
None => Self::report(&format!(
|
||||
"Invalid selection for {}. Select a line, polyline segment or arc.",
|
||||
self.noun()
|
||||
)),
|
||||
}
|
||||
}
|
||||
Step::AngularSecond { first } => {
|
||||
if handle.is_null() {
|
||||
return Self::report(Self::NO_OBJECT);
|
||||
}
|
||||
let Some(entity) = self.picked_entity.take() else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
let Some(second) = Self::angular_line(&entity, handle, point) else {
|
||||
return Self::report(&format!(
|
||||
"Invalid selection for {}. Select a line, polyline segment or arc.",
|
||||
self.noun()
|
||||
));
|
||||
};
|
||||
if second.line == first.line {
|
||||
return Self::report(Self::SAME_POINT);
|
||||
}
|
||||
let cross = first.dir.x * second.dir.y - first.dir.y * second.dir.x;
|
||||
if cross.abs() < 1.0e-9 {
|
||||
return CmdResult::CancelWithMessage(Self::PARALLEL_LINES.to_string());
|
||||
}
|
||||
self.step = Step::AngularLocation {
|
||||
data: AngularData::Lines { first, second },
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::AngularVertex | Step::AngularPoint1 { .. } | Step::AngularPoint2 { .. } => {
|
||||
CmdResult::CheckConstraintPoint(CoincidentPick {
|
||||
handle: (!handle.is_null()).then_some(handle),
|
||||
point,
|
||||
whole_curve: false,
|
||||
})
|
||||
}
|
||||
Step::Location { .. }
|
||||
| Step::Value { .. }
|
||||
| Step::AngularLocation { .. }
|
||||
| Step::AngularValue { .. } => self.on_point(point),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -644,6 +943,35 @@ impl CadCommand for DimConstraintCommand {
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::AngularVertex => {
|
||||
self.step = Step::AngularPoint1 {
|
||||
vertex: (reference, point),
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::AngularPoint1 { vertex } => {
|
||||
if reference == vertex.0 {
|
||||
return Self::report(Self::SAME_POINT);
|
||||
}
|
||||
self.step = Step::AngularPoint2 {
|
||||
vertex,
|
||||
first: (reference, point),
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::AngularPoint2 { vertex, first } => {
|
||||
if reference == vertex.0 || reference == first.0 {
|
||||
return Self::report(Self::SAME_POINT);
|
||||
}
|
||||
self.step = Step::AngularLocation {
|
||||
data: AngularData::Points {
|
||||
vertex,
|
||||
first,
|
||||
second: (reference, point),
|
||||
},
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
@@ -657,11 +985,49 @@ impl CadCommand for DimConstraintCommand {
|
||||
whole_curve: false,
|
||||
})
|
||||
}
|
||||
Step::AngularVertex | Step::AngularPoint1 { .. } | Step::AngularPoint2 { .. } => {
|
||||
CmdResult::CheckConstraintPoint(CoincidentPick {
|
||||
handle: None,
|
||||
point,
|
||||
whole_curve: false,
|
||||
})
|
||||
}
|
||||
Step::Object
|
||||
| Step::PointLineLine { .. }
|
||||
| Step::LineFirst
|
||||
| Step::TwoLinesFirst
|
||||
| Step::TwoLinesSecond { .. } => Self::report(Self::NO_OBJECT),
|
||||
| Step::TwoLinesSecond { .. }
|
||||
| Step::AngularFirst
|
||||
| Step::AngularSecond { .. } => Self::report(Self::NO_OBJECT),
|
||||
Step::AngularLocation { data } => {
|
||||
let frame = match data {
|
||||
AngularData::Lines { first, second } => Self::angle_frame(
|
||||
first.ends[0].1,
|
||||
first.ends[1].1,
|
||||
second.ends[0].1,
|
||||
second.ends[1].1,
|
||||
point,
|
||||
),
|
||||
AngularData::Points {
|
||||
vertex,
|
||||
first,
|
||||
second,
|
||||
} => Self::ray_frame(vertex.1, first.1, second.1, point),
|
||||
};
|
||||
let Some((measured, sector)) = frame else {
|
||||
return Self::report(Self::NO_POINT);
|
||||
};
|
||||
self.step = Step::AngularValue {
|
||||
data,
|
||||
location: point,
|
||||
sector,
|
||||
measured,
|
||||
};
|
||||
CmdResult::ReportMeasurement(format!(
|
||||
"Dimension text = {}",
|
||||
self.angle_display(measured)
|
||||
))
|
||||
}
|
||||
Step::Location {
|
||||
first,
|
||||
second,
|
||||
@@ -684,7 +1050,9 @@ impl CadCommand for DimConstraintCommand {
|
||||
};
|
||||
CmdResult::ReportMeasurement(format!("Dimension text = {measured:.4}"))
|
||||
}
|
||||
Step::TwoLinesParallel { .. } | Step::Value { .. } => CmdResult::NeedPoint,
|
||||
Step::TwoLinesParallel { .. } | Step::Value { .. } | Step::AngularValue { .. } => {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,6 +1062,14 @@ impl CadCommand for DimConstraintCommand {
|
||||
self.step = Step::Object;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
// Enter takes the 3Point default.
|
||||
Step::AngularFirst => {
|
||||
self.step = Step::AngularVertex;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::AngularValue { measured, .. } => self
|
||||
.build_angular(self.default_name.clone(), measured_expression(measured))
|
||||
.unwrap_or(CmdResult::Cancel),
|
||||
Step::PointLinePoint => {
|
||||
self.step = Step::LineFirst;
|
||||
CmdResult::NeedPoint
|
||||
|
||||
@@ -50,7 +50,7 @@ pub use tools::{
|
||||
perpendicular, symmetric, tangent, vertical,
|
||||
};
|
||||
pub use value::{
|
||||
angle_tool, dimensional_tools, distance_tool, AngleConstraintCommand,
|
||||
angle_tool, dimensional_tools, distance_tool,
|
||||
DistanceConstraintCommand, DistanceMode,
|
||||
};
|
||||
|
||||
@@ -123,9 +123,9 @@ impl CadModule for ParametricModule {
|
||||
default: "DCLINEAR",
|
||||
},
|
||||
RibbonItem::LargeTool(dimensional_tools::aligned()),
|
||||
RibbonItem::LargeTool(dimensional_tools::angular()),
|
||||
RibbonItem::LargeTool(dimensional_tools::diameter()),
|
||||
RibbonItem::LargeTool(dimensional_tools::radius()),
|
||||
RibbonItem::LargeTool(dimensional_tools::diameter()),
|
||||
RibbonItem::LargeTool(dimensional_tools::angular()),
|
||||
RibbonItem::LargeTool(dimensional_tools::convert()),
|
||||
RibbonItem::LabeledDropdown {
|
||||
id: "DCVISIBILITY", label: "Show/Hide",
|
||||
@@ -201,8 +201,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
groups[1].tools.iter().map(item_id).collect::<Vec<_>>(),
|
||||
[
|
||||
"DC_LINEAR_MENU", "DCALIGNED", "DCANGULAR", "DCDIAMETER",
|
||||
"DCRADIUS", "DCCONVERT", "DCVISIBILITY", "DCSHOWALL", "DCHIDEALL",
|
||||
"DC_LINEAR_MENU", "DCALIGNED", "DCRADIUS", "DCDIAMETER",
|
||||
"DCANGULAR", "DCCONVERT", "DCVISIBILITY", "DCSHOWALL", "DCHIDEALL",
|
||||
]
|
||||
);
|
||||
assert_eq!(groups[2].tools.iter().map(item_id).collect::<Vec<_>>(), ["DELCONSTRAINT", "PARAMETERS"]);
|
||||
|
||||
@@ -372,99 +372,6 @@ impl CadCommand for DistanceConstraintCommand {
|
||||
}
|
||||
}
|
||||
|
||||
/// Constrains the angle (in degrees) from `fixed`'s direction to `moving`'s.
|
||||
pub struct AngleConstraintCommand {
|
||||
command_name: &'static str,
|
||||
fixed_handle: Handle,
|
||||
moving_handle: Handle,
|
||||
default_value: f64,
|
||||
/// Snapshot of `Scene::named_parameters`' names at construction time —
|
||||
/// see `parse_driving_value`'s doc comment for why a snapshot.
|
||||
known_param_names: Vec<String>,
|
||||
}
|
||||
|
||||
impl AngleConstraintCommand {
|
||||
/// `None` unless both `fixed` and `moving` are lines.
|
||||
pub fn new(scene: &Scene, fixed: Handle, moving: Handle) -> Option<Self> {
|
||||
Self::with_name(scene, fixed, moving, "ACONSTRAINT")
|
||||
}
|
||||
|
||||
pub fn with_name(
|
||||
scene: &Scene,
|
||||
fixed: Handle,
|
||||
moving: Handle,
|
||||
command_name: &'static str,
|
||||
) -> Option<Self> {
|
||||
let fixed_entity = scene.document.get_entity(fixed)?;
|
||||
let moving_entity = scene.document.get_entity(moving)?;
|
||||
let (EntityType::Line(f), EntityType::Line(m)) = (fixed_entity, moving_entity) else {
|
||||
return None;
|
||||
};
|
||||
let a1 = (f.end.y - f.start.y).atan2(f.end.x - f.start.x);
|
||||
let a2 = (m.end.y - m.start.y).atan2(m.end.x - m.start.x);
|
||||
let default_value = (a2 - a1).to_degrees();
|
||||
let known_param_names = scene
|
||||
.named_parameters()
|
||||
.iter()
|
||||
.map(|p| p.name.clone())
|
||||
.collect();
|
||||
Some(Self {
|
||||
command_name,
|
||||
fixed_handle: fixed,
|
||||
moving_handle: moving,
|
||||
default_value,
|
||||
known_param_names,
|
||||
})
|
||||
}
|
||||
|
||||
fn build(&self, target: DrivingValue) -> Option<CmdResult> {
|
||||
if matches!(&target, DrivingValue::Literal(value) if !value.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
Some(CmdResult::AddParametricConstraint {
|
||||
kind: ConstraintKind::Angle,
|
||||
refs: vec![
|
||||
ParametricRef::whole(self.fixed_handle),
|
||||
ParametricRef::whole(self.moving_handle),
|
||||
],
|
||||
driving_param: Some(target),
|
||||
label: "Angle constraint",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for AngleConstraintCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
self.command_name
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
format!("Specify angle in degrees <{:.4}>: ", self.default_value)
|
||||
}
|
||||
|
||||
fn input_kind(&self) -> InputKind {
|
||||
InputKind::SingleToken
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
self.build(DrivingValue::Literal(self.default_value))
|
||||
.unwrap_or(CmdResult::Cancel)
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let value = parse_driving_value(text, &self.known_param_names)?;
|
||||
self.build(value)
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &[
|
||||
|
||||
@@ -363,6 +363,55 @@ pub(crate) fn next_dimensional_parameter_name(
|
||||
.unwrap_or_else(|| "d1".to_string())
|
||||
}
|
||||
|
||||
/// The first free `ang1`, `ang2`, … name a new angular constraint takes.
|
||||
pub(crate) fn next_angular_parameter_name(
|
||||
table: &super::named_parameters::ParameterTable,
|
||||
) -> String {
|
||||
(1..)
|
||||
.map(|n| format!("ang{n}"))
|
||||
.find(|name| !table.contains(name))
|
||||
.unwrap_or_else(|| "ang1".to_string())
|
||||
}
|
||||
|
||||
/// An angle as the reference displays it: a negative or over-full value is
|
||||
/// brought into one turn (`-30` → `330`, `400` → `40`), a full turn stays.
|
||||
pub(crate) fn normalize_angle_display(degrees: f64) -> f64 {
|
||||
if !degrees.is_finite() || (degrees.abs() - 360.0).abs() < 1.0e-9 {
|
||||
degrees
|
||||
} else {
|
||||
degrees.rem_euclid(360.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// The angular precision (DIMADEC, or DIMDEC when unset) of a dimension
|
||||
/// style, by name or the drawing's current one.
|
||||
pub(crate) fn angle_decimals(document: &acadrust::CadDocument, style_name: Option<&str>) -> usize {
|
||||
let requested = style_name
|
||||
.map(str::trim)
|
||||
.filter(|name| !name.is_empty())
|
||||
.unwrap_or_else(|| document.header.current_dimstyle_name.trim());
|
||||
let style = document
|
||||
.dim_styles
|
||||
.iter()
|
||||
.find(|style| style.name.eq_ignore_ascii_case(requested))
|
||||
.or_else(|| {
|
||||
document
|
||||
.dim_styles
|
||||
.iter()
|
||||
.find(|style| style.name.eq_ignore_ascii_case("Standard"))
|
||||
});
|
||||
style
|
||||
.map(|style| {
|
||||
if style.dimadec < 0 {
|
||||
style.dimdec
|
||||
} else {
|
||||
style.dimadec
|
||||
}
|
||||
})
|
||||
.unwrap_or(0)
|
||||
.clamp(0, 8) as usize
|
||||
}
|
||||
|
||||
/// The constraint-bar label that draws a dynamic dimension's lock mark.
|
||||
pub const DYNAMIC_DIMENSION_GLYPH: &str = "\u{1F512}";
|
||||
|
||||
@@ -385,12 +434,18 @@ pub(crate) fn dynamic_dimension_text(
|
||||
reference: bool,
|
||||
expression: Option<&str>,
|
||||
annotational: bool,
|
||||
angle_decimals: Option<usize>,
|
||||
) -> String {
|
||||
let formula = expression
|
||||
.map(str::trim)
|
||||
.filter(|source| !source.is_empty() && source.parse::<f64>().is_err());
|
||||
let shown = match formula {
|
||||
Some(source) => source.to_string(),
|
||||
// An angle shows at angular precision, within one turn (`ang1=27`).
|
||||
None if angle_decimals.is_some() => {
|
||||
let decimals = angle_decimals.unwrap_or(0);
|
||||
format!("{:.decimals$}", normalize_angle_display(value))
|
||||
}
|
||||
// An annotational dimension shows the value at dimension precision
|
||||
// (`d1=100.0000`), as the reference does.
|
||||
None if annotational => format!("{value:.4}"),
|
||||
@@ -510,10 +565,107 @@ fn dynamic_dimension_follow_points(
|
||||
true
|
||||
}
|
||||
|
||||
/// The end points of a line reference: a line's two ends, or the picked
|
||||
/// polyline segment's vertices.
|
||||
fn line_ends(entity: &acadrust::EntityType, reference: ParametricRef) -> Option<(Vector3, Vector3)> {
|
||||
let (start, end) = match reference.segment_index() {
|
||||
Some(index) => {
|
||||
let index = index as i32;
|
||||
let end = resolve_point(entity, index + 1).map_or(0, |_| index + 1);
|
||||
(index, end)
|
||||
}
|
||||
None => (0, 1),
|
||||
};
|
||||
resolve_point(entity, start).zip(resolve_point(entity, end))
|
||||
}
|
||||
|
||||
/// Where an angular constraint's sides are now: both lines' ends for a
|
||||
/// two-line angle, `[vertex, first, second]` for a three-point one.
|
||||
fn angular_follow_points(
|
||||
document: &acadrust::CadDocument,
|
||||
constraint: &ParametricConstraint,
|
||||
) -> Option<Vec<Vector3>> {
|
||||
match (constraint.kind, constraint.refs.as_slice()) {
|
||||
(ConstraintKind::Angle, [first, second]) => {
|
||||
let (a, b) = line_ends(document.get_entity(first.entity)?, *first)?;
|
||||
let (c, d) = line_ends(document.get_entity(second.entity)?, *second)?;
|
||||
Some(vec![a, b, c, d])
|
||||
}
|
||||
(ConstraintKind::Angle3Point, [first, vertex, second]) => {
|
||||
let world = |reference: &ParametricRef| {
|
||||
resolve_point(document.get_entity(reference.entity)?, reference.marker?)
|
||||
};
|
||||
Some(vec![world(vertex)?, world(first)?, world(second)?])
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Rebuilds a dynamic angular dimension on its sides' current positions,
|
||||
/// keeping its arc point; true when anything moved.
|
||||
fn dynamic_dimension_follow_angle(
|
||||
dimension: &mut acadrust::entities::Dimension,
|
||||
points: &[Vector3],
|
||||
) -> bool {
|
||||
use acadrust::entities::Dimension;
|
||||
let same = |a: Vector3, b: Vector3| (a - b).length_squared() < 1.0e-16;
|
||||
let to_dvec = |p: Vector3| glam::DVec3::new(p.x, p.y, p.z);
|
||||
let text = dimension.base().user_text.clone();
|
||||
let rebuilt = match (&*dimension, points) {
|
||||
(Dimension::Angular2Ln(d), [a, b, c, e]) => {
|
||||
if same(d.first_point, *a)
|
||||
&& same(d.second_point, *b)
|
||||
&& same(d.angle_vertex, *c)
|
||||
&& same(d.definition_point, *e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
crate::modules::annotate::angular_dim::angular_two_line_entity(
|
||||
to_dvec(*a),
|
||||
to_dvec(*b),
|
||||
to_dvec(*c),
|
||||
to_dvec(*e),
|
||||
to_dvec(d.dimension_arc),
|
||||
text,
|
||||
)
|
||||
}
|
||||
(Dimension::Angular3Pt(d), [vertex, first, second]) => {
|
||||
if same(d.angle_vertex, *vertex)
|
||||
&& same(d.first_point, *first)
|
||||
&& same(d.second_point, *second)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
crate::modules::annotate::angular_dim::angular_three_point_entity(
|
||||
to_dvec(*vertex),
|
||||
to_dvec(*first),
|
||||
to_dvec(*second),
|
||||
to_dvec(d.definition_point),
|
||||
text,
|
||||
)
|
||||
}
|
||||
_ => return false,
|
||||
};
|
||||
let Some(acadrust::EntityType::Dimension(mut rebuilt)) = rebuilt else {
|
||||
return false;
|
||||
};
|
||||
let base = dimension.base();
|
||||
let target = rebuilt.base_mut();
|
||||
target.common = base.common.clone();
|
||||
target.style_name = base.style_name.clone();
|
||||
target.text_rotation = base.text_rotation;
|
||||
target.attachment_point = base.attachment_point;
|
||||
target.line_spacing_style = base.line_spacing_style;
|
||||
target.line_spacing_factor = base.line_spacing_factor;
|
||||
*dimension = rebuilt;
|
||||
true
|
||||
}
|
||||
|
||||
/// What stays put when a dimensional constraint's value changes: its first
|
||||
/// point, and — when the distance runs perpendicular to a line that owns
|
||||
/// that point (2Lines, line-first Point & line) — the whole line, so the
|
||||
/// other object moves, as in the reference.
|
||||
/// other object moves, as in the reference. An angle keeps its first side
|
||||
/// (a two-line angle) or its vertex and first point (a three-point one).
|
||||
pub(crate) fn dimensional_anchor_refs(
|
||||
document: &acadrust::CadDocument,
|
||||
refs: &[ParametricRef],
|
||||
@@ -521,6 +673,29 @@ pub(crate) fn dimensional_anchor_refs(
|
||||
let Some(&first) = refs.first() else {
|
||||
return Vec::new();
|
||||
};
|
||||
// Three point references: a three-point angle `[first, vertex, second]`.
|
||||
if refs.len() == 3
|
||||
&& refs
|
||||
.iter()
|
||||
.all(|reference| reference.marker.is_some() && reference.segment_index().is_none())
|
||||
{
|
||||
return vec![refs[1], first];
|
||||
}
|
||||
// A two-line angle whose first side is a polyline segment: pin its ends.
|
||||
if refs.len() == 2 && first.marker.is_none() {
|
||||
return vec![first];
|
||||
}
|
||||
if let (2, Some(index)) = (refs.len(), first.segment_index()) {
|
||||
let index = index as i32;
|
||||
let end = document
|
||||
.get_entity(first.entity)
|
||||
.and_then(|entity| resolve_point(entity, index + 1))
|
||||
.map_or(0, |_| index + 1);
|
||||
return vec![
|
||||
ParametricRef::point(first.entity, index),
|
||||
ParametricRef::point(first.entity, end),
|
||||
];
|
||||
}
|
||||
let mut anchors = vec![first];
|
||||
if let Some(&line) = refs.get(2).filter(|line| line.entity == first.entity) {
|
||||
let ends = match line.segment_index() {
|
||||
@@ -1803,7 +1978,12 @@ impl super::Scene {
|
||||
/// origins to where the solve left the constraint points.
|
||||
pub(crate) fn refresh_dynamic_dimension_texts(&mut self) {
|
||||
let format = self.constraint_name_format;
|
||||
let mut updates: Vec<(Handle, String, Option<(Vector3, Vector3)>)> = Vec::new();
|
||||
let mut updates: Vec<(
|
||||
Handle,
|
||||
String,
|
||||
Option<(Vector3, Vector3)>,
|
||||
Option<Vec<Vector3>>,
|
||||
)> = Vec::new();
|
||||
// A reference (driven) constraint's parameter follows the geometry.
|
||||
let mut followed: Vec<(String, f64)> = Vec::new();
|
||||
for set in &self.parametric_constraints {
|
||||
@@ -1826,7 +2006,11 @@ impl super::Scene {
|
||||
_ => None,
|
||||
};
|
||||
let reference = !constraint.enabled;
|
||||
let measured = points.map(|(first, second)| {
|
||||
let angular = matches!(
|
||||
constraint.kind,
|
||||
ConstraintKind::Angle | ConstraintKind::Angle3Point
|
||||
);
|
||||
let measured = points.filter(|_| !angular).map(|(first, second)| {
|
||||
let delta = second - first;
|
||||
let axis = match self.document.get_entity(*dimension) {
|
||||
Some(acadrust::EntityType::Dimension(
|
||||
@@ -1863,6 +2047,16 @@ impl super::Scene {
|
||||
None => continue,
|
||||
};
|
||||
let annotational = self.dimension_is_annotational(*dimension);
|
||||
let decimals = angular.then(|| {
|
||||
let style = match self.document.get_entity(*dimension) {
|
||||
Some(acadrust::EntityType::Dimension(d)) => d.base().style_name.clone(),
|
||||
_ => String::new(),
|
||||
};
|
||||
angle_decimals(&self.document, Some(&style))
|
||||
});
|
||||
let angle_points = angular
|
||||
.then(|| angular_follow_points(&self.document, constraint))
|
||||
.flatten();
|
||||
updates.push((
|
||||
*dimension,
|
||||
dynamic_dimension_text(
|
||||
@@ -1872,8 +2066,10 @@ impl super::Scene {
|
||||
reference,
|
||||
source.as_deref(),
|
||||
annotational,
|
||||
decimals,
|
||||
),
|
||||
points,
|
||||
angle_points,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1883,7 +2079,7 @@ impl super::Scene {
|
||||
let _ = self.named_parameters.set(&name, &source);
|
||||
}
|
||||
}
|
||||
for (handle, text, points) in updates {
|
||||
for (handle, text, points, angle_points) in updates {
|
||||
let Some(acadrust::EntityType::Dimension(mut dimension)) =
|
||||
self.document.get_entity(handle).cloned()
|
||||
else {
|
||||
@@ -1895,6 +2091,11 @@ impl super::Scene {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if let Some(angle_points) = angle_points {
|
||||
if dynamic_dimension_follow_angle(&mut dimension, &angle_points) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if dimension.base().user_text.as_deref() != Some(text.as_str()) {
|
||||
crate::entities::dimension::set_dimension_text_override(
|
||||
dimension.base_mut(),
|
||||
@@ -1995,6 +2196,31 @@ impl super::Scene {
|
||||
let _ = self.document.layers.add(layer);
|
||||
}
|
||||
|
||||
/// True when an angular constraint drives with the parameter `name`.
|
||||
pub(crate) fn parameter_is_angular(&self, name: &str) -> bool {
|
||||
self.parametric_constraints
|
||||
.iter()
|
||||
.flat_map(|set| set.constraints.iter())
|
||||
.any(|constraint| {
|
||||
matches!(constraint.kind, ConstraintKind::Angle | ConstraintKind::Angle3Point)
|
||||
&& matches!(&constraint.driving_param, Some(DrivingValue::Named(used)) if used == name)
|
||||
})
|
||||
}
|
||||
|
||||
/// A parameter's value as -PARAMETERS prints it: an angle at angular
|
||||
/// precision within one turn, anything else with four decimals.
|
||||
pub(crate) fn parameter_value_text(&self, name: &str) -> String {
|
||||
let Ok(value) = self.named_parameters.resolve(name) else {
|
||||
return "**".to_string();
|
||||
};
|
||||
if self.parameter_is_angular(name) {
|
||||
let decimals = angle_decimals(&self.document, None);
|
||||
format!("{:.decimals$}", normalize_angle_display(value))
|
||||
} else {
|
||||
format!("{value:.4}")
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -2026,7 +2252,15 @@ impl super::Scene {
|
||||
if self.dimension_is_annotational(handle) {
|
||||
continue;
|
||||
}
|
||||
for code in [ov::DIMSCALE, ov::DIMTIH, ov::DIMTOH] {
|
||||
for code in [
|
||||
ov::DIMSCALE,
|
||||
ov::DIMGAP,
|
||||
ov::DIMEXO,
|
||||
ov::DIMEXE,
|
||||
ov::DIMASZ,
|
||||
ov::DIMTIH,
|
||||
ov::DIMTOH,
|
||||
] {
|
||||
ov::set(&mut document, handle, code, None);
|
||||
}
|
||||
}
|
||||
@@ -2078,9 +2312,28 @@ impl super::Scene {
|
||||
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 {
|
||||
// Text gap, extension offset and overshoot and arrow size follow
|
||||
// the text height on screen (the reference's ratios), whatever
|
||||
// the style says in drawing units.
|
||||
let sizes = [
|
||||
(ov::DIMGAP, text_height * 0.25),
|
||||
(ov::DIMEXO, text_height * 0.25),
|
||||
(ov::DIMEXE, text_height * 0.5),
|
||||
(ov::DIMASZ, text_height),
|
||||
];
|
||||
let sizes_set = sizes.iter().all(|(code, size)| {
|
||||
ov::real(xdata, *code).is_some_and(|value| (value - size).abs() < 1e-12)
|
||||
});
|
||||
// The reference draws dynamic distance text horizontally; an
|
||||
// angle's text keeps its style's alignment.
|
||||
let angular = matches!(
|
||||
dimension,
|
||||
acadrust::entities::Dimension::Angular2Ln(_)
|
||||
| acadrust::entities::Dimension::Angular3Pt(_)
|
||||
);
|
||||
let horizontal = angular
|
||||
|| (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 && sizes_set {
|
||||
continue;
|
||||
}
|
||||
ov::set(
|
||||
@@ -2089,6 +2342,16 @@ impl super::Scene {
|
||||
ov::DIMSCALE,
|
||||
Some(acadrust::xdata::XDataValue::Real(scale)),
|
||||
);
|
||||
if !sizes_set {
|
||||
for (code, size) in sizes {
|
||||
ov::set(
|
||||
&mut self.document,
|
||||
handle,
|
||||
code,
|
||||
Some(acadrust::xdata::XDataValue::Real(size)),
|
||||
);
|
||||
}
|
||||
}
|
||||
if !horizontal {
|
||||
for code in [ov::DIMTIH, ov::DIMTOH] {
|
||||
ov::set(
|
||||
|
||||
Reference in new issue
Block a user