Merge pull request #1431 from ramox81:codex/angular-constraint-parity

This commit is contained in:
Hakan Seven committed 2026-09-22 12:28:04 +03:00
commit d37ce4d617
12 files changed
+1278 -205

No files matched your search

+225 -5
View File
@@ -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
View File
@@ -1569,10 +1569,8 @@ impl OpenCADStudio {
table
.iter()
.map(|parameter| {
let value = table
.resolve(&parameter.name)
.map(|value| format!("{value:.4}"))
.unwrap_or_else(|_| "**".to_string());
let value =
self.tabs[i].scene.parameter_value_text(&parameter.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) ─────────────────────────────
+8
View File
@@ -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
View File
@@ -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(),
+20
View File
@@ -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.
+5 -3
View File
@@ -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
View File
@@ -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,
+47 -1
View File
@@ -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)
}
+386 -10
View File
@@ -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
+5 -5
View File
@@ -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"]);
-93
View File
@@ -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: &[
+271 -8
View File
@@ -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(