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 <[email protected]>
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);
}
}
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 {
first_line,
first_ends,
@@ -5886,6 +6052,16 @@ impl OpenCADStudio {
CmdResult::Dispatch(cmd) => {
// End this interactive front-end, then run the assembled command
// 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].snap_result = None;
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));
}
"DCONSTRAINT" | "DCRADIUS" | "DCDIAMETER" => {
if cmd == "DCRADIUS" {
self.dim_constraint_last = "Radius";
} else if cmd == "DCDIAMETER" {
"DCRADIUS" => {
use crate::modules::parametric::{DimConstraintAxis, DimConstraintCommand};
use crate::scene::parametric_constraints::next_radial_parameter_name;
// 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";
}
let handles = self.tabs[i].scene.selected_handles_in_order();
+10
View File
@@ -2368,6 +2368,9 @@ impl OpenCADStudio {
acadrust::EntityType::Dimension(
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_),
) => t!("Angular Dimensional Constraint"),
acadrust::EntityType::Dimension(Dimension::Radius(_)) => {
t!("Radial Dimension (Dynamic)")
}
_ => t!("Linear Dimensional Constraint"),
}
.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) {
// A dynamic dimension shows the reference's grips: a
// triangle at each constraint point aimed at the other
+9
View File
@@ -1650,6 +1650,15 @@ pub enum CmdResult {
expression: String,
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
/// ends stay put), then hands the second line's solved ends back through
/// `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.
let text = {
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)
} else {
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
// the text, unless DIMTOFL asks for the inside line as well.
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 {
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)
}
/// 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
/// radius, or whichever end of a diameter's chord is nearer the text.
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) {
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
// hook, a gap, then the text, all running away from the arc.
// 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,
world: DVec3,
) -> 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,
self.text_override.clone(),
);
if let Some(angle) = self.text_angle {
dim.base.text_rotation = angle;
}
plane.place_entity(EntityType::Dimension(Dimension::Radius(dim)))
radial_dimension_entity(source, world, self.text_override.clone(), self.text_angle)
}
}
/// 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 {
fn set_working_plane(&mut self, _plane: WorkingPlane) {
}
+103 -23
View File
@@ -16,6 +16,8 @@ pub enum DimConstraintAxis {
Aligned,
/// The angle between two lines, or at a vertex between two points.
Angular,
/// The radius of a circle or arc.
Radius,
}
/// A picked line (a line entity or one polyline segment) an Aligned
@@ -106,6 +108,16 @@ enum Step {
sector: u8,
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>:`
Value {
first: ParametricRef,
@@ -155,14 +167,16 @@ impl DimConstraintCommand {
"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 const INVALID_RADIAL: &'static str =
"Invalid selection for Radial. Select a circle or arc.";
pub fn new(axis: DimConstraintAxis, default_name: String) -> Self {
Self {
axis,
step: if axis == DimConstraintAxis::Angular {
Step::AngularFirst
} else {
Step::First
step: match axis {
DimConstraintAxis::Angular => Step::AngularFirst,
DimConstraintAxis::Radius => Step::RadialSelect,
_ => Step::First,
},
picked_entity: None,
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 {
match self.axis {
DimConstraintAxis::Linear => "DCLINEAR",
@@ -311,6 +344,7 @@ impl DimConstraintCommand {
DimConstraintAxis::Vertical => "DCVERTICAL",
DimConstraintAxis::Aligned => "DCALIGNED",
DimConstraintAxis::Angular => "DCANGULAR",
DimConstraintAxis::Radius => "DCRADIUS",
}
}
@@ -321,6 +355,7 @@ impl DimConstraintCommand {
DimConstraintAxis::Vertical => "Vertical",
DimConstraintAxis::Aligned => "Aligned",
DimConstraintAxis::Angular => "Angular",
DimConstraintAxis::Radius => "Radial",
}
}
@@ -474,8 +509,8 @@ impl DimConstraintCommand {
}
DimConstraintAxis::Horizontal => (ConstraintKind::DistanceX, DVec3::X),
DimConstraintAxis::Vertical => (ConstraintKind::DistanceY, DVec3::Y),
// Angular never measures here; its own steps decide the sector.
DimConstraintAxis::Aligned | DimConstraintAxis::Angular => (
// Angular and Radius never measure here; their own steps decide.
DimConstraintAxis::Aligned | DimConstraintAxis::Angular | DimConstraintAxis::Radius => (
ConstraintKind::Distance,
(second - first).normalize_or(DVec3::X),
),
@@ -525,7 +560,9 @@ impl DimConstraintCommand {
DimConstraintAxis::Linear => "Linear constraint",
DimConstraintAxis::Horizontal => "Horizontal 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 { .. } => {
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:")
}
Step::AngularValue { measured, .. } => format!(
"{name} Enter value or name and value <{}={}>:",
self.default_name,
measured_expression(measured)
),
Step::AngularValue { measured, .. } | Step::RadialValue { radius: 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,
@@ -609,6 +649,7 @@ impl CadCommand for DimConstraintCommand {
| Step::Value { .. }
| Step::AngularFirst
| Step::AngularValue { .. }
| Step::RadialValue { .. }
)
}
@@ -643,16 +684,16 @@ impl CadCommand for DimConstraintCommand {
}
None
}
Step::Value { .. } | Step::AngularValue { .. } => {
Step::Value { .. } | Step::AngularValue { .. } | Step::RadialValue { .. } => {
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()),
};
if matches!(self.step, Step::AngularValue { .. }) {
self.build_angular(name, expression)
} else {
self.build(name, expression)
match self.step {
Step::AngularValue { .. } => self.build_angular(name, expression),
Step::RadialValue { .. } => self.build_radial(name, expression),
_ => self.build(name, expression),
}
}
_ => None,
@@ -676,6 +717,7 @@ impl CadCommand for DimConstraintCommand {
| Step::AngularVertex
| Step::AngularPoint1 { .. }
| Step::AngularPoint2 { .. }
| Step::RadialSelect
)
}
@@ -867,10 +909,35 @@ impl CadCommand for DimConstraintCommand {
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::Value { .. }
| 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::TwoLinesSecond { .. }
| 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 } => {
let frame = match data {
AngularData::Lines { first, second } => Self::angle_frame(
@@ -1050,9 +1126,10 @@ impl CadCommand for DimConstraintCommand {
};
CmdResult::ReportMeasurement(format!("Dimension text = {measured:.4}"))
}
Step::TwoLinesParallel { .. } | Step::Value { .. } | Step::AngularValue { .. } => {
CmdResult::NeedPoint
}
Step::TwoLinesParallel { .. }
| Step::Value { .. }
| Step::AngularValue { .. }
| Step::RadialValue { .. } => CmdResult::NeedPoint,
}
}
@@ -1070,6 +1147,9 @@ impl CadCommand for DimConstraintCommand {
Step::AngularValue { measured, .. } => self
.build_angular(self.default_name.clone(), measured_expression(measured))
.unwrap_or(CmdResult::Cancel),
Step::RadialValue { radius, .. } => self
.build_radial(self.default_name.clone(), measured_expression(radius))
.unwrap_or(CmdResult::Cancel),
Step::PointLinePoint => {
self.step = Step::LineFirst;
CmdResult::NeedPoint
+20 -11
View File
@@ -123,10 +123,14 @@ impl CadModule for ParametricModule {
default: "DCLINEAR",
},
RibbonItem::LargeTool(dimensional_tools::aligned()),
RibbonItem::LargeTool(dimensional_tools::radius()),
RibbonItem::LargeTool(dimensional_tools::diameter()),
RibbonItem::LargeTool(dimensional_tools::angular()),
RibbonItem::LargeTool(dimensional_tools::convert()),
// Two rows of small icon-only buttons, as in the
// reference: Radius, Diameter / Angular, Convert.
RibbonItem::ToolGrid {
columns: vec![
vec![dimensional_tools::radius(), dimensional_tools::angular()],
vec![dimensional_tools::diameter(), dimensional_tools::convert()],
],
},
RibbonItem::LabeledDropdown {
id: "DCVISIBILITY", label: "Show/Hide",
icon: IconKind::Svg(include_bytes!("../../../assets/icons/constrain/show.svg")),
@@ -200,10 +204,17 @@ mod tests {
);
assert_eq!(
groups[1].tools.iter().map(item_id).collect::<Vec<_>>(),
[
"DC_LINEAR_MENU", "DCALIGNED", "DCRADIUS", "DCDIAMETER",
"DCANGULAR", "DCCONVERT", "DCVISIBILITY", "DCSHOWALL", "DCHIDEALL",
]
["DC_LINEAR_MENU", "DCALIGNED", "GRID", "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"]);
@@ -218,8 +229,6 @@ mod tests {
assert!(groups[0].tools[1..13]
.iter()
.all(|item| matches!(item, RibbonItem::LargeTool(_))));
assert!(groups[1].tools[2..6]
.iter()
.all(|item| matches!(item, RibbonItem::LargeTool(_))));
assert!(matches!(groups[1].tools[1], RibbonItem::LargeTool(_)));
}
}
+89 -11
View File
@@ -353,24 +353,36 @@ pub(crate) fn equal_size_follower(
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.
pub(crate) fn next_dimensional_parameter_name(
table: &super::named_parameters::ParameterTable,
) -> String {
(1..)
.map(|n| format!("d{n}"))
.find(|name| !table.contains(name))
.unwrap_or_else(|| "d1".to_string())
next_prefixed_parameter_name(table, "d")
}
/// 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())
next_prefixed_parameter_name(table, "ang")
}
/// 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
@@ -565,6 +577,52 @@ fn dynamic_dimension_follow_points(
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
/// polyline segment's vertices.
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 {
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]`.
if refs.len() == 3
&& refs
@@ -1983,6 +2046,7 @@ impl super::Scene {
String,
Option<(Vector3, Vector3)>,
Option<Vec<Vector3>>,
Option<(Vector3, f64)>,
)> = Vec::new();
// A reference (driven) constraint's parameter follows the geometry.
let mut followed: Vec<(String, f64)> = Vec::new();
@@ -2010,7 +2074,15 @@ impl super::Scene {
constraint.kind,
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 axis = match self.document.get_entity(*dimension) {
Some(acadrust::EntityType::Dimension(
@@ -2026,7 +2098,7 @@ impl super::Scene {
}
_ => delta.length(),
}
});
}));
let (name, value, source) = match &constraint.driving_param {
Some(DrivingValue::Named(name)) => {
let value = match (reference, measured) {
@@ -2070,6 +2142,7 @@ impl super::Scene {
),
points,
angle_points,
radial,
));
}
}
@@ -2079,7 +2152,7 @@ impl super::Scene {
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)) =
self.document.get_entity(handle).cloned()
else {
@@ -2096,6 +2169,11 @@ impl super::Scene {
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()) {
crate::entities::dimension::set_dimension_text_override(
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
// its magnitude, as the reference does (`d1=-50` shortens the line to
// 50; `0` folds the points together).
if constraint.kind == ConstraintKind::Distance {
return Some(value.abs());
// A radius or diameter likewise (`rad1=-20` is a radius of 20); zero is
// 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 {
+30 -6
View File
@@ -480,6 +480,30 @@ pub(super) fn make_tip(tip: String) -> Element<'static, Message> {
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 {
let palette = theme.palette();
container::Style {
@@ -511,7 +535,7 @@ pub(super) fn render_small<'a>(
let active = is_active_tool(t.id, active_tool, &state);
let event = t.event.clone();
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))
.on_press(Message::RibbonToolClick { tool_id, event })
.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 tool_id = t.id.to_string();
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![
container(make_icon(t.icon, SMALL_ICON)).width(Length::Fixed(SMALL_W)),
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))
.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))
.on_press(Message::RibbonToolClick {
@@ -658,7 +682,7 @@ pub(super) fn render_small<'a>(
.on_press(Message::ToggleRibbonDropdown(id.to_string()))
.style(move |theme: &Theme, status| tool_btn_style(theme, dd_open, status))
.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"));
PosReport::new(*id, row![
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))
.unwrap_or(id);
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"));
// 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 tool_id = t.id.to_string();
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(
column![
container(make_icon(t.icon, LARGE_ICON))