Merge pull request #1447 from ramox81:codex/diameter-constraint-parity

#1447 branched from the same commit as #1443 and rewrote the radial
dimensional constraint a second time, so the two designs had to be
reconciled rather than stacked. #1447's is the one kept: it covers
DCRADIUS and DCDIAMETER through one command, places the dimension by the
direction the location was picked in rather than storing a text point,
and names the parameters `radN` / `diaN`. #1443's radial command result,
its entity builder call and its follow-up code are gone; its own
independent work stays — the ribbon's small-button grid and tool
descriptions, the magnitude reading of a negative radius, and the centre
anchor a parameter edit resizes a circle about.

The DIMCONSTRAINT default follows #1443: running DCRADIUS or DCDIAMETER
directly leaves it alone, and the Dispatch arm sets it when DIMCONSTRAINT
is what asked for the command.

The second commit's radial drawing — the dimension line broken around the
text, the elbowed leader, the centre-mark and arrowhead rules — applied to
every radius and diameter dimension, not just a constraint's, and three
layout-parity tests said the old layout was the measured one. It is now
scoped to a dimensional constraint's own dimension, as #1443 scoped its
version, and ordinary radial dimensions draw as they did.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5 committed 2026-09-22 17:51:16 +03:00
commit 889a19c362
9 files changed
+706 -415

No files matched your search

+172 -166
View File
@@ -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,
+20 -12
View File
@@ -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(
+9 -13
View File
@@ -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,
+16 -9
View File
@@ -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`.
+155 -56
View File
@@ -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, &params);
}
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, &params);
}
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, &params);
}
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, &params);
if !suppressed && params.text_movement != 2 {
append_radial_leader(g, tip, tip - center, params.text_position, &params);
}
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<Vector3> {
(p.x * p.x + p.y * p.y + p.z * p.z > 1e-16).then_some(p)
}
/// A dimensional constraint's dynamic dimension: the reference keeps it on
/// its own hidden layer and draws it by its own rules.
fn dynamic_constraint_dimension(dim: &Dimension) -> bool {
dim.base()
.common
.layer
.eq_ignore_ascii_case(crate::scene::parametric_constraints::DYNAMIC_DIMENSION_LAYER)
}
/// The point on the circle a radial leader leaves from: the arc point of a
/// radius, or whichever end of a diameter's chord is nearer the text.
fn radial_leader_tip(dim: &Dimension, text: Vector3) -> Vector3 {
@@ -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),
+19
View File
@@ -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<String>,
) -> Option<EntityType> {
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) {
}
+25
View File
@@ -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<String>,
) -> Option<EntityType> {
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<DVec3> {
let delta = location - center;
(delta.length() > 1.0e-9).then(|| delta.normalize())
}
impl CadCommand for RadiusDimensionCommand {
fn set_working_plane(&mut self, _plane: WorkingPlane) {
}
+114 -70
View File
@@ -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 <rad1=30>:`
/// 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 <dia1=50>:`
RadialValue {
circle: ParametricRef,
radius: f64,
target: RadialTarget,
location: DVec3,
measured: f64,
},
/// `Enter value or name and value <d1=100>:`
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<CmdResult> {
let Step::RadialValue {
circle, location, ..
} = self.step
else {
return None;
};
if expression.trim().is_empty() {
return None;
}
Some(CmdResult::AddRadialConstraint {
circle,
location,
renamed: name != self.default_name,
name,
expression,
})
}
fn command_name(&self) -> &'static str {
match self.axis {
DimConstraintAxis::Linear => "DCLINEAR",
@@ -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<RadialTarget> {
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<CmdResult> {
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,
}
}
+176 -89
View File
@@ -353,36 +353,37 @@ pub(crate) fn equal_size_follower(
Some(entity)
}
/// The first free `<prefix>1`, `<prefix>2`, … name a new constraint takes.
fn next_prefixed_parameter_name(
/// 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<Vec<Vector3>>,
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<f64>, 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,