From 19204d1b2cc984064b28897daf0945ce0f987981 Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Tue, 22 Sep 2026 15:21:11 +0300 Subject: [PATCH 1/2] DCDIAMETER/DCRADIUS: the reference flow with a real radial dimension The commands ask for the circle or arc themselves, report the measured size, take a dimension line location and then the value or name and value, creating a radius or diameter dimension as the constraint's dynamic dimension with a radN/diaN parameter, refusing a second radial constraint on the same object and any non-positive value. Co-Authored-By: Claude Opus 5 --- src/app/command_driver.rs | 171 +++++++++++++++++++++++ src/app/commands/draw.rs | 32 +++-- src/app/properties.rs | 6 + src/command.rs | 16 +++ src/modules/annotate/diameter_dim.rs | 19 +++ src/modules/annotate/radius_dim.rs | 25 ++++ src/modules/parametric/dim_constraint.rs | 152 ++++++++++++++++++-- src/scene/parametric_constraints.rs | 134 +++++++++++++++++- 8 files changed, 531 insertions(+), 24 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index b5bd1a2d0..64e868352 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -4246,6 +4246,177 @@ impl OpenCADStudio { self.command_line.push_error(&message); return self.apply_cmd_result(CmdResult::Cancel); } + CmdResult::AddRadialConstraint { + circle, + center, + radius, + location, + diameter, + name, + expression, + renamed, + } => { + use crate::modules::annotate::{diameter_dim, radius_dim}; + use crate::scene::named_parameters::{is_valid_name, DrivingValue}; + use crate::scene::parametric_constraints::{ + dynamic_dimension_text, ConstraintKind, ParametricRef, + }; + + let scope = self.tabs[i].current_parametric_scope(); + let kind = if diameter { + ConstraintKind::Diameter + } else { + ConstraintKind::Radius + }; + let refs = vec![circle]; + // A circle already carrying a radius or diameter constraint + // is refused after the value, as the reference does. + let duplicate = self.tabs[i] + .scene + .parametric_constraint_set(scope) + .is_some_and(|set| { + set.constraints.iter().any(|c| { + c.enabled + && matches!( + c.kind, + ConstraintKind::Radius | ConstraintKind::Diameter + ) + && c.refs.iter().any(|r| r.entity == circle.entity) + }) + }); + 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 circle has no zero or negative size: the value is + // refused and the prompt comes back. + 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 entity = if diameter { + diameter_dim::diameter_constraint_entity(center, radius, location, Some(text)) + } else { + radius_dim::radius_constraint_entity(center, radius, 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) + }); + // The centre stays where it is; the value moves the rim. + let anchors = vec![ParametricRef::center(circle.entity)]; + let pending = self.begin_undo( + i, + if diameter { + "Diameter constraint" + } else { + "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)]); + let changes = vec![(circle.entity, crate::scene::ChangeKind::Modified)]; + 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::AddAngularConstraint { refs, points, diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 26344dbad..a51fdbdfd 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -1891,12 +1891,30 @@ 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" { - self.dim_constraint_last = "Diameter"; - } + "DCRADIUS" | "DCDIAMETER" => { + use crate::modules::parametric::{DimConstraintAxis, DimConstraintCommand}; + use crate::scene::parametric_constraints::next_radial_parameter_name; + let diameter = cmd == "DCDIAMETER"; + self.dim_constraint_last = if diameter { "Diameter" } else { "Radius" }; + // The reference asks for the circle itself, whatever is + // selected, so a pick-first set is left alone. + let name = next_radial_parameter_name( + self.tabs[i].scene.named_parameters(), + diameter, + ); + let command = DimConstraintCommand::new( + if diameter { + DimConstraintAxis::Diameter + } else { + DimConstraintAxis::Radius + }, + name, + ); + self.command_line.push_info(&command.prompt()); + self.tabs[i].active_cmd = Some(Box::new(command)); + } + + "DCONSTRAINT" => { let handles = self.tabs[i].scene.selected_handles_in_order(); if handles.is_empty() { use crate::modules::draw::select::SelectObjectsCommand; @@ -1914,8 +1932,6 @@ impl OpenCADStudio { "DCHORIZONTAL" => ("DCHORIZONTAL", DistanceMode::X), "DCVERTICAL" => ("DCVERTICAL", DistanceMode::Y), "DCALIGNED" => ("DCALIGNED", DistanceMode::Aligned), - "DCRADIUS" => ("DCRADIUS", DistanceMode::Radius), - "DCDIAMETER" => ("DCDIAMETER", DistanceMode::Diameter), _ => ("DCONSTRAINT", DistanceMode::Auto), }; match DistanceConstraintCommand::with_mode( diff --git a/src/app/properties.rs b/src/app/properties.rs index fd29ab28b..6f2938ea7 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -2368,6 +2368,12 @@ impl OpenCADStudio { acadrust::EntityType::Dimension( Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_), ) => t!("Angular Dimensional Constraint"), + acadrust::EntityType::Dimension(Dimension::Radius(_)) => { + t!("Radius Dimensional Constraint") + } + acadrust::EntityType::Dimension(Dimension::Diameter(_)) => { + t!("Diameter Dimensional Constraint") + } _ => t!("Linear Dimensional Constraint"), } .into_owned() diff --git a/src/command.rs b/src/command.rs index 199017a5b..bc2cc4d61 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1635,6 +1635,22 @@ pub enum CmdResult { renamed: bool, label: &'static str, }, + /// Adds a radius or diameter constraint with its dynamic radial + /// dimension and the `radN`/`diaN` parameter that drives it. + AddRadialConstraint { + circle: crate::scene::parametric_constraints::ParametricRef, + center: DVec3, + radius: f64, + /// Where the dimension line was picked; its direction from the + /// centre places the dimension. + location: DVec3, + diameter: bool, + name: String, + expression: String, + /// The user named the parameter (`name=expression`); an existing + /// name is then refused. + renamed: bool, + }, /// 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 diff --git a/src/modules/annotate/diameter_dim.rs b/src/modules/annotate/diameter_dim.rs index ef9255aae..6db2470fd 100644 --- a/src/modules/annotate/diameter_dim.rs +++ b/src/modules/annotate/diameter_dim.rs @@ -87,6 +87,25 @@ impl DiameterDimensionCommand { } } +/// A diameter dimension for a dimensional constraint: the dimension line +/// runs through the centre towards `location`, and the style places the +/// text. +pub(crate) fn diameter_constraint_entity( + center: DVec3, + radius: f64, + location: DVec3, + text: Option, +) -> Option { + let direction = super::radius_dim::radial_direction(center, location)?; + let chord = center + direction * radius; + let far_chord = center - direction * radius; + let mut dim = DimensionDiameter::new(v3(chord), v3(far_chord)); + dim.leader_length = chord.distance(location).max(radius * 0.5); + dim.base.actual_measurement = dim.measurement(); + crate::entities::dimension::set_dimension_text_override(&mut dim.base, text); + Some(EntityType::Dimension(Dimension::Diameter(dim))) +} + impl CadCommand for DiameterDimensionCommand { fn set_working_plane(&mut self, _plane: WorkingPlane) { } diff --git a/src/modules/annotate/radius_dim.rs b/src/modules/annotate/radius_dim.rs index e595aef72..8dfbd9fef 100644 --- a/src/modules/annotate/radius_dim.rs +++ b/src/modules/annotate/radius_dim.rs @@ -88,6 +88,31 @@ impl RadiusDimensionCommand { } } +/// A radius dimension for a dimensional constraint: the leader leaves the +/// centre towards `location`, and the style places the text. +pub(crate) fn radius_constraint_entity( + center: DVec3, + radius: f64, + location: DVec3, + text: Option, +) -> Option { + let direction = radial_direction(center, location)?; + let chord = center + direction * radius; + let mut dim = DimensionRadius::new(v3(center), v3(chord)); + dim.base.definition_point = v3(chord); + dim.leader_length = chord.distance(location).max(radius * 0.5); + dim.base.actual_measurement = dim.measurement(); + crate::entities::dimension::set_dimension_text_override(&mut dim.base, text); + Some(EntityType::Dimension(Dimension::Radius(dim))) +} + +/// The unit direction a radial constraint's dimension points in, from the +/// centre to where its dimension line was picked. +pub(crate) fn radial_direction(center: DVec3, location: DVec3) -> Option { + let delta = location - center; + (delta.length() > 1.0e-9).then(|| delta.normalize()) +} + impl CadCommand for RadiusDimensionCommand { fn set_working_plane(&mut self, _plane: WorkingPlane) { } diff --git a/src/modules/parametric/dim_constraint.rs b/src/modules/parametric/dim_constraint.rs index 63fc1df6b..548c4cc96 100644 --- a/src/modules/parametric/dim_constraint.rs +++ b/src/modules/parametric/dim_constraint.rs @@ -16,6 +16,10 @@ pub enum DimConstraintAxis { Aligned, /// The angle between two lines, or at a vertex between two points. Angular, + /// A circle's or arc's radius. + Radius, + /// A circle's or arc's diameter. + Diameter, } /// A picked line (a line entity or one polyline segment) an Aligned @@ -106,6 +110,16 @@ enum Step { sector: u8, measured: f64, }, + /// Radius/Diameter: `Select arc or circle:` + RadialObject, + /// `Specify dimension line location:` of a radius or diameter. + RadialLocation { target: RadialTarget }, + /// `Enter value or name and value :` + RadialValue { + target: RadialTarget, + location: DVec3, + measured: f64, + }, /// `Enter value or name and value :` Value { first: ParametricRef, @@ -135,6 +149,14 @@ enum AngularData { }, } +/// The circle or arc a Radius/Diameter constraint measures. +#[derive(Clone, Copy)] +struct RadialTarget { + circle: ParametricRef, + center: DVec3, + radius: f64, +} + /// 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. @@ -159,10 +181,10 @@ impl DimConstraintCommand { 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 | DimConstraintAxis::Diameter => Step::RadialObject, + _ => Step::First, }, picked_entity: None, default_name, @@ -311,6 +333,8 @@ impl DimConstraintCommand { DimConstraintAxis::Vertical => "DCVERTICAL", DimConstraintAxis::Aligned => "DCALIGNED", DimConstraintAxis::Angular => "DCANGULAR", + DimConstraintAxis::Radius => "DCRADIUS", + DimConstraintAxis::Diameter => "DCDIAMETER", } } @@ -321,9 +345,47 @@ impl DimConstraintCommand { DimConstraintAxis::Vertical => "Vertical", DimConstraintAxis::Aligned => "Aligned", DimConstraintAxis::Angular => "Angular", + DimConstraintAxis::Radius => "Radius", + DimConstraintAxis::Diameter => "Diameter", } } + /// The circle or arc a radial pick landed on, with its centre and radius. + fn radial_target(entity: &EntityType, handle: Handle) -> Option { + let (center, radius) = match entity { + EntityType::Circle(circle) => (circle.center, circle.radius), + EntityType::Arc(arc) => (arc.center, arc.radius), + _ => return None, + }; + (radius > 1.0e-12).then_some(RadialTarget { + circle: ParametricRef::whole(handle), + center: DVec3::new(center.x, center.y, center.z), + radius, + }) + } + + fn build_radial(&self, name: String, expression: String) -> Option { + let Step::RadialValue { + target, location, .. + } = self.step + else { + return None; + }; + if expression.trim().is_empty() { + return None; + } + Some(CmdResult::AddRadialConstraint { + circle: target.circle, + center: target.center, + radius: target.radius, + location, + diameter: self.axis == DimConstraintAxis::Diameter, + renamed: name != self.default_name, + name, + expression, + }) + } + fn report(message: &str) -> CmdResult { CmdResult::ReportError(message.to_string()) } @@ -475,7 +537,10 @@ 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 => ( + DimConstraintAxis::Aligned + | DimConstraintAxis::Angular + | DimConstraintAxis::Radius + | DimConstraintAxis::Diameter => ( ConstraintKind::Distance, (second - first).normalize_or(DVec3::X), ), @@ -525,7 +590,10 @@ 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 + | DimConstraintAxis::Diameter => "Aligned constraint", }, }) } @@ -567,9 +635,17 @@ impl CadCommand for DimConstraintCommand { Step::AngularPoint2 { .. } => { format!("{name} Specify second angle constraint point:") } - Step::AngularLocation { .. } | Step::Location { .. } => { + Step::RadialObject => format!("{name} Select arc or circle:"), + Step::AngularLocation { .. } + | Step::Location { .. } + | Step::RadialLocation { .. } => { format!("{name} Specify dimension line location:") } + Step::RadialValue { measured, .. } => format!( + "{name} Enter value or name and value <{}={}>:", + self.default_name, + measured_expression(measured) + ), Step::AngularValue { measured, .. } => format!( "{name} Enter value or name and value <{}={}>:", self.default_name, @@ -609,6 +685,7 @@ impl CadCommand for DimConstraintCommand { | Step::Value { .. } | Step::AngularFirst | Step::AngularValue { .. } + | Step::RadialValue { .. } ) } @@ -643,16 +720,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 +753,7 @@ impl CadCommand for DimConstraintCommand { | Step::AngularVertex | Step::AngularPoint1 { .. } | Step::AngularPoint2 { .. } + | Step::RadialObject ) } @@ -867,10 +945,38 @@ impl CadCommand for DimConstraintCommand { whole_curve: false, }) } + Step::RadialObject => { + if handle.is_null() { + return Self::report(Self::NO_OBJECT); + } + let Some(entity) = self.picked_entity.take() else { + return CmdResult::NeedPoint; + }; + let Some(target) = Self::radial_target(&entity, handle) else { + return Self::report(&format!( + "Invalid selection for {}. Select a circle or arc.", + self.noun() + )); + }; + let measured = if self.axis == DimConstraintAxis::Diameter { + target.radius * 2.0 + } else { + target.radius + }; + self.step = Step::RadialLocation { target }; + // The measured size is reported before the location, as the + // reference prints it, with trailing zeros suppressed. + CmdResult::ReportMeasurement(format!( + "Dimension text = {}", + measured_expression(measured) + )) + } Step::Location { .. } | Step::Value { .. } | Step::AngularLocation { .. } - | Step::AngularValue { .. } => self.on_point(point), + | Step::AngularValue { .. } + | Step::RadialLocation { .. } + | Step::RadialValue { .. } => self.on_point(point), } } @@ -1050,9 +1156,24 @@ impl CadCommand for DimConstraintCommand { }; CmdResult::ReportMeasurement(format!("Dimension text = {measured:.4}")) } - Step::TwoLinesParallel { .. } | Step::Value { .. } | Step::AngularValue { .. } => { + Step::RadialObject => Self::report(Self::NO_OBJECT), + Step::RadialLocation { target } => { + let measured = if self.axis == DimConstraintAxis::Diameter { + target.radius * 2.0 + } else { + target.radius + }; + self.step = Step::RadialValue { + target, + location: point, + measured, + }; CmdResult::NeedPoint } + Step::TwoLinesParallel { .. } + | Step::Value { .. } + | Step::AngularValue { .. } + | Step::RadialValue { .. } => CmdResult::NeedPoint, } } @@ -1077,6 +1198,9 @@ impl CadCommand for DimConstraintCommand { Step::Value { measured, .. } => self .build(self.default_name.clone(), measured_expression(measured)) .unwrap_or(CmdResult::Cancel), + Step::RadialValue { measured, .. } => self + .build_radial(self.default_name.clone(), measured_expression(measured)) + .unwrap_or(CmdResult::Cancel), _ => CmdResult::Cancel, } } diff --git a/src/scene/parametric_constraints.rs b/src/scene/parametric_constraints.rs index 0b8cbe752..21069764b 100644 --- a/src/scene/parametric_constraints.rs +++ b/src/scene/parametric_constraints.rs @@ -363,6 +363,19 @@ pub(crate) fn next_dimensional_parameter_name( .unwrap_or_else(|| "d1".to_string()) } +/// The first free `rad1`/`dia1`, `rad2`/`dia2`, … name a new radius or +/// diameter constraint takes. +pub(crate) fn next_radial_parameter_name( + table: &super::named_parameters::ParameterTable, + diameter: bool, +) -> String { + let prefix = if diameter { "dia" } else { "rad" }; + (1..) + .map(|n| format!("{prefix}{n}")) + .find(|name| !table.contains(name)) + .unwrap_or_else(|| format!("{prefix}1")) +} + /// The first free `ang1`, `ang2`, … name a new angular constraint takes. pub(crate) fn next_angular_parameter_name( table: &super::named_parameters::ParameterTable, @@ -601,6 +614,99 @@ fn angular_follow_points( } } +/// A radial constraint's circle or arc: its centre and radius now. +fn radial_geometry( + document: &acadrust::CadDocument, + constraint: &ParametricConstraint, +) -> Option<(glam::DVec3, f64)> { + let reference = constraint.refs.first()?; + let (center, radius) = match document.get_entity(reference.entity)? { + acadrust::EntityType::Circle(circle) => (circle.center, circle.radius), + acadrust::EntityType::Arc(arc) => (arc.center, arc.radius), + _ => return None, + }; + Some((glam::DVec3::new(center.x, center.y, center.z), radius)) +} + +/// Rebuilds a dynamic radial dimension on its circle's current centre and +/// radius, keeping the direction its dimension line was placed in; true +/// when anything moved. +fn dynamic_dimension_follow_radial( + dimension: &mut acadrust::entities::Dimension, + center: glam::DVec3, + radius: f64, +) -> bool { + use acadrust::entities::Dimension; + let to_dvec = |p: Vector3| glam::DVec3::new(p.x, p.y, p.z); + let same = |a: Vector3, b: Vector3| (a - b).length_squared() < 1.0e-16; + let text = dimension.base().user_text.clone(); + let (rebuilt, leader) = match &*dimension { + Dimension::Radius(d) => { + let direction = to_dvec(d.definition_point) - to_dvec(d.angle_vertex); + let direction = direction + .try_normalize() + .unwrap_or(glam::DVec3::X); + ( + crate::modules::annotate::radius_dim::radius_constraint_entity( + center, + radius, + center + direction * radius.max(1.0e-9), + text, + ), + d.leader_length, + ) + } + Dimension::Diameter(d) => { + let direction = to_dvec(d.angle_vertex) - to_dvec(d.definition_point); + let direction = direction + .try_normalize() + .unwrap_or(glam::DVec3::X); + ( + crate::modules::annotate::diameter_dim::diameter_constraint_entity( + center, + radius, + center + direction * radius.max(1.0e-9), + text, + ), + d.leader_length, + ) + } + _ => return false, + }; + let Some(acadrust::EntityType::Dimension(mut rebuilt)) = rebuilt else { + return false; + }; + let unchanged = match (&*dimension, &rebuilt) { + (Dimension::Radius(old), Dimension::Radius(new)) => { + same(old.angle_vertex, new.angle_vertex) + && same(old.definition_point, new.definition_point) + } + (Dimension::Diameter(old), Dimension::Diameter(new)) => { + same(old.angle_vertex, new.angle_vertex) + && same(old.definition_point, new.definition_point) + } + _ => false, + }; + if unchanged { + return false; + } + match &mut rebuilt { + Dimension::Radius(new) => new.leader_length = leader, + Dimension::Diameter(new) => new.leader_length = leader, + _ => {} + } + 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 +} + /// Rebuilds a dynamic angular dimension on its sides' current positions, /// keeping its arc point; true when anything moved. fn dynamic_dimension_follow_angle( @@ -1983,6 +2089,7 @@ impl super::Scene { String, Option<(Vector3, Vector3)>, Option>, + Option<(glam::DVec3, f64)>, )> = Vec::new(); // A reference (driven) constraint's parameter follows the geometry. let mut followed: Vec<(String, f64)> = Vec::new(); @@ -2010,6 +2117,20 @@ impl super::Scene { constraint.kind, ConstraintKind::Angle | ConstraintKind::Angle3Point ); + let radial = matches!( + constraint.kind, + ConstraintKind::Radius | ConstraintKind::Diameter + ); + let radial_now = radial + .then(|| radial_geometry(&self.document, constraint)) + .flatten(); + let radial_measured = radial_now.map(|(_, radius)| { + if constraint.kind == ConstraintKind::Diameter { + radius * 2.0 + } else { + radius + } + }); let measured = points.filter(|_| !angular).map(|(first, second)| { let delta = second - first; let axis = match self.document.get_entity(*dimension) { @@ -2027,6 +2148,7 @@ impl super::Scene { _ => delta.length(), } }); + let measured = radial_measured.or(measured); let (name, value, source) = match &constraint.driving_param { Some(DrivingValue::Named(name)) => { let value = match (reference, measured) { @@ -2068,8 +2190,11 @@ impl super::Scene { annotational, decimals, ), - points, + // A radial dimension follows its own circle, not a pair + // of constraint points. + points.filter(|_| !radial), angle_points, + radial_now, )); } } @@ -2079,7 +2204,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 +2221,11 @@ impl super::Scene { changed = true; } } + if let Some((center, radius)) = radial { + if dynamic_dimension_follow_radial(&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(), From 368aa69f4ee1bade447457cf412c5360f6342cbd Mon Sep 17 00:00:00 2001 From: ramox81 <184937705+ramox81@users.noreply.github.com> Date: Tue, 22 Sep 2026 17:11:09 +0300 Subject: [PATCH 2/2] Radial dimensions: text on the line, a short leader when it moves out The text of a radius or diameter constraint sits on its dimension line, which breaks around it, and no centre mark is drawn while that line is there. Text that no longer fits inside moves beside the circle at the end of a short leader that leaves the rim, elbows and hooks under the text, with the arrowheads keeping the circle. A dynamic dimension now keeps only its text at a constant screen size; arrowheads and extension lines keep the style's size in drawing units. Co-Authored-By: Claude Opus 5 --- src/app/command_driver.rs | 1 + src/entities/dimension.rs | 143 +++++++++++++++++++--------- src/scene/parametric_constraints.rs | 48 ++++++++-- 3 files changed, 136 insertions(+), 56 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 64e868352..04901b47b 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -1844,6 +1844,7 @@ impl OpenCADStudio { dov::DIMEXO, dov::DIMEXE, dov::DIMASZ, + dov::DIMTAD, dov::DIMTIH, dov::DIMTOH, ] { diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index 661b8785a..c9b6284da 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -3860,6 +3860,7 @@ fn tessellate_dimension_inner( ticks: dimtsz_raw > 1e-9, arrow_len: dimasz, text_width: text_layout.width, + text_height: dim_txt as f32, dimatfit: style.map(|s| s.dimatfit).unwrap_or(3), dimtix: style.is_some_and(|s| s.dimtix), dimtofl: style.map(|s| s.dimtofl).unwrap_or(false), @@ -4691,6 +4692,8 @@ struct DimLineParams { /// Arrowhead length (DIMASZ, scaled) — used to decide arrow-outside fit. arrow_len: f32, text_width: f32, + /// Text height (DIMTXT, scaled) — a radial leader is sized by it. + text_height: f32, dimatfit: i16, dimtix: bool, dimtofl: bool, @@ -4776,21 +4779,28 @@ fn dimension_geometry( } let radius = (point - center).length(); let text_is_outside = text.distance(center) > radius + 1e-5; - // Text inside the arc: the dimension line runs from the arc point - // 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); + // The dimension line runs from the centre to the arc point and + // is broken where the text sits on it. Text that no longer fits + // inside is reached by a leader from the arc point, and the + // arrowhead stays on the arc either way. + if !jogged && !suppress.dim2 { + add_segment_with_text_break(&mut g.dim_lines, center, point, params.text_break); } if text_is_outside && !suppress.dim2 { - append_radial_leader(&mut g, point, text, ¶ms); + append_radial_leader(&mut g, point, point - center, text, ¶ms); } if !suppress.dim2 { - // The arrowhead sits on the arc with its body toward the text. - let body = if text_is_outside { point - center } else { center - point }; - append_arrow(&mut g, point, normalized_or(body, Vec3::X), arrow1); + // The arrowhead's tip is on the arc, its body inside. + append_arrow( + &mut g, + point, + normalized_or(center - point, Vec3::X), + arrow1, + ); } - if text_is_outside { + // The centre mark belongs to a radius drawn without its inside + // line; the line itself already marks the centre. + if jogged || suppress.dim2 { append_center_mark(&mut g, center, params.dimcen, radius); } } @@ -4807,9 +4817,8 @@ fn dimension_geometry( params, suppress, ); - let center = (chord + far_chord) * 0.5; - let radius = chord.distance(far_chord) * 0.5; - append_center_mark(&mut g, center, params.dimcen, radius); + // The centre mark belongs to a diameter drawn without its line + // across the circle; `append_diameter_dimension` adds it there. } Dimension::Angular2Ln(d) => { // A two-line angular dimension stores two LINES, not two rays: @@ -5282,29 +5291,26 @@ fn append_linear_dimension( /// hook one arrow long that the text sits against; aligned text is reached by a /// straight leader that stops a gap short of it. `text_width` already carries /// a gap on each side. -fn append_radial_leader(g: &mut DimGeom, tip: Vec3, text: Vec3, params: &DimLineParams) { - if params.horizontal_text { - let side = if text.x >= tip.x { 1.0 } else { -1.0 }; - let (hook_start, text_edge) = match params.leader_anchor { - Some(anchor) => (anchor, anchor + Vec3::X * (side * params.arrow_len)), - None => { - let edge = Vec3::new(text.x - side * params.text_width * 0.5, text.y, text.z); - (edge - Vec3::X * (side * params.arrow_len), edge) - } - }; - let slope = (hook_start - tip).y.abs().atan2((hook_start - tip).x.abs()); - // A leader already within 15° of horizontal runs straight to the text. - if slope > 15.0_f32.to_radians() { - add_segment(&mut g.dim_lines, tip, hook_start); - add_segment(&mut g.dim_lines, hook_start, text_edge); - } else { - add_segment(&mut g.dim_lines, tip, text_edge); - } - } else { - let toward = normalized_or(text - tip, Vec3::X); - let reach = ((text - tip).length() - params.text_width * 0.5).max(0.0); - add_segment(&mut g.dim_lines, tip, tip + toward * reach); - } +/// A radial dimension's leader: it leaves the rim along the dimension line, +/// elbows level with the text and hooks under it, as the reference draws it. +fn append_radial_leader( + g: &mut DimGeom, + tip: Vec3, + direction: Vec3, + text: Vec3, + params: &DimLineParams, +) { + let side = if text.x >= tip.x { 1.0 } else { -1.0 }; + let edge = match params.leader_anchor { + Some(anchor) => anchor, + None => Vec3::new(text.x - side * params.text_width * 0.5, text.y, text.z), + }; + let elbow = tip + + normalized_or(direction, Vec3::X) * radial_leader_run(params.text_height as f64) as f32; + // The hook is horizontal at the text's height; the run reaches it. + let elbow = Vec3::new(elbow.x, edge.y, edge.z); + add_segment(&mut g.dim_lines, tip, elbow); + add_segment(&mut g.dim_lines, elbow, edge); } fn append_diameter_dimension( @@ -5324,11 +5330,10 @@ fn append_diameter_dimension( } let center = (chord + far_chord) * 0.5; let text_is_outside = params.text_position.distance(center) > diameter * 0.5 + 1e-5; - if text_is_outside && !params.dimtofl { - // Outside text without DIMTOFL: no line across the circle, just a - // leader from the near side with the arrowhead on the circle and its - // body toward the text. DIMTMOVE 2 keeps the arrowhead and drops the - // leader. + if text_is_outside { + // Text that no longer fits inside is carried by a leader from the + // nearer end of the dimension line; the line and both arrowheads + // stay inside the circle. DIMTMOVE 2 drops the leader. let (tip, suppressed) = if params.text_position.distance_squared(chord) <= params.text_position.distance_squared(far_chord) { @@ -5336,13 +5341,9 @@ fn append_diameter_dimension( } else { (far_chord, suppress.dim2) }; - if !suppressed { - if params.text_movement != 2 { - append_radial_leader(g, tip, params.text_position, ¶ms); - } - append_arrow(g, tip, normalized_or(tip - center, axis), arrow1); + if !suppressed && params.text_movement != 2 { + append_radial_leader(g, tip, tip - center, params.text_position, ¶ms); } - return; } // Text projected outside the diameter requires inward-pointing arrowheads. let text_along = (params.text_position - chord).dot(axis); @@ -5350,6 +5351,9 @@ fn append_diameter_dimension( let arrows_outside = if params.ticks || params.arrow_len <= 1e-6 { false + } else if text_is_outside { + // The leader carries the text; the arrowheads keep the circle. + false } else if text_outside { true } else if diameter < 2.0 * params.arrow_len { @@ -5392,6 +5396,10 @@ fn append_diameter_dimension( } } + if !draw_inside_line { + let center = (chord + far_chord) * 0.5; + append_center_mark(g, center, params.dimcen, diameter * 0.5); + } if arrows_outside { append_arrow(g, chord, -axis, arrow1); append_arrow(g, far_chord, axis, arrow2); @@ -7558,6 +7566,33 @@ fn radial_leader_tip(dim: &Dimension, text: Vector3) -> Vector3 { } } +/// How far a radial leader runs out from the rim before its hook, and how +/// long that hook is: about one text height, so the text always clears the +/// circle. +fn radial_leader_run(text_height: f64) -> f64 { + text_height +} + +/// Where a radius or diameter dimension's text sits once it no longer fits +/// inside: beside the circle, level with the end of the leader's run, its +/// near edge one arrow away so the hook has room. +fn radial_outside_text( + tip: Vector3, + ux: f64, + uy: f64, + text_height: f64, + text_width: f64, +) -> Vector3 { + let run = radial_leader_run(text_height); + let elbow = Vector3::new(tip.x + ux * run, tip.y + uy * run, tip.z); + let side = if ux >= 0.0 { 1.0 } else { -1.0 }; + Vector3::new( + elbow.x + side * (run + text_width * 0.5), + elbow.y, + elbow.z, + ) +} + fn dimension_text_pos_f64( dim: &Dimension, style: Option<&DimStyle>, @@ -7706,6 +7741,9 @@ fn dimension_text_pos_f64( let ux = dx / radius; let uy = dy / radius; let outside = dimension_text_is_outside(dim, style); + if outside { + return radial_outside_text(d.definition_point, ux, uy, text_height, text_w); + } let (mut x, mut y) = if outside { let distance = arrow + text_w * 0.5 + dimgap; ( @@ -7747,6 +7785,16 @@ fn dimension_text_pos_f64( let dx = d.definition_point.x - d.angle_vertex.x; let dy = d.definition_point.y - d.angle_vertex.y; let len = (dx * dx + dy * dy).sqrt().max(1e-12); + if dimension_text_is_outside(dim, style) { + // Beyond the end the dimension line was placed towards. + return radial_outside_text( + d.angle_vertex, + -dx / len, + -dy / len, + text_height, + text_w, + ); + } text_on_dim_line( d.angle_vertex, d.definition_point, @@ -7854,6 +7902,7 @@ pub(crate) fn baked_large_radial_geometry( ticks: tick_size > 1.0e-9, arrow_len: arrow_size, text_width: text.width, + text_height: text_height as f32, dimatfit: style.map(|style| style.dimatfit).unwrap_or(3), dimtix: style.is_some_and(|style| style.dimtix), dimtofl: style.is_some_and(|style| style.dimtofl), diff --git a/src/scene/parametric_constraints.rs b/src/scene/parametric_constraints.rs index 21069764b..a57edd0ae 100644 --- a/src/scene/parametric_constraints.rs +++ b/src/scene/parametric_constraints.rs @@ -2388,6 +2388,7 @@ impl super::Scene { ov::DIMEXO, ov::DIMEXE, ov::DIMASZ, + ov::DIMTAD, ov::DIMTIH, ov::DIMTOH, ] { @@ -2426,7 +2427,7 @@ impl super::Scene { continue; } let style_name = dimension.base().style_name.clone(); - let text_height = self + let style = self .document .dim_styles .iter() @@ -2434,23 +2435,40 @@ impl super::Scene { style.name.eq_ignore_ascii_case(&style_name) || (style_name.trim().is_empty() && style.name.eq_ignore_ascii_case("Standard")) - }) + }); + let text_height = style .map(|style| style.dimtxt) .filter(|height| *height > 1e-9) .unwrap_or(0.18); + let size_of = |value: Option, fallback: f64| { + value.filter(|size| *size > 1e-9).unwrap_or(fallback) + }; + let arrow_size = size_of(style.map(|style| style.dimasz), text_height); + let extension_over = size_of(style.map(|style| style.dimexe), text_height * 0.5); + let extension_offset = size_of(style.map(|style| style.dimexo), text_height * 0.25); let scale = f64::from(wpp) * f64::from(DYNAMIC_DIMENSION_TEXT_PX) / text_height; use crate::entities::dim_override as ov; let xdata = &dimension.base().common.extended_data; let current = ov::real(xdata, ov::DIMSCALE); - // Text gap, extension offset and overshoot and arrow size follow - // the text height on screen (the reference's ratios), whatever - // the style says in drawing units. + // The gap hugs the text, so it follows the text's screen size. + // Arrowheads and extension lines keep the style's size in + // drawing units: dividing by the screen factor the style's + // DIMSCALE re-applies leaves them unchanged as the view zooms. let sizes = [ (ov::DIMGAP, text_height * 0.25), - (ov::DIMEXO, text_height * 0.25), - (ov::DIMEXE, text_height * 0.5), - (ov::DIMASZ, text_height), + (ov::DIMEXO, extension_offset / scale), + (ov::DIMEXE, extension_over / scale), + (ov::DIMASZ, arrow_size / scale), ]; + // A radius or diameter constraint reads on its own dimension + // line, which breaks around the text, so the text is centred on + // it rather than lifted above it. + let radial = matches!( + dimension, + acadrust::entities::Dimension::Radius(_) + | acadrust::entities::Dimension::Diameter(_) + ); + let centred = !radial || ov::int(xdata, ov::DIMTAD) == Some(0); let sizes_set = sizes.iter().all(|(code, size)| { ov::real(xdata, *code).is_some_and(|value| (value - size).abs() < 1e-12) }); @@ -2463,9 +2481,21 @@ impl super::Scene { ); let horizontal = angular || (ov::int(xdata, ov::DIMTIH) == Some(1) && ov::int(xdata, ov::DIMTOH) == Some(1)); - if current.is_some_and(|value| (value - scale).abs() < 1e-9) && horizontal && sizes_set { + if current.is_some_and(|value| (value - scale).abs() < 1e-9) + && horizontal + && sizes_set + && centred + { continue; } + if radial && !centred { + ov::set( + &mut self.document, + handle, + ov::DIMTAD, + Some(acadrust::xdata::XDataValue::Integer16(0)), + ); + } ov::set( &mut self.document, handle,