DCRADIUS: reference radial dimensional constraint with a dynamic radial dimension

Select arc or circle / Dimension text / dimension line location / value or
name prompts with a radN parameter, invalid-selection, miss, duplicate and
invalid-expression messages, negative values as magnitudes, a dynamic radial
dimension on the constraints layer drawn as the reference does (mid-radius
text on the center-to-rim line, no hook, no center mark, one text grip), the
stored text position following the radius, center-anchored resizing, the
Properties title and Constraint section, DIMCONSTRAINT's Radius default,
the small-button Dimensional ribbon grid and tooltip descriptions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ramox81andClaude Fable 5.1 committed 2026-09-22 13:09:47 +03:00
1 parent 64c020293a
commit aab42e2aa9
11 files changed
+516 -87

No files matched your search

+176
View File
@@ -4442,6 +4442,172 @@ impl OpenCADStudio {
self.commit_undo_delta(i, pd); self.commit_undo_delta(i, pd);
} }
} }
CmdResult::AddRadialConstraint {
circle,
location,
name,
expression,
renamed,
} => {
use crate::scene::named_parameters::{is_valid_name, DrivingValue};
use crate::scene::parametric_constraints::{dynamic_dimension_text, ConstraintKind};
let scope = self.tabs[i].current_parametric_scope();
let kind = ConstraintKind::Radius;
let refs = vec![circle];
// A second radius on the same circle is refused after the
// value, 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 == 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();
}
// A negative radius is its magnitude (`rad1=-20` is a radius
// of 20); a zero radius has no circle.
let value = match table.resolve(&name) {
Ok(value) if value.is_finite() && value != 0.0 => 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 text = dynamic_dimension_text(
&name,
value,
self.tabs[i].scene.constraint_name_format,
false,
Some(&expression),
annotational,
None,
);
let source = self.tabs[i]
.scene
.document
.get_entity(circle.entity)
.and_then(|entity| {
crate::scene::dimension_assoc::radial_source_at(
entity,
acadrust::types::Vector3::new(location.x, location.y, location.z),
)
});
let Some(source) = source else {
self.reprompt_active_command(
i,
crate::modules::parametric::DimConstraintCommand::NO_OBJECT,
);
return Task::none();
};
let mut entity = crate::modules::annotate::radius_dim::radial_dimension_entity(
source,
location,
Some(text),
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)
});
// The center stays; a value other than the measured radius
// resizes the circle about it.
let anchors = crate::scene::parametric_constraints::dimensional_anchor_refs(
&self.tabs[i].scene.document,
&refs,
);
let pending = self.begin_undo(i, "Radius constraint", 2, 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)));
set.dimensions.insert(id, dimension);
self.tabs[i].scene.note_parametric_constraint_applied(
scope,
id,
self.constraint_bar_display,
);
self.tabs[i]
.scene
.attach_dimension_association(dimension, vec![Some(circle.entity)]);
self.tabs[i].scene.bump_entities_with_parametric_policy(
&[(circle.entity, crate::scene::ChangeKind::Modified)],
&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 { CmdResult::MakeParallel {
first_line, first_line,
first_ends, first_ends,
@@ -5886,6 +6052,16 @@ impl OpenCADStudio {
CmdResult::Dispatch(cmd) => { CmdResult::Dispatch(cmd) => {
// End this interactive front-end, then run the assembled command // End this interactive front-end, then run the assembled command
// through the normal dispatcher. Selection is left untouched. // through the normal dispatcher. Selection is left untouched.
// DIMCONSTRAINT's Radius option becomes its next default; the
// DCRADIUS command run on its own leaves the default alone.
if cmd == "DCRADIUS"
&& self.tabs[i]
.active_cmd
.as_ref()
.is_some_and(|command| command.name() == "DIMCONSTRAINT")
{
self.dim_constraint_last = "Radius";
}
self.tabs[i].active_cmd = None; self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None; self.tabs[i].snap_result = None;
self.tabs[i].scene.clear_preview_wire(); self.tabs[i].scene.clear_preview_wire();
+14 -4
View File
@@ -1891,10 +1891,20 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(command)); self.tabs[i].active_cmd = Some(Box::new(command));
} }
"DCONSTRAINT" | "DCRADIUS" | "DCDIAMETER" => { "DCRADIUS" => {
if cmd == "DCRADIUS" { use crate::modules::parametric::{DimConstraintAxis, DimConstraintCommand};
self.dim_constraint_last = "Radius"; use crate::scene::parametric_constraints::next_radial_parameter_name;
} else if cmd == "DCDIAMETER" { // Run on its own it leaves DIMCONSTRAINT's default alone.
let name = next_radial_parameter_name(self.tabs[i].scene.named_parameters());
// The circle or arc is picked inside the command.
self.tabs[i].scene.deselect_all();
let command = DimConstraintCommand::new(DimConstraintAxis::Radius, name);
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
"DCONSTRAINT" | "DCDIAMETER" => {
if cmd == "DCDIAMETER" {
self.dim_constraint_last = "Diameter"; self.dim_constraint_last = "Diameter";
} }
let handles = self.tabs[i].scene.selected_handles_in_order(); let handles = self.tabs[i].scene.selected_handles_in_order();
+10
View File
@@ -2368,6 +2368,9 @@ impl OpenCADStudio {
acadrust::EntityType::Dimension( acadrust::EntityType::Dimension(
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_), Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_),
) => t!("Angular Dimensional Constraint"), ) => t!("Angular Dimensional Constraint"),
acadrust::EntityType::Dimension(Dimension::Radius(_)) => {
t!("Radial Dimension (Dynamic)")
}
_ => t!("Linear Dimensional Constraint"), _ => t!("Linear Dimensional Constraint"),
} }
.into_owned() .into_owned()
@@ -2850,6 +2853,13 @@ handles={handles_ms:.1} panel={:.1} ribbon={ribbon_ms:.1} tail={:.1} selected={}
); );
} }
} }
} else if matches!(
contextual.as_ref(),
acadrust::EntityType::Dimension(acadrust::entities::Dimension::Radius(_))
) && self.tabs[i].scene.is_dynamic_dimension(handle)
{
// A dynamic radius shows only its text square.
entity_grips.retain(|grip| grip.id == 2);
} else if self.tabs[i].scene.is_dynamic_dimension(handle) { } else if self.tabs[i].scene.is_dynamic_dimension(handle) {
// A dynamic dimension shows the reference's grips: a // A dynamic dimension shows the reference's grips: a
// triangle at each constraint point aimed at the other // triangle at each constraint point aimed at the other
+9
View File
@@ -1650,6 +1650,15 @@ pub enum CmdResult {
expression: String, expression: String,
renamed: bool, renamed: bool,
}, },
/// Adds a radius constraint on a circle or arc with its dynamic radial
/// dimension (text at `location`) and the `radN` parameter that drives it.
AddRadialConstraint {
circle: crate::scene::parametric_constraints::ParametricRef,
location: DVec3,
name: String,
expression: String,
renamed: bool,
},
/// Aligned's 2Lines: makes `second_line` parallel to `first_line` (whose /// Aligned's 2Lines: makes `second_line` parallel to `first_line` (whose
/// ends stay put), then hands the second line's solved ends back through /// ends stay put), then hands the second line's solved ends back through
/// `CadCommand::accept_parallel_line`. /// `CadCommand::accept_parallel_line`.
+26 -2
View File
@@ -1134,7 +1134,11 @@ impl Grippable for Dimension {
// of the world origin, so it stays on the visible text and grabbable. // of the world origin, so it stays on the visible text and grabbable.
let text = { let text = {
let p = self.base().text_middle_point; let p = self.base().text_middle_point;
if p.x * p.x + p.y * p.y + p.z * p.z > 1e-16 { // A dynamic radius constraint draws its text at mid-radius, not
// at the stored point; its grip sits on the drawn text.
let drawn_elsewhere =
matches!(self, Dimension::Radius(_)) && dynamic_constraint_dimension(self);
if p.x * p.x + p.y * p.y + p.z * p.z > 1e-16 && !drawn_elsewhere {
dv3(&p) dv3(&p)
} else { } else {
dv3(&dimension_text_pos_f64(self, None, 2.5, 1.0)) dv3(&dimension_text_pos_f64(self, None, 2.5, 1.0))
@@ -4780,7 +4784,12 @@ fn dimension_geometry(
// to the centre. Text outside: only a leader from the arc point to // to the centre. Text outside: only a leader from the arc point to
// the text, unless DIMTOFL asks for the inside line as well. // the text, unless DIMTOFL asks for the inside line as well.
if !jogged && !suppress.dim2 && (!text_is_outside || params.dimtofl) { if !jogged && !suppress.dim2 && (!text_is_outside || params.dimtofl) {
add_segment(&mut g.dim_lines, center, point); if dynamic_constraint_dimension(dim) {
// The line passes behind the mid-radius text.
add_segment_with_text_break(&mut g.dim_lines, center, point, params.text_break);
} else {
add_segment(&mut g.dim_lines, center, point);
}
} }
if text_is_outside && !suppress.dim2 { if text_is_outside && !suppress.dim2 {
append_radial_leader(&mut g, point, text, &params); append_radial_leader(&mut g, point, text, &params);
@@ -7542,6 +7551,15 @@ fn stored_text_point(dim: &Dimension) -> Option<Vector3> {
(p.x * p.x + p.y * p.y + p.z * p.z > 1e-16).then_some(p) (p.x * p.x + p.y * p.y + p.z * p.z > 1e-16).then_some(p)
} }
/// A dimensional constraint's dynamic dimension: the reference keeps it on
/// its own hidden layer and draws it by its own rules.
fn dynamic_constraint_dimension(dim: &Dimension) -> bool {
dim.base()
.common
.layer
.eq_ignore_ascii_case(crate::scene::parametric_constraints::DYNAMIC_DIMENSION_LAYER)
}
/// The point on the circle a radial leader leaves from: the arc point of a /// The point on the circle a radial leader leaves from: the arc point of a
/// radius, or whichever end of a diameter's chord is nearer the text. /// radius, or whichever end of a diameter's chord is nearer the text.
fn radial_leader_tip(dim: &Dimension, text: Vector3) -> Vector3 { fn radial_leader_tip(dim: &Dimension, text: Vector3) -> Vector3 {
@@ -7598,6 +7616,12 @@ fn dimension_text_pos_f64(
if let Some(point) = stored_text_point(dim) { if let Some(point) = stored_text_point(dim) {
return match dim { return match dim {
// A dynamic radius constraint draws its text at mid-radius on the
// dimension line, whatever text position the file stores (the
// picked location), as the reference does.
Dimension::Radius(d) if dynamic_constraint_dimension(dim) => {
(d.angle_vertex + d.definition_point) * 0.5
}
// DIMTMOVE 1 stores where the leader's hook starts: one arrow of // DIMTMOVE 1 stores where the leader's hook starts: one arrow of
// hook, a gap, then the text, all running away from the arc. // hook, a gap, then the text, all running away from the arc.
// Measured on 39 such dimensions the stored point sits 2.4 text // Measured on 39 such dimensions the stored point sits 2.4 text
+31 -23
View File
@@ -62,32 +62,40 @@ impl RadiusDimensionCommand {
source: crate::scene::dimension_assoc::RadialSourceGeometry, source: crate::scene::dimension_assoc::RadialSourceGeometry,
world: DVec3, world: DVec3,
) -> EntityType { ) -> EntityType {
let plane = WorkingPlane::new( radial_dimension_entity(source, world, self.text_override.clone(), self.text_angle)
DVec3::from_array(source.plane.origin),
DVec3::from_array(source.plane.x_axis),
DVec3::from_array(source.plane.y_axis),
);
let center = plane.to_local(dvec(source.center_world()));
let chord = plane.to_local(dvec(source.chord_at(world.to_array())));
let point = plane.to_local(world);
let mut dim = DimensionRadius::new(v3(center), v3(chord));
dim.base.definition_point = v3(chord);
dim.base.text_middle_point = v3(point);
dim.base.insertion_point = v3(point);
dim.base.text_user_positioned = true;
dim.leader_length = chord.distance(point);
dim.base.actual_measurement = dim.measurement();
crate::entities::dimension::set_dimension_text_override(
&mut dim.base,
self.text_override.clone(),
);
if let Some(angle) = self.text_angle {
dim.base.text_rotation = angle;
}
plane.place_entity(EntityType::Dimension(Dimension::Radius(dim)))
} }
} }
/// A radius dimension of `source` with its text at `world`: the dimension
/// line runs from the center through the text (DIMUPT).
pub(crate) fn radial_dimension_entity(
source: crate::scene::dimension_assoc::RadialSourceGeometry,
world: DVec3,
text_override: Option<String>,
text_angle: Option<f64>,
) -> EntityType {
let plane = WorkingPlane::new(
DVec3::from_array(source.plane.origin),
DVec3::from_array(source.plane.x_axis),
DVec3::from_array(source.plane.y_axis),
);
let center = plane.to_local(dvec(source.center_world()));
let chord = plane.to_local(dvec(source.chord_at(world.to_array())));
let point = plane.to_local(world);
let mut dim = DimensionRadius::new(v3(center), v3(chord));
dim.base.definition_point = v3(chord);
dim.base.text_middle_point = v3(point);
dim.base.insertion_point = v3(point);
dim.base.text_user_positioned = true;
dim.leader_length = chord.distance(point);
dim.base.actual_measurement = dim.measurement();
crate::entities::dimension::set_dimension_text_override(&mut dim.base, text_override);
if let Some(angle) = text_angle {
dim.base.text_rotation = angle;
}
plane.place_entity(EntityType::Dimension(Dimension::Radius(dim)))
}
impl CadCommand for RadiusDimensionCommand { impl CadCommand for RadiusDimensionCommand {
fn set_working_plane(&mut self, _plane: WorkingPlane) { fn set_working_plane(&mut self, _plane: WorkingPlane) {
} }
+103 -23
View File
@@ -16,6 +16,8 @@ pub enum DimConstraintAxis {
Aligned, Aligned,
/// The angle between two lines, or at a vertex between two points. /// The angle between two lines, or at a vertex between two points.
Angular, Angular,
/// The radius of a circle or arc.
Radius,
} }
/// A picked line (a line entity or one polyline segment) an Aligned /// A picked line (a line entity or one polyline segment) an Aligned
@@ -106,6 +108,16 @@ enum Step {
sector: u8, sector: u8,
measured: f64, measured: f64,
}, },
/// Radius: `Select arc or circle:`
RadialSelect,
/// `Specify dimension line location:` of a radius.
RadialLocation { circle: ParametricRef, radius: f64 },
/// `Enter value or name and value <rad1=30>:`
RadialValue {
circle: ParametricRef,
radius: f64,
location: DVec3,
},
/// `Enter value or name and value <d1=100>:` /// `Enter value or name and value <d1=100>:`
Value { Value {
first: ParametricRef, first: ParametricRef,
@@ -155,14 +167,16 @@ impl DimConstraintCommand {
"The object or point is already selected. Select a different object or constraint point."; "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 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 const PARALLEL_LINES: &'static str = "Lines are parallel.";
pub const INVALID_RADIAL: &'static str =
"Invalid selection for Radial. Select a circle or arc.";
pub fn new(axis: DimConstraintAxis, default_name: String) -> Self { pub fn new(axis: DimConstraintAxis, default_name: String) -> Self {
Self { Self {
axis, axis,
step: if axis == DimConstraintAxis::Angular { step: match axis {
Step::AngularFirst DimConstraintAxis::Angular => Step::AngularFirst,
} else { DimConstraintAxis::Radius => Step::RadialSelect,
Step::First _ => Step::First,
}, },
picked_entity: None, picked_entity: None,
default_name, default_name,
@@ -304,6 +318,25 @@ impl DimConstraintCommand {
}) })
} }
fn build_radial(&self, name: String, expression: String) -> Option<CmdResult> {
let Step::RadialValue {
circle, location, ..
} = self.step
else {
return None;
};
if expression.trim().is_empty() {
return None;
}
Some(CmdResult::AddRadialConstraint {
circle,
location,
renamed: name != self.default_name,
name,
expression,
})
}
fn command_name(&self) -> &'static str { fn command_name(&self) -> &'static str {
match self.axis { match self.axis {
DimConstraintAxis::Linear => "DCLINEAR", DimConstraintAxis::Linear => "DCLINEAR",
@@ -311,6 +344,7 @@ impl DimConstraintCommand {
DimConstraintAxis::Vertical => "DCVERTICAL", DimConstraintAxis::Vertical => "DCVERTICAL",
DimConstraintAxis::Aligned => "DCALIGNED", DimConstraintAxis::Aligned => "DCALIGNED",
DimConstraintAxis::Angular => "DCANGULAR", DimConstraintAxis::Angular => "DCANGULAR",
DimConstraintAxis::Radius => "DCRADIUS",
} }
} }
@@ -321,6 +355,7 @@ impl DimConstraintCommand {
DimConstraintAxis::Vertical => "Vertical", DimConstraintAxis::Vertical => "Vertical",
DimConstraintAxis::Aligned => "Aligned", DimConstraintAxis::Aligned => "Aligned",
DimConstraintAxis::Angular => "Angular", DimConstraintAxis::Angular => "Angular",
DimConstraintAxis::Radius => "Radial",
} }
} }
@@ -474,8 +509,8 @@ impl DimConstraintCommand {
} }
DimConstraintAxis::Horizontal => (ConstraintKind::DistanceX, DVec3::X), DimConstraintAxis::Horizontal => (ConstraintKind::DistanceX, DVec3::X),
DimConstraintAxis::Vertical => (ConstraintKind::DistanceY, DVec3::Y), DimConstraintAxis::Vertical => (ConstraintKind::DistanceY, DVec3::Y),
// Angular never measures here; its own steps decide the sector. // Angular and Radius never measure here; their own steps decide.
DimConstraintAxis::Aligned | DimConstraintAxis::Angular => ( DimConstraintAxis::Aligned | DimConstraintAxis::Angular | DimConstraintAxis::Radius => (
ConstraintKind::Distance, ConstraintKind::Distance,
(second - first).normalize_or(DVec3::X), (second - first).normalize_or(DVec3::X),
), ),
@@ -525,7 +560,9 @@ impl DimConstraintCommand {
DimConstraintAxis::Linear => "Linear constraint", DimConstraintAxis::Linear => "Linear constraint",
DimConstraintAxis::Horizontal => "Horizontal distance constraint", DimConstraintAxis::Horizontal => "Horizontal distance constraint",
DimConstraintAxis::Vertical => "Vertical distance constraint", DimConstraintAxis::Vertical => "Vertical distance constraint",
DimConstraintAxis::Aligned | DimConstraintAxis::Angular => "Aligned constraint", DimConstraintAxis::Aligned | DimConstraintAxis::Angular | DimConstraintAxis::Radius => {
"Aligned constraint"
}
}, },
}) })
} }
@@ -567,14 +604,17 @@ impl CadCommand for DimConstraintCommand {
Step::AngularPoint2 { .. } => { Step::AngularPoint2 { .. } => {
format!("{name} Specify second angle constraint point:") format!("{name} Specify second angle constraint point:")
} }
Step::AngularLocation { .. } | Step::Location { .. } => { Step::RadialSelect => format!("{name} Select arc or circle:"),
Step::AngularLocation { .. } | Step::Location { .. } | Step::RadialLocation { .. } => {
format!("{name} Specify dimension line location:") format!("{name} Specify dimension line location:")
} }
Step::AngularValue { measured, .. } => format!( Step::AngularValue { measured, .. } | Step::RadialValue { radius: measured, .. } => {
"{name} Enter value or name and value <{}={}>:", format!(
self.default_name, "{name} Enter value or name and value <{}={}>:",
measured_expression(measured) self.default_name,
), measured_expression(measured)
)
}
Step::Value { measured, .. } => format!( Step::Value { measured, .. } => format!(
"{name} Enter value or name and value <{}={}>:", "{name} Enter value or name and value <{}={}>:",
self.default_name, self.default_name,
@@ -609,6 +649,7 @@ impl CadCommand for DimConstraintCommand {
| Step::Value { .. } | Step::Value { .. }
| Step::AngularFirst | Step::AngularFirst
| Step::AngularValue { .. } | Step::AngularValue { .. }
| Step::RadialValue { .. }
) )
} }
@@ -643,16 +684,16 @@ impl CadCommand for DimConstraintCommand {
} }
None None
} }
Step::Value { .. } | Step::AngularValue { .. } => { Step::Value { .. } | Step::AngularValue { .. } | Step::RadialValue { .. } => {
let text = text.trim(); let text = text.trim();
let (name, expression) = match text.split_once('=') { let (name, expression) = match text.split_once('=') {
Some((name, expression)) => (name.trim().to_string(), expression.trim().to_string()), Some((name, expression)) => (name.trim().to_string(), expression.trim().to_string()),
None => (self.default_name.clone(), text.to_string()), None => (self.default_name.clone(), text.to_string()),
}; };
if matches!(self.step, Step::AngularValue { .. }) { match self.step {
self.build_angular(name, expression) Step::AngularValue { .. } => self.build_angular(name, expression),
} else { Step::RadialValue { .. } => self.build_radial(name, expression),
self.build(name, expression) _ => self.build(name, expression),
} }
} }
_ => None, _ => None,
@@ -676,6 +717,7 @@ impl CadCommand for DimConstraintCommand {
| Step::AngularVertex | Step::AngularVertex
| Step::AngularPoint1 { .. } | Step::AngularPoint1 { .. }
| Step::AngularPoint2 { .. } | Step::AngularPoint2 { .. }
| Step::RadialSelect
) )
} }
@@ -867,10 +909,35 @@ impl CadCommand for DimConstraintCommand {
whole_curve: false, whole_curve: false,
}) })
} }
Step::RadialSelect => {
if handle.is_null() {
return Self::report(Self::NO_OBJECT);
}
let Some(entity) = self.picked_entity.take() else {
return CmdResult::NeedPoint;
};
let radius = match &entity {
EntityType::Circle(circle) => circle.radius,
EntityType::Arc(arc) => arc.radius,
_ => return Self::report(Self::INVALID_RADIAL),
};
self.step = Step::RadialLocation {
circle: ParametricRef::whole(handle),
radius,
};
// The reference reports the radius before asking where the
// dimension goes.
CmdResult::ReportMeasurement(format!(
"Dimension text = {}",
measured_expression(radius)
))
}
Step::Location { .. } Step::Location { .. }
| Step::Value { .. } | Step::Value { .. }
| Step::AngularLocation { .. } | Step::AngularLocation { .. }
| Step::AngularValue { .. } => self.on_point(point), | Step::AngularValue { .. }
| Step::RadialLocation { .. }
| Step::RadialValue { .. } => self.on_point(point),
} }
} }
@@ -998,7 +1065,16 @@ impl CadCommand for DimConstraintCommand {
| Step::TwoLinesFirst | Step::TwoLinesFirst
| Step::TwoLinesSecond { .. } | Step::TwoLinesSecond { .. }
| Step::AngularFirst | Step::AngularFirst
| Step::AngularSecond { .. } => Self::report(Self::NO_OBJECT), | Step::AngularSecond { .. }
| Step::RadialSelect => Self::report(Self::NO_OBJECT),
Step::RadialLocation { circle, radius } => {
self.step = Step::RadialValue {
circle,
radius,
location: point,
};
CmdResult::NeedPoint
}
Step::AngularLocation { data } => { Step::AngularLocation { data } => {
let frame = match data { let frame = match data {
AngularData::Lines { first, second } => Self::angle_frame( AngularData::Lines { first, second } => Self::angle_frame(
@@ -1050,9 +1126,10 @@ impl CadCommand for DimConstraintCommand {
}; };
CmdResult::ReportMeasurement(format!("Dimension text = {measured:.4}")) CmdResult::ReportMeasurement(format!("Dimension text = {measured:.4}"))
} }
Step::TwoLinesParallel { .. } | Step::Value { .. } | Step::AngularValue { .. } => { Step::TwoLinesParallel { .. }
CmdResult::NeedPoint | Step::Value { .. }
} | Step::AngularValue { .. }
| Step::RadialValue { .. } => CmdResult::NeedPoint,
} }
} }
@@ -1070,6 +1147,9 @@ impl CadCommand for DimConstraintCommand {
Step::AngularValue { measured, .. } => self Step::AngularValue { measured, .. } => self
.build_angular(self.default_name.clone(), measured_expression(measured)) .build_angular(self.default_name.clone(), measured_expression(measured))
.unwrap_or(CmdResult::Cancel), .unwrap_or(CmdResult::Cancel),
Step::RadialValue { radius, .. } => self
.build_radial(self.default_name.clone(), measured_expression(radius))
.unwrap_or(CmdResult::Cancel),
Step::PointLinePoint => { Step::PointLinePoint => {
self.step = Step::LineFirst; self.step = Step::LineFirst;
CmdResult::NeedPoint CmdResult::NeedPoint
+20 -11
View File
@@ -123,10 +123,14 @@ impl CadModule for ParametricModule {
default: "DCLINEAR", default: "DCLINEAR",
}, },
RibbonItem::LargeTool(dimensional_tools::aligned()), RibbonItem::LargeTool(dimensional_tools::aligned()),
RibbonItem::LargeTool(dimensional_tools::radius()), // Two rows of small icon-only buttons, as in the
RibbonItem::LargeTool(dimensional_tools::diameter()), // reference: Radius, Diameter / Angular, Convert.
RibbonItem::LargeTool(dimensional_tools::angular()), RibbonItem::ToolGrid {
RibbonItem::LargeTool(dimensional_tools::convert()), columns: vec![
vec![dimensional_tools::radius(), dimensional_tools::angular()],
vec![dimensional_tools::diameter(), dimensional_tools::convert()],
],
},
RibbonItem::LabeledDropdown { RibbonItem::LabeledDropdown {
id: "DCVISIBILITY", label: "Show/Hide", id: "DCVISIBILITY", label: "Show/Hide",
icon: IconKind::Svg(include_bytes!("../../../assets/icons/constrain/show.svg")), icon: IconKind::Svg(include_bytes!("../../../assets/icons/constrain/show.svg")),
@@ -200,10 +204,17 @@ mod tests {
); );
assert_eq!( assert_eq!(
groups[1].tools.iter().map(item_id).collect::<Vec<_>>(), groups[1].tools.iter().map(item_id).collect::<Vec<_>>(),
[ ["DC_LINEAR_MENU", "DCALIGNED", "GRID", "DCVISIBILITY", "DCSHOWALL", "DCHIDEALL"]
"DC_LINEAR_MENU", "DCALIGNED", "DCRADIUS", "DCDIAMETER", );
"DCANGULAR", "DCCONVERT", "DCVISIBILITY", "DCSHOWALL", "DCHIDEALL", let RibbonItem::ToolGrid { columns } = &groups[1].tools[2] else {
] panic!("radius, diameter, angular and convert must be a small-button grid");
};
assert_eq!(
columns
.iter()
.map(|column| column.iter().map(|tool| tool.id).collect::<Vec<_>>())
.collect::<Vec<_>>(),
[["DCRADIUS", "DCANGULAR"], ["DCDIAMETER", "DCCONVERT"]]
); );
assert_eq!(groups[2].tools.iter().map(item_id).collect::<Vec<_>>(), ["DELCONSTRAINT", "PARAMETERS"]); assert_eq!(groups[2].tools.iter().map(item_id).collect::<Vec<_>>(), ["DELCONSTRAINT", "PARAMETERS"]);
@@ -218,8 +229,6 @@ mod tests {
assert!(groups[0].tools[1..13] assert!(groups[0].tools[1..13]
.iter() .iter()
.all(|item| matches!(item, RibbonItem::LargeTool(_)))); .all(|item| matches!(item, RibbonItem::LargeTool(_))));
assert!(groups[1].tools[2..6] assert!(matches!(groups[1].tools[1], RibbonItem::LargeTool(_)));
.iter()
.all(|item| matches!(item, RibbonItem::LargeTool(_))));
} }
} }
+89 -11
View File
@@ -353,24 +353,36 @@ pub(crate) fn equal_size_follower(
Some(entity) Some(entity)
} }
/// The first free `<prefix>1`, `<prefix>2`, … name a new constraint takes.
fn next_prefixed_parameter_name(
table: &super::named_parameters::ParameterTable,
prefix: &str,
) -> String {
(1..)
.map(|n| format!("{prefix}{n}"))
.find(|name| !table.contains(name))
.unwrap_or_else(|| format!("{prefix}1"))
}
/// The first free `d1`, `d2`, … name a new dimensional constraint takes. /// The first free `d1`, `d2`, … name a new dimensional constraint takes.
pub(crate) fn next_dimensional_parameter_name( pub(crate) fn next_dimensional_parameter_name(
table: &super::named_parameters::ParameterTable, table: &super::named_parameters::ParameterTable,
) -> String { ) -> String {
(1..) next_prefixed_parameter_name(table, "d")
.map(|n| format!("d{n}"))
.find(|name| !table.contains(name))
.unwrap_or_else(|| "d1".to_string())
} }
/// The first free `ang1`, `ang2`, … name a new angular constraint takes. /// The first free `ang1`, `ang2`, … name a new angular constraint takes.
pub(crate) fn next_angular_parameter_name( pub(crate) fn next_angular_parameter_name(
table: &super::named_parameters::ParameterTable, table: &super::named_parameters::ParameterTable,
) -> String { ) -> String {
(1..) next_prefixed_parameter_name(table, "ang")
.map(|n| format!("ang{n}")) }
.find(|name| !table.contains(name))
.unwrap_or_else(|| "ang1".to_string()) /// The first free `rad1`, `rad2`, … name a new radius constraint takes.
pub(crate) fn next_radial_parameter_name(
table: &super::named_parameters::ParameterTable,
) -> String {
next_prefixed_parameter_name(table, "rad")
} }
/// An angle as the reference displays it: a negative or over-full value is /// An angle as the reference displays it: a negative or over-full value is
@@ -565,6 +577,52 @@ fn dynamic_dimension_follow_points(
true true
} }
/// A circle's or arc's center and radius.
fn radial_geometry(entity: &acadrust::EntityType) -> Option<(Vector3, f64)> {
match entity {
acadrust::EntityType::Circle(circle) => Some((circle.center_wcs(), circle.radius)),
acadrust::EntityType::Arc(arc) => Some((arc.center_wcs(), arc.radius)),
_ => None,
}
}
/// Moves a dynamic radial dimension onto its circle's current center and
/// radius: the dimension line keeps its direction and the text keeps its
/// distance beyond the circle, as in the reference; true when it moved.
fn dynamic_dimension_follow_radius(
dimension: &mut acadrust::entities::Dimension,
center: Vector3,
radius: f64,
) -> bool {
let acadrust::entities::Dimension::Radius(radial) = dimension else {
return false;
};
let to_dvec = |p: Vector3| glam::DVec3::new(p.x, p.y, p.z);
let to_vec = |p: glam::DVec3| Vector3::new(p.x, p.y, p.z);
let old_center = to_dvec(radial.angle_vertex);
let old_radius = (to_dvec(radial.definition_point) - old_center).length();
let center = to_dvec(center);
if (old_center - center).length_squared() < 1.0e-16 && (old_radius - radius).abs() < 1.0e-9 {
return false;
}
let toward_text = to_dvec(radial.base.text_middle_point) - old_center;
let direction = toward_text
.try_normalize()
.or_else(|| (to_dvec(radial.definition_point) - old_center).try_normalize())
.unwrap_or(glam::DVec3::X);
let beyond = toward_text.length() - old_radius;
let chord = center + direction * radius;
let text = center + direction * (radius + beyond);
radial.angle_vertex = to_vec(center);
radial.definition_point = to_vec(chord);
radial.base.definition_point = to_vec(chord);
radial.base.text_middle_point = to_vec(text);
radial.base.insertion_point = to_vec(text);
radial.leader_length = (text - chord).length();
radial.base.actual_measurement = radius;
true
}
/// The end points of a line reference: a line's two ends, or the picked /// The end points of a line reference: a line's two ends, or the picked
/// polyline segment's vertices. /// polyline segment's vertices.
fn line_ends(entity: &acadrust::EntityType, reference: ParametricRef) -> Option<(Vector3, Vector3)> { fn line_ends(entity: &acadrust::EntityType, reference: ParametricRef) -> Option<(Vector3, Vector3)> {
@@ -673,6 +731,11 @@ pub(crate) fn dimensional_anchor_refs(
let Some(&first) = refs.first() else { let Some(&first) = refs.first() else {
return Vec::new(); return Vec::new();
}; };
// A whole circle or arc (a radius or diameter): its center stays and the
// value resizes it.
if refs.len() == 1 && first.marker.is_none() {
return vec![ParametricRef::center(first.entity)];
}
// Three point references: a three-point angle `[first, vertex, second]`. // Three point references: a three-point angle `[first, vertex, second]`.
if refs.len() == 3 if refs.len() == 3
&& refs && refs
@@ -1983,6 +2046,7 @@ impl super::Scene {
String, String,
Option<(Vector3, Vector3)>, Option<(Vector3, Vector3)>,
Option<Vec<Vector3>>, Option<Vec<Vector3>>,
Option<(Vector3, f64)>,
)> = Vec::new(); )> = Vec::new();
// A reference (driven) constraint's parameter follows the geometry. // A reference (driven) constraint's parameter follows the geometry.
let mut followed: Vec<(String, f64)> = Vec::new(); let mut followed: Vec<(String, f64)> = Vec::new();
@@ -2010,7 +2074,15 @@ impl super::Scene {
constraint.kind, constraint.kind,
ConstraintKind::Angle | ConstraintKind::Angle3Point ConstraintKind::Angle | ConstraintKind::Angle3Point
); );
let measured = points.filter(|_| !angular).map(|(first, second)| { // A radius: the circle's or arc's center and radius now.
let radial = (constraint.kind == ConstraintKind::Radius)
.then(|| {
let reference = constraint.refs.first()?;
radial_geometry(self.document.get_entity(reference.entity)?)
})
.flatten();
let measured = radial.map(|(_, radius)| radius);
let measured = measured.or(points.filter(|_| !angular).map(|(first, second)| {
let delta = second - first; let delta = second - first;
let axis = match self.document.get_entity(*dimension) { let axis = match self.document.get_entity(*dimension) {
Some(acadrust::EntityType::Dimension( Some(acadrust::EntityType::Dimension(
@@ -2026,7 +2098,7 @@ impl super::Scene {
} }
_ => delta.length(), _ => delta.length(),
} }
}); }));
let (name, value, source) = match &constraint.driving_param { let (name, value, source) = match &constraint.driving_param {
Some(DrivingValue::Named(name)) => { Some(DrivingValue::Named(name)) => {
let value = match (reference, measured) { let value = match (reference, measured) {
@@ -2070,6 +2142,7 @@ impl super::Scene {
), ),
points, points,
angle_points, angle_points,
radial,
)); ));
} }
} }
@@ -2079,7 +2152,7 @@ impl super::Scene {
let _ = self.named_parameters.set(&name, &source); let _ = self.named_parameters.set(&name, &source);
} }
} }
for (handle, text, points, angle_points) in updates { for (handle, text, points, angle_points, radial) in updates {
let Some(acadrust::EntityType::Dimension(mut dimension)) = let Some(acadrust::EntityType::Dimension(mut dimension)) =
self.document.get_entity(handle).cloned() self.document.get_entity(handle).cloned()
else { else {
@@ -2096,6 +2169,11 @@ impl super::Scene {
changed = true; changed = true;
} }
} }
if let Some((center, radius)) = radial {
if dynamic_dimension_follow_radius(&mut dimension, center, radius) {
changed = true;
}
}
if dimension.base().user_text.as_deref() != Some(text.as_str()) { if dimension.base().user_text.as_deref() != Some(text.as_str()) {
crate::entities::dimension::set_dimension_text_override( crate::entities::dimension::set_dimension_text_override(
dimension.base_mut(), dimension.base_mut(),
+8 -7
View File
@@ -996,14 +996,15 @@ fn resolved_target(params: &ParameterTable, constraint: &ParametricConstraint) -
// A dimensional distance keeps its sign in the parameter but measures // A dimensional distance keeps its sign in the parameter but measures
// its magnitude, as the reference does (`d1=-50` shortens the line to // its magnitude, as the reference does (`d1=-50` shortens the line to
// 50; `0` folds the points together). // 50; `0` folds the points together).
if constraint.kind == ConstraintKind::Distance { // A radius or diameter likewise (`rad1=-20` is a radius of 20); zero is
return Some(value.abs()); // no circle at all.
match constraint.kind {
ConstraintKind::Distance => Some(value.abs()),
ConstraintKind::Radius | ConstraintKind::Diameter => {
(value != 0.0).then(|| value.abs())
}
_ => Some(value),
} }
let must_be_positive = matches!(
constraint.kind,
ConstraintKind::Radius | ConstraintKind::Diameter
);
(!must_be_positive || value > 0.0).then_some(value)
} }
fn oriented_distance(target: f64, current_projection: f64) -> f64 { fn oriented_distance(target: f64, current_projection: f64) -> f64 {
+30 -6
View File
@@ -480,6 +480,30 @@ pub(super) fn make_tip(tip: String) -> Element<'static, Message> {
text(tip).size(11).into() text(tip).size(11).into()
} }
/// The sentence the reference shows under a tool's name, where known.
fn tool_description(id: &str) -> Option<&'static str> {
Some(match id {
"DCLINEAR" => "Constrains the horizontal or vertical distance between points",
"DCHORIZONTAL" => "Constrains the X-distance between points on an object, or between two points on different objects",
"DCVERTICAL" => "Constrains the Y-distance between points on an object, or between two points on different objects",
"DCALIGNED" => "Constrains the distance between two points on an object or between two points on different objects",
"DCRADIUS" => "Constrains the radius of a circle or arc",
"DCDIAMETER" => "Constrains the diameter of a circle or arc",
"DCANGULAR" => "Constrains the angle between line or polyline segments, the angle swept out by an arc or a polyline arc segment, or the angle between three points on objects",
"DCCONVERT" => "Convert dimensions to dimensional constraints",
_ => return None,
})
}
/// A tool's tooltip: its name, its description when one is known, and the
/// command it runs.
fn tool_tip_text(label: &str, id: &str) -> String {
match tool_description(id) {
Some(description) => format!("{label}\n{}\n{} {id}", t!(description), t!("Command:")),
None => format!("{label}\n{} {id}", t!("Command:")),
}
}
pub(super) fn tip_style(theme: &Theme) -> container::Style { pub(super) fn tip_style(theme: &Theme) -> container::Style {
let palette = theme.palette(); let palette = theme.palette();
container::Style { container::Style {
@@ -511,7 +535,7 @@ pub(super) fn render_small<'a>(
let active = is_active_tool(t.id, active_tool, &state); let active = is_active_tool(t.id, active_tool, &state);
let event = t.event.clone(); let event = t.event.clone();
let tool_id = t.id.to_string(); let tool_id = t.id.to_string();
let tip_text = format!("{}\n{} {}", t!(t.label), t!("Command:"), t.id); let tip_text = tool_tip_text(&t!(t.label), t.id);
let btn = button(make_icon(t.icon, SMALL_ICON)) let btn = button(make_icon(t.icon, SMALL_ICON))
.on_press(Message::RibbonToolClick { tool_id, event }) .on_press(Message::RibbonToolClick { tool_id, event })
.style(move |theme: &Theme, status| tool_btn_style(theme, active, status)) .style(move |theme: &Theme, status| tool_btn_style(theme, active, status))
@@ -530,7 +554,7 @@ pub(super) fn render_small<'a>(
let event = t.event.clone(); let event = t.event.clone();
let tool_id = t.id.to_string(); let tool_id = t.id.to_string();
let label = t!(t.label).into_owned(); let label = t!(t.label).into_owned();
let tip_text = format!("{}\n{} {}", label, t!("Command:"), t.id); let tip_text = tool_tip_text(&label, t.id);
let content = row![ let content = row![
container(make_icon(t.icon, SMALL_ICON)).width(Length::Fixed(SMALL_W)), container(make_icon(t.icon, SMALL_ICON)).width(Length::Fixed(SMALL_W)),
text(label).size(10).wrapping(advanced_text::Wrapping::None), text(label).size(10).wrapping(advanced_text::Wrapping::None),
@@ -588,7 +612,7 @@ pub(super) fn render_small<'a>(
}) })
.or_else(|| items.first().map(|(_, lbl, _)| *lbl)) .or_else(|| items.first().map(|(_, lbl, _)| *lbl))
.unwrap_or(*id); .unwrap_or(*id);
let tip_text = format!("{}\n{} {}", t!(cur_label), t!("Command:"), last); let tip_text = tool_tip_text(&t!(cur_label), last);
let icon_btn = button(make_icon(cur_icon, SMALL_ICON)) let icon_btn = button(make_icon(cur_icon, SMALL_ICON))
.on_press(Message::RibbonToolClick { .on_press(Message::RibbonToolClick {
@@ -658,7 +682,7 @@ pub(super) fn render_small<'a>(
.on_press(Message::ToggleRibbonDropdown(id.to_string())) .on_press(Message::ToggleRibbonDropdown(id.to_string()))
.style(move |theme: &Theme, status| tool_btn_style(theme, dd_open, status)) .style(move |theme: &Theme, status| tool_btn_style(theme, dd_open, status))
.width(Length::Fixed(ARROW_W)).height(ROW_H).padding(0); .width(Length::Fixed(ARROW_W)).height(ROW_H).padding(0);
let face_tip = format!("{}\n{} {}", localized_label, t!("Command:"), last); let face_tip = tool_tip_text(&localized_label, last);
let arrow_tip = format!("{} {}", localized_label, t!("options")); let arrow_tip = format!("{} {}", localized_label, t!("options"));
PosReport::new(*id, row![ PosReport::new(*id, row![
tooltip(face_btn, make_tip(face_tip), TipPos::Right) tooltip(face_btn, make_tip(face_tip), TipPos::Right)
@@ -727,7 +751,7 @@ pub(super) fn render_large_dropdown<'a>(
.or_else(|| items.first().map(|(_, lbl, _)| *lbl)) .or_else(|| items.first().map(|(_, lbl, _)| *lbl))
.unwrap_or(id); .unwrap_or(id);
let label = t!(explicit_label.unwrap_or(cur_label)).into_owned(); let label = t!(explicit_label.unwrap_or(cur_label)).into_owned();
let tip_text = format!("{}\n{} {}", t!(cur_label), t!("Command:"), last); let tip_text = tool_tip_text(&t!(cur_label), last);
let arr_tip = format!("{} {}", label, t!("options")); let arr_tip = format!("{} {}", label, t!("options"));
// The label owns the bottom of the face. The icon's Fill container centers // The label owns the bottom of the face. The icon's Fill container centers
@@ -864,7 +888,7 @@ pub(super) fn render_large<'a>(
let event = t.event.clone(); let event = t.event.clone();
let tool_id = t.id.to_string(); let tool_id = t.id.to_string();
let label = t!(t.label).into_owned(); let label = t!(t.label).into_owned();
let tip_text = format!("{}\n{} {}", label, t!("Command:"), t.id); let tip_text = tool_tip_text(&label, t.id);
let btn = button( let btn = button(
column![ column![
container(make_icon(t.icon, LARGE_ICON)) container(make_icon(t.icon, LARGE_ICON))