Angular dimensional constraint: reference flow, dynamic angular dimension, angN parameters

DCANGULAR picks two lines, an arc or three points, takes the dimension
line location to choose the sector and the value or name, and creates the
dynamic two-line or three-point angular dimension driven by an angN
parameter. Reference messages for invalid, repeated and parallel sides,
existing names and invalid names; angle values display within one turn at
the angular precision; the first side (or vertex and first point) stays
when the value changes; the ribbon Dimensional panel follows the reference
order.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ramox81andClaude Fable 5.1 committed 2026-09-22 10:02:40 +03:00
1 parent 960d51d955
commit 4ad4041858
8 files changed
+928 -162

No files matched your search

+216 -4
View File
@@ -1517,8 +1517,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();
@@ -1580,7 +1584,6 @@ impl OpenCADStudio {
&c.refs,
)
})
.filter(|reference| reference.marker.is_some())
.collect();
self.tabs[i].dirty = true;
if touched.is_empty() {
@@ -4039,6 +4042,7 @@ impl OpenCADStudio {
direction,
name,
expression,
renamed,
label,
} => {
use crate::modules::annotate::{aligned_dim, linear_dim};
@@ -4094,7 +4098,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();
@@ -4123,6 +4134,7 @@ impl OpenCADStudio {
false,
Some(&expression),
annotational,
None,
);
let mut entity = if kind == ConstraintKind::Distance {
aligned_dim::aligned_dimension_entity(
@@ -4222,6 +4234,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) ─────────────────────────────
+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.
+46 -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,51 @@ 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;
dim.base.text_middle_point = dim.dimension_arc;
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 = dim.definition_point;
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: &[
+239 -6
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 {
@@ -2078,8 +2304,15 @@ 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);
// 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 {
continue;
}