diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 127aff46f..d4189ea22 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -1935,6 +1935,7 @@ impl OpenCADStudio { dov::DIMEXO, dov::DIMEXE, dov::DIMASZ, + dov::DIMTAD, dov::DIMTIH, dov::DIMTOH, ] { @@ -4337,6 +4338,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, @@ -4533,172 +4705,6 @@ 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, diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index c4a70cb8b..6a0b2341a 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -1891,22 +1891,32 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(command)); } - "DCRADIUS" => { + "DCRADIUS" | "DCDIAMETER" => { 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); + let diameter = cmd == "DCDIAMETER"; + // Run on its own either one leaves DIMCONSTRAINT's default + // alone; the Dispatch arm sets it when DIMCONSTRAINT is what + // asked for this command. + // 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" | "DCDIAMETER" => { - if cmd == "DCDIAMETER" { - self.dim_constraint_last = "Diameter"; - } + "DCONSTRAINT" => { let handles = self.tabs[i].scene.selected_handles_in_order(); if handles.is_empty() { use crate::modules::draw::select::SelectObjectsCommand; @@ -1924,8 +1934,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 81ec53f90..6f2938ea7 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -2369,7 +2369,10 @@ impl OpenCADStudio { Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_), ) => t!("Angular Dimensional Constraint"), acadrust::EntityType::Dimension(Dimension::Radius(_)) => { - t!("Radial Dimension (Dynamic)") + t!("Radius Dimensional Constraint") + } + acadrust::EntityType::Dimension(Dimension::Diameter(_)) => { + t!("Diameter Dimensional Constraint") } _ => t!("Linear Dimensional Constraint"), } @@ -2853,13 +2856,6 @@ 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 @@ -3712,6 +3708,11 @@ fn set_row(sections: &mut [crate::scene::model::object::PropSection], field: &st } } +/// Replace a row's value with an arbitrary control (editable field, dropdown, +/// colour picker …) rather than plain read-only text. +/// A dimensional constraint's Properties as the reference shows them: the +/// Constraint rows, then only the text rotation for a dynamic dimension +/// and the full dimension sections for an annotational one. /// A Yes/No list row, as the reference shows on/off object properties. fn yes_no_choice(flag: bool) -> crate::scene::model::object::PropValue { crate::scene::model::object::PropValue::Choice { @@ -3720,9 +3721,6 @@ fn yes_no_choice(flag: bool) -> crate::scene::model::object::PropValue { } } -/// A dimensional constraint's Properties as the reference shows them: the -/// Constraint rows, then only the text rotation for a dynamic dimension -/// and the full dimension sections for an annotational one. fn dynamic_dimension_sections( scene: &crate::scene::Scene, handle: Handle, @@ -3858,8 +3856,6 @@ fn dynamic_dimension_sections( result } -/// Replace a row's value with an arbitrary control (editable field, dropdown, -/// colour picker …) rather than plain read-only text. fn set_row_value( sections: &mut [crate::scene::model::object::PropSection], field: &str, diff --git a/src/command.rs b/src/command.rs index 41d7c7a4e..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 @@ -1650,15 +1666,6 @@ 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`. diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index 593c8ad55..e041fc779 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -1134,11 +1134,7 @@ 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; - // 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 { + if p.x * p.x + p.y * p.y + p.z * p.z > 1e-16 { dv3(&p) } else { dv3(&dimension_text_pos_f64(self, None, 2.5, 1.0)) @@ -3864,6 +3860,8 @@ fn tessellate_dimension_inner( ticks: dimtsz_raw > 1e-9, arrow_len: dimasz, text_width: text_layout.width, + text_height: dim_txt as f32, + constraint: dynamic_constraint_dimension(dim), 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), @@ -4695,6 +4693,11 @@ 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, + /// This dimension is a dimensional constraint's dynamic dimension, which + /// the reference draws by its own radial rules. + constraint: bool, dimatfit: i16, dimtix: bool, dimtofl: bool, @@ -4780,27 +4783,46 @@ 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) { - if dynamic_constraint_dimension(dim) { - // The line passes behind the mid-radius text. + if params.constraint { + // A dimensional constraint reads on its own dimension line, + // which runs from the centre to the arc point and breaks + // 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); - } else { + } + if text_is_outside && !suppress.dim2 { + append_radial_leader(&mut g, point, point - center, text, ¶ms); + } + if !suppress.dim2 { + // The arrowhead's tip is on the arc, its body inside. + append_arrow(&mut g, point, normalized_or(center - point, Vec3::X), arrow1); + } + // 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); + } + } else { + // 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); } - } - if text_is_outside && !suppress.dim2 { - append_radial_leader(&mut g, point, 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); - } - if text_is_outside { - append_center_mark(&mut g, center, params.dimcen, radius); + if text_is_outside && !suppress.dim2 { + 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); + } + if text_is_outside { + append_center_mark(&mut g, center, params.dimcen, radius); + } } } Dimension::Diameter(d) => { @@ -4816,9 +4838,11 @@ 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); + if !params.constraint { + let center = (chord + far_chord) * 0.5; + let radius = chord.distance(far_chord) * 0.5; + append_center_mark(&mut g, center, params.dimcen, radius); + } } Dimension::Angular2Ln(d) => { // A two-line angular dimension stores two LINES, not two rays: @@ -5287,11 +5311,36 @@ fn append_linear_dimension( } } -/// Leader from a point on the circle to text outside it. Horizontal text gets a -/// 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) { +/// Leader from a point on the circle to text outside it. +/// +/// A dimensional constraint's dimension leaves the rim along its dimension +/// line, elbows level with the text and hooks under it. An ordinary radial +/// dimension keeps its measured layout: horizontal text gets a hook one arrow +/// long that the text sits against, and 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, + direction: Vec3, + text: Vec3, + params: &DimLineParams, +) { + if params.constraint { + 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); + return; + } 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 { @@ -5333,11 +5382,13 @@ 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 && (params.constraint || !params.dimtofl) { + // A dimensional constraint carries its text on a leader from the + // nearer end of the dimension line, and the line and both arrowheads + // stay inside the circle. An ordinary diameter draws no line across + // the circle at all: just that leader, with one arrowhead on the + // circle and its body toward the text. DIMTMOVE 2 keeps the arrowhead + // and drops the leader. let (tip, suppressed) = if params.text_position.distance_squared(chord) <= params.text_position.distance_squared(far_chord) { @@ -5345,13 +5396,15 @@ 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); + if !suppressed && params.text_movement != 2 { + append_radial_leader(g, tip, tip - center, params.text_position, ¶ms); + } + if !params.constraint { + if !suppressed { + append_arrow(g, tip, normalized_or(tip - center, axis), arrow1); } - append_arrow(g, tip, normalized_or(tip - center, axis), arrow1); + return; } - return; } // Text projected outside the diameter requires inward-pointing arrowheads. let text_along = (params.text_position - chord).dot(axis); @@ -5359,6 +5412,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 { @@ -5401,6 +5457,10 @@ fn append_diameter_dimension( } } + if params.constraint && !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); @@ -7551,15 +7611,6 @@ fn stored_text_point(dim: &Dimension) -> Option { (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 { @@ -7576,6 +7627,42 @@ fn radial_leader_tip(dim: &Dimension, text: Vector3) -> Vector3 { } } +/// 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) +} + +/// 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>, @@ -7616,12 +7703,6 @@ 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 @@ -7730,6 +7811,11 @@ fn dimension_text_pos_f64( let ux = dx / radius; let uy = dy / radius; let outside = dimension_text_is_outside(dim, style); + if outside && dynamic_constraint_dimension(dim) { + // A constraint's text moves beside the circle at the end of + // its leader; an ordinary radius keeps its measured offset. + 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; ( @@ -7771,6 +7857,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) && dynamic_constraint_dimension(dim) { + // 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, @@ -7878,6 +7974,9 @@ 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, + // A LargeRadial is never a dimensional constraint's dimension. + constraint: false, 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/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 204d07435..3ddc335a0 100644 --- a/src/modules/annotate/radius_dim.rs +++ b/src/modules/annotate/radius_dim.rs @@ -96,6 +96,31 @@ pub(crate) fn radial_dimension_entity( plane.place_entity(EntityType::Dimension(Dimension::Radius(dim))) } +/// 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 8fda9bdfa..548c4cc96 100644 --- a/src/modules/parametric/dim_constraint.rs +++ b/src/modules/parametric/dim_constraint.rs @@ -16,8 +16,10 @@ pub enum DimConstraintAxis { Aligned, /// The angle between two lines, or at a vertex between two points. Angular, - /// The radius of a circle or arc. + /// 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 @@ -108,15 +110,15 @@ 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 :` + /// 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 { - circle: ParametricRef, - radius: f64, + target: RadialTarget, location: DVec3, + measured: f64, }, /// `Enter value or name and value :` Value { @@ -147,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. @@ -167,15 +177,13 @@ 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: match axis { DimConstraintAxis::Angular => Step::AngularFirst, - DimConstraintAxis::Radius => Step::RadialSelect, + DimConstraintAxis::Radius | DimConstraintAxis::Diameter => Step::RadialObject, _ => Step::First, }, picked_entity: None, @@ -318,25 +326,6 @@ impl DimConstraintCommand { }) } - fn build_radial(&self, name: String, expression: String) -> Option { - 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", @@ -345,6 +334,7 @@ impl DimConstraintCommand { DimConstraintAxis::Aligned => "DCALIGNED", DimConstraintAxis::Angular => "DCANGULAR", DimConstraintAxis::Radius => "DCRADIUS", + DimConstraintAxis::Diameter => "DCDIAMETER", } } @@ -355,10 +345,47 @@ impl DimConstraintCommand { DimConstraintAxis::Vertical => "Vertical", DimConstraintAxis::Aligned => "Aligned", DimConstraintAxis::Angular => "Angular", - DimConstraintAxis::Radius => "Radial", + 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()) } @@ -509,8 +536,11 @@ impl DimConstraintCommand { } DimConstraintAxis::Horizontal => (ConstraintKind::DistanceX, DVec3::X), DimConstraintAxis::Vertical => (ConstraintKind::DistanceY, DVec3::Y), - // Angular and Radius never measure here; their own steps decide. - DimConstraintAxis::Aligned | DimConstraintAxis::Angular | DimConstraintAxis::Radius => ( + // Angular never measures here; its own steps decide the sector. + DimConstraintAxis::Aligned + | DimConstraintAxis::Angular + | DimConstraintAxis::Radius + | DimConstraintAxis::Diameter => ( ConstraintKind::Distance, (second - first).normalize_or(DVec3::X), ), @@ -560,9 +590,10 @@ impl DimConstraintCommand { DimConstraintAxis::Linear => "Linear constraint", DimConstraintAxis::Horizontal => "Horizontal distance constraint", DimConstraintAxis::Vertical => "Vertical distance constraint", - DimConstraintAxis::Aligned | DimConstraintAxis::Angular | DimConstraintAxis::Radius => { - "Aligned constraint" - } + DimConstraintAxis::Aligned + | DimConstraintAxis::Angular + | DimConstraintAxis::Radius + | DimConstraintAxis::Diameter => "Aligned constraint", }, }) } @@ -604,17 +635,22 @@ impl CadCommand for DimConstraintCommand { Step::AngularPoint2 { .. } => { format!("{name} Specify second angle constraint point:") } - Step::RadialSelect => format!("{name} Select arc or circle:"), - Step::AngularLocation { .. } | Step::Location { .. } | Step::RadialLocation { .. } => { + Step::RadialObject => format!("{name} Select arc or circle:"), + Step::AngularLocation { .. } + | Step::Location { .. } + | Step::RadialLocation { .. } => { format!("{name} Specify dimension line location:") } - Step::AngularValue { measured, .. } | Step::RadialValue { radius: measured, .. } => { - format!( - "{name} Enter value or name and value <{}={}>:", - self.default_name, - measured_expression(measured) - ) - } + 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, + measured_expression(measured) + ), Step::Value { measured, .. } => format!( "{name} Enter value or name and value <{}={}>:", self.default_name, @@ -717,7 +753,7 @@ impl CadCommand for DimConstraintCommand { | Step::AngularVertex | Step::AngularPoint1 { .. } | Step::AngularPoint2 { .. } - | Step::RadialSelect + | Step::RadialObject ) } @@ -909,27 +945,30 @@ impl CadCommand for DimConstraintCommand { whole_curve: false, }) } - Step::RadialSelect => { + Step::RadialObject => { 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), + let Some(target) = Self::radial_target(&entity, handle) else { + return Self::report(&format!( + "Invalid selection for {}. Select a circle or arc.", + self.noun() + )); }; - self.step = Step::RadialLocation { - circle: ParametricRef::whole(handle), - radius, + let measured = if self.axis == DimConstraintAxis::Diameter { + target.radius * 2.0 + } else { + target.radius }; - // The reference reports the radius before asking where the - // dimension goes. + 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(radius) + measured_expression(measured) )) } Step::Location { .. } @@ -1065,16 +1104,7 @@ impl CadCommand for DimConstraintCommand { | Step::TwoLinesFirst | Step::TwoLinesSecond { .. } | Step::AngularFirst - | Step::AngularSecond { .. } - | Step::RadialSelect => Self::report(Self::NO_OBJECT), - Step::RadialLocation { circle, radius } => { - self.step = Step::RadialValue { - circle, - radius, - location: point, - }; - CmdResult::NeedPoint - } + | Step::AngularSecond { .. } => Self::report(Self::NO_OBJECT), Step::AngularLocation { data } => { let frame = match data { AngularData::Lines { first, second } => Self::angle_frame( @@ -1126,6 +1156,20 @@ impl CadCommand for DimConstraintCommand { }; CmdResult::ReportMeasurement(format!("Dimension text = {measured:.4}")) } + 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 { .. } @@ -1147,9 +1191,6 @@ 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 @@ -1157,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 f6eb587a7..a41567d4f 100644 --- a/src/scene/parametric_constraints.rs +++ b/src/scene/parametric_constraints.rs @@ -353,36 +353,37 @@ pub(crate) fn equal_size_follower( Some(entity) } -/// The first free `1`, `2`, … name a new constraint takes. -fn next_prefixed_parameter_name( +/// The first free `d1`, `d2`, … name a new dimensional constraint takes. +pub(crate) fn next_dimensional_parameter_name( table: &super::named_parameters::ParameterTable, - prefix: &str, ) -> String { + (1..) + .map(|n| format!("d{n}")) + .find(|name| !table.contains(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 `d1`, `d2`, … name a new dimensional constraint takes. -pub(crate) fn next_dimensional_parameter_name( - table: &super::named_parameters::ParameterTable, -) -> 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 { - 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") + (1..) + .map(|n| format!("ang{n}")) + .find(|name| !table.contains(name)) + .unwrap_or_else(|| "ang1".to_string()) } /// An angle as the reference displays it: a negative or over-full value is @@ -577,52 +578,6 @@ 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)> { @@ -659,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( @@ -731,9 +779,9 @@ 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. A single whole reference can also be a line's length, - // whose start is what stays, so the entity decides. + // A whole circle or arc (a radius or diameter): its centre stays and the + // value resizes it. This is what a parameter edit through -PARAMETERS or + // Properties goes through; the command itself passes the anchor directly. if refs.len() == 1 && first.marker.is_none() && matches!( @@ -2053,7 +2101,7 @@ impl super::Scene { String, Option<(Vector3, Vector3)>, Option>, - Option<(Vector3, f64)>, + Option<(glam::DVec3, f64)>, )> = Vec::new(); // A reference (driven) constraint's parameter follows the geometry. let mut followed: Vec<(String, f64)> = Vec::new(); @@ -2081,15 +2129,21 @@ impl super::Scene { constraint.kind, ConstraintKind::Angle | ConstraintKind::Angle3Point ); - // 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)?) - }) + let radial = matches!( + constraint.kind, + ConstraintKind::Radius | ConstraintKind::Diameter + ); + let radial_now = radial + .then(|| radial_geometry(&self.document, constraint)) .flatten(); - let measured = radial.map(|(_, radius)| radius); - let measured = measured.or(points.filter(|_| !angular).map(|(first, second)| { + 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) { Some(acadrust::EntityType::Dimension( @@ -2105,7 +2159,8 @@ 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) { @@ -2147,9 +2202,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, + radial_now, )); } } @@ -2177,7 +2234,7 @@ impl super::Scene { } } if let Some((center, radius)) = radial { - if dynamic_dimension_follow_radius(&mut dimension, center, radius) { + if dynamic_dimension_follow_radial(&mut dimension, center, radius) { changed = true; } } @@ -2343,6 +2400,7 @@ impl super::Scene { ov::DIMEXO, ov::DIMEXE, ov::DIMASZ, + ov::DIMTAD, ov::DIMTIH, ov::DIMTOH, ] { @@ -2381,7 +2439,7 @@ impl super::Scene { continue; } let style_name = dimension.base().style_name.clone(); - let text_height = self + let style = self .document .dim_styles .iter() @@ -2389,23 +2447,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) }); @@ -2418,9 +2493,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,