forked from 1b-agent/opencadstudio-upstream
The synthesised dimension text copied the dimension's ByLayer colour, and its glyphs take their colour from that entity, so the layer colour showed. Fixes #898 Co-Authored-By: Claude Opus 5.5 <[email protected]>
9182 lines
334 KiB
Rust
9182 lines
334 KiB
Rust
use codec::entities::{
|
||
Dimension, DimensionAligned, DimensionAngular2Ln, DimensionAngular3Pt, DimensionArc,
|
||
DimensionBase, DimensionDiameter, DimensionLargeRadial, DimensionLinear, DimensionOrdinate,
|
||
DimensionRadius,
|
||
};
|
||
use glam::{DVec3, Vec3};
|
||
|
||
use crate::command::EntityTransform;
|
||
use crate::entities::common::{
|
||
center_grip, edit_angle_prop as edit_angle, edit_prop as edit, lineweight_label,
|
||
lineweight_options, parse_f64, ro_prop as ro, square_grip,
|
||
};
|
||
use crate::entities::traits::{Grippable, PropertyEditable, Transformable};
|
||
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
|
||
use crate::t;
|
||
|
||
pub(crate) fn dimension_text_override(base: &DimensionBase) -> Option<&str> {
|
||
base.user_text
|
||
.as_deref()
|
||
.or_else(|| (!base.text.is_empty()).then_some(base.text.as_str()))
|
||
}
|
||
|
||
pub(crate) fn set_dimension_text_override(base: &mut DimensionBase, text: Option<String>) {
|
||
base.text = text.clone().unwrap_or_default();
|
||
base.user_text = text;
|
||
}
|
||
|
||
/// Recompute the fields a script cannot set: the base definition point and
|
||
/// the stored measurement, exactly as grip edits do.
|
||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||
pub(crate) fn normalize_scripted_dimension(dim: &mut Dimension) {
|
||
let definition_point = dimension_definition_point(dim);
|
||
dim.base_mut().definition_point = definition_point;
|
||
dim.base_mut().actual_measurement = dim.measurement();
|
||
}
|
||
|
||
fn dimension_definition_point(dim: &Dimension) -> codec::types::Vector3 {
|
||
match dim {
|
||
Dimension::Aligned(d) => d.definition_point,
|
||
Dimension::Linear(d) => d.definition_point,
|
||
Dimension::Radius(d) => d.definition_point,
|
||
Dimension::Diameter(d) => d.definition_point,
|
||
Dimension::Angular2Ln(d) => d.definition_point,
|
||
Dimension::Angular3Pt(d) => d.definition_point,
|
||
Dimension::Ordinate(d) => d.definition_point,
|
||
Dimension::Arc(d) => d.definition_point,
|
||
Dimension::LargeRadial(d) => d.definition_point,
|
||
}
|
||
}
|
||
|
||
fn base_props(base: &DimensionBase) -> Vec<crate::scene::model::object::Property> {
|
||
vec![
|
||
crate::scene::model::object::Property {
|
||
label: t!("Text").into_owned(),
|
||
field: "text",
|
||
value: crate::scene::model::object::PropValue::PlainText(base.text.clone()),
|
||
},
|
||
crate::scene::model::object::Property {
|
||
label: t!("User Text").into_owned(),
|
||
field: "user_text",
|
||
value: crate::scene::model::object::PropValue::PlainText(
|
||
base.user_text.clone().unwrap_or_default(),
|
||
),
|
||
},
|
||
crate::scene::model::object::Property {
|
||
label: t!("Style").into_owned(),
|
||
field: "style_name",
|
||
value: crate::scene::model::object::PropValue::PlainText(base.style_name.clone()),
|
||
},
|
||
edit(t!("Text X").as_ref(), "text_x", base.text_middle_point.x),
|
||
edit(t!("Text Y").as_ref(), "text_y", base.text_middle_point.y),
|
||
edit(t!("Text Z").as_ref(), "text_z", base.text_middle_point.z),
|
||
edit(
|
||
t!("Text Rotation (deg)").as_ref(),
|
||
"text_rotation",
|
||
base.text_rotation.to_degrees(),
|
||
),
|
||
edit(
|
||
t!("Horizontal Dir (deg)").as_ref(),
|
||
"horizontal_direction",
|
||
base.horizontal_direction.to_degrees(),
|
||
),
|
||
edit(
|
||
t!("Line Spacing").as_ref(),
|
||
"line_spacing_factor",
|
||
base.line_spacing_factor,
|
||
),
|
||
ro(
|
||
t!("Measurement").as_ref(),
|
||
"measurement",
|
||
format!("{:.4}", base.actual_measurement),
|
||
),
|
||
]
|
||
}
|
||
|
||
fn properties(dim: &Dimension) -> Vec<PropSection> {
|
||
let compact_radial = match dim {
|
||
Dimension::Radius(radius) => Some((&radius.base, radius.leader_length)),
|
||
Dimension::Diameter(diameter) => Some((&diameter.base, diameter.leader_length)),
|
||
_ => None,
|
||
};
|
||
if let Some((base, leader_length)) = compact_radial {
|
||
return vec![PropSection {
|
||
title: t!("Misc").into_owned(),
|
||
props: vec![
|
||
Property {
|
||
label: t!("Dimension style").into_owned(),
|
||
field: "style_name",
|
||
value: PropValue::PlainText(base.style_name.clone()),
|
||
},
|
||
edit(
|
||
t!("Leader Length").as_ref(),
|
||
"leader_length",
|
||
leader_length,
|
||
),
|
||
],
|
||
}];
|
||
}
|
||
if let Dimension::LargeRadial(d) = dim {
|
||
return vec![PropSection {
|
||
title: t!("Misc").into_owned(),
|
||
props: vec![Property {
|
||
label: t!("Dim style").into_owned(),
|
||
field: "style_name",
|
||
value: PropValue::PlainText(d.base.style_name.clone()),
|
||
}],
|
||
}];
|
||
}
|
||
let compact_linear = match dim {
|
||
Dimension::Linear(d) => Some((
|
||
&d.base,
|
||
d.rotation,
|
||
d.ext_line_rotation,
|
||
)),
|
||
Dimension::Aligned(d) => Some((
|
||
&d.base,
|
||
(d.second_point.y - d.first_point.y)
|
||
.atan2(d.second_point.x - d.first_point.x),
|
||
d.ext_line_rotation,
|
||
)),
|
||
_ => None,
|
||
};
|
||
if let Some((base, rotation, ext_line_rotation)) = compact_linear {
|
||
return vec![PropSection {
|
||
title: t!("Misc").into_owned(),
|
||
props: vec![
|
||
crate::scene::model::object::Property {
|
||
label: t!("Dimension style").into_owned(),
|
||
field: "style_name",
|
||
value: crate::scene::model::object::PropValue::PlainText(
|
||
base.style_name.clone(),
|
||
),
|
||
},
|
||
edit_angle(
|
||
t!("Dim line angle").as_ref(),
|
||
"rotation",
|
||
rotation.to_degrees(),
|
||
),
|
||
edit_angle(
|
||
t!("Extension line angle").as_ref(),
|
||
"ext_line_rotation",
|
||
ext_line_rotation.to_degrees(),
|
||
),
|
||
],
|
||
}];
|
||
}
|
||
if matches!(dim, Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_)) {
|
||
return vec![PropSection {
|
||
title: t!("Misc").into_owned(),
|
||
props: vec![crate::scene::model::object::Property {
|
||
label: t!("Dimension style").into_owned(),
|
||
field: "style_name",
|
||
value: crate::scene::model::object::PropValue::PlainText(
|
||
dim.base().style_name.clone(),
|
||
),
|
||
}],
|
||
}];
|
||
}
|
||
if let Dimension::Ordinate(d) = dim {
|
||
return vec![PropSection {
|
||
title: t!("Misc").into_owned(),
|
||
props: vec![
|
||
Property {
|
||
label: t!("Dimension style").into_owned(),
|
||
field: "style_name",
|
||
value: PropValue::PlainText(d.base.style_name.clone()),
|
||
},
|
||
edit_angle(
|
||
t!("Rotation").as_ref(),
|
||
"ordinate_rotation",
|
||
-d.base.horizontal_direction.to_degrees(),
|
||
),
|
||
],
|
||
}];
|
||
}
|
||
if let Dimension::Arc(d) = dim {
|
||
return vec![PropSection {
|
||
title: t!("Misc").into_owned(),
|
||
props: vec![Property {
|
||
label: t!("Dimension style").into_owned(),
|
||
field: "style_name",
|
||
value: PropValue::PlainText(d.base.style_name.clone()),
|
||
}],
|
||
}];
|
||
}
|
||
let mut props = base_props(dim.base());
|
||
match dim {
|
||
Dimension::Aligned(d) => {
|
||
props.extend(linear_like_props(
|
||
d.first_point,
|
||
d.second_point,
|
||
d.definition_point,
|
||
));
|
||
props.push(edit(
|
||
t!("Ext Rotation (deg)").as_ref(),
|
||
"ext_line_rotation",
|
||
d.ext_line_rotation.to_degrees(),
|
||
));
|
||
}
|
||
Dimension::Linear(d) => {
|
||
props.extend(linear_like_props(
|
||
d.first_point,
|
||
d.second_point,
|
||
d.definition_point,
|
||
));
|
||
props.push(edit_angle(t!("Rotation").as_ref(), "rotation", d.rotation.to_degrees()));
|
||
props.push(edit(
|
||
t!("Ext Rotation (deg)").as_ref(),
|
||
"ext_line_rotation",
|
||
d.ext_line_rotation.to_degrees(),
|
||
));
|
||
}
|
||
Dimension::Radius(d) => {
|
||
props.extend(radius_like_props(d.angle_vertex, d.definition_point));
|
||
props.push(edit(t!("Leader Length").as_ref(), "leader_length", d.leader_length));
|
||
}
|
||
Dimension::Diameter(d) => {
|
||
props.extend(radius_like_props(d.angle_vertex, d.definition_point));
|
||
props.push(edit(t!("Leader Length").as_ref(), "leader_length", d.leader_length));
|
||
}
|
||
Dimension::Angular2Ln(d) => {
|
||
props.extend(angular_props(
|
||
d.angle_vertex,
|
||
d.first_point,
|
||
d.second_point,
|
||
d.definition_point,
|
||
));
|
||
props.push(edit(t!("Arc X").as_ref(), "dimension_arc_x", d.dimension_arc.x));
|
||
props.push(edit(t!("Arc Y").as_ref(), "dimension_arc_y", d.dimension_arc.y));
|
||
props.push(edit(t!("Arc Z").as_ref(), "dimension_arc_z", d.dimension_arc.z));
|
||
}
|
||
Dimension::Angular3Pt(d) => {
|
||
props.extend(angular_props(
|
||
d.angle_vertex,
|
||
d.first_point,
|
||
d.second_point,
|
||
d.definition_point,
|
||
));
|
||
}
|
||
Dimension::Ordinate(d) => {
|
||
props.push(edit(t!("Origin X").as_ref(), "definition_x", d.definition_point.x));
|
||
props.push(edit(t!("Origin Y").as_ref(), "definition_y", d.definition_point.y));
|
||
props.push(edit(t!("Origin Z").as_ref(), "definition_z", d.definition_point.z));
|
||
props.push(edit(t!("Feature X").as_ref(), "feature_x", d.feature_location.x));
|
||
props.push(edit(t!("Feature Y").as_ref(), "feature_y", d.feature_location.y));
|
||
props.push(edit(t!("Feature Z").as_ref(), "feature_z", d.feature_location.z));
|
||
props.push(edit(t!("Leader X").as_ref(), "leader_x", d.leader_endpoint.x));
|
||
props.push(edit(t!("Leader Y").as_ref(), "leader_y", d.leader_endpoint.y));
|
||
props.push(edit(t!("Leader Z").as_ref(), "leader_z", d.leader_endpoint.z));
|
||
props.push(ro(
|
||
t!("Ordinate Type").as_ref(),
|
||
"ordinate_type",
|
||
if d.is_ordinate_type_x { "X" } else { "Y" },
|
||
));
|
||
}
|
||
Dimension::Arc(d) => {
|
||
props.extend(angular_props(
|
||
d.center_point,
|
||
d.first_extension_point,
|
||
d.second_extension_point,
|
||
d.definition_point,
|
||
));
|
||
props.push(edit(
|
||
t!("Arc Start (deg)").as_ref(),
|
||
"arc_start_parameter",
|
||
d.arc_start_parameter.to_degrees(),
|
||
));
|
||
props.push(edit(
|
||
t!("Arc End (deg)").as_ref(),
|
||
"arc_end_parameter",
|
||
d.arc_end_parameter.to_degrees(),
|
||
));
|
||
props.push(ro(t!("Partial").as_ref(), "is_partial", d.is_partial.to_string()));
|
||
props.push(ro(t!("Has Leader").as_ref(), "has_leader", d.has_leader.to_string()));
|
||
props.push(edit(t!("Leader 1 X").as_ref(), "leader1_x", d.first_leader_point.x));
|
||
props.push(edit(t!("Leader 1 Y").as_ref(), "leader1_y", d.first_leader_point.y));
|
||
props.push(edit(t!("Leader 1 Z").as_ref(), "leader1_z", d.first_leader_point.z));
|
||
props.push(edit(t!("Leader 2 X").as_ref(), "leader2_x", d.second_leader_point.x));
|
||
props.push(edit(t!("Leader 2 Y").as_ref(), "leader2_y", d.second_leader_point.y));
|
||
props.push(edit(t!("Leader 2 Z").as_ref(), "leader2_z", d.second_leader_point.z));
|
||
}
|
||
Dimension::LargeRadial(_) => unreachable!("handled by compact large-radial properties"),
|
||
}
|
||
vec![PropSection {
|
||
title: t!("Geometry").into_owned(),
|
||
props,
|
||
}]
|
||
}
|
||
|
||
fn linear_like_props(
|
||
first: codec::types::Vector3,
|
||
second: codec::types::Vector3,
|
||
definition: codec::types::Vector3,
|
||
) -> Vec<crate::scene::model::object::Property> {
|
||
vec![
|
||
edit(t!("First X").as_ref(), "first_x", first.x),
|
||
edit(t!("First Y").as_ref(), "first_y", first.y),
|
||
edit(t!("First Z").as_ref(), "first_z", first.z),
|
||
edit(t!("Second X").as_ref(), "second_x", second.x),
|
||
edit(t!("Second Y").as_ref(), "second_y", second.y),
|
||
edit(t!("Second Z").as_ref(), "second_z", second.z),
|
||
edit(t!("Definition X").as_ref(), "definition_x", definition.x),
|
||
edit(t!("Definition Y").as_ref(), "definition_y", definition.y),
|
||
edit(t!("Definition Z").as_ref(), "definition_z", definition.z),
|
||
]
|
||
}
|
||
|
||
fn radius_like_props(
|
||
center: codec::types::Vector3,
|
||
point: codec::types::Vector3,
|
||
) -> Vec<crate::scene::model::object::Property> {
|
||
vec![
|
||
edit(t!("Center X").as_ref(), "center_x", center.x),
|
||
edit(t!("Center Y").as_ref(), "center_y", center.y),
|
||
edit(t!("Center Z").as_ref(), "center_z", center.z),
|
||
edit(t!("Point X").as_ref(), "point_x", point.x),
|
||
edit(t!("Point Y").as_ref(), "point_y", point.y),
|
||
edit(t!("Point Z").as_ref(), "point_z", point.z),
|
||
]
|
||
}
|
||
|
||
fn angular_props(
|
||
vertex: codec::types::Vector3,
|
||
first: codec::types::Vector3,
|
||
second: codec::types::Vector3,
|
||
definition: codec::types::Vector3,
|
||
) -> Vec<crate::scene::model::object::Property> {
|
||
vec![
|
||
edit(t!("Vertex X").as_ref(), "vertex_x", vertex.x),
|
||
edit(t!("Vertex Y").as_ref(), "vertex_y", vertex.y),
|
||
edit(t!("Vertex Z").as_ref(), "vertex_z", vertex.z),
|
||
edit(t!("First X").as_ref(), "first_x", first.x),
|
||
edit(t!("First Y").as_ref(), "first_y", first.y),
|
||
edit(t!("First Z").as_ref(), "first_z", first.z),
|
||
edit(t!("Second X").as_ref(), "second_x", second.x),
|
||
edit(t!("Second Y").as_ref(), "second_y", second.y),
|
||
edit(t!("Second Z").as_ref(), "second_z", second.z),
|
||
edit(t!("Definition X").as_ref(), "definition_x", definition.x),
|
||
edit(t!("Definition Y").as_ref(), "definition_y", definition.y),
|
||
edit(t!("Definition Z").as_ref(), "definition_z", definition.z),
|
||
]
|
||
}
|
||
|
||
fn apply_base_prop(base: &mut DimensionBase, field: &str, value: &str) -> bool {
|
||
match field {
|
||
"text" | "user_text" | "text_override" => {
|
||
set_dimension_text_override(base, if value.trim().is_empty() {
|
||
None
|
||
} else {
|
||
Some(value.to_string())
|
||
});
|
||
true
|
||
}
|
||
"style_name" => {
|
||
base.style_name = value.to_string();
|
||
reset_automatic_text_position(base);
|
||
true
|
||
}
|
||
// Editing the text position in the properties panel pins it to a
|
||
// user-defined location (stops following DIMTAD). See #94.
|
||
"text_x" => {
|
||
let changed = assign_f64(value, &mut base.text_middle_point.x);
|
||
base.text_user_positioned |= changed;
|
||
changed
|
||
}
|
||
"text_y" => {
|
||
let changed = assign_f64(value, &mut base.text_middle_point.y);
|
||
base.text_user_positioned |= changed;
|
||
changed
|
||
}
|
||
"text_z" => {
|
||
let changed = assign_f64(value, &mut base.text_middle_point.z);
|
||
base.text_user_positioned |= changed;
|
||
changed
|
||
}
|
||
"text_rotation" => assign_deg(value, &mut base.text_rotation),
|
||
"horizontal_direction" => assign_deg(value, &mut base.horizontal_direction),
|
||
"line_spacing_factor" => assign_f64(value, &mut base.line_spacing_factor),
|
||
_ => false,
|
||
}
|
||
}
|
||
|
||
fn assign_f64(value: &str, target: &mut f64) -> bool {
|
||
let Some(v) = parse_f64(value) else {
|
||
return false;
|
||
};
|
||
*target = v;
|
||
true
|
||
}
|
||
|
||
/// Parse a value entered in DEGREES and store it as radians. Dimension angle
|
||
/// fields are kept in radians internally but shown/edited in degrees, matching
|
||
/// arc.rs / text.rs so the properties panel reads consistently.
|
||
fn assign_deg(value: &str, target: &mut f64) -> bool {
|
||
let Some(v) = parse_f64(value) else {
|
||
return false;
|
||
};
|
||
*target = v.to_radians();
|
||
true
|
||
}
|
||
|
||
fn apply_geom_prop(dim: &mut Dimension, field: &str, value: &str) {
|
||
let ordinate_auto_text = match dim {
|
||
Dimension::Ordinate(ordinate) if !ordinate.base.text_user_positioned => {
|
||
Some(dimension_text_pos_f64(dim, None, 2.5, 1.0))
|
||
}
|
||
_ => None,
|
||
};
|
||
if let Dimension::Ordinate(ordinate) = dim {
|
||
match field {
|
||
"ordinate_rotation" => {
|
||
if let Some(angle) = parse_f64(value) {
|
||
ordinate.base.horizontal_direction = -angle.to_radians();
|
||
ordinate.refresh_measurement();
|
||
}
|
||
return;
|
||
}
|
||
"text_x" | "text_y" => {
|
||
let old_text = ordinate_auto_text.unwrap_or(ordinate.base.text_middle_point);
|
||
let mut new_text = old_text;
|
||
let changed = if field == "text_x" {
|
||
assign_f64(value, &mut new_text.x)
|
||
} else {
|
||
assign_f64(value, &mut new_text.y)
|
||
};
|
||
if changed {
|
||
let delta = new_text - old_text;
|
||
ordinate.leader_endpoint = ordinate.leader_endpoint + delta;
|
||
ordinate.base.text_middle_point = new_text;
|
||
ordinate.base.text_user_positioned = true;
|
||
}
|
||
return;
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
if apply_base_prop(dim.base_mut(), field, value) {
|
||
return;
|
||
}
|
||
match dim {
|
||
Dimension::Aligned(d) => apply_linear_fields_aligned(d, field, value),
|
||
Dimension::Linear(d) => apply_linear_fields_linear(d, field, value),
|
||
Dimension::Radius(d) => apply_radius_fields(d, field, value),
|
||
Dimension::Diameter(d) => apply_diameter_fields(d, field, value),
|
||
Dimension::Angular2Ln(d) => apply_angular2_fields(d, field, value),
|
||
Dimension::Angular3Pt(d) => apply_angular3_fields(d, field, value),
|
||
Dimension::Ordinate(d) => apply_ordinate_fields(d, field, value),
|
||
Dimension::Arc(d) => apply_arc_fields(d, field, value),
|
||
Dimension::LargeRadial(d) => apply_large_radial_fields(d, field, value),
|
||
}
|
||
let definition_point = dimension_definition_point(dim);
|
||
dim.base_mut().definition_point = definition_point;
|
||
dim.base_mut().actual_measurement = dim.measurement();
|
||
}
|
||
|
||
fn apply_linear_fields_aligned(d: &mut DimensionAligned, field: &str, value: &str) {
|
||
if field == "rotation" {
|
||
let Some(angle) = parse_f64(value).map(f64::to_radians) else {
|
||
return;
|
||
};
|
||
let old_angle = (d.second_point.y - d.first_point.y)
|
||
.atan2(d.second_point.x - d.first_point.x);
|
||
let delta = angle - old_angle;
|
||
let origin_x = d.first_point.x;
|
||
let origin_y = d.first_point.y;
|
||
let rotate = |point: &mut codec::types::Vector3| {
|
||
let x = point.x - origin_x;
|
||
let y = point.y - origin_y;
|
||
let (sin, cos) = delta.sin_cos();
|
||
point.x = origin_x + x * cos - y * sin;
|
||
point.y = origin_y + x * sin + y * cos;
|
||
};
|
||
rotate(&mut d.second_point);
|
||
rotate(&mut d.definition_point);
|
||
rotate(&mut d.base.definition_point);
|
||
rotate(&mut d.base.text_middle_point);
|
||
rotate(&mut d.base.insertion_point);
|
||
return;
|
||
}
|
||
apply_linear_common(
|
||
&mut d.first_point,
|
||
&mut d.second_point,
|
||
&mut d.definition_point,
|
||
field,
|
||
value,
|
||
);
|
||
// Only the oblique field touches ext_line_rotation — otherwise editing a
|
||
// coordinate (e.g. First X) would corrupt the dimension's obliquing. #181.
|
||
if field == "ext_line_rotation" {
|
||
let _ = assign_deg(value, &mut d.ext_line_rotation);
|
||
}
|
||
}
|
||
|
||
fn apply_linear_fields_linear(d: &mut DimensionLinear, field: &str, value: &str) {
|
||
apply_linear_common(
|
||
&mut d.first_point,
|
||
&mut d.second_point,
|
||
&mut d.definition_point,
|
||
field,
|
||
value,
|
||
);
|
||
match field {
|
||
"rotation" => {
|
||
let _ = assign_deg(value, &mut d.rotation);
|
||
}
|
||
"ext_line_rotation" => {
|
||
let _ = assign_deg(value, &mut d.ext_line_rotation);
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
fn apply_linear_common(
|
||
first: &mut codec::types::Vector3,
|
||
second: &mut codec::types::Vector3,
|
||
definition: &mut codec::types::Vector3,
|
||
field: &str,
|
||
value: &str,
|
||
) {
|
||
match field {
|
||
"first_x" => {
|
||
let _ = assign_f64(value, &mut first.x);
|
||
}
|
||
"first_y" => {
|
||
let _ = assign_f64(value, &mut first.y);
|
||
}
|
||
"first_z" => {
|
||
let _ = assign_f64(value, &mut first.z);
|
||
}
|
||
"second_x" => {
|
||
let _ = assign_f64(value, &mut second.x);
|
||
}
|
||
"second_y" => {
|
||
let _ = assign_f64(value, &mut second.y);
|
||
}
|
||
"second_z" => {
|
||
let _ = assign_f64(value, &mut second.z);
|
||
}
|
||
"definition_x" => {
|
||
let _ = assign_f64(value, &mut definition.x);
|
||
}
|
||
"definition_y" => {
|
||
let _ = assign_f64(value, &mut definition.y);
|
||
}
|
||
"definition_z" => {
|
||
let _ = assign_f64(value, &mut definition.z);
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
fn apply_radius_fields(d: &mut DimensionRadius, field: &str, value: &str) {
|
||
apply_radius_common(&mut d.angle_vertex, &mut d.definition_point, field, value);
|
||
if field == "leader_length" {
|
||
let _ = assign_f64(value, &mut d.leader_length);
|
||
}
|
||
}
|
||
|
||
fn apply_diameter_fields(d: &mut DimensionDiameter, field: &str, value: &str) {
|
||
apply_radius_common(&mut d.angle_vertex, &mut d.definition_point, field, value);
|
||
if field == "leader_length" {
|
||
let _ = assign_f64(value, &mut d.leader_length);
|
||
}
|
||
}
|
||
|
||
fn apply_radius_common(
|
||
center: &mut codec::types::Vector3,
|
||
point: &mut codec::types::Vector3,
|
||
field: &str,
|
||
value: &str,
|
||
) {
|
||
match field {
|
||
"center_x" => {
|
||
let _ = assign_f64(value, &mut center.x);
|
||
}
|
||
"center_y" => {
|
||
let _ = assign_f64(value, &mut center.y);
|
||
}
|
||
"center_z" => {
|
||
let _ = assign_f64(value, &mut center.z);
|
||
}
|
||
"point_x" => {
|
||
let _ = assign_f64(value, &mut point.x);
|
||
}
|
||
"point_y" => {
|
||
let _ = assign_f64(value, &mut point.y);
|
||
}
|
||
"point_z" => {
|
||
let _ = assign_f64(value, &mut point.z);
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
fn apply_angular2_fields(d: &mut DimensionAngular2Ln, field: &str, value: &str) {
|
||
apply_angular_common(
|
||
&mut d.angle_vertex,
|
||
&mut d.first_point,
|
||
&mut d.second_point,
|
||
&mut d.definition_point,
|
||
field,
|
||
value,
|
||
);
|
||
match field {
|
||
"dimension_arc_x" => {
|
||
let _ = assign_f64(value, &mut d.dimension_arc.x);
|
||
}
|
||
"dimension_arc_y" => {
|
||
let _ = assign_f64(value, &mut d.dimension_arc.y);
|
||
}
|
||
"dimension_arc_z" => {
|
||
let _ = assign_f64(value, &mut d.dimension_arc.z);
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
fn apply_angular3_fields(d: &mut DimensionAngular3Pt, field: &str, value: &str) {
|
||
apply_angular_common(
|
||
&mut d.angle_vertex,
|
||
&mut d.first_point,
|
||
&mut d.second_point,
|
||
&mut d.definition_point,
|
||
field,
|
||
value,
|
||
);
|
||
}
|
||
|
||
fn apply_angular_common(
|
||
vertex: &mut codec::types::Vector3,
|
||
first: &mut codec::types::Vector3,
|
||
second: &mut codec::types::Vector3,
|
||
definition: &mut codec::types::Vector3,
|
||
field: &str,
|
||
value: &str,
|
||
) {
|
||
match field {
|
||
"vertex_x" => {
|
||
let _ = assign_f64(value, &mut vertex.x);
|
||
}
|
||
"vertex_y" => {
|
||
let _ = assign_f64(value, &mut vertex.y);
|
||
}
|
||
"vertex_z" => {
|
||
let _ = assign_f64(value, &mut vertex.z);
|
||
}
|
||
"first_x" => {
|
||
let _ = assign_f64(value, &mut first.x);
|
||
}
|
||
"first_y" => {
|
||
let _ = assign_f64(value, &mut first.y);
|
||
}
|
||
"first_z" => {
|
||
let _ = assign_f64(value, &mut first.z);
|
||
}
|
||
"second_x" => {
|
||
let _ = assign_f64(value, &mut second.x);
|
||
}
|
||
"second_y" => {
|
||
let _ = assign_f64(value, &mut second.y);
|
||
}
|
||
"second_z" => {
|
||
let _ = assign_f64(value, &mut second.z);
|
||
}
|
||
"definition_x" => {
|
||
let _ = assign_f64(value, &mut definition.x);
|
||
}
|
||
"definition_y" => {
|
||
let _ = assign_f64(value, &mut definition.y);
|
||
}
|
||
"definition_z" => {
|
||
let _ = assign_f64(value, &mut definition.z);
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
fn apply_ordinate_fields(d: &mut DimensionOrdinate, field: &str, value: &str) {
|
||
match field {
|
||
"definition_x" => {
|
||
let _ = assign_f64(value, &mut d.definition_point.x);
|
||
}
|
||
"definition_y" => {
|
||
let _ = assign_f64(value, &mut d.definition_point.y);
|
||
}
|
||
"definition_z" => {
|
||
let _ = assign_f64(value, &mut d.definition_point.z);
|
||
}
|
||
"feature_x" => {
|
||
let _ = assign_f64(value, &mut d.feature_location.x);
|
||
}
|
||
"feature_y" => {
|
||
let _ = assign_f64(value, &mut d.feature_location.y);
|
||
}
|
||
"feature_z" => {
|
||
let _ = assign_f64(value, &mut d.feature_location.z);
|
||
}
|
||
"leader_x" => {
|
||
let _ = assign_f64(value, &mut d.leader_endpoint.x);
|
||
}
|
||
"leader_y" => {
|
||
let _ = assign_f64(value, &mut d.leader_endpoint.y);
|
||
}
|
||
"leader_z" => {
|
||
let _ = assign_f64(value, &mut d.leader_endpoint.z);
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
fn apply_arc_fields(d: &mut DimensionArc, field: &str, value: &str) {
|
||
apply_angular_common(
|
||
&mut d.center_point,
|
||
&mut d.first_extension_point,
|
||
&mut d.second_extension_point,
|
||
&mut d.definition_point,
|
||
field,
|
||
value,
|
||
);
|
||
match field {
|
||
"arc_start_parameter" => {
|
||
let _ = assign_deg(value, &mut d.arc_start_parameter);
|
||
}
|
||
"arc_end_parameter" => {
|
||
let _ = assign_deg(value, &mut d.arc_end_parameter);
|
||
}
|
||
"leader1_x" => {
|
||
let _ = assign_f64(value, &mut d.first_leader_point.x);
|
||
}
|
||
"leader1_y" => {
|
||
let _ = assign_f64(value, &mut d.first_leader_point.y);
|
||
}
|
||
"leader1_z" => {
|
||
let _ = assign_f64(value, &mut d.first_leader_point.z);
|
||
}
|
||
"leader2_x" => {
|
||
let _ = assign_f64(value, &mut d.second_leader_point.x);
|
||
}
|
||
"leader2_y" => {
|
||
let _ = assign_f64(value, &mut d.second_leader_point.y);
|
||
}
|
||
"leader2_z" => {
|
||
let _ = assign_f64(value, &mut d.second_leader_point.z);
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
fn apply_large_radial_fields(d: &mut DimensionLargeRadial, field: &str, value: &str) {
|
||
if field == "large_radial_rotation" {
|
||
let Some(angle) = parse_f64(value).map(f64::to_radians) else {
|
||
return;
|
||
};
|
||
let delta = angle - large_radial_rotation(d);
|
||
let origin = d.definition_point;
|
||
let rotate = |point: &mut codec::types::Vector3| {
|
||
let x = point.x - origin.x;
|
||
let y = point.y - origin.y;
|
||
let (sin, cos) = delta.sin_cos();
|
||
point.x = origin.x + x * cos - y * sin;
|
||
point.y = origin.y + x * sin + y * cos;
|
||
};
|
||
rotate(&mut d.chord_point);
|
||
rotate(&mut d.override_center);
|
||
rotate(&mut d.jog_point);
|
||
rotate(&mut d.base.text_middle_point);
|
||
rotate(&mut d.base.insertion_point);
|
||
return;
|
||
}
|
||
match field {
|
||
"definition_x" => {
|
||
let _ = assign_f64(value, &mut d.definition_point.x);
|
||
}
|
||
"definition_y" => {
|
||
let _ = assign_f64(value, &mut d.definition_point.y);
|
||
}
|
||
"definition_z" => {
|
||
let _ = assign_f64(value, &mut d.definition_point.z);
|
||
}
|
||
"chord_x" => {
|
||
let _ = assign_f64(value, &mut d.chord_point.x);
|
||
}
|
||
"chord_y" => {
|
||
let _ = assign_f64(value, &mut d.chord_point.y);
|
||
}
|
||
"chord_z" => {
|
||
let _ = assign_f64(value, &mut d.chord_point.z);
|
||
}
|
||
"override_x" => {
|
||
let _ = assign_f64(value, &mut d.override_center.x);
|
||
}
|
||
"override_y" => {
|
||
let _ = assign_f64(value, &mut d.override_center.y);
|
||
}
|
||
"override_z" => {
|
||
let _ = assign_f64(value, &mut d.override_center.z);
|
||
}
|
||
"jog_x" => {
|
||
let _ = assign_f64(value, &mut d.jog_point.x);
|
||
}
|
||
"jog_y" => {
|
||
let _ = assign_f64(value, &mut d.jog_point.y);
|
||
}
|
||
"jog_z" => {
|
||
let _ = assign_f64(value, &mut d.jog_point.z);
|
||
}
|
||
"jog_angle" => {
|
||
let _ = assign_deg(value, &mut d.jog_angle);
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
fn large_radial_rotation(d: &DimensionLargeRadial) -> f64 {
|
||
let delta = d.chord_point - d.definition_point;
|
||
if delta.x.abs() <= 1.0e-12 && delta.y.abs() <= 1.0e-12 {
|
||
let visible = d.chord_point - d.override_center;
|
||
visible.y.atan2(visible.x)
|
||
} else {
|
||
delta.y.atan2(delta.x)
|
||
}
|
||
}
|
||
|
||
fn apply_transform(dim: &mut Dimension, t: &EntityTransform) {
|
||
crate::scene::view::transform::apply_standard_entity_transform(dim, t, |entity, p1, p2| {
|
||
codec::Entity::apply_transform(
|
||
entity,
|
||
&crate::scene::view::transform::reflection_about_xy_line(p1, p2),
|
||
);
|
||
});
|
||
}
|
||
|
||
impl PropertyEditable for Dimension {
|
||
fn geometry_properties(&self, _text_style_names: &[String]) -> Vec<PropSection> {
|
||
properties(self)
|
||
}
|
||
|
||
fn apply_geom_prop(&mut self, field: &str, value: &str) {
|
||
apply_geom_prop(self, field, value);
|
||
}
|
||
}
|
||
|
||
impl Transformable for Dimension {
|
||
fn apply_transform(&mut self, t: &EntityTransform) {
|
||
apply_transform(self, t);
|
||
}
|
||
}
|
||
|
||
// ── Grippable ─────────────────────────────────────────────────────────────────
|
||
|
||
/// f64 variant for grip positions — grips must not round UTM-scale
|
||
/// coordinates through f32 before the world-offset subtraction.
|
||
fn dv3(v: &codec::types::Vector3) -> glam::DVec3 {
|
||
glam::DVec3::new(v.x, v.y, v.z)
|
||
}
|
||
|
||
fn set_v3(target: &mut codec::types::Vector3, p: DVec3) {
|
||
target.x = p.x;
|
||
target.y = p.y;
|
||
target.z = p.z;
|
||
}
|
||
|
||
fn translate_v3(target: &mut codec::types::Vector3, d: DVec3) {
|
||
target.x += d.x;
|
||
target.y += d.y;
|
||
target.z += d.z;
|
||
}
|
||
|
||
fn apply_to_v3(target: &mut codec::types::Vector3, apply: &GripApply) {
|
||
match apply {
|
||
GripApply::Absolute(p) => set_v3(target, *p),
|
||
GripApply::Translate(d) => translate_v3(target, *d),
|
||
}
|
||
}
|
||
|
||
|
||
|
||
#[derive(Clone, Copy)]
|
||
struct DimTextRelativePosition {
|
||
along_fraction: f64,
|
||
perpendicular_offset: f64,
|
||
z_offset: f64,
|
||
}
|
||
|
||
fn capture_dim_text_relative_position(dim: &Dimension) -> Option<DimTextRelativePosition> {
|
||
let base = dim.base();
|
||
|
||
// Auto-positioned text is recomputed by the renderer from DIMSTYLE.
|
||
// Only an explicitly moved text point needs to follow a grip deformation.
|
||
if !base.text_user_positioned {
|
||
return None;
|
||
}
|
||
|
||
let text = base.text_middle_point;
|
||
if text.x * text.x + text.y * text.y + text.z * text.z <= 1e-16 {
|
||
return None;
|
||
}
|
||
|
||
let (first, second, defpt, ax, ay) = match dim {
|
||
Dimension::Linear(d) => (
|
||
d.first_point,
|
||
d.second_point,
|
||
d.definition_point,
|
||
d.rotation.cos(),
|
||
d.rotation.sin(),
|
||
),
|
||
Dimension::Aligned(d) => {
|
||
let dx = d.second_point.x - d.first_point.x;
|
||
let dy = d.second_point.y - d.first_point.y;
|
||
let len = (dx * dx + dy * dy).sqrt();
|
||
if len <= 1e-12 {
|
||
return None;
|
||
}
|
||
|
||
(
|
||
d.first_point,
|
||
d.second_point,
|
||
d.definition_point,
|
||
dx / len,
|
||
dy / len,
|
||
)
|
||
}
|
||
Dimension::Diameter(d) => {
|
||
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();
|
||
|
||
if len <= 1e-12 {
|
||
return None;
|
||
}
|
||
|
||
(
|
||
d.angle_vertex,
|
||
d.definition_point,
|
||
d.angle_vertex,
|
||
dx / len,
|
||
dy / len,
|
||
)
|
||
}
|
||
_ => return None,
|
||
};
|
||
|
||
let px = -ay;
|
||
let py = ax;
|
||
|
||
let t1 = (first.x - defpt.x) * ax + (first.y - defpt.y) * ay;
|
||
let t2 = (second.x - defpt.x) * ax + (second.y - defpt.y) * ay;
|
||
let text_t = (text.x - defpt.x) * ax + (text.y - defpt.y) * ay;
|
||
|
||
let span = t2 - t1;
|
||
let along_fraction = if span.abs() > 1e-12 {
|
||
(text_t - t1) / span
|
||
} else {
|
||
0.5
|
||
};
|
||
|
||
let along = t1 + span * along_fraction;
|
||
let line_x = defpt.x + ax * along;
|
||
let line_y = defpt.y + ay * along;
|
||
|
||
let perpendicular_offset =
|
||
(text.x - line_x) * px + (text.y - line_y) * py;
|
||
|
||
Some(DimTextRelativePosition {
|
||
along_fraction,
|
||
perpendicular_offset,
|
||
z_offset: text.z - defpt.z,
|
||
})
|
||
}
|
||
|
||
fn restore_dim_text_relative_position(
|
||
dim: &mut Dimension,
|
||
saved: DimTextRelativePosition,
|
||
) {
|
||
let (first, second, defpt, ax, ay) = match dim {
|
||
Dimension::Linear(d) => (
|
||
d.first_point,
|
||
d.second_point,
|
||
d.definition_point,
|
||
d.rotation.cos(),
|
||
d.rotation.sin(),
|
||
),
|
||
Dimension::Aligned(d) => {
|
||
let dx = d.second_point.x - d.first_point.x;
|
||
let dy = d.second_point.y - d.first_point.y;
|
||
let len = (dx * dx + dy * dy).sqrt();
|
||
if len <= 1e-12 {
|
||
return;
|
||
}
|
||
|
||
(
|
||
d.first_point,
|
||
d.second_point,
|
||
d.definition_point,
|
||
dx / len,
|
||
dy / len,
|
||
)
|
||
}
|
||
Dimension::Diameter(d) => {
|
||
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();
|
||
|
||
if len <= 1e-12 {
|
||
return;
|
||
}
|
||
|
||
(
|
||
d.angle_vertex,
|
||
d.definition_point,
|
||
d.angle_vertex,
|
||
dx / len,
|
||
dy / len,
|
||
)
|
||
}
|
||
_ => return,
|
||
};
|
||
|
||
let px = -ay;
|
||
let py = ax;
|
||
|
||
let t1 = (first.x - defpt.x) * ax + (first.y - defpt.y) * ay;
|
||
let t2 = (second.x - defpt.x) * ax + (second.y - defpt.y) * ay;
|
||
|
||
let along = t1 + (t2 - t1) * saved.along_fraction;
|
||
|
||
let base = dim.base_mut();
|
||
base.text_middle_point = Vector3::new(
|
||
defpt.x + ax * along + px * saved.perpendicular_offset,
|
||
defpt.y + ay * along + py * saved.perpendicular_offset,
|
||
defpt.z + saved.z_offset,
|
||
);
|
||
}
|
||
|
||
fn dimension_line_grip_position(dim: &Dimension) -> Option<DVec3> {
|
||
match dim {
|
||
Dimension::Angular2Ln(d) => return Some(dv3(&d.dimension_arc)),
|
||
Dimension::Angular3Pt(d) => return Some(dv3(&d.definition_point)),
|
||
Dimension::Arc(d) => return Some(dv3(&d.definition_point)),
|
||
_ => {}
|
||
}
|
||
let (first, second, defpt, ax, ay) = match dim {
|
||
Dimension::Linear(d) => (
|
||
d.first_point,
|
||
d.second_point,
|
||
d.definition_point,
|
||
d.rotation.cos(),
|
||
d.rotation.sin(),
|
||
),
|
||
Dimension::Aligned(d) => {
|
||
let dx = d.second_point.x - d.first_point.x;
|
||
let dy = d.second_point.y - d.first_point.y;
|
||
let len = (dx * dx + dy * dy).sqrt();
|
||
|
||
if len <= 1e-12 {
|
||
return None;
|
||
}
|
||
|
||
(
|
||
d.first_point,
|
||
d.second_point,
|
||
d.definition_point,
|
||
dx / len,
|
||
dy / len,
|
||
)
|
||
}
|
||
_ => return None,
|
||
};
|
||
|
||
let px = -ay;
|
||
let py = ax;
|
||
|
||
// Project both extension origins onto the current dimension line.
|
||
let off1 =
|
||
(defpt.x - first.x) * px + (defpt.y - first.y) * py;
|
||
let off2 =
|
||
(defpt.x - second.x) * px + (defpt.y - second.y) * py;
|
||
|
||
let p1 = DVec3::new(
|
||
first.x + px * off1,
|
||
first.y + py * off1,
|
||
defpt.z,
|
||
);
|
||
|
||
let p2 = DVec3::new(
|
||
second.x + px * off2,
|
||
second.y + py * off2,
|
||
defpt.z,
|
||
);
|
||
|
||
Some((p1 + p2) * 0.5)
|
||
}
|
||
|
||
fn above_dimension_text_position(dim: &Dimension) -> Option<DVec3> {
|
||
if let Some((vertex, start, end, radius)) = angular_dimension_frame(dim) {
|
||
let angle = (start + end) * 0.5;
|
||
let direction = DVec3::new(angle.cos() as f64, angle.sin() as f64, 0.0);
|
||
return Some(
|
||
DVec3::new(vertex.x as f64, vertex.y as f64, vertex.z as f64)
|
||
+ direction * (radius as f64 + 1.0),
|
||
);
|
||
}
|
||
let center = dimension_line_grip_position(dim)?;
|
||
let (ax, ay) = match dim {
|
||
Dimension::Linear(d) => (d.rotation.cos(), d.rotation.sin()),
|
||
Dimension::Aligned(d) => {
|
||
let dx = d.second_point.x - d.first_point.x;
|
||
let dy = d.second_point.y - d.first_point.y;
|
||
let length = (dx * dx + dy * dy).sqrt();
|
||
if length <= 1e-12 {
|
||
return None;
|
||
}
|
||
(dx / length, dy / length)
|
||
}
|
||
_ => return Some(center + DVec3::Y),
|
||
};
|
||
Some(center + DVec3::new(-ay, ax, 0.0))
|
||
}
|
||
|
||
impl Grippable for Dimension {
|
||
fn grips(&self) -> Vec<GripDef> {
|
||
// Auto-placed dimensions carry a zero text_middle_point sentinel; put
|
||
// the text grip at the style-default placement (default metrics) instead
|
||
// of the world origin, so it stays on the visible text and grabbable.
|
||
let text = {
|
||
let p = self.base().text_middle_point;
|
||
if p.x * p.x + p.y * p.y + p.z * p.z > 1e-16 {
|
||
dv3(&p)
|
||
} else {
|
||
dv3(&dimension_text_pos_f64(self, None, 2.5, 1.0))
|
||
}
|
||
};
|
||
match self {
|
||
Dimension::Linear(d) => vec![
|
||
square_grip(0, dv3(&d.first_point)),
|
||
center_grip(1, dv3(&d.second_point)),
|
||
center_grip(
|
||
2,
|
||
dimension_line_grip_position(self)
|
||
.unwrap_or_else(|| dv3(&d.definition_point)),
|
||
),
|
||
center_grip(3, text),
|
||
],
|
||
Dimension::Aligned(d) => vec![
|
||
square_grip(0, dv3(&d.first_point)),
|
||
center_grip(1, dv3(&d.second_point)),
|
||
center_grip(
|
||
2,
|
||
dimension_line_grip_position(self)
|
||
.unwrap_or_else(|| dv3(&d.definition_point)),
|
||
),
|
||
center_grip(3, text),
|
||
],
|
||
Dimension::Radius(d) => vec![
|
||
square_grip(0, dv3(&d.angle_vertex)),
|
||
center_grip(1, dv3(&d.definition_point)),
|
||
center_grip(2, text),
|
||
],
|
||
Dimension::Diameter(d) => vec![
|
||
square_grip(0, dv3(&d.angle_vertex)),
|
||
center_grip(1, dv3(&d.definition_point)),
|
||
center_grip(2, text),
|
||
],
|
||
Dimension::Angular2Ln(d) => vec![
|
||
square_grip(0, dv3(&d.first_point)),
|
||
center_grip(1, dv3(&d.second_point)),
|
||
center_grip(2, dv3(&d.angle_vertex)),
|
||
center_grip(3, dv3(&d.definition_point)),
|
||
center_grip(4, dv3(&d.dimension_arc)),
|
||
center_grip(5, text),
|
||
],
|
||
Dimension::Angular3Pt(d) => vec![
|
||
square_grip(0, dv3(&d.angle_vertex)),
|
||
center_grip(1, dv3(&d.first_point)),
|
||
center_grip(2, dv3(&d.second_point)),
|
||
center_grip(3, dv3(&d.definition_point)),
|
||
center_grip(4, text),
|
||
],
|
||
Dimension::Ordinate(d) => vec![
|
||
square_grip(0, dv3(&d.feature_location)),
|
||
center_grip(1, dv3(&d.leader_endpoint)),
|
||
center_grip(2, text),
|
||
],
|
||
Dimension::Arc(d) => {
|
||
let mut grips = vec![
|
||
square_grip(0, dv3(&d.center_point)),
|
||
center_grip(1, dv3(&d.first_extension_point)),
|
||
center_grip(2, dv3(&d.second_extension_point)),
|
||
center_grip(3, dv3(&d.definition_point)),
|
||
];
|
||
if d.has_leader {
|
||
grips.push(center_grip(4, dv3(&d.first_leader_point)));
|
||
grips.push(center_grip(5, dv3(&d.second_leader_point)));
|
||
grips.push(center_grip(6, text));
|
||
} else {
|
||
grips.push(center_grip(4, text));
|
||
}
|
||
grips
|
||
}
|
||
Dimension::LargeRadial(d) => vec![
|
||
square_grip(0, dv3(&d.definition_point)),
|
||
center_grip(1, dv3(&d.chord_point)),
|
||
center_grip(2, dv3(&d.override_center)),
|
||
center_grip(3, dv3(&d.jog_point)),
|
||
center_grip(4, text),
|
||
],
|
||
}
|
||
}
|
||
|
||
|
||
|
||
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
|
||
if matches!(self, Dimension::Ordinate(_)) && grip_id == 2 {
|
||
let old_text = dimension_text_pos_f64(self, None, 2.5, 1.0);
|
||
let mut new_text = old_text;
|
||
apply_to_v3(&mut new_text, &apply);
|
||
let delta = new_text - old_text;
|
||
if let Dimension::Ordinate(d) = self {
|
||
d.leader_endpoint = d.leader_endpoint + delta;
|
||
d.base.text_middle_point = new_text;
|
||
d.base.text_user_positioned = true;
|
||
d.refresh_measurement();
|
||
}
|
||
return;
|
||
}
|
||
// Last grip always moves the text.
|
||
let text_grip = match self {
|
||
Dimension::Linear(_) | Dimension::Aligned(_) => 3,
|
||
Dimension::Radius(_) | Dimension::Diameter(_) => 2,
|
||
Dimension::Angular2Ln(_) => 5,
|
||
Dimension::Angular3Pt(_) => 4,
|
||
Dimension::Ordinate(_) => 2,
|
||
Dimension::Arc(d) => if d.has_leader { 6 } else { 4 },
|
||
Dimension::LargeRadial(_) => 4,
|
||
};
|
||
if grip_id == text_grip {
|
||
apply_to_v3(&mut self.base_mut().text_middle_point, &apply);
|
||
|
||
// A diametric dimension must remain collinear when its text/leader
|
||
// grip is moved. Rotate both diameter endpoints around the fixed
|
||
// center so the leader and diameter share the same radial axis.
|
||
if let Dimension::Diameter(d) = self {
|
||
let center = d.center();
|
||
let radius = d.measurement() * 0.5;
|
||
let offset = d.base.text_middle_point - center;
|
||
|
||
if offset.length_squared() > 1e-24 && radius > 1e-12 {
|
||
let radial = offset.normalize() * radius;
|
||
|
||
// Keep the endpoint nearest the text on the text side.
|
||
d.angle_vertex = center + radial;
|
||
d.definition_point = center - radial;
|
||
d.base.definition_point = d.definition_point;
|
||
d.base.actual_measurement = d.measurement();
|
||
}
|
||
}
|
||
|
||
// Dragging the text grip pins it to a user-defined location, so it
|
||
// no longer follows the style (DIMTAD). See #94.
|
||
self.base_mut().text_user_positioned = true;
|
||
return;
|
||
}
|
||
|
||
// A manually positioned dimension text point is stored in the DWG as an
|
||
// absolute coordinate. Capture its relation to the old dimension geometry
|
||
// before moving a definition grip so that it can be reconstructed against
|
||
// the new geometry afterwards.
|
||
let relative_text = capture_dim_text_relative_position(self);
|
||
|
||
match self {
|
||
Dimension::Linear(d) => match grip_id {
|
||
0 => apply_to_v3(&mut d.first_point, &apply),
|
||
1 => apply_to_v3(&mut d.second_point, &apply),
|
||
2 => apply_to_v3(&mut d.definition_point, &apply),
|
||
_ => {}
|
||
},
|
||
Dimension::Aligned(d) => match grip_id {
|
||
0 => apply_to_v3(&mut d.first_point, &apply),
|
||
1 => apply_to_v3(&mut d.second_point, &apply),
|
||
2 => apply_to_v3(&mut d.definition_point, &apply),
|
||
_ => {}
|
||
},
|
||
Dimension::Radius(d) => match grip_id {
|
||
0 => apply_to_v3(&mut d.angle_vertex, &apply),
|
||
1 => apply_to_v3(&mut d.definition_point, &apply),
|
||
_ => {}
|
||
},
|
||
Dimension::Diameter(d) => {
|
||
let center = d.center();
|
||
let radius = d.measurement() * 0.5;
|
||
let mut target = match grip_id {
|
||
0 => d.angle_vertex,
|
||
1 => d.definition_point,
|
||
_ => center,
|
||
};
|
||
if grip_id <= 1 {
|
||
apply_to_v3(&mut target, &apply);
|
||
let offset = target - center;
|
||
if offset.length_squared() > 1e-24 && radius > 1e-12 {
|
||
let radial = offset.normalize() * radius;
|
||
if grip_id == 0 {
|
||
d.angle_vertex = center + radial;
|
||
d.definition_point = center - radial;
|
||
} else {
|
||
d.definition_point = center + radial;
|
||
d.angle_vertex = center - radial;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
Dimension::Angular2Ln(d) => match grip_id {
|
||
0 => apply_to_v3(&mut d.first_point, &apply),
|
||
1 => apply_to_v3(&mut d.second_point, &apply),
|
||
2 => apply_to_v3(&mut d.angle_vertex, &apply),
|
||
3 => apply_to_v3(&mut d.definition_point, &apply),
|
||
4 => apply_to_v3(&mut d.dimension_arc, &apply),
|
||
_ => {}
|
||
},
|
||
Dimension::Angular3Pt(d) => match grip_id {
|
||
0 => apply_to_v3(&mut d.angle_vertex, &apply),
|
||
1 => apply_to_v3(&mut d.first_point, &apply),
|
||
2 => apply_to_v3(&mut d.second_point, &apply),
|
||
3 => apply_to_v3(&mut d.definition_point, &apply),
|
||
_ => {}
|
||
},
|
||
Dimension::Ordinate(d) => match grip_id {
|
||
0 => apply_to_v3(&mut d.feature_location, &apply),
|
||
1 => {
|
||
let old = d.leader_endpoint;
|
||
apply_to_v3(&mut d.leader_endpoint, &apply);
|
||
if d.base.text_user_positioned {
|
||
d.base.text_middle_point =
|
||
d.base.text_middle_point + (d.leader_endpoint - old);
|
||
}
|
||
}
|
||
_ => {}
|
||
},
|
||
Dimension::Arc(d) => match grip_id {
|
||
0 => apply_to_v3(&mut d.center_point, &apply),
|
||
1 => apply_to_v3(&mut d.first_extension_point, &apply),
|
||
2 => apply_to_v3(&mut d.second_extension_point, &apply),
|
||
3 => apply_to_v3(&mut d.definition_point, &apply),
|
||
4 => apply_to_v3(&mut d.first_leader_point, &apply),
|
||
5 => apply_to_v3(&mut d.second_leader_point, &apply),
|
||
_ => {}
|
||
},
|
||
Dimension::LargeRadial(d) => match grip_id {
|
||
0 => apply_to_v3(&mut d.definition_point, &apply),
|
||
1 => apply_to_v3(&mut d.chord_point, &apply),
|
||
2 => apply_to_v3(&mut d.override_center, &apply),
|
||
3 => apply_to_v3(&mut d.jog_point, &apply),
|
||
_ => {}
|
||
},
|
||
}
|
||
|
||
if let Some(saved) = relative_text {
|
||
restore_dim_text_relative_position(self, saved);
|
||
}
|
||
|
||
let definition_point = dimension_definition_point(self);
|
||
self.base_mut().definition_point = definition_point;
|
||
self.base_mut().actual_measurement = self.measurement();
|
||
}
|
||
|
||
fn grip_menu(&self, grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
|
||
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
|
||
let (dim_line_grip, text_grip) = match self {
|
||
Dimension::Linear(_) | Dimension::Aligned(_) => (2, 3),
|
||
Dimension::Radius(_) | Dimension::Diameter(_) => (1, 2),
|
||
Dimension::Angular2Ln(_) => (4, 5),
|
||
Dimension::Angular3Pt(_) => (3, 4),
|
||
Dimension::Ordinate(_) => (1, 2),
|
||
Dimension::Arc(d) => (3, if d.has_leader { 6 } else { 4 }),
|
||
Dimension::LargeRadial(_) => (3, 4),
|
||
};
|
||
if grip_id == text_grip {
|
||
vec![
|
||
GripMenuItem {
|
||
label: "Stretch",
|
||
action: GripMenuAction::Stretch,
|
||
},
|
||
GripMenuItem {
|
||
label: "Move with Dim Line",
|
||
action: GripMenuAction::MoveWithDimLine,
|
||
},
|
||
GripMenuItem {
|
||
label: "Move with Leader",
|
||
action: GripMenuAction::MoveWithLeader,
|
||
},
|
||
GripMenuItem {
|
||
label: "Move Independent",
|
||
action: GripMenuAction::MoveIndependent,
|
||
},
|
||
GripMenuItem {
|
||
label: "Reset Text",
|
||
action: GripMenuAction::ResetText,
|
||
},
|
||
GripMenuItem {
|
||
label: "Rotate Text",
|
||
action: GripMenuAction::RotateText,
|
||
},
|
||
GripMenuItem {
|
||
label: "Above Dim Line",
|
||
action: GripMenuAction::AboveDimLine,
|
||
},
|
||
GripMenuItem {
|
||
label: "Center",
|
||
action: GripMenuAction::Center,
|
||
},
|
||
]
|
||
} else if matches!(self, Dimension::Ordinate(_)) {
|
||
vec![GripMenuItem {
|
||
label: "Stretch",
|
||
action: GripMenuAction::Stretch,
|
||
}]
|
||
} else if grip_id == dim_line_grip {
|
||
vec![
|
||
GripMenuItem {
|
||
label: "Stretch",
|
||
action: GripMenuAction::Stretch,
|
||
},
|
||
GripMenuItem {
|
||
label: "Reverse Arrows",
|
||
action: GripMenuAction::ReverseArrows,
|
||
},
|
||
]
|
||
} else {
|
||
vec![GripMenuItem {
|
||
label: "Stretch",
|
||
action: GripMenuAction::Stretch,
|
||
}]
|
||
}
|
||
}
|
||
|
||
fn apply_grip_menu(
|
||
&mut self,
|
||
grip_id: usize,
|
||
action: crate::scene::model::object::GripMenuAction,
|
||
) {
|
||
use crate::scene::model::object::GripMenuAction as A;
|
||
let (_dim_line_grip, text_grip) = match self {
|
||
Dimension::Linear(_) | Dimension::Aligned(_) => (2, 3),
|
||
Dimension::Radius(_) | Dimension::Diameter(_) => (1, 2),
|
||
Dimension::Angular2Ln(_) => (4, 5),
|
||
Dimension::Angular3Pt(_) => (3, 4),
|
||
Dimension::Ordinate(_) => (1, 2),
|
||
Dimension::Arc(d) => (3, if d.has_leader { 6 } else { 4 }),
|
||
Dimension::LargeRadial(_) => (3, 4),
|
||
};
|
||
match action {
|
||
A::ResetText if grip_id == text_grip => {
|
||
// Drop any text-position override — leave it to the
|
||
// renderer to recompute from the dim style.
|
||
let b = self.base_mut();
|
||
b.text_middle_point.x = 0.0;
|
||
b.text_middle_point.y = 0.0;
|
||
b.text_middle_point.z = 0.0;
|
||
b.text_user_positioned = false;
|
||
}
|
||
A::Center if grip_id == text_grip => {
|
||
if let Some(point) = dimension_line_grip_position(self) {
|
||
let base = self.base_mut();
|
||
base.text_middle_point = Vector3::new(point.x, point.y, point.z);
|
||
base.text_user_positioned = true;
|
||
}
|
||
}
|
||
A::ReverseArrows => {
|
||
let base = self.base_mut();
|
||
base.flip_arrow1 = !base.flip_arrow1;
|
||
base.flip_arrow2 = !base.flip_arrow2;
|
||
}
|
||
A::AboveDimLine if grip_id == text_grip => {
|
||
if let Some(point) = above_dimension_text_position(self) {
|
||
let base = self.base_mut();
|
||
base.text_middle_point = Vector3::new(point.x, point.y + 1.0, point.z);
|
||
base.text_user_positioned = true;
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
fn grip_menu_value_prompt(
|
||
&self,
|
||
grip_id: usize,
|
||
action: crate::scene::model::object::GripMenuAction,
|
||
) -> Option<&'static str> {
|
||
use crate::scene::model::object::GripMenuAction as A;
|
||
let text_grip = match self {
|
||
Dimension::Linear(_) | Dimension::Aligned(_) => 3,
|
||
Dimension::Radius(_) | Dimension::Diameter(_) => 2,
|
||
Dimension::Angular2Ln(_) => 5,
|
||
Dimension::Angular3Pt(_) => 4,
|
||
Dimension::Ordinate(_) => 2,
|
||
Dimension::Arc(d) => if d.has_leader { 6 } else { 4 },
|
||
Dimension::LargeRadial(_) => 4,
|
||
};
|
||
(grip_id == text_grip && matches!(action, A::RotateText))
|
||
.then_some("Specify text rotation")
|
||
}
|
||
|
||
fn apply_grip_menu_value(
|
||
&mut self,
|
||
grip_id: usize,
|
||
action: crate::scene::model::object::GripMenuAction,
|
||
value: f64,
|
||
) {
|
||
use crate::scene::model::object::GripMenuAction as A;
|
||
if self.grip_menu_value_prompt(grip_id, action).is_some()
|
||
&& matches!(action, A::RotateText)
|
||
{
|
||
self.base_mut().text_rotation = value.to_radians();
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Tessellation ─────────────────────────────────────────────────────────
|
||
//
|
||
// Per-entity tessellation entry for `Dimension`. The trait + impl live in
|
||
// this file so all dimension tess code stays alongside the entity
|
||
// definition. Shared dim machinery (`ArrowKind`, `DimGeom`, `append_arrow`,
|
||
// arrow blocks, colour resolution, `add_segment` / `add_polyline`,
|
||
// `normalized_or`, `entity_z`, `offset_snap_pts`) lives in
|
||
// `scene::convert::tessellate` and is reused by Leader / MultiLeader too.
|
||
|
||
use codec::entities::{MText, Text};
|
||
use codec::tables::DimStyle;
|
||
use codec::types::{Color as AcadColor, Vector3};
|
||
|
||
pub(crate) fn resolved_dimension_style(
|
||
source: &DimStyle,
|
||
dimension: &Dimension,
|
||
document: &CadDocument,
|
||
) -> DimStyle {
|
||
use crate::entities::dim_override as ov;
|
||
|
||
let mut style = source.clone();
|
||
let data = &dimension.base().common.extended_data;
|
||
macro_rules! real {
|
||
($field:ident, $code:ident) => {
|
||
if let Some(value) = ov::real(data, ov::$code) {
|
||
style.$field = value;
|
||
}
|
||
};
|
||
}
|
||
macro_rules! int {
|
||
($field:ident, $code:ident) => {
|
||
if let Some(value) = ov::int(data, ov::$code) {
|
||
style.$field = value;
|
||
}
|
||
};
|
||
}
|
||
macro_rules! flag {
|
||
($field:ident, $code:ident) => {
|
||
if let Some(value) = ov::int(data, ov::$code) {
|
||
style.$field = value != 0;
|
||
}
|
||
};
|
||
}
|
||
macro_rules! handle {
|
||
($field:ident, $code:ident) => {
|
||
if let Some(value) = ov::handle(data, ov::$code) {
|
||
style.$field = value;
|
||
}
|
||
};
|
||
}
|
||
|
||
real!(dimscale, DIMSCALE);
|
||
real!(dimasz, DIMASZ);
|
||
real!(dimexo, DIMEXO);
|
||
real!(dimdli, DIMDLI);
|
||
real!(dimexe, DIMEXE);
|
||
real!(dimrnd, DIMRND);
|
||
real!(dimdle, DIMDLE);
|
||
real!(dimtp, DIMTP);
|
||
real!(dimtm, DIMTM);
|
||
real!(dimfxl, DIMFXL);
|
||
real!(dimjogang, DIMJOGANG);
|
||
real!(dimtxt, DIMTXT);
|
||
real!(dimcen, DIMCEN);
|
||
real!(dimtsz, DIMTSZ);
|
||
real!(dimaltf, DIMALTF);
|
||
real!(dimlfac, DIMLFAC);
|
||
real!(dimtvp, DIMTVP);
|
||
real!(dimtfac, DIMTFAC);
|
||
real!(dimgap, DIMGAP);
|
||
real!(dimaltrnd, DIMALTRND);
|
||
real!(dimaltmzf, DIMALTMZF);
|
||
real!(dimmzf, DIMMZF);
|
||
flag!(dimtol, DIMTOL);
|
||
flag!(dimlim, DIMLIM);
|
||
flag!(dimtih, DIMTIH);
|
||
flag!(dimtoh, DIMTOH);
|
||
flag!(dimse1, DIMSE1);
|
||
flag!(dimse2, DIMSE2);
|
||
flag!(dimalt, DIMALT);
|
||
flag!(dimtofl, DIMTOFL);
|
||
flag!(dimsah, DIMSAH);
|
||
flag!(dimtix, DIMTIX);
|
||
flag!(dimsoxd, DIMSOXD);
|
||
flag!(dimsd1, DIMSD1);
|
||
flag!(dimsd2, DIMSD2);
|
||
flag!(dimupt, DIMUPT);
|
||
flag!(dimfxlon, DIMFXLON);
|
||
flag!(dimtxtdirection, DIMTXTDIRECTION);
|
||
int!(dimzin, DIMZIN);
|
||
int!(dimtad, DIMTAD);
|
||
int!(dimazin, DIMAZIN);
|
||
int!(dimarcsym, DIMARCSYM);
|
||
int!(dimclrd, DIMCLRD);
|
||
int!(dimclre, DIMCLRE);
|
||
int!(dimclrt, DIMCLRT);
|
||
int!(dimadec, DIMADEC);
|
||
int!(dimaltd, DIMALTD);
|
||
int!(dimdec, DIMDEC);
|
||
int!(dimtdec, DIMTDEC);
|
||
int!(dimaltu, DIMALTU);
|
||
int!(dimalttd, DIMALTTD);
|
||
int!(dimaunit, DIMAUNIT);
|
||
int!(dimfrac, DIMFRAC);
|
||
int!(dimlunit, DIMLUNIT);
|
||
int!(dimdsep, DIMDSEP);
|
||
int!(dimtmove, DIMTMOVE);
|
||
int!(dimjust, DIMJUST);
|
||
int!(dimtolj, DIMTOLJ);
|
||
int!(dimtzin, DIMTZIN);
|
||
int!(dimaltz, DIMALTZ);
|
||
int!(dimalttz, DIMALTTZ);
|
||
int!(dimatfit, DIMATFIT);
|
||
int!(dimtfill, DIMTFILL);
|
||
int!(dimtfillclr, DIMTFILLCLR);
|
||
int!(dimlwd, DIMLWD);
|
||
int!(dimlwe, DIMLWE);
|
||
handle!(dimldrblk, DIMLDRBLK);
|
||
handle!(dimblk, DIMBLK);
|
||
handle!(dimblk1, DIMBLK1);
|
||
handle!(dimblk2, DIMBLK2);
|
||
handle!(dimltex_handle, DIMLTYPE);
|
||
handle!(dimltex1_handle, DIMLTEX1);
|
||
handle!(dimltex2_handle, DIMLTEX2);
|
||
|
||
if let Some(value) = ov::string(data, ov::DIMPOST) {
|
||
style.dimpost = value;
|
||
}
|
||
if let Some(value) = ov::string(data, ov::DIMAPOST) {
|
||
style.dimapost = value;
|
||
}
|
||
if let Some(value) = ov::string(data, ov::DIMALTMZS) {
|
||
style.dimaltmzs = value;
|
||
}
|
||
if let Some(value) = ov::string(data, ov::DIMMZS) {
|
||
style.dimmzs = value;
|
||
}
|
||
if let Some(value) = ov::handle(data, ov::DIMTXSTY) {
|
||
style.dimtxsty_handle = value;
|
||
if let Some(record) = document.text_styles.iter().find(|record| record.handle == value) {
|
||
style.dimtxsty = record.name.clone();
|
||
}
|
||
}
|
||
|
||
// Negative DIMLFAC applies only in paper space, and there only to a
|
||
// dimension that measures model space through a viewport. The associative
|
||
// record stores the applied factor; zero marks paper-space geometry whose
|
||
// dimension reads its sheet distance.
|
||
if style.dimlfac < 0.0 {
|
||
style.dimlfac = match calculated_dimlfac(dimension) {
|
||
Some(calculated) if calculated.abs() < 1e-12 => 1.0,
|
||
Some(calculated) => calculated.abs(),
|
||
None => crate::scene::viewport_ref::MeasurementScale::user_lfac_for_space(
|
||
style.dimlfac,
|
||
dimension_in_paper_space(dimension, document),
|
||
),
|
||
};
|
||
}
|
||
style
|
||
}
|
||
|
||
/// The linear factor recorded when the dimension was last regenerated.
|
||
fn calculated_dimlfac(dimension: &Dimension) -> Option<f64> {
|
||
dimension
|
||
.base()
|
||
.common
|
||
.extended_data
|
||
.get_record("ACAD_DIMASSOC_CALC_DIMLFAC")
|
||
.and_then(|record| {
|
||
record.values.iter().find_map(|value| match value {
|
||
codec::xdata::XDataValue::Real(factor) if factor.is_finite() => Some(*factor),
|
||
_ => None,
|
||
})
|
||
})
|
||
}
|
||
|
||
/// Whether the dimension's owner is paper space, even without a Layout object.
|
||
pub(crate) fn dimension_in_paper_space(dimension: &Dimension, document: &CadDocument) -> bool {
|
||
let owner = dimension.base().common.owner_handle;
|
||
if !owner.is_valid() {
|
||
return false;
|
||
}
|
||
document.objects.values().any(|object| {
|
||
matches!(object,
|
||
codec::objects::ObjectType::Layout(layout)
|
||
if layout.name != "Model" && layout.block_record == owner
|
||
)
|
||
}) || document.block_records.iter().any(|block| {
|
||
block.handle == owner && block.name.to_ascii_uppercase().starts_with("*PAPER_SPACE")
|
||
})
|
||
}
|
||
|
||
/// The value a dimension's text reports for its measurement: the raw
|
||
/// geometric measurement times the resolved linear factor. Angular dimensions
|
||
/// never scale.
|
||
///
|
||
/// `actual_measurement` (DXF group 42) stays **raw** — that is what the format
|
||
/// specifies and what export writes — so this is the one helper UI surfaces
|
||
/// use when they want the number the user reads.
|
||
pub(crate) fn displayed_measurement(dimension: &Dimension, style: Option<&DimStyle>) -> f64 {
|
||
let raw = dimension.measurement();
|
||
if matches!(
|
||
dimension,
|
||
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_)
|
||
) {
|
||
return raw;
|
||
}
|
||
let lfac = style.map(|s| s.dimlfac).unwrap_or(1.0);
|
||
if lfac.abs() < 1e-12 {
|
||
raw
|
||
} else {
|
||
raw * lfac
|
||
}
|
||
}
|
||
|
||
/// Build the linear-dimension property groups from the assigned style plus
|
||
/// any entity-level DSTYLE overrides. Conditional rows remain visible but are
|
||
/// read-only while their controlling option is disabled.
|
||
pub fn style_sections(
|
||
style: &DimStyle,
|
||
dimension: &Dimension,
|
||
document: &CadDocument,
|
||
) -> Vec<PropSection> {
|
||
use crate::entities::dim_override as ov;
|
||
|
||
let effective_style = resolved_dimension_style(style, dimension, document);
|
||
let data = &dimension.base().common.extended_data;
|
||
let real = |code, inherited| ov::real(data, code).unwrap_or(inherited);
|
||
let int = |code, inherited| ov::int(data, code).unwrap_or(inherited);
|
||
let string = |code, inherited: &str| {
|
||
ov::string(data, code).unwrap_or_else(|| inherited.to_string())
|
||
};
|
||
let on = |value: bool| if value { "On" } else { "Off" };
|
||
let yes = |value: bool| if value { "Yes" } else { "No" };
|
||
let number = |label: &str, field: &'static str, value: f64, editable: bool| {
|
||
property(
|
||
label,
|
||
field,
|
||
if editable {
|
||
PropValue::EditText(format!("{value:.4}"))
|
||
} else {
|
||
PropValue::ReadOnly(format!("{value:.4}"))
|
||
},
|
||
)
|
||
};
|
||
let text = |label: &str, field: &'static str, value: String, editable: bool| {
|
||
property(
|
||
label,
|
||
field,
|
||
if editable {
|
||
PropValue::PlainText(value)
|
||
} else {
|
||
PropValue::ReadOnly(value)
|
||
},
|
||
)
|
||
};
|
||
let choice = |label: &str,
|
||
field: &'static str,
|
||
selected: &str,
|
||
options: &[&str],
|
||
editable: bool| {
|
||
property(
|
||
label,
|
||
field,
|
||
if editable {
|
||
choice_value(selected, options)
|
||
} else {
|
||
PropValue::ReadOnly(selected.to_string())
|
||
},
|
||
)
|
||
};
|
||
|
||
let s = &effective_style;
|
||
let dimfxlon = int(ov::DIMFXLON, s.dimfxlon as i16) != 0;
|
||
let dimtix = int(ov::DIMTIX, s.dimtix as i16) != 0;
|
||
let dimlunit = int(ov::DIMLUNIT, s.dimlunit);
|
||
let dimfrac = int(ov::DIMFRAC, s.dimfrac);
|
||
let dimalt = int(ov::DIMALT, s.dimalt as i16) != 0;
|
||
let dimtol = int(ov::DIMTOL, s.dimtol as i16) != 0;
|
||
let dimlim = int(ov::DIMLIM, s.dimlim as i16) != 0;
|
||
let dimtp = real(ov::DIMTP, s.dimtp);
|
||
let dimtm = real(ov::DIMTM, s.dimtm);
|
||
let dimgap = real(ov::DIMGAP, s.dimgap);
|
||
let tolerance_display = if dimgap < 0.0 {
|
||
"Basic"
|
||
} else if dimlim {
|
||
"Limits"
|
||
} else if dimtol && (dimtp - dimtm).abs() <= 1e-12 {
|
||
"Symmetrical"
|
||
} else if dimtol {
|
||
"Deviation"
|
||
} else {
|
||
"None"
|
||
};
|
||
let tolerance_enabled = matches!(
|
||
tolerance_display,
|
||
"Symmetrical" | "Deviation" | "Limits"
|
||
);
|
||
let alternate_tolerance_enabled = tolerance_enabled && dimalt;
|
||
let dimzin = int(ov::DIMZIN, s.dimzin);
|
||
let dimaltz = int(ov::DIMALTZ, s.dimaltz);
|
||
let dimtzin = int(ov::DIMTZIN, s.dimtzin);
|
||
let dimalttz = int(ov::DIMALTTZ, s.dimalttz);
|
||
let annotative = s.annotative
|
||
|| !crate::scene::annotative::object_scale_memberships(
|
||
document,
|
||
dimension.base().common.handle,
|
||
)
|
||
.is_empty();
|
||
|
||
let overridden_text_style = ov::handle(data, ov::DIMTXSTY);
|
||
let text_style_name = overridden_text_style
|
||
.and_then(|handle| {
|
||
document
|
||
.text_styles
|
||
.iter()
|
||
.find(|record| record.handle == handle)
|
||
.map(|record| record.name.clone())
|
||
})
|
||
.unwrap_or_else(|| s.dimtxsty.clone());
|
||
let text_height_editable = document
|
||
.text_styles
|
||
.iter()
|
||
.find(|record| {
|
||
overridden_text_style
|
||
.is_some_and(|handle| record.handle == handle)
|
||
|| (overridden_text_style.is_none()
|
||
&& record.name.eq_ignore_ascii_case(&text_style_name))
|
||
})
|
||
.is_none_or(|record| !record.has_fixed_height());
|
||
|
||
let (dim_prefix, dim_suffix) =
|
||
split_measurement_template(&string(ov::DIMPOST, &s.dimpost));
|
||
let (alt_prefix, alt_suffix) =
|
||
split_measurement_template(&string(ov::DIMAPOST, &s.dimapost));
|
||
let decimal_separator = {
|
||
let value = int(ov::DIMDSEP, s.dimdsep);
|
||
let character = value as u8 as char;
|
||
if value > 0 && !character.is_control() {
|
||
character.to_string()
|
||
} else {
|
||
value.to_string()
|
||
}
|
||
};
|
||
|
||
let mut arrow_options: Vec<String> = std::iter::once("Closed filled".to_string())
|
||
.chain(
|
||
document
|
||
.block_records
|
||
.iter()
|
||
.map(|record| record.name.clone())
|
||
.filter(|name| !name.is_empty()),
|
||
)
|
||
.collect();
|
||
let mut linetype_options: Vec<String> = document
|
||
.line_types
|
||
.iter()
|
||
.map(|line_type| line_type.name.clone())
|
||
.filter(|name| !name.is_empty())
|
||
.collect();
|
||
let text_style_options: Vec<String> = document
|
||
.text_styles
|
||
.iter()
|
||
.map(|record| record.name.clone())
|
||
.filter(|name| !name.is_empty())
|
||
.collect();
|
||
|
||
let arrow_name = |code, inherited_handle, inherited_name: &str| {
|
||
ov::handle(data, code)
|
||
.map(|handle| block_name(document, handle, inherited_name))
|
||
.unwrap_or_else(|| block_name(document, inherited_handle, inherited_name))
|
||
};
|
||
let linetype = |code, inherited| {
|
||
linetype_name(document, ov::handle(data, code).unwrap_or(inherited))
|
||
};
|
||
let arrow_1 = if matches!(dimension, Dimension::Radius(_) | Dimension::LargeRadial(_)) {
|
||
block_name(
|
||
document,
|
||
ov::handle(data, ov::DIMLDRBLK).unwrap_or(s.dimldrblk),
|
||
"Closed filled",
|
||
)
|
||
} else {
|
||
let inherited = if matches!(dimension, Dimension::Diameter(_)) && !s.dimsah {
|
||
(s.dimblk, s.dimblk_name.as_str())
|
||
} else {
|
||
(s.dimblk1, s.dimblk1_name.as_str())
|
||
};
|
||
arrow_name(ov::DIMBLK1, inherited.0, inherited.1)
|
||
};
|
||
let arrow_2 = if matches!(dimension, Dimension::Diameter(_)) && !s.dimsah {
|
||
arrow_name(ov::DIMBLK2, s.dimblk, &s.dimblk_name)
|
||
} else {
|
||
arrow_name(ov::DIMBLK2, s.dimblk2, &s.dimblk2_name)
|
||
};
|
||
for current in [&arrow_1, &arrow_2] {
|
||
if !arrow_options.contains(current) {
|
||
arrow_options.push(current.clone());
|
||
}
|
||
}
|
||
for current in [
|
||
linetype(ov::DIMLTYPE, s.dimltex_handle),
|
||
linetype(ov::DIMLTEX1, s.dimltex1_handle),
|
||
linetype(ov::DIMLTEX2, s.dimltex2_handle),
|
||
] {
|
||
if !linetype_options.contains(¤t) {
|
||
linetype_options.push(current);
|
||
}
|
||
}
|
||
|
||
let precision_options: Vec<String> = (0..=8).map(precision_label).collect();
|
||
let linear_units = [
|
||
"Scientific",
|
||
"Decimal",
|
||
"Engineering",
|
||
"Architectural",
|
||
"Fractional",
|
||
"Desktop",
|
||
];
|
||
let alternate_unit_options = [
|
||
"Scientific",
|
||
"Decimal",
|
||
"Engineering",
|
||
"Architectural stacked",
|
||
"Fractional stacked",
|
||
"Architectural",
|
||
"Fractional",
|
||
"Desktop",
|
||
];
|
||
|
||
let fill_mode = int(ov::DIMTFILL, s.dimtfill);
|
||
let fill_color = ov::color(data, ov::DIMTFILLCLR)
|
||
.unwrap_or_else(|| AcadColor::from_index(s.dimtfillclr));
|
||
let fill_value = match fill_mode {
|
||
1 => choice_value("Background", &["None", "Background", "Color"]),
|
||
2 => PropValue::ColorChoice(fill_color),
|
||
_ => choice_value("None", &["None", "Background", "Color"]),
|
||
};
|
||
|
||
let mut sections = vec![
|
||
PropSection {
|
||
title: t!("Lines & Arrows").into_owned(),
|
||
props: vec![
|
||
property(
|
||
t!("Arrow 1").as_ref(),
|
||
"dim_arrowhead_1",
|
||
PropValue::Choice {
|
||
selected: arrow_1,
|
||
options: arrow_options.clone(),
|
||
},
|
||
),
|
||
property(
|
||
t!("Arrow 2").as_ref(),
|
||
"dim_arrowhead_2",
|
||
PropValue::Choice {
|
||
selected: arrow_2,
|
||
options: arrow_options,
|
||
},
|
||
),
|
||
number(
|
||
t!("Arrow size").as_ref(),
|
||
"dim_arrow_size",
|
||
real(ov::DIMASZ, s.dimasz),
|
||
true,
|
||
),
|
||
property(
|
||
t!("Dim line lineweight").as_ref(),
|
||
"dim_line_lineweight",
|
||
PropValue::Choice {
|
||
selected: lineweight_label(int(ov::DIMLWD, s.dimlwd)),
|
||
options: lineweight_options(),
|
||
},
|
||
),
|
||
property(
|
||
t!("Ext line lineweight").as_ref(),
|
||
"dim_ext_line_lineweight",
|
||
PropValue::Choice {
|
||
selected: lineweight_label(int(ov::DIMLWE, s.dimlwe)),
|
||
options: lineweight_options(),
|
||
},
|
||
),
|
||
choice(
|
||
t!("Dim line 1").as_ref(),
|
||
"dim_line_1",
|
||
on(int(ov::DIMSD1, s.dimsd1 as i16) == 0),
|
||
&["On", "Off"],
|
||
true,
|
||
),
|
||
choice(
|
||
t!("Dim line 2").as_ref(),
|
||
"dim_line_2",
|
||
on(int(ov::DIMSD2, s.dimsd2 as i16) == 0),
|
||
&["On", "Off"],
|
||
true,
|
||
),
|
||
property(
|
||
t!("Dim line color").as_ref(),
|
||
"dim_line_color",
|
||
PropValue::ColorChoice(
|
||
ov::color(data, ov::DIMCLRD)
|
||
.unwrap_or_else(|| AcadColor::from_index(s.dimclrd)),
|
||
),
|
||
),
|
||
property(
|
||
t!("Dim line linetype").as_ref(),
|
||
"dim_linetype",
|
||
PropValue::Choice {
|
||
selected: linetype(ov::DIMLTYPE, s.dimltex_handle),
|
||
options: linetype_options.clone(),
|
||
},
|
||
),
|
||
number(
|
||
t!("Dim line ext").as_ref(),
|
||
"dim_line_ext",
|
||
real(ov::DIMDLE, s.dimdle),
|
||
true,
|
||
),
|
||
property(
|
||
t!("Ext line 1 linetype").as_ref(),
|
||
"dim_ext_linetype_1",
|
||
PropValue::Choice {
|
||
selected: linetype(ov::DIMLTEX1, s.dimltex1_handle),
|
||
options: linetype_options.clone(),
|
||
},
|
||
),
|
||
property(
|
||
t!("Ext line 2 linetype").as_ref(),
|
||
"dim_ext_linetype_2",
|
||
PropValue::Choice {
|
||
selected: linetype(ov::DIMLTEX2, s.dimltex2_handle),
|
||
options: linetype_options,
|
||
},
|
||
),
|
||
choice(
|
||
t!("Ext line 1").as_ref(),
|
||
"dim_ext_line_1",
|
||
on(int(ov::DIMSE1, s.dimse1 as i16) == 0),
|
||
&["On", "Off"],
|
||
true,
|
||
),
|
||
choice(
|
||
t!("Ext line 2").as_ref(),
|
||
"dim_ext_line_2",
|
||
on(int(ov::DIMSE2, s.dimse2 as i16) == 0),
|
||
&["On", "Off"],
|
||
true,
|
||
),
|
||
choice(
|
||
t!("Ext line fixed").as_ref(),
|
||
"dim_ext_line_fixed",
|
||
on(dimfxlon),
|
||
&["On", "Off"],
|
||
true,
|
||
),
|
||
number(
|
||
t!("Ext line fixed length").as_ref(),
|
||
"dim_ext_line_fixed_length",
|
||
real(ov::DIMFXL, s.dimfxl),
|
||
dimfxlon,
|
||
),
|
||
property(
|
||
t!("Ext line color").as_ref(),
|
||
"dim_ext_line_color",
|
||
PropValue::ColorChoice(
|
||
ov::color(data, ov::DIMCLRE)
|
||
.unwrap_or_else(|| AcadColor::from_index(s.dimclre)),
|
||
),
|
||
),
|
||
number(
|
||
t!("Ext line ext").as_ref(),
|
||
"dim_ext_line_ext",
|
||
real(ov::DIMEXE, s.dimexe),
|
||
true,
|
||
),
|
||
number(
|
||
t!("Ext line offset").as_ref(),
|
||
"dim_ext_line_offset",
|
||
real(ov::DIMEXO, s.dimexo),
|
||
true,
|
||
),
|
||
],
|
||
},
|
||
PropSection {
|
||
title: t!("Text").into_owned(),
|
||
props: vec![
|
||
property(
|
||
t!("Fill color").as_ref(),
|
||
"dim_text_fill_color",
|
||
fill_value,
|
||
),
|
||
choice(
|
||
t!("Fractional type").as_ref(),
|
||
"dim_fractional_type",
|
||
fraction_type_label(dimfrac),
|
||
&["Horizontal", "Diagonal", "Not stacked"],
|
||
matches!(dimlunit, 4 | 5),
|
||
),
|
||
property(
|
||
t!("Text color").as_ref(),
|
||
"dim_text_color",
|
||
PropValue::ColorChoice(
|
||
ov::color(data, ov::DIMCLRT)
|
||
.unwrap_or_else(|| AcadColor::from_index(s.dimclrt)),
|
||
),
|
||
),
|
||
number(
|
||
t!("Text height").as_ref(),
|
||
"dim_text_height",
|
||
real(ov::DIMTXT, s.dimtxt),
|
||
text_height_editable,
|
||
),
|
||
number(
|
||
t!("Text offset").as_ref(),
|
||
"dim_text_offset",
|
||
dimgap.abs(),
|
||
true,
|
||
),
|
||
choice(
|
||
t!("Text outside align").as_ref(),
|
||
"dim_text_outside_align",
|
||
on(int(ov::DIMTOH, s.dimtoh as i16) == 0),
|
||
&["On", "Off"],
|
||
true,
|
||
),
|
||
choice(
|
||
t!("Text pos hor").as_ref(),
|
||
"dim_text_pos_hor",
|
||
text_horizontal_label(int(ov::DIMJUST, s.dimjust)),
|
||
&[
|
||
"Centered",
|
||
"At extension line 1",
|
||
"At extension line 2",
|
||
"Over extension line 1",
|
||
"Over extension line 2",
|
||
],
|
||
true,
|
||
),
|
||
choice(
|
||
t!("Text pos vert").as_ref(),
|
||
"dim_text_pos_vert",
|
||
text_vertical_label(int(ov::DIMTAD, s.dimtad)),
|
||
&["Centered", "Above", "Outside", "JIS", "Below"],
|
||
true,
|
||
),
|
||
property(
|
||
t!("Text style").as_ref(),
|
||
"dim_text_style",
|
||
PropValue::Choice {
|
||
selected: text_style_name,
|
||
options: text_style_options,
|
||
},
|
||
),
|
||
choice(
|
||
t!("Text inside align").as_ref(),
|
||
"dim_text_inside_align",
|
||
on(int(ov::DIMTIH, s.dimtih as i16) == 0),
|
||
&["On", "Off"],
|
||
dimtix
|
||
|| matches!(
|
||
dimension,
|
||
Dimension::LargeRadial(_)
|
||
| Dimension::Angular2Ln(_)
|
||
| Dimension::Angular3Pt(_)
|
||
),
|
||
),
|
||
property(
|
||
t!("Text position X").as_ref(),
|
||
"text_x",
|
||
PropValue::EditText(format!(
|
||
"{:.4}",
|
||
dimension.base().text_middle_point.x
|
||
)),
|
||
),
|
||
property(
|
||
t!("Text position Y").as_ref(),
|
||
"text_y",
|
||
PropValue::EditText(format!(
|
||
"{:.4}",
|
||
dimension.base().text_middle_point.y
|
||
)),
|
||
),
|
||
property(
|
||
t!("Text rotation").as_ref(),
|
||
"text_rotation",
|
||
PropValue::EditText(format!(
|
||
"{:.4}",
|
||
dimension.base().text_rotation.to_degrees()
|
||
)),
|
||
),
|
||
choice(
|
||
t!("Text view direction").as_ref(),
|
||
"dim_text_view_direction",
|
||
if int(ov::DIMTXTDIRECTION, s.dimtxtdirection as i16) != 0 {
|
||
"Right-to-Left"
|
||
} else {
|
||
"Left-to-Right"
|
||
},
|
||
&["Left-to-Right", "Right-to-Left"],
|
||
true,
|
||
),
|
||
property(
|
||
t!("Measurement").as_ref(),
|
||
"measurement",
|
||
// The value the dimension text reports (DIMLFAC applied,
|
||
// including viewport compensation) — not the raw paper
|
||
// distance, which would disagree with what is drawn.
|
||
PropValue::ReadOnly(format!(
|
||
"{:.4}",
|
||
displayed_measurement(dimension, Some(s))
|
||
)),
|
||
),
|
||
property(
|
||
t!("Text override").as_ref(),
|
||
"text_override",
|
||
PropValue::PlainText(
|
||
dimension_text_override(dimension.base())
|
||
.unwrap_or("")
|
||
.to_string(),
|
||
),
|
||
),
|
||
],
|
||
},
|
||
PropSection {
|
||
title: t!("Fit").into_owned(),
|
||
props: vec![
|
||
choice(
|
||
t!("Fit").as_ref(),
|
||
"dim_fit",
|
||
fit_label(int(ov::DIMATFIT, s.dimatfit)),
|
||
&["Both text and arrows", "Arrows", "Text", "Best fit"],
|
||
true,
|
||
),
|
||
choice(
|
||
t!("Text inside").as_ref(),
|
||
"dim_text_inside",
|
||
on(dimtix),
|
||
&["On", "Off"],
|
||
true,
|
||
),
|
||
choice(
|
||
t!("Text movement").as_ref(),
|
||
"dim_text_movement",
|
||
text_movement_label(int(ov::DIMTMOVE, s.dimtmove)),
|
||
&[
|
||
"Keep dim line with text",
|
||
"Move text, add leader",
|
||
"Move text, no leader",
|
||
],
|
||
true,
|
||
),
|
||
number(
|
||
t!("Dim scale overall").as_ref(),
|
||
"dim_scale_overall",
|
||
real(ov::DIMSCALE, s.dimscale),
|
||
!annotative,
|
||
),
|
||
choice(
|
||
t!("Dim line forced").as_ref(),
|
||
"dim_line_forced",
|
||
on(int(ov::DIMTOFL, s.dimtofl as i16) != 0),
|
||
&["On", "Off"],
|
||
true,
|
||
),
|
||
choice(
|
||
t!("Dim line inside").as_ref(),
|
||
"dim_line_inside",
|
||
on(int(ov::DIMSOXD, s.dimsoxd as i16) == 0),
|
||
&["On", "Off"],
|
||
true,
|
||
),
|
||
],
|
||
},
|
||
PropSection {
|
||
title: t!("Primary Units").into_owned(),
|
||
props: vec![
|
||
text(
|
||
t!("Decimal separator").as_ref(),
|
||
"dim_decimal_separator",
|
||
decimal_separator.clone(),
|
||
true,
|
||
),
|
||
text(
|
||
t!("Dim prefix").as_ref(),
|
||
"dim_prefix",
|
||
dim_prefix.clone(),
|
||
true,
|
||
),
|
||
text(
|
||
t!("Dim suffix").as_ref(),
|
||
"dim_suffix",
|
||
dim_suffix.clone(),
|
||
true,
|
||
),
|
||
text(
|
||
t!("Dim sub-units suffix").as_ref(),
|
||
"dim_sub_units_suffix",
|
||
string(ov::DIMMZS, &s.dimmzs),
|
||
dimzin & 4 != 0,
|
||
),
|
||
number(
|
||
t!("Dim roundoff").as_ref(),
|
||
"dim_roundoff",
|
||
real(ov::DIMRND, s.dimrnd),
|
||
true,
|
||
),
|
||
number(
|
||
t!("Dim scale linear").as_ref(),
|
||
"dim_scale_linear",
|
||
// The *raw* signed DIMLFAC: a negative value is the
|
||
// paper-space / viewport-compensation convention, and
|
||
// hiding its sign would make the row uneditable in
|
||
// practice. `s.dimlfac` is sign-resolved, so fall back to
|
||
// the unresolved source style instead.
|
||
real(ov::DIMLFAC, style.dimlfac),
|
||
true,
|
||
),
|
||
number(
|
||
t!("Dim sub-units scale").as_ref(),
|
||
"dim_sub_units_scale",
|
||
real(ov::DIMMZF, s.dimmzf),
|
||
dimzin & 4 != 0,
|
||
),
|
||
choice(
|
||
t!("Dim units").as_ref(),
|
||
"dim_units",
|
||
linear_unit_label(dimlunit),
|
||
&linear_units,
|
||
true,
|
||
),
|
||
choice(
|
||
t!("Suppress leading zeros").as_ref(),
|
||
"dim_suppress_leading_zeros",
|
||
yes(dimzin & 4 != 0),
|
||
&["Yes", "No"],
|
||
true,
|
||
),
|
||
choice(
|
||
t!("Suppress trailing zeros").as_ref(),
|
||
"dim_suppress_trailing_zeros",
|
||
yes(dimzin & 8 != 0),
|
||
&["Yes", "No"],
|
||
true,
|
||
),
|
||
choice(
|
||
t!("Suppress zero feet").as_ref(),
|
||
"dim_suppress_zero_feet",
|
||
yes(suppresses_zero_feet(dimzin)),
|
||
&["Yes", "No"],
|
||
matches!(dimlunit, 3 | 4),
|
||
),
|
||
choice(
|
||
t!("Suppress zero inches").as_ref(),
|
||
"dim_suppress_zero_inches",
|
||
yes(suppresses_zero_inches(dimzin)),
|
||
&["Yes", "No"],
|
||
matches!(dimlunit, 3 | 4),
|
||
),
|
||
property(
|
||
t!("Precision").as_ref(),
|
||
"dim_precision",
|
||
PropValue::Choice {
|
||
selected: precision_label(int(ov::DIMDEC, s.dimdec)),
|
||
options: precision_options.clone(),
|
||
},
|
||
),
|
||
],
|
||
},
|
||
PropSection {
|
||
title: t!("Alternate Units").into_owned(),
|
||
props: vec![
|
||
choice(
|
||
t!("Alt enabled").as_ref(),
|
||
"dim_alt_enabled",
|
||
on(dimalt),
|
||
&["On", "Off"],
|
||
true,
|
||
),
|
||
choice(
|
||
t!("Alt format").as_ref(),
|
||
"dim_alt_format",
|
||
alternate_unit_label(int(ov::DIMALTU, s.dimaltu)),
|
||
&alternate_unit_options,
|
||
dimalt,
|
||
),
|
||
property(
|
||
t!("Alt precision").as_ref(),
|
||
"dim_alt_precision",
|
||
if dimalt {
|
||
PropValue::Choice {
|
||
selected: precision_label(int(ov::DIMALTD, s.dimaltd)),
|
||
options: precision_options.clone(),
|
||
}
|
||
} else {
|
||
PropValue::ReadOnly(precision_label(int(ov::DIMALTD, s.dimaltd)))
|
||
},
|
||
),
|
||
number(
|
||
t!("Alt scale factor").as_ref(),
|
||
"dim_alt_scale_factor",
|
||
real(ov::DIMALTF, s.dimaltf),
|
||
dimalt,
|
||
),
|
||
number(
|
||
t!("Alt sub-units scale").as_ref(),
|
||
"dim_alt_sub_units_scale",
|
||
real(ov::DIMALTMZF, s.dimaltmzf),
|
||
dimalt && dimaltz & 4 != 0,
|
||
),
|
||
number(
|
||
t!("Alt round").as_ref(),
|
||
"dim_alt_roundoff",
|
||
real(ov::DIMALTRND, s.dimaltrnd),
|
||
dimalt,
|
||
),
|
||
text(
|
||
t!("Alt prefix").as_ref(),
|
||
"dim_alt_prefix",
|
||
alt_prefix,
|
||
dimalt,
|
||
),
|
||
text(
|
||
t!("Alt suffix").as_ref(),
|
||
"dim_alt_suffix",
|
||
alt_suffix,
|
||
dimalt,
|
||
),
|
||
text(
|
||
t!("Alt sub-units suffix").as_ref(),
|
||
"dim_alt_sub_units_suffix",
|
||
string(ov::DIMALTMZS, &s.dimaltmzs),
|
||
dimalt && dimaltz & 4 != 0,
|
||
),
|
||
choice(
|
||
t!("Alt suppress leading zeros").as_ref(),
|
||
"dim_alt_suppress_leading_zeros",
|
||
yes(dimaltz & 4 != 0),
|
||
&["Yes", "No"],
|
||
dimalt,
|
||
),
|
||
choice(
|
||
t!("Alt suppress trailing zeros").as_ref(),
|
||
"dim_alt_suppress_trailing_zeros",
|
||
yes(dimaltz & 8 != 0),
|
||
&["Yes", "No"],
|
||
dimalt,
|
||
),
|
||
choice(
|
||
t!("Alt suppress zero feet").as_ref(),
|
||
"dim_alt_suppress_zero_feet",
|
||
yes(suppresses_zero_feet(dimaltz)),
|
||
&["Yes", "No"],
|
||
dimalt,
|
||
),
|
||
choice(
|
||
t!("Alt suppress zero inches").as_ref(),
|
||
"dim_alt_suppress_zero_inches",
|
||
yes(suppresses_zero_inches(dimaltz)),
|
||
&["Yes", "No"],
|
||
dimalt,
|
||
),
|
||
],
|
||
},
|
||
PropSection {
|
||
title: t!("Tolerances").into_owned(),
|
||
props: vec![
|
||
choice(
|
||
t!("Tolerance display").as_ref(),
|
||
"dim_tolerance_display",
|
||
tolerance_display,
|
||
&["None", "Symmetrical", "Deviation", "Limits", "Basic"],
|
||
true,
|
||
),
|
||
property(
|
||
t!("Tolerance precision").as_ref(),
|
||
"dim_tolerance_precision",
|
||
if tolerance_enabled {
|
||
PropValue::Choice {
|
||
selected: precision_label(int(ov::DIMTDEC, s.dimtdec)),
|
||
options: precision_options.clone(),
|
||
}
|
||
} else {
|
||
PropValue::ReadOnly(precision_label(int(ov::DIMTDEC, s.dimtdec)))
|
||
},
|
||
),
|
||
number(
|
||
t!("Tolerance limit lower").as_ref(),
|
||
"dim_tolerance_limit_lower",
|
||
dimtm,
|
||
matches!(tolerance_display, "Deviation" | "Limits"),
|
||
),
|
||
number(
|
||
t!("Tolerance limit upper").as_ref(),
|
||
"dim_tolerance_limit_upper",
|
||
dimtp,
|
||
tolerance_enabled,
|
||
),
|
||
number(
|
||
t!("Tolerance text height").as_ref(),
|
||
"dim_tolerance_text_height",
|
||
real(ov::DIMTFAC, s.dimtfac),
|
||
tolerance_enabled,
|
||
),
|
||
choice(
|
||
t!("Tolerance pos vert").as_ref(),
|
||
"dim_tolerance_pos_vert",
|
||
tolerance_vertical_label(int(ov::DIMTOLJ, s.dimtolj)),
|
||
&["Bottom", "Middle", "Top"],
|
||
true,
|
||
),
|
||
choice(
|
||
t!("Tolerance alignment").as_ref(),
|
||
"dim_tolerance_alignment",
|
||
tolerance_alignment_label(int(ov::DIMTALN, 0)),
|
||
&["Decimal Separator", "Operational Symbols"],
|
||
true,
|
||
),
|
||
choice(
|
||
t!("Tolerance suppress leading zeros").as_ref(),
|
||
"dim_tolerance_suppress_leading_zeros",
|
||
yes(dimtzin & 4 != 0),
|
||
&["Yes", "No"],
|
||
tolerance_enabled,
|
||
),
|
||
choice(
|
||
t!("Tolerance suppress trailing zeros").as_ref(),
|
||
"dim_tolerance_suppress_trailing_zeros",
|
||
yes(dimtzin & 8 != 0),
|
||
&["Yes", "No"],
|
||
tolerance_enabled,
|
||
),
|
||
choice(
|
||
t!("Tolerance suppress zero feet").as_ref(),
|
||
"dim_tolerance_suppress_zero_feet",
|
||
yes(suppresses_zero_feet(dimtzin)),
|
||
&["Yes", "No"],
|
||
tolerance_enabled,
|
||
),
|
||
choice(
|
||
t!("Tolerance suppress zero inches").as_ref(),
|
||
"dim_tolerance_suppress_zero_inches",
|
||
yes(suppresses_zero_inches(dimtzin)),
|
||
&["Yes", "No"],
|
||
tolerance_enabled,
|
||
),
|
||
property(
|
||
t!("Alt tolerance precision").as_ref(),
|
||
"dim_alt_tolerance_precision",
|
||
if alternate_tolerance_enabled {
|
||
PropValue::Choice {
|
||
selected: precision_label(int(ov::DIMALTTD, s.dimalttd)),
|
||
options: precision_options,
|
||
}
|
||
} else {
|
||
PropValue::ReadOnly(precision_label(int(ov::DIMALTTD, s.dimalttd)))
|
||
},
|
||
),
|
||
choice(
|
||
t!("Alt tolerance suppress leading zeros").as_ref(),
|
||
"dim_alt_tolerance_suppress_leading_zeros",
|
||
yes(dimalttz & 4 != 0),
|
||
&["Yes", "No"],
|
||
alternate_tolerance_enabled,
|
||
),
|
||
choice(
|
||
t!("Alt tolerance suppress trailing zeros").as_ref(),
|
||
"dim_alt_tolerance_suppress_trailing_zeros",
|
||
yes(dimalttz & 8 != 0),
|
||
&["Yes", "No"],
|
||
alternate_tolerance_enabled,
|
||
),
|
||
choice(
|
||
t!("Alt tolerance suppress zero feet").as_ref(),
|
||
"dim_alt_tolerance_suppress_zero_feet",
|
||
yes(suppresses_zero_feet(dimalttz)),
|
||
&["Yes", "No"],
|
||
alternate_tolerance_enabled,
|
||
),
|
||
choice(
|
||
t!("Alt tolerance suppress zero inches").as_ref(),
|
||
"dim_alt_tolerance_suppress_zero_inches",
|
||
yes(suppresses_zero_inches(dimalttz)),
|
||
&["Yes", "No"],
|
||
alternate_tolerance_enabled,
|
||
),
|
||
],
|
||
},
|
||
];
|
||
if matches!(
|
||
dimension,
|
||
Dimension::Radius(_) | Dimension::Diameter(_) | Dimension::LargeRadial(_)
|
||
) {
|
||
let dimcen = real(ov::DIMCEN, s.dimcen);
|
||
let center_type = if dimcen > 1e-12 {
|
||
"Mark"
|
||
} else if dimcen < -1e-12 {
|
||
"Line"
|
||
} else {
|
||
"None"
|
||
};
|
||
if let Some(lines) = sections
|
||
.iter_mut()
|
||
.find(|section| section.title == t!("Lines & Arrows").as_ref())
|
||
{
|
||
const RADIUS_LINE_FIELDS: &[&str] = &[
|
||
"dim_arrowhead_1",
|
||
"dim_arrow_size",
|
||
"dim_line_lineweight",
|
||
"dim_ext_line_lineweight",
|
||
"dim_line_2",
|
||
"dim_line_color",
|
||
"dim_linetype",
|
||
"dim_ext_linetype_1",
|
||
"dim_ext_line_1",
|
||
"dim_ext_line_color",
|
||
"dim_ext_line_ext",
|
||
"dim_ext_line_offset",
|
||
];
|
||
const DIAMETER_LINE_FIELDS: &[&str] = &[
|
||
"dim_arrowhead_1",
|
||
"dim_arrowhead_2",
|
||
"dim_arrow_size",
|
||
"dim_line_lineweight",
|
||
"dim_ext_line_lineweight",
|
||
"dim_line_1",
|
||
"dim_line_2",
|
||
"dim_line_color",
|
||
"dim_linetype",
|
||
"dim_ext_linetype_1",
|
||
"dim_ext_line_1",
|
||
"dim_ext_line_color",
|
||
"dim_ext_line_ext",
|
||
"dim_ext_line_offset",
|
||
];
|
||
const LARGE_RADIAL_LINE_FIELDS: &[&str] = &[
|
||
"dim_arrowhead_1",
|
||
"dim_arrow_size",
|
||
"dim_line_lineweight",
|
||
"dim_ext_line_lineweight",
|
||
"dim_line_2",
|
||
"dim_line_color",
|
||
"dim_linetype",
|
||
"dim_ext_linetype_1",
|
||
"dim_ext_line_1",
|
||
"dim_ext_line_color",
|
||
"dim_ext_line_ext",
|
||
"dim_ext_line_offset",
|
||
];
|
||
let retained = if matches!(dimension, Dimension::Diameter(_)) {
|
||
DIAMETER_LINE_FIELDS
|
||
} else if matches!(dimension, Dimension::LargeRadial(_)) {
|
||
LARGE_RADIAL_LINE_FIELDS
|
||
} else {
|
||
RADIUS_LINE_FIELDS
|
||
};
|
||
lines
|
||
.props
|
||
.retain(|property| retained.contains(&property.field));
|
||
lines.props.push(choice(
|
||
t!("Center mark").as_ref(),
|
||
"dim_center_type",
|
||
center_type,
|
||
&["None", "Mark", "Line"],
|
||
true,
|
||
));
|
||
lines.props.push(number(
|
||
t!("Center mark size").as_ref(),
|
||
"dim_center_size",
|
||
dimcen.abs(),
|
||
center_type != "None",
|
||
));
|
||
if let Dimension::LargeRadial(radial) = dimension {
|
||
lines.props.push(edit(
|
||
t!("Center location override X").as_ref(),
|
||
"override_x",
|
||
radial.override_center.x,
|
||
));
|
||
lines.props.push(edit(
|
||
t!("Center location override Y").as_ref(),
|
||
"override_y",
|
||
radial.override_center.y,
|
||
));
|
||
lines.props.push(edit(
|
||
t!("Jog location X").as_ref(),
|
||
"jog_x",
|
||
radial.jog_point.x,
|
||
));
|
||
lines.props.push(edit(
|
||
t!("Jog location Y").as_ref(),
|
||
"jog_y",
|
||
radial.jog_point.y,
|
||
));
|
||
lines.props.push(edit_angle(
|
||
t!("Jog angle").as_ref(),
|
||
"jog_angle",
|
||
radial.jog_angle.to_degrees(),
|
||
));
|
||
}
|
||
if matches!(dimension, Dimension::Radius(_) | Dimension::LargeRadial(_)) {
|
||
if let Some(arrow) = lines
|
||
.props
|
||
.iter_mut()
|
||
.find(|property| property.field == "dim_arrowhead_1")
|
||
{
|
||
arrow.label = t!("Arrow").into_owned();
|
||
arrow.field = "dim_radial_arrow";
|
||
}
|
||
if let Some(dim_line) = lines
|
||
.props
|
||
.iter_mut()
|
||
.find(|property| property.field == "dim_line_2")
|
||
{
|
||
dim_line.label = t!("Dim line").into_owned();
|
||
}
|
||
}
|
||
if matches!(dimension, Dimension::LargeRadial(_)) {
|
||
for property in &mut lines.props {
|
||
property.label = match property.field {
|
||
"dim_ext_line_1" => t!("Ext line").into_owned(),
|
||
"dim_ext_line_lineweight" => t!("Ext line weight").into_owned(),
|
||
"dim_ext_linetype_1" => t!("Ext line type").into_owned(),
|
||
_ => property.label.clone(),
|
||
};
|
||
}
|
||
const LARGE_RADIAL_ORDER: &[&str] = &[
|
||
"dim_radial_arrow",
|
||
"dim_arrow_size",
|
||
"dim_center_type",
|
||
"dim_center_size",
|
||
"dim_line_lineweight",
|
||
"dim_line_2",
|
||
"dim_linetype",
|
||
"dim_line_color",
|
||
"override_x",
|
||
"override_y",
|
||
"jog_x",
|
||
"jog_y",
|
||
"dim_ext_line_1",
|
||
"jog_angle",
|
||
"dim_ext_line_lineweight",
|
||
"dim_ext_linetype_1",
|
||
"dim_ext_line_ext",
|
||
"dim_ext_line_color",
|
||
"dim_ext_line_offset",
|
||
];
|
||
lines.props.sort_by_key(|property| {
|
||
LARGE_RADIAL_ORDER
|
||
.iter()
|
||
.position(|field| *field == property.field)
|
||
.unwrap_or(LARGE_RADIAL_ORDER.len())
|
||
});
|
||
}
|
||
}
|
||
if let Some(primary_units) = sections
|
||
.iter_mut()
|
||
.find(|section| section.title == t!("Primary Units").as_ref())
|
||
{
|
||
primary_units.props.retain(|property| {
|
||
!matches!(
|
||
property.field,
|
||
"dim_sub_units_suffix" | "dim_sub_units_scale"
|
||
)
|
||
});
|
||
}
|
||
if matches!(dimension, Dimension::LargeRadial(_)) {
|
||
for (section_name, excluded) in [
|
||
(t!("Text"), &["dim_text_pos_hor"][..]),
|
||
(t!("Fit"), &["dim_line_inside"][..]),
|
||
(
|
||
t!("Alternate Units"),
|
||
&["dim_alt_sub_units_scale", "dim_alt_sub_units_suffix"][..],
|
||
),
|
||
] {
|
||
if let Some(section) = sections
|
||
.iter_mut()
|
||
.find(|section| section.title == section_name.as_ref())
|
||
{
|
||
section
|
||
.props
|
||
.retain(|property| !excluded.contains(&property.field));
|
||
}
|
||
}
|
||
for (section_name, fields) in [
|
||
(
|
||
t!("Text"),
|
||
&["dim_text_outside_align", "dim_text_pos_vert"][..],
|
||
),
|
||
(t!("Fit"), &["dim_fit", "dim_text_inside"][..]),
|
||
] {
|
||
if let Some(section) = sections
|
||
.iter_mut()
|
||
.find(|section| section.title == section_name.as_ref())
|
||
{
|
||
for property in &mut section.props {
|
||
if fields.contains(&property.field) {
|
||
if let PropValue::Choice { selected, .. } = &property.value {
|
||
property.value = PropValue::ReadOnly(selected.clone());
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if let Some(fit) = sections
|
||
.iter_mut()
|
||
.find(|section| section.title == t!("Fit").as_ref())
|
||
{
|
||
const LARGE_RADIAL_FIT_ORDER: &[&str] = &[
|
||
"dim_line_forced",
|
||
"dim_scale_overall",
|
||
"dim_fit",
|
||
"dim_text_inside",
|
||
"dim_text_movement",
|
||
];
|
||
fit.props.sort_by_key(|property| {
|
||
LARGE_RADIAL_FIT_ORDER
|
||
.iter()
|
||
.position(|field| *field == property.field)
|
||
.unwrap_or(LARGE_RADIAL_FIT_ORDER.len())
|
||
});
|
||
}
|
||
if let Some(alternate_units) = sections
|
||
.iter_mut()
|
||
.find(|section| section.title == t!("Alternate Units").as_ref())
|
||
{
|
||
const LARGE_RADIAL_ALT_UNITS_ORDER: &[&str] = &[
|
||
"dim_alt_enabled",
|
||
"dim_alt_format",
|
||
"dim_alt_precision",
|
||
"dim_alt_roundoff",
|
||
"dim_alt_scale_factor",
|
||
"dim_alt_suppress_leading_zeros",
|
||
"dim_alt_suppress_trailing_zeros",
|
||
"dim_alt_suppress_zero_feet",
|
||
"dim_alt_suppress_zero_inches",
|
||
"dim_alt_prefix",
|
||
"dim_alt_suffix",
|
||
];
|
||
alternate_units.props.sort_by_key(|property| {
|
||
LARGE_RADIAL_ALT_UNITS_ORDER
|
||
.iter()
|
||
.position(|field| *field == property.field)
|
||
.unwrap_or(LARGE_RADIAL_ALT_UNITS_ORDER.len())
|
||
});
|
||
}
|
||
if let Some(tolerances) = sections
|
||
.iter_mut()
|
||
.find(|section| section.title == t!("Tolerances").as_ref())
|
||
{
|
||
const LARGE_RADIAL_TOLERANCE_ORDER: &[&str] = &[
|
||
"dim_alt_tolerance_suppress_zero_inches",
|
||
"dim_tolerance_alignment",
|
||
"dim_tolerance_display",
|
||
"dim_tolerance_limit_lower",
|
||
"dim_tolerance_limit_upper",
|
||
"dim_tolerance_pos_vert",
|
||
"dim_tolerance_precision",
|
||
"dim_tolerance_suppress_leading_zeros",
|
||
"dim_tolerance_suppress_trailing_zeros",
|
||
"dim_tolerance_suppress_zero_feet",
|
||
"dim_tolerance_suppress_zero_inches",
|
||
"dim_tolerance_text_height",
|
||
"dim_alt_tolerance_precision",
|
||
"dim_alt_tolerance_suppress_leading_zeros",
|
||
"dim_alt_tolerance_suppress_trailing_zeros",
|
||
"dim_alt_tolerance_suppress_zero_feet",
|
||
];
|
||
tolerances.props.sort_by_key(|property| {
|
||
LARGE_RADIAL_TOLERANCE_ORDER
|
||
.iter()
|
||
.position(|field| *field == property.field)
|
||
.unwrap_or(LARGE_RADIAL_TOLERANCE_ORDER.len())
|
||
});
|
||
}
|
||
if !dimension.base().text_user_positioned {
|
||
let text_position = styled_dimension_text_position(dimension, s, 1.0);
|
||
for section in &mut sections {
|
||
for property in &mut section.props {
|
||
match property.field {
|
||
"text_x" => {
|
||
property.value =
|
||
PropValue::EditText(format!("{:.4}", text_position.x));
|
||
}
|
||
"text_y" => {
|
||
property.value =
|
||
PropValue::EditText(format!("{:.4}", text_position.y));
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if matches!(dimension, Dimension::Arc(_)) {
|
||
if let Some(lines) = sections
|
||
.iter_mut()
|
||
.find(|section| section.title == t!("Lines & Arrows").as_ref())
|
||
{
|
||
let symbol = match int(ov::DIMARCSYM, s.dimarcsym) {
|
||
1 => "Above dimension text",
|
||
2 => "None",
|
||
_ => "Preceding dimension text",
|
||
};
|
||
lines.props.insert(
|
||
3,
|
||
choice(
|
||
t!("Arc length symbol").as_ref(),
|
||
"dim_arc_symbol",
|
||
symbol,
|
||
&[
|
||
"Preceding dimension text",
|
||
"Above dimension text",
|
||
"None",
|
||
],
|
||
true,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
if matches!(dimension, Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_)) {
|
||
if let Some(text_section) = sections
|
||
.iter_mut()
|
||
.find(|section| section.title == t!("Text").as_ref())
|
||
{
|
||
text_section
|
||
.props
|
||
.retain(|property| property.field != "dim_fractional_type");
|
||
}
|
||
|
||
let angle_zero_suppression = int(ov::DIMAZIN, s.dimazin);
|
||
let angle_unit = int(ov::DIMAUNIT, s.dimaunit);
|
||
if let Some(primary_units) = sections
|
||
.iter_mut()
|
||
.find(|section| section.title == t!("Primary Units").as_ref())
|
||
{
|
||
primary_units.props = vec![
|
||
text(
|
||
t!("Decimal separator").as_ref(),
|
||
"dim_decimal_separator",
|
||
decimal_separator,
|
||
true,
|
||
),
|
||
text(t!("Prefix").as_ref(), "dim_prefix", dim_prefix, true),
|
||
text(t!("Suffix").as_ref(), "dim_suffix", dim_suffix, true),
|
||
choice(
|
||
t!("Angle format").as_ref(),
|
||
"dim_angle_units",
|
||
angular_unit_label(angle_unit),
|
||
&[
|
||
"Decimal degrees",
|
||
"Degrees/minutes/seconds",
|
||
"Gradians",
|
||
"Radians",
|
||
],
|
||
true,
|
||
),
|
||
choice(
|
||
t!("Suppress leading zeros").as_ref(),
|
||
"dim_angle_suppress_leading_zeros",
|
||
yes(angle_zero_suppression & 1 != 0),
|
||
&["Yes", "No"],
|
||
true,
|
||
),
|
||
choice(
|
||
t!("Suppress trailing zeros").as_ref(),
|
||
"dim_angle_suppress_trailing_zeros",
|
||
yes(angle_zero_suppression & 2 != 0),
|
||
&["Yes", "No"],
|
||
true,
|
||
),
|
||
property(
|
||
t!("Precision").as_ref(),
|
||
"dim_angle_precision",
|
||
PropValue::Choice {
|
||
selected: int(ov::DIMADEC, s.dimadec).to_string(),
|
||
options: (-1..=8).map(|value| value.to_string()).collect(),
|
||
},
|
||
),
|
||
];
|
||
}
|
||
sections.retain(|section| section.title != t!("Alternate Units").as_ref());
|
||
let by_order = |order: &'static [&'static str]| {
|
||
move |property: &Property| {
|
||
order
|
||
.iter()
|
||
.position(|field| *field == property.field)
|
||
.unwrap_or(order.len())
|
||
}
|
||
};
|
||
if let Some(tolerances) = sections
|
||
.iter_mut()
|
||
.find(|section| section.title == t!("Tolerances").as_ref())
|
||
{
|
||
tolerances.props.retain(|property| {
|
||
!property.field.starts_with("dim_alt_tolerance_")
|
||
&& !matches!(
|
||
property.field,
|
||
"dim_tolerance_suppress_zero_feet" | "dim_tolerance_suppress_zero_inches"
|
||
)
|
||
});
|
||
tolerances.props.sort_by_key(by_order(&[
|
||
"dim_tolerance_alignment",
|
||
"dim_tolerance_display",
|
||
"dim_tolerance_limit_lower",
|
||
"dim_tolerance_limit_upper",
|
||
"dim_tolerance_pos_vert",
|
||
"dim_tolerance_precision",
|
||
"dim_tolerance_suppress_leading_zeros",
|
||
"dim_tolerance_suppress_trailing_zeros",
|
||
"dim_tolerance_text_height",
|
||
]));
|
||
}
|
||
// An arc has no dimension line extension.
|
||
if let Some(lines) = sections
|
||
.iter_mut()
|
||
.find(|section| section.title == t!("Lines & Arrows").as_ref())
|
||
{
|
||
lines.props.retain(|property| property.field != "dim_line_ext");
|
||
}
|
||
if let Some(fit) = sections
|
||
.iter_mut()
|
||
.find(|section| section.title == t!("Fit").as_ref())
|
||
{
|
||
fit.props.sort_by_key(by_order(&[
|
||
"dim_line_forced",
|
||
"dim_line_inside",
|
||
"dim_scale_overall",
|
||
"dim_fit",
|
||
"dim_text_inside",
|
||
"dim_text_movement",
|
||
]));
|
||
}
|
||
if let Some(units) = sections
|
||
.iter_mut()
|
||
.find(|section| section.title == t!("Primary Units").as_ref())
|
||
{
|
||
units.props.sort_by_key(by_order(&[
|
||
"dim_decimal_separator",
|
||
"dim_prefix",
|
||
"dim_suffix",
|
||
"dim_angle_suppress_leading_zeros",
|
||
"dim_angle_suppress_trailing_zeros",
|
||
"dim_angle_precision",
|
||
"dim_angle_units",
|
||
]));
|
||
}
|
||
// The measurement reads at the angular precision, and automatic text
|
||
// reports where it is drawn rather than an unset point.
|
||
if let Some(text_section) = sections
|
||
.iter_mut()
|
||
.find(|section| section.title == t!("Text").as_ref())
|
||
{
|
||
let automatic = stored_text_point(dimension)
|
||
.is_none()
|
||
.then(|| styled_dimension_text_position(dimension, s, 1.0));
|
||
for property in &mut text_section.props {
|
||
match property.field {
|
||
"measurement" => {
|
||
// The row shows the number at the angular precision,
|
||
// without the text's degree sign.
|
||
let value = format_angular_value(
|
||
displayed_measurement(dimension, Some(s)),
|
||
Some(s),
|
||
);
|
||
property.value =
|
||
PropValue::ReadOnly(value.trim_end_matches('°').to_string());
|
||
}
|
||
"text_x" => {
|
||
if let Some(position) = automatic {
|
||
property.value = PropValue::EditText(format!("{:.4}", position.x));
|
||
}
|
||
}
|
||
"text_y" => {
|
||
if let Some(position) = automatic {
|
||
property.value = PropValue::EditText(format!("{:.4}", position.y));
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if matches!(dimension, Dimension::Ordinate(_)) {
|
||
for section in &mut sections {
|
||
section.props.retain(|property| match section.title.as_str() {
|
||
title if title == t!("Lines & Arrows").as_ref() => matches!(
|
||
property.field,
|
||
"dim_arrow_size"
|
||
| "dim_ext_line_lineweight"
|
||
| "dim_ext_linetype_1"
|
||
| "dim_ext_line_1"
|
||
| "dim_ext_line_fixed"
|
||
| "dim_ext_line_fixed_length"
|
||
| "dim_ext_line_color"
|
||
| "dim_ext_line_offset"
|
||
),
|
||
title if title == t!("Text").as_ref() => !matches!(
|
||
property.field,
|
||
"dim_text_outside_align" | "dim_text_pos_hor" | "dim_text_inside_align"
|
||
),
|
||
title if title == t!("Fit").as_ref() => matches!(
|
||
property.field,
|
||
"dim_text_movement" | "dim_scale_overall"
|
||
),
|
||
_ => true,
|
||
});
|
||
}
|
||
let dim_scale = if s.dimscale > 1e-6 { s.dimscale } else { 1.0 };
|
||
let text_position = dimension_text_pos_f64(
|
||
dimension,
|
||
Some(s),
|
||
real(ov::DIMTXT, s.dimtxt) * dim_scale,
|
||
dim_scale,
|
||
);
|
||
for section in &mut sections {
|
||
for property in &mut section.props {
|
||
match property.field {
|
||
"text_x" => {
|
||
property.value = PropValue::EditText(format!("{:.4}", text_position.x));
|
||
}
|
||
"text_y" => {
|
||
property.value = PropValue::EditText(format!("{:.4}", text_position.y));
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
sections
|
||
}
|
||
|
||
fn dimension_style_scale(style: &DimStyle, fallback: f64) -> f64 {
|
||
if style.dimscale > 1.0e-6 {
|
||
style.dimscale
|
||
} else {
|
||
fallback
|
||
}
|
||
}
|
||
|
||
fn styled_dimension_text_position(
|
||
dimension: &Dimension,
|
||
style: &DimStyle,
|
||
fallback_scale: f64,
|
||
) -> Vector3 {
|
||
let scale = dimension_style_scale(style, fallback_scale);
|
||
dimension_text_pos_f64(dimension, Some(style), style.dimtxt * scale, scale)
|
||
}
|
||
|
||
pub(crate) fn materialize_large_radial_text_position(
|
||
document: &mut CadDocument,
|
||
handle: Handle,
|
||
value: &str,
|
||
) {
|
||
if parse_f64(value).is_none() {
|
||
return;
|
||
}
|
||
let Some(EntityType::Dimension(dimension)) = document.get_entity(handle) else {
|
||
return;
|
||
};
|
||
if !matches!(dimension, Dimension::LargeRadial(_))
|
||
|| dimension.base().text_user_positioned
|
||
{
|
||
return;
|
||
}
|
||
let source = document.dim_styles.iter().find(|style| {
|
||
style.name.eq_ignore_ascii_case(&dimension.base().style_name)
|
||
|| (dimension.base().style_name.trim().is_empty()
|
||
&& style.name.eq_ignore_ascii_case("Standard"))
|
||
});
|
||
let position = source
|
||
.map(|style| resolved_dimension_style(style, dimension, document))
|
||
.map(|style| styled_dimension_text_position(dimension, &style, 1.0))
|
||
.unwrap_or_else(|| dimension_text_pos_f64(dimension, None, 2.5, 1.0));
|
||
if let Some(EntityType::Dimension(Dimension::LargeRadial(radial))) =
|
||
document.get_entity_mut(handle)
|
||
{
|
||
radial.base.text_middle_point = position;
|
||
}
|
||
}
|
||
|
||
fn property(label: &str, field: &'static str, value: PropValue) -> Property {
|
||
Property {
|
||
label: label.to_string(),
|
||
field,
|
||
value,
|
||
}
|
||
}
|
||
|
||
fn split_measurement_template(value: &str) -> (String, String) {
|
||
value
|
||
.split_once("<>")
|
||
.map(|(prefix, suffix)| (prefix.to_string(), suffix.to_string()))
|
||
.unwrap_or_else(|| (String::new(), value.to_string()))
|
||
}
|
||
|
||
fn fraction_type_label(value: i16) -> &'static str {
|
||
match value {
|
||
1 => "Diagonal",
|
||
2 => "Not stacked",
|
||
_ => "Horizontal",
|
||
}
|
||
}
|
||
|
||
fn suppresses_zero_feet(value: i16) -> bool {
|
||
matches!(value & 3, 0 | 3)
|
||
}
|
||
|
||
fn suppresses_zero_inches(value: i16) -> bool {
|
||
matches!(value & 3, 0 | 2)
|
||
}
|
||
|
||
fn text_horizontal_label(value: i16) -> &'static str {
|
||
match value {
|
||
1 => "At extension line 1",
|
||
2 => "At extension line 2",
|
||
3 => "Over extension line 1",
|
||
4 => "Over extension line 2",
|
||
_ => "Centered",
|
||
}
|
||
}
|
||
|
||
fn text_vertical_label(value: i16) -> &'static str {
|
||
match value {
|
||
1 => "Above",
|
||
2 => "Outside",
|
||
3 => "JIS",
|
||
4 => "Below",
|
||
_ => "Centered",
|
||
}
|
||
}
|
||
|
||
fn fit_label(value: i16) -> &'static str {
|
||
match value {
|
||
1 => "Arrows",
|
||
2 => "Text",
|
||
3 => "Best fit",
|
||
_ => "Both text and arrows",
|
||
}
|
||
}
|
||
|
||
fn text_movement_label(value: i16) -> &'static str {
|
||
match value {
|
||
1 => "Move text, add leader",
|
||
2 => "Move text, no leader",
|
||
_ => "Keep dim line with text",
|
||
}
|
||
}
|
||
|
||
fn alternate_unit_label(value: i16) -> &'static str {
|
||
match value {
|
||
1 => "Scientific",
|
||
3 => "Engineering",
|
||
4 => "Architectural stacked",
|
||
5 => "Fractional stacked",
|
||
6 => "Architectural",
|
||
7 => "Fractional",
|
||
8 => "Desktop",
|
||
_ => "Decimal",
|
||
}
|
||
}
|
||
|
||
fn angular_unit_label(value: i16) -> &'static str {
|
||
match value {
|
||
1 => "Degrees/minutes/seconds",
|
||
2 => "Gradians",
|
||
3 => "Radians",
|
||
_ => "Decimal degrees",
|
||
}
|
||
}
|
||
|
||
fn tolerance_vertical_label(value: i16) -> &'static str {
|
||
match value {
|
||
0 => "Bottom",
|
||
2 => "Top",
|
||
_ => "Middle",
|
||
}
|
||
}
|
||
|
||
fn precision_label(value: i16) -> String {
|
||
let decimals = value.clamp(0, 8) as usize;
|
||
if decimals == 0 {
|
||
"0".to_string()
|
||
} else {
|
||
format!("0.{}", "0".repeat(decimals))
|
||
}
|
||
}
|
||
|
||
fn tolerance_alignment_label(value: i16) -> &'static str {
|
||
if value == 0 {
|
||
"Decimal Separator"
|
||
} else {
|
||
"Operational Symbols"
|
||
}
|
||
}
|
||
|
||
fn choice_value(
|
||
selected: &str,
|
||
options: &[&str],
|
||
) -> crate::scene::model::object::PropValue {
|
||
crate::scene::model::object::PropValue::Choice {
|
||
selected: selected.to_string(),
|
||
options: options.iter().map(|value| (*value).to_string()).collect(),
|
||
}
|
||
}
|
||
|
||
fn block_name(document: &CadDocument, handle: codec::Handle, fallback: &str) -> String {
|
||
if handle.is_null() {
|
||
return "Closed filled".to_string();
|
||
}
|
||
document
|
||
.block_records
|
||
.iter()
|
||
.find(|record| record.handle == handle)
|
||
.map(|record| record.name.clone())
|
||
.unwrap_or_else(|| fallback.to_string())
|
||
}
|
||
|
||
fn linetype_name(document: &CadDocument, handle: codec::Handle) -> String {
|
||
document
|
||
.line_types
|
||
.iter()
|
||
.find(|line_type| line_type.handle == handle)
|
||
.map(|line_type| line_type.name.clone())
|
||
.unwrap_or_else(|| "ByBlock".to_string())
|
||
}
|
||
|
||
fn linear_unit_label(value: i16) -> &'static str {
|
||
match value {
|
||
1 => "Scientific",
|
||
3 => "Engineering",
|
||
4 => "Architectural",
|
||
5 => "Fractional",
|
||
6 => "Desktop",
|
||
_ => "Decimal",
|
||
}
|
||
}
|
||
|
||
use codec::{CadDocument, EntityType, Handle};
|
||
|
||
use crate::scene::convert::tess_util::aci_to_rgba;
|
||
use crate::scene::convert::tessellate::{
|
||
add_polyline, add_segment, append_arrow, arrow_from_block,
|
||
arrow_from_block_with_deferred_hatch, normalized_or, ArrowKind, DimGeom,
|
||
};
|
||
use crate::scene::model::wire_model::{SnapHint, TangentGeom, WireModel};
|
||
|
||
fn apply_dimension_breaks(
|
||
document: &CadDocument,
|
||
dimension: Handle,
|
||
lines: &mut Vec<[f32; 3]>,
|
||
) {
|
||
let references: Vec<_> = document
|
||
.objects
|
||
.values()
|
||
.filter_map(|object| {
|
||
let codec::objects::ObjectType::DataObject(object) = object else {
|
||
return None;
|
||
};
|
||
let codec::objects::DataObjectData::BreakData(data) = &object.data
|
||
else {
|
||
return None;
|
||
};
|
||
(data.dimension_reference == dimension).then_some(&data.point_references)
|
||
})
|
||
.flatten()
|
||
.filter(|reference| {
|
||
let points = [reference.first_point, reference.second_point];
|
||
points.iter().all(|point| {
|
||
point.x.is_finite() && point.y.is_finite() && point.z.is_finite()
|
||
})
|
||
})
|
||
.collect();
|
||
if references.is_empty() || lines.len() < 2 {
|
||
return;
|
||
}
|
||
|
||
let mut output = Vec::with_capacity(lines.len() + references.len() * 3);
|
||
for run in lines.split(|point| point[0].is_nan()) {
|
||
for segment in run.windows(2) {
|
||
let a = Vec3::from_array(segment[0]);
|
||
let b = Vec3::from_array(segment[1]);
|
||
let direction = b - a;
|
||
let length_squared = direction.length_squared();
|
||
if length_squared <= 1e-12 {
|
||
continue;
|
||
}
|
||
let mut intervals = vec![(0.0f32, 1.0f32)];
|
||
for reference in &references {
|
||
let first = vec3_local(reference.first_point);
|
||
let second = vec3_local(reference.second_point);
|
||
let t1 = (first - a).dot(direction) / length_squared;
|
||
let t2 = (second - a).dot(direction) / length_squared;
|
||
let closest1 = a + direction * t1.clamp(0.0, 1.0);
|
||
let closest2 = a + direction * t2.clamp(0.0, 1.0);
|
||
let requested_gap = (second - first).length();
|
||
let tolerance = (requested_gap * 0.25)
|
||
.max(direction.length() * 1e-5)
|
||
.max(1e-4);
|
||
if (first - closest1).length() > tolerance
|
||
|| (second - closest2).length() > tolerance
|
||
{
|
||
continue;
|
||
}
|
||
let half_point_gap = if requested_gap <= 1e-6 {
|
||
(tolerance / direction.length()).min(0.1)
|
||
} else {
|
||
0.0
|
||
};
|
||
let cut_start = (t1.min(t2) - half_point_gap).clamp(0.0, 1.0);
|
||
let cut_end = (t1.max(t2) + half_point_gap).clamp(0.0, 1.0);
|
||
if cut_end <= cut_start {
|
||
continue;
|
||
}
|
||
let mut remaining = Vec::new();
|
||
for (start, end) in intervals {
|
||
if cut_end <= start || cut_start >= end {
|
||
remaining.push((start, end));
|
||
continue;
|
||
}
|
||
if cut_start > start {
|
||
remaining.push((start, cut_start));
|
||
}
|
||
if cut_end < end {
|
||
remaining.push((cut_end, end));
|
||
}
|
||
}
|
||
intervals = remaining;
|
||
}
|
||
for (start, end) in intervals {
|
||
if end - start > 1e-6 {
|
||
add_segment(
|
||
&mut output,
|
||
a + direction * start,
|
||
a + direction * end,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
*lines = output;
|
||
}
|
||
|
||
fn dimension_jog_point(dimension: &Dimension) -> Option<Vec3> {
|
||
if !matches!(dimension, Dimension::Linear(_) | Dimension::Aligned(_)) {
|
||
return None;
|
||
}
|
||
dimension
|
||
.base()
|
||
.common
|
||
.extended_data
|
||
.get_record("ACAD_DSTYLE_DIMJAG_POSITION")
|
||
.and_then(|record| {
|
||
record.values.iter().rev().find_map(|value| match value {
|
||
codec::xdata::XDataValue::Point3D(point) => Some(vec3_local(*point)),
|
||
_ => None,
|
||
})
|
||
})
|
||
}
|
||
|
||
fn apply_dimension_jog(
|
||
lines: &mut Vec<[f32; 3]>,
|
||
requested: Vec3,
|
||
normal: Vec3,
|
||
size: f32,
|
||
angle: f32,
|
||
) {
|
||
if lines.len() < 2 {
|
||
return;
|
||
}
|
||
let segments = lines
|
||
.windows(2)
|
||
.enumerate()
|
||
.filter(|(_, segment)| segment[0][0].is_finite() && segment[1][0].is_finite())
|
||
.map(|(index, segment)| {
|
||
(
|
||
index,
|
||
[segment[0].map(f64::from), segment[1].map(f64::from)],
|
||
)
|
||
})
|
||
.collect::<Vec<_>>();
|
||
let coordinates = segments.iter().map(|(_, segment)| *segment).collect::<Vec<_>>();
|
||
let Some((candidate, center)) = kernel::space::nearest_segment_point(
|
||
&coordinates,
|
||
requested.to_array().map(f64::from),
|
||
) else {
|
||
return;
|
||
};
|
||
let target = segments[candidate].0;
|
||
let Some(jog) = kernel::space::dimension_jog_points(
|
||
coordinates[candidate],
|
||
center,
|
||
normal.to_array().map(f64::from),
|
||
size as f64,
|
||
angle as f64,
|
||
) else {
|
||
return;
|
||
};
|
||
let jog = jog.map(|point| point.map(|value| value as f32));
|
||
let first = lines[target];
|
||
let second = lines[target + 1];
|
||
|
||
let mut output = Vec::with_capacity(lines.len() + 4);
|
||
output.extend_from_slice(&lines[..target]);
|
||
if !output.is_empty() && !output.last().is_some_and(|point| point[0].is_nan()) {
|
||
output.push([f32::NAN; 3]);
|
||
}
|
||
if first != jog[0] {
|
||
output.push(first);
|
||
output.push(jog[0]);
|
||
output.push([f32::NAN; 3]);
|
||
}
|
||
output.extend(jog);
|
||
if jog[3] != second {
|
||
output.push([f32::NAN; 3]);
|
||
output.push(jog[3]);
|
||
output.push(second);
|
||
}
|
||
if target + 2 < lines.len() {
|
||
if !output.last().is_some_and(|point| point[0].is_nan())
|
||
&& !lines[target + 2][0].is_nan()
|
||
{
|
||
output.push([f32::NAN; 3]);
|
||
}
|
||
output.extend_from_slice(&lines[target + 2..]);
|
||
}
|
||
*lines = output;
|
||
}
|
||
|
||
pub trait DimensionTess {
|
||
fn tessellate(
|
||
&self,
|
||
document: &CadDocument,
|
||
handle: Handle,
|
||
selected: bool,
|
||
entity_color: [f32; 4],
|
||
line_weight_px: f32,
|
||
anno_scale: f32,
|
||
selected_set: &rustc_hash::FxHashSet<codec::Handle>,
|
||
active_viewport: Option<codec::Handle>,
|
||
bg_color: [f32; 4],
|
||
view_aabb: Option<[f32; 4]>,
|
||
world_per_pixel: Option<f32>,
|
||
) -> Vec<WireModel>;
|
||
}
|
||
|
||
impl DimensionTess for Dimension {
|
||
fn tessellate(
|
||
&self,
|
||
document: &CadDocument,
|
||
handle: Handle,
|
||
selected: bool,
|
||
entity_color: [f32; 4],
|
||
line_weight_px: f32,
|
||
anno_scale: f32,
|
||
selected_set: &rustc_hash::FxHashSet<codec::Handle>,
|
||
active_viewport: Option<codec::Handle>,
|
||
bg_color: [f32; 4],
|
||
view_aabb: Option<[f32; 4]>,
|
||
world_per_pixel: Option<f32>,
|
||
) -> Vec<WireModel> {
|
||
tessellate_dimension_inner(
|
||
document,
|
||
handle,
|
||
self,
|
||
selected,
|
||
entity_color,
|
||
line_weight_px,
|
||
anno_scale,
|
||
selected_set,
|
||
active_viewport,
|
||
bg_color,
|
||
view_aabb,
|
||
world_per_pixel,
|
||
)
|
||
}
|
||
}
|
||
|
||
fn tessellate_dimension_inner(
|
||
document: &CadDocument,
|
||
handle: Handle,
|
||
dim: &Dimension,
|
||
selected: bool,
|
||
entity_color: [f32; 4],
|
||
line_weight_px: f32,
|
||
anno_scale: f32,
|
||
// LOD hints — when present, synthesised dim text routes through the
|
||
// top-level LOD ladder (baseline / greek / full) instead of the render
|
||
// path so far-out drawings collapse to a colored rect or baseline.
|
||
selected_set: &rustc_hash::FxHashSet<codec::Handle>,
|
||
active_viewport: Option<codec::Handle>,
|
||
bg_color: [f32; 4],
|
||
view_aabb: Option<[f32; 4]>,
|
||
world_per_pixel: Option<f32>,
|
||
) -> Vec<WireModel> {
|
||
if let Dimension::Arc(arc) = dim {
|
||
if let Some((local_arc, normal)) = arc_dimension_in_ocs(arc) {
|
||
let mut wires = tessellate_dimension_inner(
|
||
document,
|
||
handle,
|
||
&Dimension::Arc(local_arc),
|
||
selected,
|
||
entity_color,
|
||
line_weight_px,
|
||
anno_scale,
|
||
selected_set,
|
||
active_viewport,
|
||
bg_color,
|
||
None,
|
||
world_per_pixel,
|
||
);
|
||
for wire in &mut wires {
|
||
map_wire_ocs_to_wcs(wire, normal);
|
||
}
|
||
return wires;
|
||
}
|
||
}
|
||
if let Dimension::LargeRadial(radial) = dim {
|
||
if let Some((local_radial, normal)) = large_radial_dimension_in_ocs(radial) {
|
||
let mut wires = tessellate_dimension_inner(
|
||
document,
|
||
handle,
|
||
&Dimension::LargeRadial(local_radial),
|
||
selected,
|
||
entity_color,
|
||
line_weight_px,
|
||
anno_scale,
|
||
selected_set,
|
||
active_viewport,
|
||
bg_color,
|
||
None,
|
||
world_per_pixel,
|
||
);
|
||
for wire in &mut wires {
|
||
map_wire_ocs_to_wcs(wire, normal);
|
||
}
|
||
return wires;
|
||
}
|
||
}
|
||
if let Some((local, normal)) = linear_dimension_in_ocs(dim) {
|
||
let mut wires = tessellate_dimension_inner(
|
||
document,
|
||
handle,
|
||
&local,
|
||
selected,
|
||
entity_color,
|
||
line_weight_px,
|
||
anno_scale,
|
||
selected_set,
|
||
active_viewport,
|
||
bg_color,
|
||
None,
|
||
world_per_pixel,
|
||
);
|
||
for wire in &mut wires {
|
||
map_wire_ocs_to_wcs(wire, normal);
|
||
}
|
||
return wires;
|
||
}
|
||
let name = handle.value().to_string();
|
||
// (Baked-block fast path moved up into scene::tessellate_entity so the
|
||
// recursive call goes through the LOD ladder, not the kernel path.)
|
||
|
||
let style_name = &dim.base().style_name;
|
||
let source_style = document.dim_styles.iter().find(|s| {
|
||
s.name.eq_ignore_ascii_case(style_name)
|
||
|| (style_name.trim().is_empty() && s.name.eq_ignore_ascii_case("Standard"))
|
||
});
|
||
let effective_style = source_style.map(|style| resolved_dimension_style(style, dim, document));
|
||
let style = effective_style.as_ref();
|
||
|
||
// A positive style scale is fixed. A zero style scale uses the multiplier
|
||
// resolved by the scene for the current annotation or viewport context.
|
||
let dim_scale = style
|
||
.map(|s| {
|
||
if s.dimscale > 1e-6 {
|
||
s.dimscale
|
||
} else {
|
||
anno_scale as f64
|
||
}
|
||
})
|
||
.unwrap_or(1.0);
|
||
|
||
let (
|
||
dimasz_raw,
|
||
dimexo,
|
||
dimexe,
|
||
dim_txt,
|
||
dimtsz_raw,
|
||
dimsah,
|
||
dimse1,
|
||
dimse2,
|
||
dimsd1,
|
||
dimsd2,
|
||
dimdle,
|
||
dimfxl,
|
||
dimfxlon,
|
||
dimsoxd,
|
||
dimcen,
|
||
) = style
|
||
.map(|s| {
|
||
(
|
||
s.dimasz * dim_scale,
|
||
(s.dimexo * dim_scale) as f32,
|
||
(s.dimexe * dim_scale) as f32,
|
||
s.dimtxt * dim_scale,
|
||
s.dimtsz * dim_scale,
|
||
s.dimsah,
|
||
s.dimse1,
|
||
s.dimse2,
|
||
s.dimsd1,
|
||
s.dimsd2,
|
||
(s.dimdle * dim_scale) as f32,
|
||
(s.dimfxl * dim_scale) as f32,
|
||
s.dimfxlon,
|
||
s.dimsoxd,
|
||
(s.dimcen * dim_scale) as f32,
|
||
)
|
||
})
|
||
.unwrap_or((
|
||
0.18, 0.0, 0.0, 2.5, 0.0, false, false, false, false, false, 0.0, 1.0, false, false,
|
||
0.09,
|
||
));
|
||
|
||
// Arrow selection precedence:
|
||
// 1. DIMTSZ>0 → oblique tick (overrides arrow blocks).
|
||
// 2. Radius and large-radius dimensions → DIMLDRBLK.
|
||
// 3. DIMSAH false → DIMBLK on both ends.
|
||
// 4. DIMSAH true → DIMBLK1 (first end), DIMBLK2 (second end).
|
||
// Unknown / NULL block handles fall back to ClosedFilled.
|
||
let dimasz = (dimasz_raw as f32).max(0.001);
|
||
let defer_arrow_hatches = {
|
||
let name = dim.base().block_name.trim();
|
||
let mut memo = std::collections::HashMap::new();
|
||
!name.is_empty()
|
||
&& crate::scene::render_graph::block_contains_hatch(
|
||
document,
|
||
name,
|
||
&mut memo,
|
||
)
|
||
};
|
||
let (arrow1, arrow2) = if dimtsz_raw > 1e-9 {
|
||
let t = ArrowKind::Tick {
|
||
size: (dimtsz_raw as f32).max(0.001),
|
||
};
|
||
(t.clone(), t)
|
||
} else if let Some(s) = style {
|
||
if matches!(dim, Dimension::Radius(_) | Dimension::LargeRadial(_)) {
|
||
let handle = crate::entities::dim_override::handle(
|
||
&dim.base().common.extended_data,
|
||
crate::entities::dim_override::DIMLDRBLK,
|
||
)
|
||
.unwrap_or(s.dimldrblk);
|
||
let arrow = arrow_from_block_with_deferred_hatch(
|
||
document,
|
||
handle,
|
||
dimasz,
|
||
defer_arrow_hatches,
|
||
);
|
||
(arrow.clone(), arrow)
|
||
} else if matches!(dim, Dimension::Diameter(_)) {
|
||
let data = &dim.base().common.extended_data;
|
||
let first = crate::entities::dim_override::handle(
|
||
data,
|
||
crate::entities::dim_override::DIMBLK1,
|
||
)
|
||
.unwrap_or(if dimsah { s.dimblk1 } else { s.dimblk });
|
||
let second = crate::entities::dim_override::handle(
|
||
data,
|
||
crate::entities::dim_override::DIMBLK2,
|
||
)
|
||
.unwrap_or(if dimsah { s.dimblk2 } else { s.dimblk });
|
||
(
|
||
arrow_from_block_with_deferred_hatch(
|
||
document,
|
||
first,
|
||
dimasz,
|
||
defer_arrow_hatches,
|
||
),
|
||
arrow_from_block_with_deferred_hatch(
|
||
document,
|
||
second,
|
||
dimasz,
|
||
defer_arrow_hatches,
|
||
),
|
||
)
|
||
} else if dimsah {
|
||
(
|
||
arrow_from_block_with_deferred_hatch(
|
||
document,
|
||
s.dimblk1,
|
||
dimasz,
|
||
defer_arrow_hatches,
|
||
),
|
||
arrow_from_block_with_deferred_hatch(
|
||
document,
|
||
s.dimblk2,
|
||
dimasz,
|
||
defer_arrow_hatches,
|
||
),
|
||
)
|
||
} else {
|
||
let a = arrow_from_block_with_deferred_hatch(
|
||
document,
|
||
s.dimblk,
|
||
dimasz,
|
||
defer_arrow_hatches,
|
||
);
|
||
(a.clone(), a)
|
||
}
|
||
} else {
|
||
let a = ArrowKind::Triangle {
|
||
size: dimasz,
|
||
filled: true,
|
||
size_mul: 1.0,
|
||
};
|
||
(a.clone(), a)
|
||
};
|
||
|
||
let text_layout = dimension_text_layout(dim, style, dim_txt, dim_scale);
|
||
|
||
let mut geom = dimension_geometry(
|
||
dim,
|
||
&arrow1,
|
||
&arrow2,
|
||
DimLineParams {
|
||
dimexo,
|
||
dimexe,
|
||
dimdle,
|
||
dimfxl,
|
||
dimfxlon,
|
||
dimsoxd,
|
||
dimcen,
|
||
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),
|
||
text_position: text_layout.position,
|
||
horizontal_text: text_layout.horizontal,
|
||
text_movement: style.map(|s| s.dimtmove).unwrap_or(0),
|
||
text_break: text_layout.break_box,
|
||
leader_anchor: style
|
||
.filter(|s| s.dimtmove == 1)
|
||
.and_then(|_| stored_text_point(dim))
|
||
.map(vec3_local),
|
||
},
|
||
SuppressFlags {
|
||
ext1: dimse1,
|
||
ext2: dimse2,
|
||
dim1: dimsd1,
|
||
dim2: dimsd2,
|
||
},
|
||
);
|
||
|
||
if !dimse1 {
|
||
if let Some(points) = crate::scene::dimension_assoc::radial_extension_points(
|
||
document,
|
||
handle,
|
||
dimexo as f64,
|
||
dimexe as f64,
|
||
) {
|
||
let points: Vec<Vec3> = points.into_iter().map(vec3_local).collect();
|
||
add_polyline(&mut geom.ext_lines, &points);
|
||
}
|
||
}
|
||
|
||
// DIMTMOVE=1 connects the dimension line to rendered text.
|
||
if let Some(s) = style {
|
||
if s.dimtmove == 1 {
|
||
if let Some((anchor, txt, under_text)) = dimtmove_leader_endpoints(dim, text_layout) {
|
||
let gap = dim_txt as f32 * 0.5;
|
||
if under_text || (txt - anchor).length() > gap * 2.0 {
|
||
add_segment_with_text_break(
|
||
&mut geom.dim_lines,
|
||
anchor,
|
||
txt,
|
||
text_layout.break_box,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
// DIMUPT governs interactive creation-time text placement; saved
|
||
// geometry already carries the resulting position.
|
||
let _ = s.dimupt;
|
||
let _ = s.dimarcsym;
|
||
// DIMUNIT is the obsolete pre-R2000 linear unit format; DIMLUNIT
|
||
// supersedes it. Read but not honoured.
|
||
let _ = s.dimunit;
|
||
}
|
||
if let Some(point) = dimension_jog_point(dim) {
|
||
let jog_angle = style
|
||
.map(|style| style.dimjogang as f32)
|
||
.unwrap_or(std::f32::consts::FRAC_PI_4);
|
||
apply_dimension_jog(
|
||
&mut geom.dim_lines,
|
||
point,
|
||
vec3_local(dim.base().normal),
|
||
dim_txt as f32 * CELL_WIDTH as f32,
|
||
jog_angle,
|
||
);
|
||
}
|
||
apply_dimension_breaks(document, handle, &mut geom.dim_lines);
|
||
apply_dimension_breaks(document, handle, &mut geom.ext_lines);
|
||
// Dimension entity fields that the render path doesn't yet use but are
|
||
// preserved on save:
|
||
// - base.insertion_point: legacy anchor reference; render uses
|
||
// text_middle_point + dim-line geometry instead.
|
||
// - base.block_name: generated anonymous block name for
|
||
// the dim graphics — we re-tessellate so don't need it.
|
||
// - base.version: DXF format marker (metadata only).
|
||
let _ = (
|
||
dim.base().insertion_point,
|
||
&dim.base().block_name,
|
||
dim.base().version,
|
||
);
|
||
|
||
// Per-spec colours: DIMCLRD (dim/arrows), DIMCLRE (ext), DIMCLRT (text).
|
||
// 0=ByBlock and 256=ByLayer fall through to entity_color. Each honours the
|
||
// per-object ACAD_DSTYLE override (codes 176/177/178) the Properties panel
|
||
// reads and writes, so an edited colour renders without touching the style.
|
||
let xd = &dim.base().common.extended_data;
|
||
let dim_color = if selected {
|
||
WireModel::SELECTED
|
||
} else {
|
||
resolve_dim_color(
|
||
dim_color_index(
|
||
xd,
|
||
crate::entities::dim_override::DIMCLRD,
|
||
style.map(|s| s.dimclrd).unwrap_or(0),
|
||
),
|
||
entity_color,
|
||
)
|
||
};
|
||
let ext_color = if selected {
|
||
WireModel::SELECTED
|
||
} else {
|
||
resolve_dim_color(
|
||
dim_color_index(
|
||
xd,
|
||
crate::entities::dim_override::DIMCLRE,
|
||
style.map(|s| s.dimclre).unwrap_or(0),
|
||
),
|
||
entity_color,
|
||
)
|
||
};
|
||
let text_color = if selected {
|
||
entity_color // text wire color set by inner tessellate; keep entity tint
|
||
} else {
|
||
resolve_dim_color(
|
||
dim_color_index(
|
||
xd,
|
||
crate::entities::dim_override::DIMCLRT,
|
||
style.map(|s| s.dimclrt).unwrap_or(0),
|
||
),
|
||
entity_color,
|
||
)
|
||
};
|
||
|
||
let snap_pts = dimension_snap_pts(dim);
|
||
let key_vertices: Vec<[f64; 3]> = geom
|
||
.dim_lines
|
||
.iter()
|
||
.chain(geom.ext_lines.iter())
|
||
.copied()
|
||
.filter(|p| !(p[0].is_nan() || p[1].is_nan() || p[2].is_nan()))
|
||
.map(|[x, y, z]| [x as f64, y as f64, z as f64])
|
||
.collect();
|
||
|
||
// DIMLWD (dim line + arrows) and DIMLWE (extension lines). Negative
|
||
// codes fall through to the entity's own resolved weight.
|
||
let lw_dim = resolve_dim_lineweight_px(style.map(|s| s.dimlwd).unwrap_or(-2), line_weight_px);
|
||
let lw_ext = resolve_dim_lineweight_px(style.map(|s| s.dimlwe).unwrap_or(-2), line_weight_px);
|
||
|
||
// DIMLTEX (dim line) / DIMLTEX1 (ext1) / DIMLTEX2 (ext2) — linetype
|
||
// handles → pattern. Looked up in document.line_types by handle.
|
||
let lt_scale = document.header.linetype_scale as f32 * dim.base().common.linetype_scale as f32;
|
||
let (dim_pat_len, dim_pat) = style
|
||
.map(|s| resolve_pattern_by_handle(document, s.dimltex_handle, lt_scale))
|
||
.unwrap_or((0.0, [0.0; 8]));
|
||
let (ext1_pat_len, ext1_pat) = style
|
||
.map(|s| resolve_pattern_by_handle(document, s.dimltex1_handle, lt_scale))
|
||
.unwrap_or((0.0, [0.0; 8]));
|
||
let (ext2_pat_len, ext2_pat) = style
|
||
.map(|s| resolve_pattern_by_handle(document, s.dimltex2_handle, lt_scale))
|
||
.unwrap_or((0.0, [0.0; 8]));
|
||
|
||
let mut wires = Vec::new();
|
||
|
||
if !geom.ext_lines.is_empty() {
|
||
// If ext1 and ext2 have different linetypes, split into two wires so
|
||
// each can carry its own pattern. Otherwise emit as a single wire.
|
||
let split = ext1_pat_len != ext2_pat_len || ext1_pat != ext2_pat;
|
||
if split {
|
||
let (ext1, ext2) = split_ext_lines(&geom.ext_lines);
|
||
if !ext1.is_empty() {
|
||
wires.push(WireModel {
|
||
bg_adapt: None,
|
||
point_marker: None,
|
||
taper_widths: Vec::new(),
|
||
pattern_stations: Vec::new(),
|
||
world_width: 0.0,
|
||
depth_override: None,
|
||
display_visible: true,
|
||
snap_only: false,
|
||
plot_visible: true,
|
||
fill_is_3d: false,
|
||
fill_is_2d_solid: false,
|
||
render_instance: None,
|
||
pick_tris: Vec::new(),
|
||
pick_tris_low: Vec::new(),
|
||
dash_from_start: false,
|
||
dash_align_end: None,
|
||
text_verts: Vec::new(),
|
||
name: name.clone(),
|
||
points: ext1,
|
||
points_low: Vec::new(),
|
||
color: ext_color,
|
||
selected,
|
||
aci: 0,
|
||
pattern_length: ext1_pat_len,
|
||
pattern: ext1_pat,
|
||
line_weight_px: lw_ext,
|
||
snap_pts: vec![],
|
||
tangent_geoms: vec![],
|
||
key_vertices: vec![],
|
||
aabb: WireModel::UNBOUNDED_AABB,
|
||
plinegen: true,
|
||
fill_tris: vec![],
|
||
fill_tris_low: Vec::new(),
|
||
});
|
||
}
|
||
if !ext2.is_empty() {
|
||
wires.push(WireModel {
|
||
bg_adapt: None,
|
||
point_marker: None,
|
||
taper_widths: Vec::new(),
|
||
pattern_stations: Vec::new(),
|
||
world_width: 0.0,
|
||
depth_override: None,
|
||
display_visible: true,
|
||
snap_only: false,
|
||
plot_visible: true,
|
||
fill_is_3d: false,
|
||
fill_is_2d_solid: false,
|
||
render_instance: None,
|
||
pick_tris: Vec::new(),
|
||
pick_tris_low: Vec::new(),
|
||
dash_from_start: false,
|
||
dash_align_end: None,
|
||
text_verts: Vec::new(),
|
||
name: name.clone(),
|
||
points: ext2,
|
||
points_low: Vec::new(),
|
||
color: ext_color,
|
||
selected,
|
||
aci: 0,
|
||
pattern_length: ext2_pat_len,
|
||
pattern: ext2_pat,
|
||
line_weight_px: lw_ext,
|
||
snap_pts: vec![],
|
||
tangent_geoms: vec![],
|
||
key_vertices: vec![],
|
||
aabb: WireModel::UNBOUNDED_AABB,
|
||
plinegen: true,
|
||
fill_tris: vec![],
|
||
fill_tris_low: Vec::new(),
|
||
});
|
||
}
|
||
} else {
|
||
wires.push(WireModel {
|
||
bg_adapt: None,
|
||
point_marker: None,
|
||
taper_widths: Vec::new(),
|
||
pattern_stations: Vec::new(),
|
||
world_width: 0.0,
|
||
depth_override: None,
|
||
display_visible: true,
|
||
snap_only: false,
|
||
plot_visible: true,
|
||
fill_is_3d: false,
|
||
fill_is_2d_solid: false,
|
||
render_instance: None,
|
||
pick_tris: Vec::new(),
|
||
pick_tris_low: Vec::new(),
|
||
dash_from_start: false,
|
||
dash_align_end: None,
|
||
text_verts: Vec::new(),
|
||
name: name.clone(),
|
||
points: geom.ext_lines,
|
||
points_low: Vec::new(),
|
||
color: ext_color,
|
||
selected,
|
||
aci: 0,
|
||
pattern_length: ext1_pat_len,
|
||
pattern: ext1_pat,
|
||
line_weight_px: lw_ext,
|
||
snap_pts: vec![],
|
||
tangent_geoms: vec![],
|
||
key_vertices: vec![],
|
||
aabb: WireModel::UNBOUNDED_AABB,
|
||
plinegen: true,
|
||
fill_tris: vec![],
|
||
fill_tris_low: Vec::new(),
|
||
});
|
||
}
|
||
}
|
||
|
||
wires.push(WireModel {
|
||
bg_adapt: None,
|
||
point_marker: None,
|
||
taper_widths: Vec::new(),
|
||
pattern_stations: Vec::new(),
|
||
world_width: 0.0,
|
||
depth_override: None,
|
||
display_visible: true,
|
||
snap_only: false,
|
||
plot_visible: true,
|
||
fill_is_3d: false,
|
||
fill_is_2d_solid: false,
|
||
render_instance: None,
|
||
pick_tris: Vec::new(),
|
||
pick_tris_low: Vec::new(),
|
||
dash_from_start: false,
|
||
dash_align_end: None,
|
||
text_verts: Vec::new(),
|
||
name: name.clone(),
|
||
points: geom.dim_lines,
|
||
points_low: Vec::new(),
|
||
color: dim_color,
|
||
selected,
|
||
aci: 0,
|
||
pattern_length: dim_pat_len,
|
||
pattern: dim_pat,
|
||
line_weight_px: lw_dim,
|
||
snap_pts,
|
||
tangent_geoms: vec![],
|
||
key_vertices,
|
||
aabb: WireModel::UNBOUNDED_AABB,
|
||
plinegen: true,
|
||
fill_tris: geom.arrow_fill,
|
||
// fill_tris_low intentionally empty: this fill renders on the top-level
|
||
// path, where consumers (face3d_gpu, xclip) treat a short low half as
|
||
// all-zero, so it draws at f32 precision (sub-metre error at UTM scale)
|
||
// — not a crash. Follow-up: double-single-split via points_to_ds to
|
||
// match emit_wire's paired fill path.
|
||
fill_tris_low: Vec::new(),
|
||
});
|
||
|
||
if let Some(symbol) = style.and_then(|style| {
|
||
arc_length_symbol_points(dim, Some(style), dim_txt, dim_scale, style.dimarcsym)
|
||
}) {
|
||
let mut points = Vec::new();
|
||
add_polyline(&mut points, &symbol);
|
||
wires.push(WireModel {
|
||
bg_adapt: None,
|
||
point_marker: None,
|
||
taper_widths: Vec::new(),
|
||
pattern_stations: Vec::new(),
|
||
world_width: 0.0,
|
||
depth_override: None,
|
||
display_visible: true,
|
||
snap_only: false,
|
||
plot_visible: true,
|
||
fill_is_3d: false,
|
||
fill_is_2d_solid: false,
|
||
render_instance: None,
|
||
pick_tris: Vec::new(),
|
||
pick_tris_low: Vec::new(),
|
||
dash_from_start: false,
|
||
dash_align_end: None,
|
||
text_verts: Vec::new(),
|
||
name: name.clone(),
|
||
points,
|
||
points_low: Vec::new(),
|
||
color: if selected { WireModel::SELECTED } else { text_color },
|
||
selected,
|
||
aci: 0,
|
||
pattern_length: 0.0,
|
||
pattern: [0.0; 8],
|
||
line_weight_px: 1.0,
|
||
snap_pts: vec![],
|
||
tangent_geoms: vec![],
|
||
key_vertices: vec![],
|
||
aabb: WireModel::UNBOUNDED_AABB,
|
||
plinegen: true,
|
||
fill_tris: vec![],
|
||
fill_tris_low: Vec::new(),
|
||
});
|
||
}
|
||
|
||
// DIMTFILL: 0=none, 1=drawing background (mask), 2=DIMTFILLCLR.
|
||
if let Some(s) = style {
|
||
if s.dimtfill == 1 || s.dimtfill == 2 {
|
||
if let Some(rect) = text_fill_rect(dim, style, dim_txt, dim_scale) {
|
||
let fill_color = if selected {
|
||
WireModel::SELECTED
|
||
} else if s.dimtfill == 1 {
|
||
// Drawing-background fill: mask out geometry behind the text.
|
||
bg_color
|
||
} else {
|
||
let c = AcadColor::from_index(s.dimtfillclr);
|
||
aci_to_rgba(&c)
|
||
};
|
||
wires.push(WireModel {
|
||
bg_adapt: None,
|
||
point_marker: None,
|
||
taper_widths: Vec::new(),
|
||
pattern_stations: Vec::new(),
|
||
world_width: 0.0,
|
||
depth_override: None,
|
||
display_visible: true,
|
||
snap_only: false,
|
||
plot_visible: true,
|
||
fill_is_3d: false,
|
||
fill_is_2d_solid: false,
|
||
render_instance: None,
|
||
pick_tris: Vec::new(),
|
||
pick_tris_low: Vec::new(),
|
||
dash_from_start: false,
|
||
dash_align_end: None,
|
||
text_verts: Vec::new(),
|
||
name: name.clone(),
|
||
points: vec![],
|
||
points_low: Vec::new(),
|
||
color: fill_color,
|
||
selected,
|
||
aci: 0,
|
||
pattern_length: 0.0,
|
||
pattern: [0.0; 8],
|
||
line_weight_px: 1.0,
|
||
snap_pts: vec![],
|
||
tangent_geoms: vec![],
|
||
key_vertices: vec![],
|
||
aabb: WireModel::UNBOUNDED_AABB,
|
||
plinegen: true,
|
||
fill_tris: rect,
|
||
// fill_tris_low intentionally empty: this fill renders on the
|
||
// top-level path, where consumers (face3d_gpu, xclip) treat a
|
||
// short low half as all-zero, so it draws at f32 precision
|
||
// (sub-metre error at UTM scale) — not a crash. Follow-up:
|
||
// double-single-split via points_to_ds to match emit_wire.
|
||
fill_tris_low: Vec::new(),
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// A negative DIMGAP denotes the Basic tolerance display: frame the
|
||
// dimension text while keeping the absolute gap as the frame margin.
|
||
if style.is_some_and(|s| s.dimgap < 0.0) {
|
||
if let Some(rect) = text_fill_rect(dim, style, dim_txt, dim_scale) {
|
||
let p1 = rect[0];
|
||
let p2 = rect[1];
|
||
let p3 = rect[2];
|
||
let p4 = rect[5];
|
||
wires.push(WireModel {
|
||
bg_adapt: None,
|
||
point_marker: None,
|
||
taper_widths: Vec::new(),
|
||
pattern_stations: Vec::new(),
|
||
world_width: 0.0,
|
||
depth_override: None,
|
||
display_visible: true,
|
||
snap_only: false,
|
||
plot_visible: true,
|
||
fill_is_3d: false,
|
||
fill_is_2d_solid: false,
|
||
render_instance: None,
|
||
pick_tris: Vec::new(),
|
||
pick_tris_low: Vec::new(),
|
||
dash_from_start: false,
|
||
dash_align_end: None,
|
||
text_verts: Vec::new(),
|
||
name: name.clone(),
|
||
points: vec![p1, p2, p2, p3, p3, p4, p4, p1],
|
||
points_low: Vec::new(),
|
||
color: if selected { WireModel::SELECTED } else { text_color },
|
||
selected,
|
||
aci: 0,
|
||
pattern_length: 0.0,
|
||
pattern: [0.0; 8],
|
||
line_weight_px: 1.0,
|
||
snap_pts: vec![],
|
||
tangent_geoms: vec![],
|
||
key_vertices: vec![],
|
||
aabb: WireModel::UNBOUNDED_AABB,
|
||
plinegen: true,
|
||
fill_tris: vec![],
|
||
fill_tris_low: Vec::new(),
|
||
});
|
||
}
|
||
}
|
||
|
||
if let Some(mut synth_text_entity) =
|
||
dimension_text_entity(dim, dim_txt, style, document, dim_scale)
|
||
{
|
||
// The glyphs take their colour from the text entity itself, which
|
||
// copies the dimension's (usually ByLayer); an explicit DIMCLRT has to
|
||
// be on the entity or the layer colour wins. (#898)
|
||
let text_index = dim_color_index(
|
||
xd,
|
||
crate::entities::dim_override::DIMCLRT,
|
||
style.map(|s| s.dimclrt).unwrap_or(0),
|
||
);
|
||
if (1..=255).contains(&text_index) {
|
||
synth_text_entity.common_mut().color = AcadColor::from_index(text_index);
|
||
}
|
||
// Tolerance Text rendered separately so DIMTFAC scales its height
|
||
// and DIMTOLJ aligns it vertically against the primary text.
|
||
let tol_entity = dimension_tolerance_entity(dim, style, &synth_text_entity, dim_txt);
|
||
// Route synthesised dim text through tessellate_entity so the
|
||
// baseline/greek/full LOD ladder applies (zoom-out behaviour
|
||
// matches top-level Text / MText). The text already has dim_scale
|
||
// baked into its height, so anno_scale stays 1.0.
|
||
let text_wires = crate::scene::tessellate_entity_dim_text(
|
||
document,
|
||
selected_set,
|
||
active_viewport,
|
||
bg_color,
|
||
1.0,
|
||
&synth_text_entity,
|
||
view_aabb,
|
||
world_per_pixel,
|
||
text_color,
|
||
);
|
||
for mut w in text_wires {
|
||
w.name = name.clone();
|
||
wires.push(w);
|
||
}
|
||
|
||
if let Some(tol_entity_e) = tol_entity {
|
||
let tol_wires = crate::scene::tessellate_entity_dim_text(
|
||
document,
|
||
selected_set,
|
||
active_viewport,
|
||
bg_color,
|
||
1.0,
|
||
&tol_entity_e,
|
||
view_aabb,
|
||
world_per_pixel,
|
||
text_color,
|
||
);
|
||
for mut w in tol_wires {
|
||
w.name = name.clone();
|
||
wires.push(w);
|
||
}
|
||
}
|
||
}
|
||
|
||
wires
|
||
}
|
||
/// The ACI index a dimension colour renders with: the per-object ACAD_DSTYLE
|
||
/// override when the entity carries one, else the dimension style's value.
|
||
/// The Properties panel reads and writes these same overrides, so the renderer
|
||
/// has to consult them or an edited colour shows in the panel and nowhere else.
|
||
fn dim_color_index(
|
||
xd: &codec::xdata::ExtendedData,
|
||
code: i16,
|
||
inherited: i16,
|
||
) -> i16 {
|
||
crate::entities::dim_override::int(xd, code).unwrap_or(inherited)
|
||
}
|
||
|
||
fn resolve_dim_color(idx: i16, fallback: [f32; 4]) -> [f32; 4] {
|
||
// DIMCLR* convention: 0 = BYBLOCK, 256 = BYLAYER → entity colour wins.
|
||
if idx == 0 || idx == 256 {
|
||
return fallback;
|
||
}
|
||
aci_to_rgba(&AcadColor::from_index(idx))
|
||
}
|
||
|
||
/// Resolve a DIMLWD / DIMLWE table value (the i16 lineweight code) into a
|
||
/// pixel width. -1 (ByLayer) / -2 (ByBlock) / -3 (Default) fall through to
|
||
/// the entity's already-resolved width.
|
||
fn resolve_dim_lineweight_px(code: i16, fallback_px: f32) -> f32 {
|
||
const MM_TO_PX: f32 = 96.0 / 25.4;
|
||
if code < 0 {
|
||
return fallback_px;
|
||
}
|
||
// i16 value 0..=211 represents 1/100 mm.
|
||
let mm = code as f32 / 100.0;
|
||
(mm * MM_TO_PX).max(1.0)
|
||
}
|
||
|
||
/// Look up a linetype in the document's line_types table by handle and
|
||
/// resolve it to a (pattern_length, pattern) pair compatible with WireModel.
|
||
fn resolve_pattern_by_handle(
|
||
doc: &CadDocument,
|
||
handle: codec::types::Handle,
|
||
scale: f32,
|
||
) -> (f32, [f32; 8]) {
|
||
if handle.is_null() {
|
||
return (0.0, [0.0; 8]);
|
||
}
|
||
let name = doc
|
||
.line_types
|
||
.iter()
|
||
.find(|lt| lt.handle == handle)
|
||
.map(|lt| lt.name.clone());
|
||
match name {
|
||
Some(n) => crate::scene::view::render::resolve_pattern(&doc.line_types, &n, scale),
|
||
None => (0.0, [0.0; 8]),
|
||
}
|
||
}
|
||
|
||
/// Split the combined ext-lines point list (NaN-separated segment pairs)
|
||
/// into "first" / "second" halves. `append_linear_dimension` writes ext1
|
||
/// before ext2, so the first segment is ext1 and the second is ext2.
|
||
fn split_ext_lines(points: &[[f32; 3]]) -> (Vec<[f32; 3]>, Vec<[f32; 3]>) {
|
||
let mut groups: Vec<Vec<[f32; 3]>> = Vec::new();
|
||
let mut current: Vec<[f32; 3]> = Vec::new();
|
||
for &p in points {
|
||
if p[0].is_nan() {
|
||
if !current.is_empty() {
|
||
groups.push(std::mem::take(&mut current));
|
||
}
|
||
} else {
|
||
current.push(p);
|
||
}
|
||
}
|
||
if !current.is_empty() {
|
||
groups.push(current);
|
||
}
|
||
let mut iter = groups.into_iter();
|
||
let first = iter.next().unwrap_or_default();
|
||
let rest: Vec<[f32; 3]> = iter.flatten().collect();
|
||
(first, rest)
|
||
}
|
||
|
||
/// Endpoints for the DIMTMOVE=1 leader.
|
||
fn dimtmove_leader_endpoints(
|
||
dim: &Dimension,
|
||
text: DimensionTextLayout,
|
||
) -> Option<(Vec3, Vec3, bool)> {
|
||
let lv = |v| vec3_local(v);
|
||
let txt = text.position;
|
||
let linear = |first: Vec3, second: Vec3, definition: Vec3, axis: [f64; 2]| {
|
||
let bounds = text.break_box?;
|
||
let xy = |point: Vec3| [point.x as f64, point.y as f64];
|
||
let (leader, under_text) = kernel::geom2d::linear_dimension_leader(
|
||
kernel::geom2d::Line { start: xy(first), end: xy(second) },
|
||
xy(definition),
|
||
axis,
|
||
xy(txt),
|
||
[
|
||
(bounds.half_width + bounds.padding) as f64,
|
||
(bounds.half_height + bounds.padding) as f64,
|
||
],
|
||
bounds.rotation,
|
||
)?;
|
||
let point = |p: [f64; 2]| Vec3::new(p[0] as f32, p[1] as f32, first.z);
|
||
Some((point(leader.start), point(leader.end), under_text))
|
||
};
|
||
match dim {
|
||
Dimension::Linear(d) => {
|
||
return linear(
|
||
lv(d.first_point),
|
||
lv(d.second_point),
|
||
lv(d.definition_point),
|
||
[d.rotation.cos(), d.rotation.sin()],
|
||
);
|
||
}
|
||
Dimension::Aligned(d) => {
|
||
let first = lv(d.first_point);
|
||
let second = lv(d.second_point);
|
||
return linear(
|
||
first,
|
||
second,
|
||
lv(d.definition_point),
|
||
[d.second_point.x - d.first_point.x, d.second_point.y - d.first_point.y],
|
||
);
|
||
}
|
||
_ => {}
|
||
}
|
||
let anchor = match dim {
|
||
Dimension::Radius(_) => return None,
|
||
Dimension::Diameter(d) => {
|
||
let chord = lv(d.angle_vertex);
|
||
let far_chord = lv(d.definition_point);
|
||
if chord.distance_squared(txt) <= far_chord.distance_squared(txt) {
|
||
chord
|
||
} else {
|
||
far_chord
|
||
}
|
||
}
|
||
Dimension::Angular2Ln(d) => lv(d.dimension_arc),
|
||
Dimension::Angular3Pt(d) => lv(d.definition_point),
|
||
Dimension::LargeRadial(d) => {
|
||
let chord = lv(d.chord_point);
|
||
let jog = lv(d.jog_point);
|
||
let override_center = lv(d.override_center);
|
||
let (near, far) =
|
||
jogged_radial_break(chord, jog, override_center, d.jog_angle as f32);
|
||
let curves = [(chord, near), (near, far), (far, override_center)].map(
|
||
|(start, end)| {
|
||
kernel::geom2d::Curve::Line(kernel::geom2d::Line {
|
||
start: [start.x as f64, start.y as f64],
|
||
end: [end.x as f64, end.y as f64],
|
||
})
|
||
},
|
||
);
|
||
let (_, closest) = kernel::geom2d::nearest_of(
|
||
curves.iter(),
|
||
[txt.x as f64, txt.y as f64],
|
||
)?;
|
||
Vec3::new(closest.point[0] as f32, closest.point[1] as f32, chord.z)
|
||
}
|
||
_ => return None,
|
||
};
|
||
Some((anchor, txt, false))
|
||
}
|
||
|
||
/// Build a rectangle of filled triangles sitting under the dim text, used
|
||
/// when DIMTFILL = 2 (explicit fill colour). The rect width is estimated
|
||
/// from the formatted text length × character-cell width; an absolutely
|
||
/// correct box would need full text metrics from the font cache.
|
||
fn text_fill_rect(
|
||
dim: &Dimension,
|
||
style: Option<&DimStyle>,
|
||
text_height: f64,
|
||
dim_scale: f64,
|
||
) -> Option<Vec<[f32; 3]>> {
|
||
let value = dimension_text_value(dim, style)?;
|
||
if value.is_empty() {
|
||
return None;
|
||
}
|
||
let pos = dimension_text_pos_f64(dim, style, text_height, dim_scale);
|
||
let dimgap = style.map(|s| s.dimgap.abs()).unwrap_or(0.0) * dim_scale;
|
||
// CELL_WIDTH × text_height per character approximates the average glyph
|
||
// aspect of the bundled stick fonts. Inflate by 1 DIMGAP on each side.
|
||
let stack_scale = style.map(dimtfac_or_one).unwrap_or(1.0);
|
||
let approx_w = text_cells(&value, stack_scale) * text_height * CELL_WIDTH + dimgap * 2.0;
|
||
let approx_h = dimension_text_extent_height(dim, style, text_height) + dimgap * 2.0;
|
||
let rot = if dim.base().text_rotation.abs() > 1e-9 {
|
||
dim.base().text_rotation
|
||
} else {
|
||
dimension_text_natural_rotation(dim)
|
||
};
|
||
let (sr, cr) = rot.sin_cos();
|
||
let hx = approx_w * 0.5;
|
||
let hy = approx_h * 0.5;
|
||
let cx = (pos.x) as f32;
|
||
let cy = (pos.y) as f32;
|
||
let cz = (pos.z) as f32;
|
||
let corner = |dx: f64, dy: f64| -> [f32; 3] {
|
||
let lx = dx * cr - dy * sr;
|
||
let ly = dx * sr + dy * cr;
|
||
[cx + lx as f32, cy + ly as f32, cz]
|
||
};
|
||
let p1 = corner(-hx, -hy);
|
||
let p2 = corner(hx, -hy);
|
||
let p3 = corner(hx, hy);
|
||
let p4 = corner(-hx, hy);
|
||
Some(vec![p1, p2, p3, p1, p3, p4])
|
||
}
|
||
|
||
fn arc_length_symbol_points(
|
||
dim: &Dimension,
|
||
style: Option<&DimStyle>,
|
||
text_height: f64,
|
||
dim_scale: f64,
|
||
symbol_position: i16,
|
||
) -> Option<Vec<Vec3>> {
|
||
if !matches!(dim, Dimension::Arc(_)) || symbol_position == 2 {
|
||
return None;
|
||
}
|
||
let value = dimension_text_value(dim, style)?;
|
||
if value.is_empty() || text_height <= 1.0e-12 {
|
||
return None;
|
||
}
|
||
|
||
let position = dimension_text_pos_f64(dim, style, text_height, dim_scale);
|
||
let rotation = dimension_text_rotation(dim, style);
|
||
let (sin_rotation, cos_rotation) = rotation.sin_cos();
|
||
let stack_scale = style.map(dimtfac_or_one).unwrap_or(1.0);
|
||
let text_width = text_cells(&value, stack_scale) * text_height * CELL_WIDTH;
|
||
let symbol_width = text_height * 0.62;
|
||
let symbol_height = text_height * 0.20;
|
||
let (center_x, center_y) = if symbol_position == 1 {
|
||
(0.0, text_height * 0.72)
|
||
} else {
|
||
(-(text_width * 0.5 + symbol_width * 0.70), text_height * 0.02)
|
||
};
|
||
let transform = |x: f64, y: f64| {
|
||
let local_x = center_x + x;
|
||
let local_y = center_y + y;
|
||
Vec3::new(
|
||
(position.x + local_x * cos_rotation - local_y * sin_rotation) as f32,
|
||
(position.y + local_x * sin_rotation + local_y * cos_rotation) as f32,
|
||
position.z as f32,
|
||
)
|
||
};
|
||
|
||
let steps = 8usize;
|
||
Some(
|
||
(0..=steps)
|
||
.map(|index| {
|
||
let t = index as f64 / steps as f64;
|
||
let x = (t - 0.5) * symbol_width;
|
||
let normalized = x / (symbol_width * 0.5);
|
||
let y = symbol_height * (1.0 - normalized * normalized);
|
||
transform(x, y)
|
||
})
|
||
.collect(),
|
||
)
|
||
}
|
||
|
||
struct SuppressFlags {
|
||
ext1: bool,
|
||
ext2: bool,
|
||
dim1: bool,
|
||
dim2: bool,
|
||
}
|
||
|
||
#[derive(Clone, Copy)]
|
||
struct TextBreak {
|
||
center: Vec3,
|
||
half_width: f32,
|
||
half_height: f32,
|
||
padding: f32,
|
||
rotation: f64,
|
||
}
|
||
|
||
#[derive(Clone, Copy)]
|
||
struct DimensionTextLayout {
|
||
position: Vec3,
|
||
width: f32,
|
||
break_box: Option<TextBreak>,
|
||
horizontal: bool,
|
||
}
|
||
|
||
fn dimension_text_layout(
|
||
dimension: &Dimension,
|
||
style: Option<&DimStyle>,
|
||
text_height: f64,
|
||
dim_scale: f64,
|
||
) -> DimensionTextLayout {
|
||
let gap = style
|
||
.map(|style| (style.dimgap.abs() * dim_scale) as f32)
|
||
.unwrap_or(0.09);
|
||
let width = dimension_text_cells(dimension, style)
|
||
.map(|cells| cells as f32 * text_height as f32 * CELL_WIDTH as f32 + gap * 2.0)
|
||
.unwrap_or(0.0);
|
||
let position = vec3_local(dimension_text_pos_f64(
|
||
dimension,
|
||
style,
|
||
text_height,
|
||
dim_scale,
|
||
));
|
||
let bare_width = (width - gap * 2.0).max(0.0);
|
||
let break_box = (bare_width > 0.0).then_some(TextBreak {
|
||
center: position,
|
||
half_width: bare_width * 0.5,
|
||
// A limits stack is two lines tall; the break and the arrow fit both
|
||
// have to clear the whole block.
|
||
half_height: dimension_text_extent_height(dimension, style, text_height) as f32 * 0.5,
|
||
padding: gap,
|
||
rotation: dimension_text_rotation(dimension, style),
|
||
});
|
||
let outside = dimension_text_is_outside(dimension, style);
|
||
let horizontal = style.is_some_and(|style| {
|
||
(outside && style.dimtoh) || (!outside && style.dimtih)
|
||
});
|
||
DimensionTextLayout {
|
||
position,
|
||
width,
|
||
break_box,
|
||
horizontal,
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Copy)]
|
||
struct DimLineParams {
|
||
dimexo: f32,
|
||
dimexe: f32,
|
||
dimdle: f32,
|
||
dimfxl: f32,
|
||
dimfxlon: bool,
|
||
dimsoxd: bool,
|
||
dimcen: f32,
|
||
ticks: bool,
|
||
/// 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,
|
||
text_position: Vec3,
|
||
horizontal_text: bool,
|
||
text_movement: i16,
|
||
text_break: Option<TextBreak>,
|
||
/// DIMTMOVE 1: the point the file stores is where the leader's hook starts,
|
||
/// so the leader is drawn to it exactly and only the text is estimated.
|
||
leader_anchor: Option<Vec3>,
|
||
}
|
||
fn dimension_geometry(
|
||
dim: &Dimension,
|
||
arrow1: &ArrowKind,
|
||
arrow2: &ArrowKind,
|
||
params: DimLineParams,
|
||
suppress: SuppressFlags,
|
||
) -> DimGeom {
|
||
let lv = |v| vec3_local(v);
|
||
let mut g = DimGeom::new();
|
||
match dim {
|
||
Dimension::Aligned(d) => {
|
||
let first = lv(d.first_point);
|
||
let second = lv(d.second_point);
|
||
let def = lv(d.definition_point);
|
||
let axis = normalized_or(second - first, Vec3::X);
|
||
append_linear_dimension(
|
||
&mut g,
|
||
first,
|
||
second,
|
||
def,
|
||
axis,
|
||
arrow1,
|
||
arrow2,
|
||
params,
|
||
suppress,
|
||
d.ext_line_rotation as f32,
|
||
);
|
||
}
|
||
Dimension::Linear(d) => {
|
||
let first = lv(d.first_point);
|
||
let second = lv(d.second_point);
|
||
let def = lv(d.definition_point);
|
||
let axis = Vec3::new(d.rotation.cos() as f32, d.rotation.sin() as f32, 0.0);
|
||
append_linear_dimension(
|
||
&mut g,
|
||
first,
|
||
second,
|
||
def,
|
||
normalized_or(axis, Vec3::X),
|
||
arrow1,
|
||
arrow2,
|
||
params,
|
||
suppress,
|
||
d.ext_line_rotation as f32,
|
||
);
|
||
}
|
||
Dimension::Radius(d) => {
|
||
let center = lv(d.angle_vertex);
|
||
let point = lv(d.definition_point);
|
||
let text = params.text_position;
|
||
// Jogged radius dimensions use a shortened zig-zag leader.
|
||
let jogged = dim
|
||
.base()
|
||
.common
|
||
.extended_data
|
||
.get_record("OCS_JOGGED")
|
||
.is_some();
|
||
if jogged {
|
||
let delta = point - center;
|
||
let dist = delta.length();
|
||
let u = normalized_or(delta, Vec3::X);
|
||
let perp = Vec3::new(-u.y, u.x, 0.0);
|
||
let half = (dist * 0.06).max(1e-3);
|
||
let mid = center + u * (dist * 0.5);
|
||
let a = mid - u * half + perp * half;
|
||
let b = mid + u * half - perp * half;
|
||
if !suppress.dim2 {
|
||
add_segment(&mut g.dim_lines, center, a);
|
||
add_segment(&mut g.dim_lines, a, b);
|
||
add_segment(&mut g.dim_lines, b, point);
|
||
}
|
||
}
|
||
let radius = (point - center).length();
|
||
let text_is_outside = text.distance(center) > radius + 1e-5;
|
||
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);
|
||
}
|
||
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, 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) => {
|
||
let chord = lv(d.angle_vertex);
|
||
let far_chord = lv(d.definition_point);
|
||
append_diameter_dimension(
|
||
&mut g,
|
||
chord,
|
||
far_chord,
|
||
lv(d.base.text_middle_point),
|
||
arrow1,
|
||
arrow2,
|
||
params,
|
||
suppress,
|
||
);
|
||
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:
|
||
// `first_point`→`second_point` is one, `angle_vertex`→
|
||
// `definition_point` the other, and the angle is between them at
|
||
// their intersection. Reading `angle_vertex` as the centre and the
|
||
// other two as rays measures something else entirely — an angle of
|
||
// ten degrees came out as two hundred and seventy.
|
||
let (p1, p2) = (lv(d.first_point), lv(d.second_point));
|
||
let (p3, p4) = (lv(d.angle_vertex), lv(d.definition_point));
|
||
let arc_point = lv(d.dimension_arc);
|
||
match two_line_angle_frame(p1, p2, p3, p4, arc_point) {
|
||
Some((vertex, start, end)) => {
|
||
// An extension line runs from a side's nearer end out to
|
||
// the arc only where the arc lies beyond that side; where
|
||
// the arc crosses the side itself there is nothing to add.
|
||
let radius = vertex.distance(arc_point);
|
||
let side = |angle: f32| -> (Vec3, bool) {
|
||
let dir = Vec3::new(angle.cos(), angle.sin(), 0.0);
|
||
let deviation =
|
||
|a: Vec3, b: Vec3| normalized_or(b - a, dir).cross(dir).length();
|
||
let (a, b) = if deviation(p1, p2) <= deviation(p3, p4) {
|
||
(p1, p2)
|
||
} else {
|
||
(p3, p4)
|
||
};
|
||
let along = |p: Vec3| (p - vertex).dot(dir);
|
||
let (lo, hi) = (along(a).min(along(b)), along(a).max(along(b)));
|
||
let arc_end = vertex + dir * radius;
|
||
let near = if a.distance(arc_end) <= b.distance(arc_end) {
|
||
a
|
||
} else {
|
||
b
|
||
};
|
||
(near, radius >= lo - 1e-6 && radius <= hi + 1e-6)
|
||
};
|
||
let (first, first_covered) = side(start);
|
||
let (second, second_covered) = side(end);
|
||
let suppress = SuppressFlags {
|
||
ext1: suppress.ext1 || first_covered,
|
||
ext2: suppress.ext2 || second_covered,
|
||
dim1: suppress.dim1,
|
||
dim2: suppress.dim2,
|
||
};
|
||
append_angular_dimension(
|
||
&mut g,
|
||
vertex,
|
||
first,
|
||
second,
|
||
arc_point,
|
||
arrow1,
|
||
arrow2,
|
||
Some((start, end)),
|
||
params,
|
||
suppress,
|
||
)
|
||
}
|
||
// Parallel lines have no vertex and so no angle to draw; the
|
||
// extension lines alone say where the dimension was.
|
||
None => {
|
||
add_segment(&mut g.ext_lines, p1, p2);
|
||
add_segment(&mut g.ext_lines, p3, p4);
|
||
}
|
||
}
|
||
}
|
||
Dimension::Angular3Pt(d) => {
|
||
let vertex = lv(d.angle_vertex);
|
||
let first = lv(d.first_point);
|
||
let second = lv(d.second_point);
|
||
let arc_point = lv(d.definition_point);
|
||
// The sweep starts at whichever ray the arc point says; the
|
||
// extension lines swap with it, or they would cross the angle.
|
||
let (first, second, explicit_sweep) =
|
||
match three_point_frame(vertex, first, second, arc_point) {
|
||
Some((start, end, true)) => (second, first, Some((start, end))),
|
||
Some((start, end, false)) => (first, second, Some((start, end))),
|
||
None => (first, second, None),
|
||
};
|
||
append_angular_dimension(
|
||
&mut g,
|
||
vertex,
|
||
first,
|
||
second,
|
||
arc_point,
|
||
arrow1,
|
||
arrow2,
|
||
explicit_sweep,
|
||
params,
|
||
suppress,
|
||
);
|
||
}
|
||
Dimension::Ordinate(d) => {
|
||
if !suppress.ext1 {
|
||
let fixed_length = params
|
||
.dimfxlon
|
||
.then_some(params.dimfxl.max(0.0) as f64);
|
||
let points = d.leader_polyline(
|
||
(params.arrow_len * 2.0) as f64,
|
||
params.dimexo as f64,
|
||
fixed_length,
|
||
);
|
||
for pair in points.windows(2) {
|
||
let start = lv(pair[0]);
|
||
let end = lv(pair[1]);
|
||
if (end - start).length_squared() > 1e-12 {
|
||
add_segment(&mut g.ext_lines, start, end);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
Dimension::Arc(d) => {
|
||
let explicit_sweep = arc_dimension_angles(d);
|
||
append_angular_dimension(
|
||
&mut g,
|
||
lv(d.center_point),
|
||
lv(d.first_extension_point),
|
||
lv(d.second_extension_point),
|
||
lv(d.definition_point),
|
||
arrow1,
|
||
arrow2,
|
||
explicit_sweep,
|
||
params,
|
||
suppress,
|
||
);
|
||
if d.has_leader {
|
||
add_segment(
|
||
&mut g.dim_lines,
|
||
lv(d.first_leader_point),
|
||
lv(d.second_leader_point),
|
||
);
|
||
}
|
||
}
|
||
Dimension::LargeRadial(d) => {
|
||
let chord = lv(d.chord_point);
|
||
let jog = lv(d.jog_point);
|
||
let override_center = lv(d.override_center);
|
||
let (near, far) =
|
||
jogged_radial_break(chord, jog, override_center, d.jog_angle as f32);
|
||
let axis = normalized_or(chord - override_center, Vec3::X);
|
||
let available = chord.distance(override_center);
|
||
let arrows_outside = if params.ticks || params.arrow_len <= 1.0e-6 {
|
||
false
|
||
} else if available < params.arrow_len {
|
||
true
|
||
} else if available < params.text_width + params.arrow_len {
|
||
if params.dimtix {
|
||
true
|
||
} else {
|
||
match params.dimatfit {
|
||
0 | 1 => true,
|
||
2 => false,
|
||
_ => params.text_width <= available,
|
||
}
|
||
}
|
||
} else {
|
||
false
|
||
};
|
||
let true_center = lv(d.definition_point);
|
||
let radius = chord.distance(true_center);
|
||
let text_outside = params.text_position.distance(true_center) > radius + 1.0e-5;
|
||
let draw_inside_line = !arrows_outside || params.dimtofl;
|
||
if draw_inside_line && !suppress.dim2 {
|
||
add_segment_with_text_break(&mut g.dim_lines, chord, near, params.text_break);
|
||
add_segment_with_text_break(&mut g.dim_lines, near, far, params.text_break);
|
||
add_segment_with_text_break(
|
||
&mut g.dim_lines,
|
||
far,
|
||
override_center,
|
||
params.text_break,
|
||
);
|
||
}
|
||
if arrows_outside && !suppress.dim2 {
|
||
add_segment(
|
||
&mut g.dim_lines,
|
||
chord,
|
||
chord + axis * (params.arrow_len * 2.0),
|
||
);
|
||
}
|
||
if !suppress.dim2 {
|
||
append_arrow(
|
||
&mut g,
|
||
chord,
|
||
if arrows_outside { axis } else { -axis },
|
||
arrow1,
|
||
);
|
||
}
|
||
if text_outside && params.text_movement == 0 && !suppress.dim2 {
|
||
add_segment_with_text_break(
|
||
&mut g.dim_lines,
|
||
chord,
|
||
params.text_position,
|
||
params.text_break,
|
||
);
|
||
}
|
||
if arrows_outside || text_outside {
|
||
append_center_mark(&mut g, true_center, params.dimcen, radius);
|
||
}
|
||
}
|
||
}
|
||
g
|
||
}
|
||
|
||
fn add_segment_with_text_break(
|
||
points: &mut Vec<[f32; 3]>,
|
||
start: Vec3,
|
||
end: Vec3,
|
||
text_break: Option<TextBreak>,
|
||
) {
|
||
let segment = end - start;
|
||
let length = segment.length();
|
||
if length <= 1.0e-6 {
|
||
return;
|
||
}
|
||
let Some(text_break) = text_break else {
|
||
add_segment(points, start, end);
|
||
return;
|
||
};
|
||
let segment_curve = kernel::geom2d::Curve::Line(kernel::geom2d::Line {
|
||
start: [start.x as f64, start.y as f64],
|
||
end: [end.x as f64, end.y as f64],
|
||
});
|
||
let tolerance = kernel::geom2d::Tolerance::new((length as f64 * 1.0e-9).max(1.0e-9));
|
||
let inside = |padding: f32| {
|
||
let (sin, cos) = text_break.rotation.sin_cos();
|
||
let x_axis = Vec3::new(cos as f32, sin as f32, 0.0);
|
||
let y_axis = Vec3::new(-sin as f32, cos as f32, 0.0);
|
||
let half_width = text_break.half_width + padding;
|
||
let half_height = text_break.half_height + padding;
|
||
let corners = [
|
||
text_break.center - x_axis * half_width - y_axis * half_height,
|
||
text_break.center + x_axis * half_width - y_axis * half_height,
|
||
text_break.center + x_axis * half_width + y_axis * half_height,
|
||
text_break.center - x_axis * half_width + y_axis * half_height,
|
||
];
|
||
let boundary: Vec<_> = (0..4)
|
||
.map(|index| {
|
||
let next = (index + 1) % 4;
|
||
kernel::geom2d::Curve::Line(kernel::geom2d::Line {
|
||
start: [corners[index].x as f64, corners[index].y as f64],
|
||
end: [corners[next].x as f64, corners[next].y as f64],
|
||
})
|
||
})
|
||
.collect();
|
||
kernel::geom2d::inside_spans(&boundary, &segment_curve, tolerance)
|
||
};
|
||
if inside(0.0).is_empty() {
|
||
add_segment(points, start, end);
|
||
return;
|
||
}
|
||
let mut cursor = 0.0_f32;
|
||
for [cut_start, cut_end] in inside(text_break.padding.max(0.0)) {
|
||
let cut_start = (cut_start as f32).clamp(0.0, 1.0);
|
||
let cut_end = (cut_end as f32).clamp(0.0, 1.0);
|
||
if cut_start - cursor > 1.0e-6 {
|
||
add_segment(points, start.lerp(end, cursor), start.lerp(end, cut_start));
|
||
}
|
||
cursor = cursor.max(cut_end);
|
||
}
|
||
if 1.0 - cursor > 1.0e-6 {
|
||
add_segment(points, start.lerp(end, cursor), end);
|
||
}
|
||
}
|
||
|
||
fn jogged_radial_break(
|
||
chord: Vec3,
|
||
jog: Vec3,
|
||
override_center: Vec3,
|
||
jog_angle: f32,
|
||
) -> (Vec3, Vec3) {
|
||
let radial = normalized_or(chord - override_center, Vec3::X);
|
||
let (sin, cos) = jog_angle.sin_cos();
|
||
let transverse = normalized_or(
|
||
Vec3::new(
|
||
radial.x * cos - radial.y * sin,
|
||
radial.x * sin + radial.y * cos,
|
||
0.0,
|
||
),
|
||
Vec3::Y,
|
||
);
|
||
let half = ((chord - override_center).length() * 0.04).max(1e-3);
|
||
let first = jog - transverse * half;
|
||
let second = jog + transverse * half;
|
||
if chord.distance_squared(first) <= chord.distance_squared(second) {
|
||
(first, second)
|
||
} else {
|
||
(second, first)
|
||
}
|
||
}
|
||
|
||
fn append_linear_dimension(
|
||
g: &mut DimGeom,
|
||
first: Vec3,
|
||
second: Vec3,
|
||
def: Vec3,
|
||
axis: Vec3,
|
||
arrow1: &ArrowKind,
|
||
arrow2: &ArrowKind,
|
||
params: DimLineParams,
|
||
suppress: SuppressFlags,
|
||
ext_line_rotation: f32,
|
||
) {
|
||
let perp = Vec3::new(-axis.y, axis.x, 0.0);
|
||
let dim_line_pos = def.dot(perp);
|
||
let offset1 = dim_line_pos - first.dot(perp);
|
||
let offset2 = dim_line_pos - second.dot(perp);
|
||
let d1 = first + perp * offset1;
|
||
let d2 = second + perp * offset2;
|
||
let sign1 = if offset1 >= 0.0 { 1.0_f32 } else { -1.0 };
|
||
let sign2 = if offset2 >= 0.0 { 1.0_f32 } else { -1.0 };
|
||
|
||
// ext_line_rotation (DIMEDIT "Oblique"): rotate the extension lines by
|
||
// this angle relative to perpendicular. The ext line still starts at
|
||
// the def point; only the direction differs.
|
||
let ext_dir = if ext_line_rotation.abs() > 1e-6 {
|
||
let c = ext_line_rotation.cos();
|
||
let s = ext_line_rotation.sin();
|
||
// Rotate `perp` by ext_line_rotation around Z.
|
||
Vec3::new(perp.x * c - perp.y * s, perp.x * s + perp.y * c, 0.0)
|
||
} else {
|
||
perp
|
||
};
|
||
|
||
// DIMFXLON / DIMFXL: fixed extension-line length from the dim line back
|
||
// toward (but not past) the definition point. Otherwise grow from the
|
||
// def point with DIMEXO gap, extending DIMEXE past the dim line.
|
||
// When oblique, lengths are measured along ext_dir instead of perp.
|
||
let (ext1_start, ext1_end, ext2_start, ext2_end) = if params.dimfxlon {
|
||
let fxl = params.dimfxl.max(0.0);
|
||
let s1 = d1 - ext_dir * (sign1 * fxl);
|
||
let e1 = d1 + ext_dir * (sign1 * params.dimexe);
|
||
let s2 = d2 - ext_dir * (sign2 * fxl);
|
||
let e2 = d2 + ext_dir * (sign2 * params.dimexe);
|
||
(s1, e1, s2, e2)
|
||
} else {
|
||
(
|
||
first + ext_dir * (sign1 * params.dimexo),
|
||
d1 + ext_dir * (sign1 * params.dimexe),
|
||
second + ext_dir * (sign2 * params.dimexo),
|
||
d2 + ext_dir * (sign2 * params.dimexe),
|
||
)
|
||
};
|
||
if !suppress.ext1 {
|
||
add_segment(&mut g.ext_lines, ext1_start, ext1_end);
|
||
}
|
||
if !suppress.ext2 {
|
||
add_segment(&mut g.ext_lines, ext2_start, ext2_end);
|
||
}
|
||
|
||
// DIMDLE: dim line overshoots the ext line by `dimdle` at each end,
|
||
// but only when ticks are in use (DIMTSZ > 0). With arrowheads this
|
||
// is ignored.
|
||
let dle = if params.ticks { params.dimdle } else { 0.0 };
|
||
let dir_d1_to_d2 = normalized_or(d2 - d1, axis);
|
||
let d1_out = d1 - dir_d1_to_d2 * dle;
|
||
let d2_out = d2 + dir_d1_to_d2 * dle;
|
||
|
||
// When text plus arrows do not fit, DIMATFIT decides which component moves
|
||
// first: 0=both, 1=arrows, 2=text, 3=best fit. The text is measured along
|
||
// the dimension line where it is drawn: horizontal text on a vertical
|
||
// dimension takes up its height, and text dragged toward one end runs into
|
||
// that arrow long before the sum of widths says the gap is full.
|
||
let gap = (d2 - d1).length();
|
||
let text_span = params.text_break.map(|text_break| {
|
||
let axis_angle = dir_d1_to_d2.y.atan2(dir_d1_to_d2.x) as f64;
|
||
let (sin, cos) = (text_break.rotation - axis_angle).sin_cos();
|
||
let half = (text_break.half_width as f64 * cos).abs()
|
||
+ (text_break.half_height as f64 * sin).abs()
|
||
+ text_break.padding as f64;
|
||
let along = (text_break.center - d1).dot(dir_d1_to_d2) as f64;
|
||
(along - half, along + half)
|
||
});
|
||
let text_extent = text_span
|
||
.map(|(lo, hi)| (hi - lo) as f32)
|
||
.unwrap_or(params.text_width);
|
||
// Horizontal text beside a non-horizontal dimension is reached by a leg
|
||
// and a hook instead of the dimension line itself.
|
||
let hooks_out_horizontally =
|
||
params.horizontal_text && !params.ticks && dir_d1_to_d2.y.abs() > 1e-3;
|
||
let text_hits_an_arrow = text_span.is_some_and(|(lo, hi)| {
|
||
let center = (lo + hi) * 0.5;
|
||
center > 0.0
|
||
&& center < gap as f64
|
||
&& (lo < params.arrow_len as f64 || hi > (gap - params.arrow_len) as f64)
|
||
});
|
||
let arrows_outside = if params.ticks || params.arrow_len <= 1e-6 {
|
||
false
|
||
} else if gap < 2.0 * params.arrow_len || text_hits_an_arrow {
|
||
true
|
||
} else if gap < text_extent + 2.0 * params.arrow_len {
|
||
match params.dimatfit {
|
||
0 | 1 => true,
|
||
2 => false,
|
||
_ => text_extent <= gap,
|
||
}
|
||
} else {
|
||
false
|
||
};
|
||
|
||
let line_dir = normalized_or(d2_out - d1_out, axis);
|
||
let line_len = (d2_out - d1_out).length();
|
||
let split = params
|
||
.text_break
|
||
.map(|text_break| (text_break.center - d1_out).dot(line_dir))
|
||
.filter(|along| *along > 0.0 && *along < line_len)
|
||
.unwrap_or(line_len * 0.5);
|
||
let split_point = d1_out + line_dir * split;
|
||
let draw_inside_line = !arrows_outside || params.dimtofl;
|
||
if draw_inside_line && !suppress.dim1 && split > 1e-6 {
|
||
add_segment_with_text_break(&mut g.dim_lines, d1_out, split_point, params.text_break);
|
||
}
|
||
if draw_inside_line && !suppress.dim2 && line_len - split > 1e-6 {
|
||
add_segment_with_text_break(&mut g.dim_lines, split_point, d2_out, params.text_break);
|
||
}
|
||
// Outside the extension lines the dimension line runs two arrow lengths,
|
||
// or on to the text when the text sits further out along the line.
|
||
let reach = |default: f32, toward_text: Option<f32>| toward_text.map_or(default, |t| t.max(default));
|
||
let stub = params.arrow_len * 2.0;
|
||
let text_beyond_d1 = text_span
|
||
.filter(|(_, hi)| *hi < 0.0 && !hooks_out_horizontally)
|
||
.map(|(_, hi)| -hi as f32);
|
||
let text_beyond_d2 = text_span
|
||
.filter(|(lo, _)| *lo > gap as f64 && !hooks_out_horizontally)
|
||
.map(|(lo, _)| (lo - gap as f64) as f32);
|
||
if (arrows_outside && !params.dimsoxd) || text_beyond_d1.is_some() || text_beyond_d2.is_some() {
|
||
if !suppress.dim1 && (arrows_outside || text_beyond_d1.is_some()) {
|
||
add_segment(&mut g.dim_lines, d1 - dir_d1_to_d2 * reach(stub, text_beyond_d1), d1);
|
||
}
|
||
if !suppress.dim2 && (arrows_outside || text_beyond_d2.is_some()) {
|
||
add_segment(&mut g.dim_lines, d2, d2 + dir_d1_to_d2 * reach(stub, text_beyond_d2));
|
||
}
|
||
}
|
||
|
||
if arrows_outside {
|
||
// Tip on the ext line, body pointing outward.
|
||
append_arrow(g, d1, normalized_or(d1 - d2, -axis), arrow1);
|
||
append_arrow(g, d2, normalized_or(d2 - d1, axis), arrow2);
|
||
} else {
|
||
append_arrow(g, d1, normalized_or(d2 - d1, axis), arrow1);
|
||
append_arrow(g, d2, normalized_or(d1 - d2, -axis), arrow2);
|
||
}
|
||
|
||
// Horizontal text outside the extension lines of a dimension that is not
|
||
// horizontal itself: the dimension line runs on to the text's height and
|
||
// turns a hook one arrow long toward the text, which sits against it.
|
||
if params.horizontal_text && !params.ticks && dir_d1_to_d2.y.abs() > 1e-3 {
|
||
if let Some(text_break) = params.text_break {
|
||
let along = (text_break.center - d1).dot(dir_d1_to_d2);
|
||
let outside = if along < 0.0 {
|
||
Some((d1, -dir_d1_to_d2, suppress.dim1))
|
||
} else if along > gap {
|
||
Some((d2, dir_d1_to_d2, suppress.dim2))
|
||
} else {
|
||
None
|
||
};
|
||
if let Some((end, outward, suppressed)) = outside {
|
||
let travel = (text_break.center.y - end.y) / outward.y;
|
||
if travel > 0.0 && !suppressed {
|
||
let elbow = end + outward * travel;
|
||
let side = if text_break.center.x >= elbow.x { 1.0 } else { -1.0 };
|
||
let hook_end = Vec3::new(elbow.x + side * params.arrow_len, elbow.y, elbow.z);
|
||
add_segment(&mut g.dim_lines, end, elbow);
|
||
add_segment(&mut g.dim_lines, elbow, hook_end);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 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 {
|
||
Some(anchor) => (anchor, anchor + Vec3::X * (side * params.arrow_len)),
|
||
None => {
|
||
let edge = Vec3::new(text.x - side * params.text_width * 0.5, text.y, text.z);
|
||
(edge - Vec3::X * (side * params.arrow_len), edge)
|
||
}
|
||
};
|
||
let slope = (hook_start - tip).y.abs().atan2((hook_start - tip).x.abs());
|
||
// A leader already within 15° of horizontal runs straight to the text.
|
||
if slope > 15.0_f32.to_radians() {
|
||
add_segment(&mut g.dim_lines, tip, hook_start);
|
||
add_segment(&mut g.dim_lines, hook_start, text_edge);
|
||
} else {
|
||
add_segment(&mut g.dim_lines, tip, text_edge);
|
||
}
|
||
} else {
|
||
let toward = normalized_or(text - tip, Vec3::X);
|
||
let reach = ((text - tip).length() - params.text_width * 0.5).max(0.0);
|
||
add_segment(&mut g.dim_lines, tip, tip + toward * reach);
|
||
}
|
||
}
|
||
|
||
fn append_diameter_dimension(
|
||
g: &mut DimGeom,
|
||
chord: Vec3,
|
||
far_chord: Vec3,
|
||
text_anchor: Vec3,
|
||
arrow1: &ArrowKind,
|
||
arrow2: &ArrowKind,
|
||
params: DimLineParams,
|
||
suppress: SuppressFlags,
|
||
) {
|
||
let axis = normalized_or(far_chord - chord, Vec3::X);
|
||
let diameter = chord.distance(far_chord);
|
||
if diameter <= 1e-6 {
|
||
return;
|
||
}
|
||
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.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)
|
||
{
|
||
(chord, suppress.dim1)
|
||
} else {
|
||
(far_chord, suppress.dim2)
|
||
};
|
||
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);
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
// Text projected outside the diameter requires inward-pointing arrowheads.
|
||
let text_along = (params.text_position - chord).dot(axis);
|
||
let text_outside = text_along < 0.0 || text_along > diameter;
|
||
|
||
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 {
|
||
true
|
||
} else if diameter < params.text_width + 2.0 * params.arrow_len {
|
||
match params.dimatfit {
|
||
0 | 1 => true,
|
||
2 => false,
|
||
_ => params.text_width <= diameter,
|
||
}
|
||
} else {
|
||
false
|
||
};
|
||
|
||
let extension = if params.ticks { params.dimdle } else { 0.0 };
|
||
let first = chord - axis * extension;
|
||
let second = far_chord + axis * extension;
|
||
let line_length = first.distance(second);
|
||
let split = params
|
||
.text_break
|
||
.map(|text_break| (text_break.center - first).dot(axis))
|
||
.filter(|along| *along > 0.0 && *along < line_length)
|
||
.unwrap_or(line_length * 0.5);
|
||
let split_point = first + axis * split;
|
||
|
||
let draw_inside_line = text_outside || !arrows_outside || params.dimtofl;
|
||
if draw_inside_line && !suppress.dim1 && split > 1e-6 {
|
||
add_segment_with_text_break(&mut g.dim_lines, first, split_point, params.text_break);
|
||
}
|
||
if draw_inside_line && !suppress.dim2 && line_length - split > 1e-6 {
|
||
add_segment_with_text_break(&mut g.dim_lines, split_point, second, params.text_break);
|
||
}
|
||
if arrows_outside && !params.dimsoxd {
|
||
let stub = params.arrow_len * 2.0;
|
||
if !suppress.dim1 {
|
||
add_segment(&mut g.dim_lines, chord - axis * stub, chord);
|
||
}
|
||
if !suppress.dim2 {
|
||
add_segment(&mut g.dim_lines, far_chord, far_chord + axis * stub);
|
||
}
|
||
}
|
||
|
||
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);
|
||
} else {
|
||
append_arrow(g, chord, axis, arrow1);
|
||
append_arrow(g, far_chord, -axis, arrow2);
|
||
}
|
||
|
||
if params.text_movement == 0 {
|
||
let (tip, direction, suppressed) = if params.text_position.distance_squared(chord)
|
||
<= params.text_position.distance_squared(far_chord)
|
||
{
|
||
(chord, -axis, suppress.dim1)
|
||
} else {
|
||
(far_chord, axis, suppress.dim2)
|
||
};
|
||
if text_outside && !suppressed {
|
||
let leader_length = (text_anchor - tip).dot(direction);
|
||
|
||
if leader_length > 1e-6 {
|
||
let text_extension = params.text_width * 0.5;
|
||
add_segment(
|
||
&mut g.dim_lines,
|
||
tip,
|
||
tip + direction * (leader_length + text_extension),
|
||
);
|
||
}
|
||
} else if text_along < 0.0 && !suppress.dim1 {
|
||
add_segment(&mut g.dim_lines, chord, params.text_position);
|
||
} else if text_along > diameter && !suppress.dim2 {
|
||
add_segment(&mut g.dim_lines, far_chord, params.text_position);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Draw a center mark for radius/diameter dimensions.
|
||
/// DIMCEN > 0 → small "+" of half-length |DIMCEN| at the centre.
|
||
/// DIMCEN < 0 → small "+" *plus* four line segments extending from the
|
||
/// circle (radius - |DIMCEN|) outward to (radius + |DIMCEN|).
|
||
/// DIMCEN = 0 → no mark.
|
||
fn append_center_mark(g: &mut DimGeom, center: Vec3, dimcen: f32, radius: f32) {
|
||
let mag = dimcen.abs();
|
||
if mag <= 1e-6 {
|
||
return;
|
||
}
|
||
// Small "+" at the centre.
|
||
let h = mag;
|
||
add_segment(
|
||
&mut g.dim_lines,
|
||
Vec3::new(center.x - h, center.y, center.z),
|
||
Vec3::new(center.x + h, center.y, center.z),
|
||
);
|
||
add_segment(
|
||
&mut g.dim_lines,
|
||
Vec3::new(center.x, center.y - h, center.z),
|
||
Vec3::new(center.x, center.y + h, center.z),
|
||
);
|
||
if dimcen < 0.0 && radius > mag + 1e-6 {
|
||
let inner = (radius - mag).max(0.0);
|
||
let outer = radius + mag;
|
||
// Four short radial strokes spanning the circle edge.
|
||
add_segment(
|
||
&mut g.dim_lines,
|
||
Vec3::new(center.x + inner, center.y, center.z),
|
||
Vec3::new(center.x + outer, center.y, center.z),
|
||
);
|
||
add_segment(
|
||
&mut g.dim_lines,
|
||
Vec3::new(center.x - inner, center.y, center.z),
|
||
Vec3::new(center.x - outer, center.y, center.z),
|
||
);
|
||
add_segment(
|
||
&mut g.dim_lines,
|
||
Vec3::new(center.x, center.y + inner, center.z),
|
||
Vec3::new(center.x, center.y + outer, center.z),
|
||
);
|
||
add_segment(
|
||
&mut g.dim_lines,
|
||
Vec3::new(center.x, center.y - inner, center.z),
|
||
Vec3::new(center.x, center.y - outer, center.z),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// Vertex and sweep of the angle between two lines, as a two-line angular
|
||
/// dimension stores them: `a1`→`a2` and `b1`→`b2`, with `arc_point` sitting on
|
||
/// the arc that shows which of the four angles at the crossing is meant.
|
||
///
|
||
/// Returns `None` for parallel lines, which cross nowhere and enclose nothing.
|
||
fn two_line_angle_frame(
|
||
a1: Vec3,
|
||
a2: Vec3,
|
||
b1: Vec3,
|
||
b2: Vec3,
|
||
arc_point: Vec3,
|
||
) -> Option<(Vec3, f32, f32)> {
|
||
let (u, v) = (a2 - a1, b2 - b1);
|
||
let (t, _) = kernel::geom2d::line_line(
|
||
[a1.x as f64, a1.y as f64],
|
||
[u.x as f64, u.y as f64],
|
||
[b1.x as f64, b1.y as f64],
|
||
[v.x as f64, v.y as f64],
|
||
)?;
|
||
let vertex = a1 + u * t as f32;
|
||
|
||
// Two lines cross at four angles; the arc point picks one. Each line
|
||
// contributes its direction and its reverse, so try the four pairs and keep
|
||
// the one whose sweep both contains the arc point and is the shorter way
|
||
// round — the dimension marks an angle, never its reflex twin.
|
||
let angle_of = |d: Vec3| d.y.atan2(d.x);
|
||
let target = angle_of(arc_point - vertex);
|
||
let mut best: Option<(f32, f32, f32)> = None;
|
||
for su in [1.0f32, -1.0] {
|
||
for sv in [1.0f32, -1.0] {
|
||
let (from_u, from_v) = (angle_of(u * su), angle_of(v * sv));
|
||
// Either line can be the one the sweep starts from; taking only
|
||
// u→v leaves out half the angles at the crossing, and the half left
|
||
// out is the one wanted whenever the lines are nearly parallel.
|
||
for (start, end) in [(from_u, from_v), (from_v, from_u)] {
|
||
let sweep = (end - start).rem_euclid(std::f32::consts::TAU);
|
||
if sweep <= 1e-6 || sweep > std::f32::consts::PI {
|
||
continue;
|
||
}
|
||
let into_target = (target - start).rem_euclid(std::f32::consts::TAU);
|
||
if into_target > sweep {
|
||
continue;
|
||
}
|
||
if best.is_none_or(|(_, _, known)| sweep < known) {
|
||
best = Some((start, start + sweep, sweep));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
let (start, end, _) = best?;
|
||
Some((vertex, start, end))
|
||
}
|
||
|
||
/// The sweep of a three-point angular dimension: counter-clockwise from the
|
||
/// first ray to the second when the arc point lies in that sweep, otherwise
|
||
/// the other way round (`swapped`). Rays need no crossing, so a straight
|
||
/// angle draws, and a reflex one is kept as picked.
|
||
fn three_point_frame(
|
||
vertex: Vec3,
|
||
first: Vec3,
|
||
second: Vec3,
|
||
arc_point: Vec3,
|
||
) -> Option<(f32, f32, bool)> {
|
||
let angle_of = |d: Vec3| d.y.atan2(d.x);
|
||
let (r1, r2, at) = (first - vertex, second - vertex, arc_point - vertex);
|
||
if r1.length_squared() <= 1e-12 || r2.length_squared() <= 1e-12 || at.length_squared() <= 1e-12
|
||
{
|
||
return None;
|
||
}
|
||
let tau = std::f32::consts::TAU;
|
||
let (a1, a2) = (angle_of(r1), angle_of(r2));
|
||
let sweep = (a2 - a1).rem_euclid(tau);
|
||
if sweep <= 1e-6 {
|
||
return None;
|
||
}
|
||
if (angle_of(at) - a1).rem_euclid(tau) <= sweep {
|
||
Some((a1, a1 + sweep, false))
|
||
} else {
|
||
Some((a2, a2 + (tau - sweep), true))
|
||
}
|
||
}
|
||
|
||
pub(crate) fn arc_dimension_angles(dimension: &DimensionArc) -> Option<(f32, f32)> {
|
||
let raw = dimension.arc_end_parameter - dimension.arc_start_parameter;
|
||
let mut sweep = raw.rem_euclid(std::f64::consts::TAU);
|
||
if sweep <= 1.0e-12 && raw.abs() > 1.0e-12 {
|
||
sweep = std::f64::consts::TAU;
|
||
}
|
||
if sweep > 1.0e-12 {
|
||
let start = dimension.arc_start_parameter as f32;
|
||
return Some((start, start + sweep as f32));
|
||
}
|
||
|
||
let start = (dimension.first_extension_point.y - dimension.center_point.y)
|
||
.atan2(dimension.first_extension_point.x - dimension.center_point.x);
|
||
let end = (dimension.second_extension_point.y - dimension.center_point.y)
|
||
.atan2(dimension.second_extension_point.x - dimension.center_point.x);
|
||
let sweep = (end - start).rem_euclid(std::f64::consts::TAU);
|
||
(sweep > 1.0e-12).then_some((start as f32, (start + sweep) as f32))
|
||
}
|
||
|
||
pub(crate) fn arc_dimension_in_ocs(
|
||
dimension: &DimensionArc,
|
||
) -> Option<(DimensionArc, Vector3)> {
|
||
let length = dimension.base.normal.length();
|
||
if !length.is_finite() || length <= 1.0e-12 {
|
||
return None;
|
||
}
|
||
let normal = dimension.base.normal / length;
|
||
if (normal - Vector3::UNIT_Z).length() <= 1.0e-12 {
|
||
return None;
|
||
}
|
||
let normal_tuple = (normal.x, normal.y, normal.z);
|
||
let to_ocs = |point: Vector3| {
|
||
let point = crate::scene::view::transform::wcs_point_to_ocs(
|
||
(point.x, point.y, point.z),
|
||
normal_tuple,
|
||
);
|
||
Vector3::new(point.0, point.1, point.2)
|
||
};
|
||
let mut local = dimension.clone();
|
||
local.center_point = to_ocs(dimension.center_point);
|
||
let elevation = local.center_point.z;
|
||
let on_plane = |point: Vector3| {
|
||
let mut point = to_ocs(point);
|
||
point.z = elevation;
|
||
point
|
||
};
|
||
local.definition_point = on_plane(dimension.definition_point);
|
||
local.first_extension_point = on_plane(dimension.first_extension_point);
|
||
local.second_extension_point = on_plane(dimension.second_extension_point);
|
||
local.first_leader_point = on_plane(dimension.first_leader_point);
|
||
local.second_leader_point = on_plane(dimension.second_leader_point);
|
||
local.base.definition_point = local.definition_point;
|
||
local.base.text_middle_point = on_plane(dimension.base.text_middle_point);
|
||
local.base.insertion_point = on_plane(dimension.base.insertion_point);
|
||
local.base.normal = Vector3::UNIT_Z;
|
||
Some((local, normal))
|
||
}
|
||
|
||
pub(crate) fn large_radial_dimension_in_ocs(
|
||
dimension: &DimensionLargeRadial,
|
||
) -> Option<(DimensionLargeRadial, Vector3)> {
|
||
let length = dimension.base.normal.length();
|
||
if !length.is_finite() || length <= 1.0e-12 {
|
||
return None;
|
||
}
|
||
let normal = dimension.base.normal / length;
|
||
if (normal - Vector3::UNIT_Z).length() <= 1.0e-12 {
|
||
return None;
|
||
}
|
||
let normal_tuple = (normal.x, normal.y, normal.z);
|
||
let to_ocs = |point: Vector3| {
|
||
let point = crate::scene::view::transform::wcs_point_to_ocs(
|
||
(point.x, point.y, point.z),
|
||
normal_tuple,
|
||
);
|
||
Vector3::new(point.0, point.1, point.2)
|
||
};
|
||
let mut local = dimension.clone();
|
||
local.definition_point = to_ocs(dimension.definition_point);
|
||
let elevation = local.definition_point.z;
|
||
let on_plane = |point: Vector3| {
|
||
let mut point = to_ocs(point);
|
||
point.z = elevation;
|
||
point
|
||
};
|
||
local.chord_point = on_plane(dimension.chord_point);
|
||
local.override_center = on_plane(dimension.override_center);
|
||
local.jog_point = on_plane(dimension.jog_point);
|
||
local.base.definition_point = local.definition_point;
|
||
local.base.text_middle_point = on_plane(dimension.base.text_middle_point);
|
||
local.base.insertion_point = on_plane(dimension.base.insertion_point);
|
||
local.base.normal = Vector3::UNIT_Z;
|
||
Some((local, normal))
|
||
}
|
||
|
||
/// A linear or aligned dimension in a plane off world Z, moved into its own
|
||
/// OCS. Their builders read the offset and extension directions in XY, so
|
||
/// they draw it there and the result is mapped back onto the plane. `None`
|
||
/// for a dimension in world XY or of another kind.
|
||
pub(crate) fn linear_dimension_in_ocs(dimension: &Dimension) -> Option<(Dimension, Vector3)> {
|
||
let length = dimension.base().normal.length();
|
||
if !length.is_finite() || length <= 1.0e-12 {
|
||
return None;
|
||
}
|
||
let normal = dimension.base().normal / length;
|
||
if (normal - Vector3::UNIT_Z).length() <= 1.0e-12 {
|
||
return None;
|
||
}
|
||
let normal_tuple = (normal.x, normal.y, normal.z);
|
||
let to_ocs = |point: Vector3| {
|
||
let point = crate::scene::view::transform::wcs_point_to_ocs(
|
||
(point.x, point.y, point.z),
|
||
normal_tuple,
|
||
);
|
||
Vector3::new(point.0, point.1, point.2)
|
||
};
|
||
let mut local = dimension.clone();
|
||
match &mut local {
|
||
Dimension::Linear(d) => {
|
||
d.first_point = to_ocs(d.first_point);
|
||
d.second_point = to_ocs(d.second_point);
|
||
d.definition_point = to_ocs(d.definition_point);
|
||
d.base.definition_point = d.definition_point;
|
||
d.base.text_middle_point = to_ocs(d.base.text_middle_point);
|
||
d.base.normal = Vector3::UNIT_Z;
|
||
}
|
||
Dimension::Aligned(d) => {
|
||
d.first_point = to_ocs(d.first_point);
|
||
d.second_point = to_ocs(d.second_point);
|
||
d.definition_point = to_ocs(d.definition_point);
|
||
d.base.definition_point = d.definition_point;
|
||
d.base.text_middle_point = to_ocs(d.base.text_middle_point);
|
||
d.base.normal = Vector3::UNIT_Z;
|
||
}
|
||
_ => return None,
|
||
}
|
||
Some((local, normal))
|
||
}
|
||
|
||
fn map_wire_ocs_to_wcs(wire: &mut WireModel, normal: Vector3) {
|
||
let normal_tuple = (normal.x, normal.y, normal.z);
|
||
let map = |x: f64, y: f64, z: f64| {
|
||
crate::scene::view::transform::ocs_point_to_wcs((x, y, z), normal_tuple)
|
||
};
|
||
let map_split = |high: &mut Vec<[f32; 3]>, low: &mut Vec<[f32; 3]>| {
|
||
let mut mapped_high = Vec::with_capacity(high.len());
|
||
let mut mapped_low = Vec::with_capacity(high.len());
|
||
for (index, point) in high.iter().enumerate() {
|
||
if point[0].is_nan() {
|
||
mapped_high.push(*point);
|
||
mapped_low.push([0.0; 3]);
|
||
continue;
|
||
}
|
||
let residual = low.get(index).copied().unwrap_or([0.0; 3]);
|
||
let point = map(
|
||
point[0] as f64 + residual[0] as f64,
|
||
point[1] as f64 + residual[1] as f64,
|
||
point[2] as f64 + residual[2] as f64,
|
||
);
|
||
let (xh, xl) = WireModel::split_ds(point.0);
|
||
let (yh, yl) = WireModel::split_ds(point.1);
|
||
let (zh, zl) = WireModel::split_ds(point.2);
|
||
mapped_high.push([xh, yh, zh]);
|
||
mapped_low.push([xl, yl, zl]);
|
||
}
|
||
*high = mapped_high;
|
||
*low = mapped_low;
|
||
};
|
||
|
||
map_split(&mut wire.points, &mut wire.points_low);
|
||
map_split(&mut wire.fill_tris, &mut wire.fill_tris_low);
|
||
map_split(&mut wire.pick_tris, &mut wire.pick_tris_low);
|
||
wire.text_verts = crate::scene::model::wire_model::map_text_verts(
|
||
&wire.text_verts,
|
||
map,
|
||
);
|
||
for (point, _) in &mut wire.snap_pts {
|
||
let mapped = map(point.x, point.y, point.z);
|
||
*point = DVec3::new(mapped.0, mapped.1, mapped.2);
|
||
}
|
||
for point in &mut wire.key_vertices {
|
||
let mapped = map(point[0], point[1], point[2]);
|
||
*point = [mapped.0, mapped.1, mapped.2];
|
||
}
|
||
if let Some(marker) = &mut wire.point_marker {
|
||
let origin = map(marker.origin.x, marker.origin.y, marker.origin.z);
|
||
let vector = |value: DVec3| {
|
||
let mapped = map(value.x, value.y, value.z);
|
||
DVec3::new(mapped.0, mapped.1, mapped.2)
|
||
};
|
||
marker.origin = DVec3::new(origin.0, origin.1, origin.2);
|
||
marker.normal = vector(marker.normal).normalize_or(DVec3::Z);
|
||
marker.axis_x = vector(marker.axis_x).normalize_or(DVec3::X);
|
||
marker.axis_y = vector(marker.axis_y).normalize_or(DVec3::Y);
|
||
}
|
||
for tangent in &mut wire.tangent_geoms {
|
||
match tangent {
|
||
TangentGeom::Line { p1, p2 } => {
|
||
let first = map(p1[0] as f64, p1[1] as f64, p1[2] as f64);
|
||
let second = map(p2[0] as f64, p2[1] as f64, p2[2] as f64);
|
||
*p1 = [first.0 as f32, first.1 as f32, first.2 as f32];
|
||
*p2 = [second.0 as f32, second.1 as f32, second.2 as f32];
|
||
}
|
||
TangentGeom::Circle { center, radius } => {
|
||
let center = map(
|
||
center[0] as f64,
|
||
center[1] as f64,
|
||
center[2] as f64,
|
||
);
|
||
let (x_axis, y_axis) = crate::scene::view::transform::ocs_axes(normal_tuple);
|
||
*tangent = TangentGeom::PlanarCircle {
|
||
center: [center.0, center.1, center.2],
|
||
axis_x: [x_axis.0, x_axis.1, x_axis.2],
|
||
axis_y: [y_axis.0, y_axis.1, y_axis.2],
|
||
radius: *radius as f64,
|
||
};
|
||
}
|
||
TangentGeom::PlanarCircle { center, axis_x, axis_y, .. }
|
||
| TangentGeom::Arc { center, axis_x, axis_y, .. } => {
|
||
let mapped = map(center[0], center[1], center[2]);
|
||
let map_vector = |axis: [f64; 3]| {
|
||
let mapped = map(axis[0], axis[1], axis[2]);
|
||
[mapped.0, mapped.1, mapped.2]
|
||
};
|
||
*center = [mapped.0, mapped.1, mapped.2];
|
||
*axis_x = map_vector(*axis_x);
|
||
*axis_y = map_vector(*axis_y);
|
||
}
|
||
TangentGeom::PlanarEllipse {
|
||
center,
|
||
major_axis,
|
||
normal,
|
||
..
|
||
} => {
|
||
let mapped = map(center[0], center[1], center[2]);
|
||
let map_vector = |axis: [f64; 3]| {
|
||
let mapped = map(axis[0], axis[1], axis[2]);
|
||
[mapped.0, mapped.1, mapped.2]
|
||
};
|
||
*center = [mapped.0, mapped.1, mapped.2];
|
||
*major_axis = map_vector(*major_axis);
|
||
*normal = map_vector(*normal);
|
||
}
|
||
}
|
||
}
|
||
wire.aabb = WireModel::UNBOUNDED_AABB;
|
||
}
|
||
|
||
fn angular_dimension_frame(dim: &Dimension) -> Option<(Vec3, f32, f32, f32)> {
|
||
let (vertex, start, end, arc_point) = match dim {
|
||
Dimension::Angular2Ln(value) => {
|
||
let first_start = vec3_local(value.first_point);
|
||
let first_end = vec3_local(value.second_point);
|
||
let second_start = vec3_local(value.angle_vertex);
|
||
let second_end = vec3_local(value.definition_point);
|
||
let arc_point = vec3_local(value.dimension_arc);
|
||
let (vertex, start, end) = two_line_angle_frame(
|
||
first_start,
|
||
first_end,
|
||
second_start,
|
||
second_end,
|
||
arc_point,
|
||
)?;
|
||
(vertex, start, end, arc_point)
|
||
}
|
||
Dimension::Angular3Pt(value) => {
|
||
let vertex = vec3_local(value.angle_vertex);
|
||
let first = vec3_local(value.first_point);
|
||
let second = vec3_local(value.second_point);
|
||
let arc_point = vec3_local(value.definition_point);
|
||
let (start, end, _) = three_point_frame(vertex, first, second, arc_point)?;
|
||
(vertex, start, end, arc_point)
|
||
}
|
||
Dimension::Arc(value) => {
|
||
let vertex = vec3_local(value.center_point);
|
||
let arc_point = vec3_local(value.definition_point);
|
||
let (start, end) = arc_dimension_angles(value)?;
|
||
(vertex, start, end, arc_point)
|
||
}
|
||
_ => return None,
|
||
};
|
||
let radius = vertex.distance(arc_point);
|
||
(radius > 1.0e-6).then_some((vertex, start, end, radius))
|
||
}
|
||
|
||
fn append_angular_dimension(
|
||
g: &mut DimGeom,
|
||
vertex: Vec3,
|
||
first: Vec3,
|
||
second: Vec3,
|
||
arc_point: Vec3,
|
||
arrow1: &ArrowKind,
|
||
arrow2: &ArrowKind,
|
||
explicit_sweep: Option<(f32, f32)>,
|
||
params: DimLineParams,
|
||
suppress: SuppressFlags,
|
||
) {
|
||
let radius = vertex.distance(arc_point);
|
||
if radius <= 1e-6 {
|
||
add_segment(&mut g.ext_lines, vertex, first);
|
||
add_segment(&mut g.ext_lines, vertex, second);
|
||
return;
|
||
}
|
||
// Extension lines run from each measured point out to the dimension arc so
|
||
// there is no gap between a ray and its arc endpoint. (#181 / DIM-027)
|
||
let measured_start = (first.y - vertex.y).atan2(first.x - vertex.x);
|
||
let measured_end = (second.y - vertex.y).atan2(second.x - vertex.x);
|
||
let (start, mut end) = explicit_sweep.unwrap_or((measured_start, measured_end));
|
||
let dir1 = Vec3::new(start.cos(), start.sin(), 0.0);
|
||
let dir2 = Vec3::new(end.cos(), end.sin(), 0.0);
|
||
let arc_start = vertex + dir1 * radius;
|
||
let arc_end = vertex + dir2 * radius;
|
||
let extension = |origin: Vec3, endpoint: Vec3, direction: Vec3| {
|
||
if params.dimfxlon {
|
||
(
|
||
endpoint - direction * params.dimfxl.max(0.0),
|
||
endpoint + direction * params.dimexe,
|
||
)
|
||
} else {
|
||
let toward_arc = normalized_or(endpoint - origin, direction);
|
||
(
|
||
origin + toward_arc * params.dimexo,
|
||
endpoint + toward_arc * params.dimexe,
|
||
)
|
||
}
|
||
};
|
||
let (ext1_start, ext1_end) = extension(first, arc_start, dir1);
|
||
let (ext2_start, ext2_end) = extension(second, arc_end, dir2);
|
||
if !suppress.ext1 {
|
||
add_segment(&mut g.ext_lines, ext1_start, ext1_end);
|
||
}
|
||
if !suppress.ext2 {
|
||
add_segment(&mut g.ext_lines, ext2_start, ext2_end);
|
||
}
|
||
|
||
let mut delta = end - start;
|
||
// Wrap a negative sweep forwards, but leave a zero one alone: two rays that
|
||
// point the same way enclose no angle, and turning that into a full turn
|
||
// drew a whole circle where there was nothing to draw.
|
||
while delta < 0.0 {
|
||
delta += std::f32::consts::TAU;
|
||
}
|
||
if delta.abs() <= 1e-6 {
|
||
return;
|
||
}
|
||
if explicit_sweep.is_none() && delta > std::f32::consts::PI {
|
||
end -= std::f32::consts::TAU;
|
||
delta = end - start;
|
||
}
|
||
|
||
let arc_length = radius * delta.abs();
|
||
let arrows_outside = if params.ticks || params.arrow_len <= 1.0e-6 {
|
||
false
|
||
} else if arc_length < params.arrow_len * 2.0 {
|
||
true
|
||
} else if arc_length < params.text_width + params.arrow_len * 2.0 {
|
||
match params.dimatfit {
|
||
0 | 1 => true,
|
||
2 => false,
|
||
_ => params.text_width <= arc_length,
|
||
}
|
||
} else {
|
||
false
|
||
};
|
||
let draw_inside_line = !arrows_outside || params.dimtofl;
|
||
let arc_extension = if params.ticks { params.dimdle / radius } else { 0.0 };
|
||
let direction = delta.signum();
|
||
let draw_start = start - direction * arc_extension;
|
||
let draw_delta = delta + direction * arc_extension * 2.0;
|
||
let arc_pts = sample_angular_arc(vertex, radius, draw_start, draw_delta);
|
||
let steps = arc_pts.len().saturating_sub(1);
|
||
if draw_inside_line {
|
||
for index in 0..steps {
|
||
let t = (index as f32 + 0.5) / steps as f32;
|
||
if (t < 0.5 && suppress.dim1) || (t >= 0.5 && suppress.dim2) {
|
||
continue;
|
||
}
|
||
let a = arc_pts[index];
|
||
let b = arc_pts[index + 1];
|
||
add_segment_with_text_break(&mut g.dim_lines, a, b, params.text_break);
|
||
}
|
||
}
|
||
|
||
if arrows_outside && !params.dimsoxd {
|
||
let stub_angle = (params.arrow_len * 2.0 / radius).min(std::f32::consts::FRAC_PI_2);
|
||
if !suppress.dim1 {
|
||
append_sampled_arc(
|
||
&mut g.dim_lines,
|
||
vertex,
|
||
radius,
|
||
start - direction * stub_angle,
|
||
start,
|
||
);
|
||
}
|
||
if !suppress.dim2 {
|
||
append_sampled_arc(
|
||
&mut g.dim_lines,
|
||
vertex,
|
||
radius,
|
||
end,
|
||
end + direction * stub_angle,
|
||
);
|
||
}
|
||
}
|
||
|
||
if let Some(text_break) = params.text_break {
|
||
let text_angle = (text_break.center.y - vertex.y).atan2(text_break.center.x - vertex.x);
|
||
let into = (text_angle - start).rem_euclid(std::f32::consts::TAU);
|
||
if into > delta.abs() + 1.0e-6 {
|
||
let back_to_start = (start - text_angle).rem_euclid(std::f32::consts::TAU);
|
||
let forward_from_end = (text_angle - end).rem_euclid(std::f32::consts::TAU);
|
||
if back_to_start <= forward_from_end && !suppress.dim1 {
|
||
append_sampled_arc_with_text_break(
|
||
&mut g.dim_lines,
|
||
vertex,
|
||
radius,
|
||
text_angle,
|
||
start,
|
||
params.text_break,
|
||
);
|
||
} else if !suppress.dim2 {
|
||
append_sampled_arc_with_text_break(
|
||
&mut g.dim_lines,
|
||
vertex,
|
||
radius,
|
||
end,
|
||
text_angle,
|
||
params.text_break,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
let start_tangent = Vec3::new(-start.sin(), start.cos(), 0.0) * direction;
|
||
let end_tangent = Vec3::new(-end.sin(), end.cos(), 0.0) * direction;
|
||
let draw_arrows = !arrows_outside || !params.dimsoxd;
|
||
if draw_arrows && !suppress.dim1 {
|
||
append_arrow(
|
||
g,
|
||
arc_start,
|
||
if arrows_outside { -start_tangent } else { start_tangent },
|
||
arrow1,
|
||
);
|
||
}
|
||
if draw_arrows && !suppress.dim2 {
|
||
append_arrow(
|
||
g,
|
||
arc_end,
|
||
if arrows_outside { end_tangent } else { -end_tangent },
|
||
arrow2,
|
||
);
|
||
}
|
||
}
|
||
|
||
fn append_sampled_arc(
|
||
lines: &mut Vec<[f32; 3]>,
|
||
vertex: Vec3,
|
||
radius: f32,
|
||
start: f32,
|
||
end: f32,
|
||
) {
|
||
for pair in sample_angular_arc(vertex, radius, start, end - start).windows(2) {
|
||
add_segment(lines, pair[0], pair[1]);
|
||
}
|
||
}
|
||
|
||
fn append_sampled_arc_with_text_break(
|
||
lines: &mut Vec<[f32; 3]>,
|
||
vertex: Vec3,
|
||
radius: f32,
|
||
start: f32,
|
||
end: f32,
|
||
text_break: Option<TextBreak>,
|
||
) {
|
||
for pair in sample_angular_arc(vertex, radius, start, end - start).windows(2) {
|
||
add_segment_with_text_break(lines, pair[0], pair[1], text_break);
|
||
}
|
||
}
|
||
|
||
fn sample_angular_arc(vertex: Vec3, radius: f32, start: f32, sweep: f32) -> Vec<Vec3> {
|
||
if sweep.abs() <= 1.0e-6 {
|
||
return vec![vertex + Vec3::new(start.cos() * radius, start.sin() * radius, 0.0)];
|
||
}
|
||
let end = start + sweep;
|
||
let (from, to, reverse) = if sweep >= 0.0 {
|
||
(start, end, false)
|
||
} else {
|
||
(end, start, true)
|
||
};
|
||
let mut points: Vec<_> = kernel::geom2d::tessellate::arc(
|
||
[vertex.x as f64, vertex.y as f64],
|
||
radius as f64,
|
||
from as f64,
|
||
to as f64,
|
||
vertex.z as f64,
|
||
kernel::geom2d::tessellate::DEFAULT_SEGMENTS_PER_RADIAN,
|
||
)
|
||
.into_iter()
|
||
.map(|point| Vec3::new(point[0] as f32, point[1] as f32, point[2] as f32))
|
||
.collect();
|
||
if reverse {
|
||
points.reverse();
|
||
}
|
||
points
|
||
}
|
||
|
||
fn dimension_snap_pts(dim: &Dimension) -> Vec<(glam::DVec3, SnapHint)> {
|
||
let lv = |v: codec::types::Vector3| glam::DVec3::new(v.x, v.y, v.z);
|
||
let node = |v: codec::types::Vector3| (lv(v), SnapHint::Node);
|
||
match dim {
|
||
Dimension::Linear(d) => vec![
|
||
node(d.first_point),
|
||
node(d.second_point),
|
||
node(d.definition_point),
|
||
],
|
||
Dimension::Aligned(d) => vec![
|
||
node(d.first_point),
|
||
node(d.second_point),
|
||
node(d.definition_point),
|
||
],
|
||
Dimension::Radius(d) => vec![node(d.angle_vertex), node(d.definition_point)],
|
||
Dimension::Diameter(d) => vec![node(d.angle_vertex), node(d.definition_point)],
|
||
Dimension::Angular2Ln(d) => vec![
|
||
node(d.first_point),
|
||
node(d.second_point),
|
||
node(d.angle_vertex),
|
||
node(d.definition_point),
|
||
node(d.dimension_arc),
|
||
],
|
||
Dimension::Angular3Pt(d) => vec![
|
||
node(d.angle_vertex),
|
||
node(d.first_point),
|
||
node(d.second_point),
|
||
node(d.definition_point),
|
||
],
|
||
Dimension::Ordinate(d) => vec![
|
||
node(d.feature_location),
|
||
node(d.leader_endpoint),
|
||
],
|
||
Dimension::Arc(d) => {
|
||
let mut points = vec![
|
||
node(d.center_point),
|
||
node(d.first_extension_point),
|
||
node(d.second_extension_point),
|
||
node(d.definition_point),
|
||
];
|
||
if d.has_leader {
|
||
points.push(node(d.first_leader_point));
|
||
points.push(node(d.second_leader_point));
|
||
}
|
||
points
|
||
}
|
||
Dimension::LargeRadial(d) => vec![
|
||
node(d.definition_point),
|
||
node(d.chord_point),
|
||
node(d.override_center),
|
||
node(d.jog_point),
|
||
],
|
||
}
|
||
}
|
||
|
||
/// Cheap heuristic: does this string contain anything the MText parser would
|
||
/// interpret? Used by `dimension_text_entity` to pick between a synthetic
|
||
/// `Text` (plain DXF special chars only) and a synthetic `MText` (full inline
|
||
/// format-code pipeline) for the dim text override.
|
||
fn value_has_mtext_codes(s: &str) -> bool {
|
||
let mut chars = s.chars().peekable();
|
||
while let Some(c) = chars.next() {
|
||
if c == '{' || c == '}' {
|
||
return true;
|
||
}
|
||
if c == '\\' {
|
||
if let Some(&next) = chars.peek() {
|
||
// Any backslash followed by a known MText escape letter.
|
||
if matches!(
|
||
next,
|
||
'H' | 'W'
|
||
| 'Q'
|
||
| 'T'
|
||
| 'A'
|
||
| 'C'
|
||
| 'c'
|
||
| 'f'
|
||
| 'F'
|
||
| 'p'
|
||
| 'L'
|
||
| 'l'
|
||
| 'O'
|
||
| 'o'
|
||
| 'K'
|
||
| 'k'
|
||
| 'S'
|
||
| 's'
|
||
| 'P'
|
||
| 'n'
|
||
| 'N'
|
||
| 't'
|
||
| 'U'
|
||
| 'u'
|
||
| 'M'
|
||
| 'X'
|
||
| '~'
|
||
| '{'
|
||
| '}'
|
||
) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
false
|
||
}
|
||
|
||
fn dimension_text_entity(
|
||
dim: &Dimension,
|
||
text_height: f64,
|
||
style: Option<&DimStyle>,
|
||
document: &CadDocument,
|
||
dim_scale: f64,
|
||
) -> Option<EntityType> {
|
||
// Deviations are emitted by `dimension_tolerance_entity` at their own
|
||
// height and alignment; keep the primary entity free of duplicate text.
|
||
let (value, _) = dimension_text_parts(dim, style)?;
|
||
// Use f64 position directly to avoid f32 round-trip precision loss at large
|
||
// coordinates (e.g. Turkish UTM ~4,000,000 m). tessellate() will apply
|
||
// world_offset when rendering this synthetic entity.
|
||
let pos_f64 = dimension_text_pos_f64(dim, style, text_height, dim_scale);
|
||
// A limits stack draws each half at DIMTFAC × DIMTXT, like the source application.
|
||
let text_height = match style {
|
||
Some(s) if s.dimlim => text_height * dimtfac_or_one(s),
|
||
_ => text_height,
|
||
};
|
||
let base = dim.base();
|
||
|
||
let rotation = dimension_text_rotation(dim, style);
|
||
|
||
// Text style resolution priority:
|
||
// 1. DIMTXSTY by handle (most reliable; survives rename)
|
||
// 2. DIMTXSTY by name
|
||
// 3. dim's own style_name (rare fallback)
|
||
let style_name = style
|
||
.and_then(|s| {
|
||
if !s.dimtxsty_handle.is_null() {
|
||
document
|
||
.text_styles
|
||
.iter()
|
||
.find(|ts| ts.handle == s.dimtxsty_handle)
|
||
.map(|ts| ts.name.clone())
|
||
} else {
|
||
None
|
||
}
|
||
})
|
||
.or_else(|| {
|
||
style
|
||
.map(|s| s.dimtxsty.clone())
|
||
.filter(|n| !n.trim().is_empty())
|
||
})
|
||
.unwrap_or_else(|| base.style_name.clone());
|
||
|
||
// Route through MText whenever the value carries inline format codes
|
||
// (`\f`, `\C`, `\H`, `\S`, brace scopes, …). Otherwise stay on the Text
|
||
// path — single-line dim text doesn't need the full MText pipeline.
|
||
if value_has_mtext_codes(&value) {
|
||
use codec::entities::dimension::AttachmentPointType as DA;
|
||
use codec::entities::AttachmentPoint as MA;
|
||
let attachment_point = match base.attachment_point {
|
||
DA::TopLeft => MA::TopLeft,
|
||
DA::TopCenter => MA::TopCenter,
|
||
DA::TopRight => MA::TopRight,
|
||
DA::MiddleLeft => MA::MiddleLeft,
|
||
DA::MiddleCenter => MA::MiddleCenter,
|
||
DA::MiddleRight => MA::MiddleRight,
|
||
DA::BottomLeft => MA::BottomLeft,
|
||
DA::BottomCenter => MA::BottomCenter,
|
||
DA::BottomRight => MA::BottomRight,
|
||
};
|
||
let mut mtext = MText::with_value(value, pos_f64);
|
||
mtext.height = text_height;
|
||
mtext.rotation = rotation;
|
||
mtext.style = style_name;
|
||
mtext.attachment_point = attachment_point;
|
||
if base.line_spacing_factor.abs() > 1e-9 {
|
||
mtext.line_spacing_factor = base.line_spacing_factor;
|
||
}
|
||
mtext.normal = base.normal;
|
||
mtext.common = base.common.clone();
|
||
return Some(EntityType::MText(mtext));
|
||
}
|
||
|
||
let value = if style.is_some_and(|style| style.dimtxtdirection) {
|
||
value.chars().rev().collect()
|
||
} else {
|
||
value
|
||
};
|
||
let mut text = Text::with_value(value, pos_f64)
|
||
.with_height(text_height)
|
||
.with_rotation(rotation);
|
||
text.style = style_name;
|
||
|
||
// Map AttachmentPointType (1..9 grid) to Text horizontal + vertical
|
||
// alignments. 1=TopLeft … 9=BottomRight (column-major).
|
||
let (ha, va) = attachment_to_text_align(base.attachment_point);
|
||
text.horizontal_alignment = ha;
|
||
text.vertical_alignment = va;
|
||
// line_spacing_factor controls multi-line text spacing in MText. Our
|
||
// synthetic Text is single-line so this is a no-op, but pass through
|
||
// for completeness.
|
||
let _ = base.line_spacing_factor;
|
||
// normal would rotate the dim plane out of XY. The local 2D pipeline
|
||
// assumes XY, so non-XY normals are read but not applied here.
|
||
let _ = base.normal;
|
||
|
||
text.common = base.common.clone();
|
||
Some(EntityType::Text(text))
|
||
}
|
||
|
||
fn attachment_to_text_align(
|
||
attach: codec::entities::dimension::AttachmentPointType,
|
||
) -> (
|
||
codec::entities::text::TextHorizontalAlignment,
|
||
codec::entities::text::TextVerticalAlignment,
|
||
) {
|
||
use codec::entities::dimension::AttachmentPointType as A;
|
||
use codec::entities::text::{TextHorizontalAlignment as H, TextVerticalAlignment as V};
|
||
match attach {
|
||
A::TopLeft => (H::Left, V::Top),
|
||
A::TopCenter => (H::Center, V::Top),
|
||
A::TopRight => (H::Right, V::Top),
|
||
A::MiddleLeft => (H::Left, V::Middle),
|
||
A::MiddleCenter => (H::Center, V::Middle),
|
||
A::MiddleRight => (H::Right, V::Middle),
|
||
A::BottomLeft => (H::Left, V::Bottom),
|
||
A::BottomCenter => (H::Center, V::Bottom),
|
||
A::BottomRight => (H::Right, V::Bottom),
|
||
}
|
||
}
|
||
|
||
/// Reading rotation for a dimension's measurement text, resolving the style
|
||
/// flags the same way the live renderer does: explicit text_rotation, then
|
||
/// horizontal_direction, then DIMTIH/DIMTOH force-horizontal, else the natural
|
||
/// dim-line angle (+90° for DIMJUST 3/4). Shared by the live text entity and
|
||
/// the bake so a reloaded dimension's text reads at the same angle. (#181)
|
||
fn dimension_text_rotation(dim: &Dimension, style: Option<&DimStyle>) -> f64 {
|
||
let base = dim.base();
|
||
let dimtih = style.map(|s| s.dimtih).unwrap_or(false);
|
||
let dimtoh = style.map(|s| s.dimtoh).unwrap_or(false);
|
||
let dimjust = style.map(|s| s.dimjust).unwrap_or(0);
|
||
let outside = dimension_text_is_outside(dim, style);
|
||
if base.text_rotation.abs() > 1e-9 {
|
||
base.text_rotation
|
||
} else if matches!(dim, Dimension::Ordinate(_)) {
|
||
dimension_text_natural_rotation(dim)
|
||
} else if base.horizontal_direction.abs() > 1e-9 {
|
||
base.horizontal_direction
|
||
} else if (outside && dimtoh) || (!outside && dimtih) {
|
||
0.0
|
||
} else {
|
||
let mut r = dimension_text_natural_rotation(dim);
|
||
if dimjust == 3 || dimjust == 4 {
|
||
r += std::f64::consts::FRAC_PI_2;
|
||
}
|
||
r
|
||
}
|
||
}
|
||
|
||
fn dimension_text_is_outside(dim: &Dimension, style: Option<&DimStyle>) -> bool {
|
||
let Some(style) = style else {
|
||
return false;
|
||
};
|
||
if let Some((vertex, start, end, radius)) = angular_dimension_frame(dim) {
|
||
if let Some(point) = stored_text_point(dim) {
|
||
let text = vec3_local(point);
|
||
let angle = (text.y - vertex.y).atan2(text.x - vertex.x);
|
||
return (angle - start).rem_euclid(std::f32::consts::TAU)
|
||
> end - start + 1.0e-6;
|
||
}
|
||
if style.dimtix {
|
||
return false;
|
||
}
|
||
let scale = if style.dimscale > 1e-9 { style.dimscale } else { 1.0 };
|
||
let height = style.dimtxt * scale;
|
||
let gap = style.dimgap.abs() * scale;
|
||
let text_width = dimension_text_cells(dim, Some(style))
|
||
.map(|cells| cells * height * CELL_WIDTH + gap * 2.0)
|
||
.unwrap_or(0.0);
|
||
let arrow = style.dimasz * scale;
|
||
let span = radius as f64 * (end - start).abs() as f64;
|
||
let insufficient = text_width + arrow * 2.0 > span;
|
||
return insufficient
|
||
&& match style.dimatfit {
|
||
0 | 2 => true,
|
||
1 | 3 => text_width > span,
|
||
_ => text_width > span,
|
||
};
|
||
}
|
||
if let Dimension::LargeRadial(radial) = dim {
|
||
let radius = radial.definition_point.distance(&radial.chord_point);
|
||
if let Some(point) = stored_text_point(dim) {
|
||
return radial.definition_point.distance(&point) > radius + 1.0e-9;
|
||
}
|
||
if style.dimtix {
|
||
return false;
|
||
}
|
||
let available = radial.override_center.distance(&radial.chord_point);
|
||
let scale = if style.dimscale > 1.0e-9 {
|
||
style.dimscale
|
||
} else {
|
||
1.0
|
||
};
|
||
let height = style.dimtxt * scale;
|
||
let gap = style.dimgap.abs() * scale;
|
||
let text_width = dimension_text_cells(dim, Some(style))
|
||
.map(|cells| cells * height * CELL_WIDTH + gap * 2.0)
|
||
.unwrap_or(0.0);
|
||
let arrow = style.dimasz * scale;
|
||
let insufficient = text_width + arrow > available;
|
||
return insufficient
|
||
&& match style.dimatfit {
|
||
0 | 2 => true,
|
||
1 | 3 => text_width > available,
|
||
_ => text_width > available,
|
||
};
|
||
}
|
||
if let Dimension::Radius(radius) = dim {
|
||
let dx = radius.definition_point.x - radius.angle_vertex.x;
|
||
let dy = radius.definition_point.y - radius.angle_vertex.y;
|
||
let available = dx.hypot(dy);
|
||
if let Some(text) = stored_text_point(dim) {
|
||
return (text.x - radius.angle_vertex.x)
|
||
.hypot(text.y - radius.angle_vertex.y)
|
||
> available + 1e-9;
|
||
}
|
||
if style.dimtix {
|
||
return false;
|
||
}
|
||
let scale = if style.dimscale > 1e-9 { style.dimscale } else { 1.0 };
|
||
let height = style.dimtxt * scale;
|
||
let gap = style.dimgap.abs() * scale;
|
||
let text_width = dimension_text_cells(dim, Some(style))
|
||
.map(|cells| cells * height * CELL_WIDTH + gap * 2.0)
|
||
.unwrap_or(0.0);
|
||
let arrow = style.dimasz * scale;
|
||
let insufficient = text_width + arrow > available;
|
||
return insufficient
|
||
&& match style.dimatfit {
|
||
0 | 2 => true,
|
||
1 | 3 => text_width > available,
|
||
_ => text_width > available,
|
||
};
|
||
}
|
||
let (first, second, axis) = match dim {
|
||
Dimension::Linear(d) => (
|
||
d.first_point,
|
||
d.second_point,
|
||
Vector3::new(d.rotation.cos(), d.rotation.sin(), 0.0),
|
||
),
|
||
Dimension::Aligned(d) => {
|
||
let delta = d.second_point - d.first_point;
|
||
let length = (delta.x * delta.x + delta.y * delta.y).sqrt().max(1e-12);
|
||
(d.first_point, d.second_point, delta / length)
|
||
}
|
||
Dimension::Diameter(d) => {
|
||
let delta = d.definition_point - d.angle_vertex;
|
||
let length = (delta.x * delta.x + delta.y * delta.y).sqrt().max(1e-12);
|
||
(d.angle_vertex, d.definition_point, delta / length)
|
||
}
|
||
_ => return false,
|
||
};
|
||
let first_axis = first.x * axis.x + first.y * axis.y;
|
||
let second_axis = second.x * axis.x + second.y * axis.y;
|
||
let lo = first_axis.min(second_axis);
|
||
let hi = first_axis.max(second_axis);
|
||
if let Some(point) = stored_text_point(dim) {
|
||
let position = point.x * axis.x + point.y * axis.y;
|
||
return position < lo || position > hi;
|
||
}
|
||
if style.dimtix {
|
||
return false;
|
||
}
|
||
let scale = if style.dimscale > 1e-9 { style.dimscale } else { 1.0 };
|
||
let height = style.dimtxt * scale;
|
||
let gap = style.dimgap.abs() * scale;
|
||
let text_width = dimension_text_cells(dim, Some(style))
|
||
.map(|cells| cells * height * CELL_WIDTH + gap * 2.0)
|
||
.unwrap_or(0.0);
|
||
let arrow = style.dimasz * scale;
|
||
let span = hi - lo;
|
||
let insufficient = text_width + arrow * 2.0 > span;
|
||
insufficient
|
||
&& match style.dimatfit {
|
||
0 | 2 => true,
|
||
1 | 3 => text_width > span,
|
||
_ => text_width > span,
|
||
}
|
||
}
|
||
|
||
/// Where a dynamic dimension's lock mark sits: just after the text on its
|
||
/// baseline, with the direction that points away from the text.
|
||
pub(crate) fn dynamic_dimension_lock_anchor(
|
||
document: &CadDocument,
|
||
dim: &Dimension,
|
||
anno_scale: f64,
|
||
) -> Option<(Vector3, Vector3)> {
|
||
let style_name = &dim.base().style_name;
|
||
let source_style = document.dim_styles.iter().find(|s| {
|
||
s.name.eq_ignore_ascii_case(style_name)
|
||
|| (style_name.trim().is_empty() && s.name.eq_ignore_ascii_case("Standard"))
|
||
});
|
||
let effective_style = source_style.map(|style| resolved_dimension_style(style, dim, document));
|
||
let style = effective_style.as_ref();
|
||
let dim_scale = style
|
||
.map(|s| if s.dimscale > 1e-6 { s.dimscale } else { anno_scale })
|
||
.unwrap_or(1.0);
|
||
let text_height = style.map(|s| s.dimtxt * dim_scale).unwrap_or(2.5);
|
||
let value = dimension_text_value(dim, style)?;
|
||
let stack_scale = style.map(dimtfac_or_one).unwrap_or(1.0);
|
||
let half_width = text_cells(&value, stack_scale) * text_height * CELL_WIDTH * 0.5;
|
||
let pos = dimension_text_pos_f64(dim, style, text_height, dim_scale);
|
||
// The text angle the renderer draws (DIMTIH/DIMTOH overrides make a
|
||
// dynamic dimension's text horizontal).
|
||
let (sr, cr) = dimension_text_rotation(dim, style).sin_cos();
|
||
let outward = Vector3::new(cr, sr, 0.0);
|
||
// A glyph-sized gap keeps the lock clear of the last digit.
|
||
let reach = half_width + text_height * 0.9;
|
||
Some((
|
||
Vector3::new(pos.x + outward.x * reach, pos.y + outward.y * reach, pos.z),
|
||
outward,
|
||
))
|
||
}
|
||
|
||
/// The two ends of an angular dimension's arc, each with the direction
|
||
/// pointing away from the arc: where a dynamic angle's triangle grips sit.
|
||
pub(crate) fn angular_arc_ends(dim: &Dimension) -> Option<[(glam::DVec3, glam::DVec3); 2]> {
|
||
let (vertex, start, end, radius) = angular_dimension_frame(dim)?;
|
||
let point = |angle: f32| {
|
||
let p = vertex + Vec3::new(angle.cos(), angle.sin(), 0.0) * radius;
|
||
glam::DVec3::new(p.x as f64, p.y as f64, p.z as f64)
|
||
};
|
||
let away = |angle: f32, sign: f32| {
|
||
glam::DVec3::new(
|
||
(-angle.sin() * sign) as f64,
|
||
(angle.cos() * sign) as f64,
|
||
0.0,
|
||
)
|
||
};
|
||
Some([(point(start), away(start, -1.0)), (point(end), away(end, 1.0))])
|
||
}
|
||
|
||
fn dimension_text_natural_rotation(dim: &Dimension) -> f64 {
|
||
let angle = match dim {
|
||
Dimension::Linear(d) => d.rotation,
|
||
Dimension::Aligned(d) => {
|
||
let dx = d.second_point.x - d.first_point.x;
|
||
let dy = d.second_point.y - d.first_point.y;
|
||
dy.atan2(dx)
|
||
}
|
||
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_) | Dimension::Arc(_) => angular_dimension_frame(dim)
|
||
.map(|(_, start, end, _)| {
|
||
((start + end) * 0.5 + std::f32::consts::FRAC_PI_2) as f64
|
||
})
|
||
.unwrap_or(0.0),
|
||
Dimension::Radius(d) => (d.definition_point.y - d.angle_vertex.y)
|
||
.atan2(d.definition_point.x - d.angle_vertex.x),
|
||
Dimension::Diameter(d) => (d.definition_point.y - d.angle_vertex.y)
|
||
.atan2(d.definition_point.x - d.angle_vertex.x),
|
||
Dimension::LargeRadial(d) => (d.chord_point.y - d.override_center.y)
|
||
.atan2(d.chord_point.x - d.override_center.x),
|
||
Dimension::Ordinate(d) => {
|
||
let axis_rotation = -d.base.horizontal_direction;
|
||
if d.is_ordinate_type_x {
|
||
axis_rotation + std::f64::consts::FRAC_PI_2
|
||
} else {
|
||
axis_rotation
|
||
}
|
||
}
|
||
};
|
||
// Clamp to (-π/2, π/2] so text never appears upside-down.
|
||
let pi = std::f64::consts::PI;
|
||
if angle > pi / 2.0 {
|
||
angle - pi
|
||
} else if angle <= -pi / 2.0 {
|
||
angle + pi
|
||
} else {
|
||
angle
|
||
}
|
||
}
|
||
|
||
pub(crate) fn dimension_text_value(dim: &Dimension, style: Option<&DimStyle>) -> Option<String> {
|
||
let (main, tol) = dimension_text_parts(dim, style)?;
|
||
// Tolerance is appended inline for callers (e.g. fill rect width) that
|
||
// don't render a separate tolerance entity. The visual pipeline that
|
||
// does emit a separate tolerance text re-derives the parts itself.
|
||
match tol {
|
||
Some(t) => Some(format!("{} {}", main, t)),
|
||
None => Some(main),
|
||
}
|
||
}
|
||
|
||
/// Returns (primary_text, tolerance_suffix). The tolerance is emitted as a
|
||
/// separate Text entity so DIMTFAC can scale its height and DIMTOLJ can
|
||
/// align it vertically against the primary value.
|
||
fn dimension_text_parts(
|
||
dim: &Dimension,
|
||
style: Option<&DimStyle>,
|
||
) -> Option<(String, Option<String>)> {
|
||
let base = dim.base();
|
||
let is_angular = matches!(dim, Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_));
|
||
|
||
// Auto-generated body used when the user did not override it. Built first
|
||
// so user_text "<>" substitution can re-use it. With DIMLIM the limits
|
||
// stack is the value itself: the source application prints the upper limit over the
|
||
// lower one and no nominal, so the tolerance slot stays empty.
|
||
let value = match limits_text(dim, style, is_angular) {
|
||
Some(limits) => limits,
|
||
None if is_angular => format_angular_value(display_measurement(dim), style),
|
||
None => format_linear_value(display_measurement(dim), style),
|
||
};
|
||
let primary_raw = match dim {
|
||
Dimension::Radius(_) | Dimension::LargeRadial(_) => format!("R{}", value),
|
||
// U+2205 is the native diameter sign and what `%%c` resolves to; the
|
||
// bundled dimension fonts carry it where Latin Ø is missing.
|
||
Dimension::Diameter(_) => format!("∅{}", value),
|
||
_ => value,
|
||
};
|
||
|
||
// Build the deviation suffix separately so the caller can render it as
|
||
// its own Text entity at DIMTFAC × DIMTXT height.
|
||
let tolerance_suffix = build_tolerance_suffix(dim, style, is_angular);
|
||
let primary = apply_dimpost(&primary_raw, style);
|
||
|
||
// Alternate units appended in brackets when DIMALT is on (linear only).
|
||
let primary = if !is_angular {
|
||
match alternate_units_text(display_measurement(dim) * effective_dimlfac(style), style) {
|
||
Some(alt) => format!("{} [{}]", primary, alt),
|
||
None => primary,
|
||
}
|
||
} else {
|
||
primary
|
||
};
|
||
|
||
// Explicit user override (mtext-style "user_text") wins, but "<>" inside
|
||
// it substitutes the measured value. " " (single space) suppresses text.
|
||
if let Some(user_text) = &base.user_text {
|
||
if user_text.is_empty() || user_text.trim().is_empty() {
|
||
return None;
|
||
}
|
||
return Some((user_text.replace("<>", &primary), tolerance_suffix));
|
||
}
|
||
if !base.text.trim().is_empty() {
|
||
return Some((base.text.replace("<>", &primary), tolerance_suffix));
|
||
}
|
||
Some((primary, tolerance_suffix))
|
||
}
|
||
|
||
/// The value the text reports. An angular dimension whose extension point
|
||
/// sits on its vertex has no measurable angle; the source application then keeps showing the
|
||
/// angle it stored (group 42, radians) rather than 0°, and so do we.
|
||
fn display_measurement(dim: &Dimension) -> f64 {
|
||
let measured = dim.measurement();
|
||
let stored = dim.base().actual_measurement;
|
||
match dim {
|
||
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_)
|
||
if measured.abs() < 1e-9 && stored.abs() > 1e-9 =>
|
||
{
|
||
stored.to_degrees()
|
||
}
|
||
_ => measured,
|
||
}
|
||
}
|
||
|
||
/// DIMLIM: the upper limit stacked over the lower one, replacing the nominal.
|
||
/// The offsets apply to the displayed number, so DIMLFAC and DIMRND come first.
|
||
fn limits_text(dim: &Dimension, style: Option<&DimStyle>, is_angular: bool) -> Option<String> {
|
||
// the source application stacks the two limits even when both offsets are zero and the
|
||
// halves read the same.
|
||
let s = style.filter(|s| s.dimlim)?;
|
||
let measurement = display_measurement(dim);
|
||
if is_angular {
|
||
return Some(format!(
|
||
"\\S{}^{};",
|
||
format_angular_value(measurement + s.dimtp, style),
|
||
format_angular_value(measurement - s.dimtm, style)
|
||
));
|
||
}
|
||
let mut nominal = measurement * effective_dimlfac(style);
|
||
if s.dimrnd > 1e-12 {
|
||
nominal = (nominal / s.dimrnd).round() * s.dimrnd;
|
||
}
|
||
let (high, low) = limit_pair(nominal + s.dimtp, nominal - s.dimtm, s);
|
||
Some(format!("\\S{}^{};", high, low))
|
||
}
|
||
|
||
/// The two limits keep the same number of decimals so the stack lines up:
|
||
/// DIMTZIN drops trailing zeros only as far as both values allow. the source application
|
||
/// writes `13.950` over `13.926` with DIMTZIN 8, not `13.95` over `13.926`.
|
||
fn limit_pair(high: f64, low: f64, s: &DimStyle) -> (String, String) {
|
||
let dimtdec = s.dimtdec.max(0) as usize;
|
||
let mut high = decimal_text(high, dimtdec);
|
||
let mut low = decimal_text(low, dimtdec);
|
||
if s.dimtzin & 8 != 0 {
|
||
let droppable = |value: &str| match value.find('.') {
|
||
Some(_) => value.len() - value.trim_end_matches('0').len(),
|
||
None => 0,
|
||
};
|
||
let drop = droppable(&high).min(droppable(&low));
|
||
for value in [&mut high, &mut low] {
|
||
value.truncate(value.len() - drop);
|
||
if value.ends_with('.') {
|
||
value.pop();
|
||
}
|
||
}
|
||
}
|
||
let finish = |value: &str| {
|
||
let value = if s.dimtzin & 4 != 0 {
|
||
strip_leading_zero(value)
|
||
} else {
|
||
value.to_string()
|
||
};
|
||
swap_decimal_sep(&value, s.dimdsep)
|
||
};
|
||
(finish(&high), finish(&low))
|
||
}
|
||
|
||
/// Formats a tolerance number with DIMTDEC, DIMTZIN and DIMDSEP.
|
||
fn tolerance_formatter(s: &DimStyle) -> impl Fn(f64) -> String + '_ {
|
||
let dimtdec = s.dimtdec.max(0) as usize;
|
||
move |v: f64| {
|
||
let raw = decimal_text(v, dimtdec);
|
||
swap_decimal_sep(&apply_linear_zero_suppression(&raw, s.dimtzin), s.dimdsep)
|
||
}
|
||
}
|
||
|
||
/// DIMTOL deviation suffix drawn beside the nominal. DIMLIM wins over DIMTOL,
|
||
/// as in the source application, and is handled by `limits_text`.
|
||
fn build_tolerance_suffix(
|
||
dim: &Dimension,
|
||
style: Option<&DimStyle>,
|
||
is_angular: bool,
|
||
) -> Option<String> {
|
||
let s = style.filter(|s| !s.dimlim)?;
|
||
let fmt = tolerance_formatter(s);
|
||
if s.dimtol {
|
||
let unit = if is_angular { "°" } else { "" };
|
||
if (s.dimtp - s.dimtm).abs() < 1e-12 && s.dimtp.abs() > 1e-12 {
|
||
return Some(format!("±{}{}", fmt(s.dimtp), unit));
|
||
}
|
||
if s.dimtp.abs() > 1e-12 || s.dimtm.abs() > 1e-12 {
|
||
let mut upper = fmt(s.dimtp);
|
||
let mut lower = fmt(s.dimtm);
|
||
let alignment = crate::entities::dim_override::int(
|
||
&dim.base().common.extended_data,
|
||
crate::entities::dim_override::DIMTALN,
|
||
)
|
||
.unwrap_or(0);
|
||
if alignment == 0 {
|
||
align_tolerance_decimals(&mut upper, &mut lower, s.dimdsep);
|
||
}
|
||
return Some(format!(
|
||
"\\S+{}{}^-{}{};",
|
||
upper, unit, lower, unit
|
||
));
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
fn align_tolerance_decimals(upper: &mut String, lower: &mut String, separator: i16) {
|
||
let separator = separator as u8 as char;
|
||
let integer_width = |value: &str| {
|
||
value
|
||
.find(separator)
|
||
.or_else(|| value.find('.'))
|
||
.unwrap_or(value.len())
|
||
};
|
||
let upper_width = integer_width(upper);
|
||
let lower_width = integer_width(lower);
|
||
let target = upper_width.max(lower_width);
|
||
if upper_width < target {
|
||
upper.insert_str(0, &" ".repeat(target - upper_width));
|
||
}
|
||
if lower_width < target {
|
||
lower.insert_str(0, &" ".repeat(target - lower_width));
|
||
}
|
||
}
|
||
|
||
/// Build the bracketed alternate-units suffix when DIMALT is enabled.
|
||
/// When DIMTOL is also on, the bracketed text includes the tolerance
|
||
/// component formatted with DIMALTTD / DIMALTTZ.
|
||
fn alternate_units_text(measurement: f64, style: Option<&DimStyle>) -> Option<String> {
|
||
let s = style?;
|
||
if !s.dimalt {
|
||
return None;
|
||
}
|
||
let scaled = measurement * s.dimaltf;
|
||
let use_sub_units = s.dimaltz & 4 != 0
|
||
&& scaled.abs() < 1.0
|
||
&& s.dimaltmzf.abs() > 1e-12;
|
||
// DIMALTMZF replaces the ordinary alternate-unit factor for sub-unit
|
||
// values; it is not an additional multiplier.
|
||
let mut v = if use_sub_units {
|
||
measurement * s.dimaltmzf
|
||
} else {
|
||
scaled
|
||
};
|
||
if s.dimaltrnd > 1e-12 {
|
||
v = (v / s.dimaltrnd).round() * s.dimaltrnd;
|
||
}
|
||
let dec = s.dimaltd.max(0) as usize;
|
||
let raw = format_with_unit(v, s.dimaltu, dec, s.dimfrac, s.dimaltz, true);
|
||
let suppressed = apply_linear_zero_suppression(&raw, s.dimaltz);
|
||
let mut sep_swapped = swap_decimal_sep(&suppressed, s.dimdsep);
|
||
if use_sub_units {
|
||
sep_swapped.push_str(&s.dimaltmzs);
|
||
}
|
||
let tolerance_factor = if use_sub_units {
|
||
s.dimaltmzf
|
||
} else {
|
||
s.dimaltf
|
||
};
|
||
// Alt-unit tolerance suffix using DIMALTTD / DIMALTTZ.
|
||
let alt_value = if s.dimtol {
|
||
let alttdec = s.dimalttd.max(0) as usize;
|
||
let alttzin = s.dimalttz;
|
||
let fmt = |x: f64| -> String {
|
||
let raw = decimal_text(x * tolerance_factor, alttdec);
|
||
swap_decimal_sep(&apply_linear_zero_suppression(&raw, alttzin), s.dimdsep)
|
||
};
|
||
if (s.dimtp - s.dimtm).abs() < 1e-12 && s.dimtp.abs() > 1e-12 {
|
||
format!("{}±{}", sep_swapped, fmt(s.dimtp))
|
||
} else if s.dimtp.abs() > 1e-12 || s.dimtm.abs() > 1e-12 {
|
||
format!("{} +{} / -{}", sep_swapped, fmt(s.dimtp), fmt(s.dimtm))
|
||
} else {
|
||
sep_swapped
|
||
}
|
||
} else if s.dimlim {
|
||
let alttdec = s.dimalttd.max(0) as usize;
|
||
let alttzin = s.dimalttz;
|
||
let fmt = |x: f64| -> String {
|
||
let raw = decimal_text(x * tolerance_factor, alttdec);
|
||
swap_decimal_sep(&apply_linear_zero_suppression(&raw, alttzin), s.dimdsep)
|
||
};
|
||
// Alternate limits stack like the primary ones.
|
||
format!(
|
||
"\\S{}^{};",
|
||
fmt(measurement + s.dimtp),
|
||
fmt(measurement - s.dimtm)
|
||
)
|
||
} else {
|
||
sep_swapped
|
||
};
|
||
// DIMAPOST wraps the alt value (same "<>" convention as DIMPOST).
|
||
let wrapped = if s.dimapost.is_empty() {
|
||
alt_value
|
||
} else if s.dimapost.contains("<>") {
|
||
s.dimapost.replace("<>", &alt_value)
|
||
} else {
|
||
format!("{}{}", alt_value, s.dimapost)
|
||
};
|
||
Some(wrapped)
|
||
}
|
||
|
||
/// Build the secondary tolerance Text entity at `DIMTXT × DIMTFAC` height,
|
||
/// positioned to the right of the primary text and vertically aligned per
|
||
/// `DIMTOLJ` (0=bottom, 1=middle, 2=top). Returns None when DIMTOL/DIMLIM
|
||
/// produce no tolerance string (e.g. both DIMTP and DIMTM are zero).
|
||
fn dimension_tolerance_entity(
|
||
dim: &Dimension,
|
||
style: Option<&DimStyle>,
|
||
primary: &EntityType,
|
||
primary_height: f64,
|
||
) -> Option<EntityType> {
|
||
let s = style?;
|
||
let is_angular = matches!(dim, Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_));
|
||
let tol = build_tolerance_suffix(dim, style, is_angular)?;
|
||
let tol_height = primary_height * dimtfac_or_one(s);
|
||
|
||
// Pull the geometry we need from the synthetic primary entity (Text or
|
||
// MText — `dimension_text_entity` routes to MText when the dim value
|
||
// carries inline format codes).
|
||
let (primary_value_len, primary_insertion, primary_rotation, primary_style, primary_common) =
|
||
match primary {
|
||
EntityType::Text(t) => (
|
||
t.value.chars().count(),
|
||
t.insertion_point,
|
||
t.rotation,
|
||
t.style.clone(),
|
||
t.common.clone(),
|
||
),
|
||
EntityType::MText(m) => (
|
||
m.value.chars().count(),
|
||
m.insertion_point,
|
||
m.rotation,
|
||
m.style.clone(),
|
||
m.common.clone(),
|
||
),
|
||
_ => return None,
|
||
};
|
||
|
||
// Approximate widths from glyph counts (~0.6 × cell size per char).
|
||
let primary_w = primary_value_len as f64 * primary_height * CELL_WIDTH;
|
||
let tol_visible_chars = tol
|
||
.strip_prefix("\\S")
|
||
.and_then(|value| value.strip_suffix(';'))
|
||
.map(|value| value.split('^').map(str::len).max().unwrap_or(0))
|
||
.unwrap_or_else(|| tol.chars().count());
|
||
let tol_w = tol_visible_chars as f64 * tol_height * CELL_WIDTH;
|
||
let gap = primary_height * 0.2;
|
||
let dx_local = primary_w * 0.5 + tol_w * 0.5 + gap;
|
||
let dy_local = match s.dimtolj {
|
||
0 => -primary_height * 0.5 + tol_height * 0.5, // bottom-aligned with primary baseline
|
||
2 => primary_height * 0.5 - tol_height * 0.5, // top-aligned with primary top
|
||
_ => 0.0, // centred (default for ±)
|
||
};
|
||
let rot = primary_rotation;
|
||
let (sr, cr) = rot.sin_cos();
|
||
let pos = Vector3::new(
|
||
primary_insertion.x + dx_local * cr - dy_local * sr,
|
||
primary_insertion.y + dx_local * sr + dy_local * cr,
|
||
primary_insertion.z,
|
||
);
|
||
if value_has_mtext_codes(&tol) {
|
||
let mut mtext = MText::with_value(tol, pos);
|
||
mtext.height = tol_height;
|
||
mtext.rotation = rot;
|
||
mtext.style = primary_style;
|
||
mtext.attachment_point = codec::entities::AttachmentPoint::MiddleCenter;
|
||
mtext.common = primary_common;
|
||
return Some(EntityType::MText(mtext));
|
||
}
|
||
|
||
let mut t = Text::with_value(tol, pos)
|
||
.with_height(tol_height)
|
||
.with_rotation(rot);
|
||
t.style = primary_style;
|
||
t.common = primary_common;
|
||
t.horizontal_alignment = codec::entities::text::TextHorizontalAlignment::Center;
|
||
t.vertical_alignment = codec::entities::text::TextVerticalAlignment::Middle;
|
||
Some(EntityType::Text(t))
|
||
}
|
||
|
||
/// Wrap a measured value with the style's DIMPOST prefix/suffix template.
|
||
/// "<>" inside DIMPOST is replaced by the value; absent "<>" appends.
|
||
fn apply_dimpost(value: &str, style: Option<&DimStyle>) -> String {
|
||
let post = style.map(|s| s.dimpost.as_str()).unwrap_or("");
|
||
if post.is_empty() {
|
||
return value.to_string();
|
||
}
|
||
if post.contains("<>") {
|
||
post.replace("<>", value)
|
||
} else {
|
||
format!("{}{}", value, post)
|
||
}
|
||
}
|
||
|
||
/// `resolved_dimension_style` has already applied the DXF sign convention,
|
||
/// so a negative value here means the caller passed an unresolved style;
|
||
/// fall back to 1.0 rather than reporting a negative length.
|
||
fn effective_dimlfac(style: Option<&DimStyle>) -> f64 {
|
||
let lfac = style.map(|s| s.dimlfac).unwrap_or(1.0);
|
||
if lfac.abs() < 1e-12 || lfac < 0.0 {
|
||
1.0
|
||
} else {
|
||
lfac
|
||
}
|
||
}
|
||
|
||
/// Average advance of one character cell as a fraction of the text height.
|
||
/// Measured on the bundled stroke fonts by rendering the text of 106 saved
|
||
/// dimension pictures: median 0.66, where the old guess of 0.60 left every
|
||
/// fit and hook estimate ten percent narrow.
|
||
const CELL_WIDTH: f64 = 0.66;
|
||
|
||
/// DIMTFAC with the unset value 0 read as 1.
|
||
fn dimtfac_or_one(s: &DimStyle) -> f64 {
|
||
if s.dimtfac.abs() < 1e-12 {
|
||
1.0
|
||
} else {
|
||
s.dimtfac
|
||
}
|
||
}
|
||
|
||
/// Vertical extent of a dimension's whole value: the first line (or limits
|
||
/// stack) plus any `\P` continuation lines at MText's default spacing.
|
||
fn dimension_text_extent_height(dim: &Dimension, style: Option<&DimStyle>, text_height: f64) -> f64 {
|
||
let lines = dimension_text_value(dim, style)
|
||
.map(|value| text_lines(&value))
|
||
.unwrap_or(1);
|
||
dimension_text_block_height(style, text_height) + (lines - 1) as f64 * text_height * 5.0 / 3.0
|
||
}
|
||
|
||
/// Vertical extent of the rendered value: one line, or a DIMLIM stack whose
|
||
/// halves each sit at DIMTFAC × DIMTXT.
|
||
fn dimension_text_block_height(style: Option<&DimStyle>, text_height: f64) -> f64 {
|
||
match style {
|
||
Some(s) if s.dimlim => {
|
||
// From the lower half's baseline to the upper half's cap, as the
|
||
// MText stack lays them out.
|
||
text_height
|
||
* dimtfac_or_one(s)
|
||
* f64::from(
|
||
crate::entities::text_support::STACK_RAISE
|
||
+ crate::entities::text_support::STACK_HALF_SCALE,
|
||
)
|
||
}
|
||
_ => text_height,
|
||
}
|
||
}
|
||
|
||
/// Approximate width of the rendered value in character cells of the nominal
|
||
/// text height. A `\S` stack is as wide as its wider half, drawn at DIMTFAC.
|
||
pub(crate) fn dimension_text_cells(dim: &Dimension, style: Option<&DimStyle>) -> Option<f64> {
|
||
let value = dimension_text_value(dim, style)?;
|
||
Some(text_cells(&value, style.map(dimtfac_or_one).unwrap_or(1.0)))
|
||
}
|
||
|
||
/// Width in cells of the widest line; `\P` starts a new line.
|
||
fn text_cells(value: &str, stack_scale: f64) -> f64 {
|
||
value
|
||
.split(r"\P")
|
||
.map(|line| line_cells(line, stack_scale))
|
||
.fold(0.0, f64::max)
|
||
}
|
||
|
||
/// Number of lines the value renders as.
|
||
fn text_lines(value: &str) -> usize {
|
||
value.split(r"\P").count().max(1)
|
||
}
|
||
|
||
/// Visible cells of one line: MText format codes (`\\f...;`, `\\H...;`, `\\C...;`,
|
||
/// braces, underline toggles) take no room, a stack is as wide as its wider half.
|
||
fn line_cells(value: &str, stack_scale: f64) -> f64 {
|
||
let mut cells = 0.0;
|
||
let mut chars = value.chars().peekable();
|
||
while let Some(c) = chars.next() {
|
||
match c {
|
||
'{' | '}' => {}
|
||
'\\' => match chars.next() {
|
||
Some('S') => {
|
||
let body: String = chars.by_ref().take_while(|c| *c != ';').collect();
|
||
let widest = body
|
||
.split(['^', '/', '#'])
|
||
.map(|half| half.chars().count())
|
||
.max()
|
||
.unwrap_or(0);
|
||
cells += widest as f64 * stack_scale;
|
||
}
|
||
Some('~') => cells += 1.0,
|
||
// Underline, overline and strike toggles take no room; an
|
||
// escaped backslash or brace is one visible character.
|
||
Some('L' | 'l' | 'O' | 'o' | 'K' | 'k') => {}
|
||
Some('\\' | '{' | '}') => cells += 1.0,
|
||
Some(_) => {
|
||
for c in chars.by_ref() {
|
||
if c == ';' {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
None => {}
|
||
},
|
||
_ => cells += 1.0,
|
||
}
|
||
}
|
||
cells
|
||
}
|
||
|
||
/// Format a linear measurement honouring DIMLFAC, DIMRND, DIMDEC, DIMZIN, DIMDSEP, DIMLUNIT.
|
||
fn format_linear_value(measurement: f64, style: Option<&DimStyle>) -> String {
|
||
let (dec, zin, rnd, dsep, lunit, frac, sub_factor, sub_suffix) = style
|
||
.map(|s| {
|
||
(
|
||
s.dimdec,
|
||
s.dimzin,
|
||
s.dimrnd,
|
||
s.dimdsep,
|
||
s.dimlunit,
|
||
s.dimfrac,
|
||
s.dimmzf,
|
||
s.dimmzs.as_str(),
|
||
)
|
||
})
|
||
.unwrap_or((4, 8, 0.0, 46, 2, 0, 1.0, ""));
|
||
|
||
let lfac = effective_dimlfac(style);
|
||
let scaled = measurement * lfac;
|
||
let use_sub_units = zin & 4 != 0 && scaled.abs() < 1.0 && sub_factor.abs() > 1e-12;
|
||
// For values below one unit, DIMMZF replaces DIMLFAC; applying it after
|
||
// DIMLFAC multiplies both factors and reports the wrong sub-unit value.
|
||
let mut v = if use_sub_units {
|
||
measurement * sub_factor
|
||
} else {
|
||
scaled
|
||
};
|
||
if rnd > 1e-12 {
|
||
v = (v / rnd).round() * rnd;
|
||
}
|
||
let dec = dec.max(0) as usize;
|
||
let raw = format_with_unit(v, lunit, dec, frac, zin, false);
|
||
let suppressed = apply_linear_zero_suppression(&raw, zin);
|
||
let mut formatted = swap_decimal_sep(&suppressed, dsep);
|
||
if use_sub_units {
|
||
formatted.push_str(sub_suffix);
|
||
}
|
||
formatted
|
||
}
|
||
|
||
/// Dispatch on DIMLUNIT / DIMALTU.
|
||
/// 1 = Scientific
|
||
/// 2 = Decimal (default)
|
||
/// 3 = Engineering (feet + decimal inches; 1 unit = 1 inch)
|
||
/// 4 = Architectural (feet + fractional inches)
|
||
/// 5 = Fractional (integer + fractional inches)
|
||
/// For alternate units, 4/5 are stacked and 6/7 are unstacked architectural
|
||
/// and fractional forms. For primary units DIMFRAC selects the stack form.
|
||
fn format_with_unit(
|
||
value: f64,
|
||
unit: i16,
|
||
dec: usize,
|
||
dimfrac: i16,
|
||
zin: i16,
|
||
alternate: bool,
|
||
) -> String {
|
||
match unit {
|
||
1 => format!("{:.*e}", dec, value),
|
||
3 => format_engineering(value, dec),
|
||
4 => format_architectural(value, dec, if alternate { 0 } else { dimfrac }, zin),
|
||
5 => format_fractional(value, dec, if alternate { 0 } else { dimfrac }),
|
||
6 if alternate => format_architectural(value, dec, 2, zin),
|
||
7 if alternate => format_fractional(value, dec, 2),
|
||
_ => decimal_text(value, dec),
|
||
}
|
||
}
|
||
|
||
/// Fixed-point text rounded half away from zero. A few scaled ULPs settle
|
||
/// decimal ties without changing values at higher requested precision.
|
||
fn decimal_text(value: f64, decimals: usize) -> String {
|
||
let scale = 10f64.powi(decimals.min(15) as i32);
|
||
let scaled = value * scale;
|
||
let tolerance = (scaled.abs().max(1.0) * f64::EPSILON * 4.0).min(0.25);
|
||
format!(
|
||
"{:.*}",
|
||
decimals,
|
||
(scaled + scaled.signum() * tolerance).round() / scale
|
||
)
|
||
}
|
||
|
||
fn format_engineering(inches: f64, dec: usize) -> String {
|
||
// Round to the printed precision *first*, then split, so inches that round
|
||
// up to a full foot carry into the feet instead of printing an
|
||
// out-of-range "2'-12.00"". Same reasoning as `format_architectural`,
|
||
// where the grid is the fraction denominator rather than a decimal place.
|
||
let scale = 10f64.powi(dec.min(15) as i32);
|
||
let ticks = (inches.abs() * scale).round();
|
||
let per_foot = 12.0 * scale;
|
||
let sign = if inches < 0.0 && ticks != 0.0 { "-" } else { "" };
|
||
let feet = (ticks / per_foot).trunc();
|
||
let rem_in = (ticks - feet * per_foot) / scale;
|
||
format!("{}{:.0}'-{:.*}\"", sign, feet, dec, rem_in)
|
||
}
|
||
|
||
fn format_architectural(inches: f64, precision: usize, dimfrac: i16, zin: i16) -> String {
|
||
let denom = fractional_denominator(precision);
|
||
// Round to the nearest 1/denom inch *first*, in integer ticks, so a fraction
|
||
// that rounds up to a whole inch carries into the inches — and on into the
|
||
// feet — instead of printing an un-reduced "11 1/1". A 3ft object that
|
||
// measures 35.9999" (float) now reads 3'-0", not 2'-11 1".
|
||
let ticks = (inches.abs() * denom as f64).round() as u64;
|
||
let sign = if inches < 0.0 && ticks != 0 { "-" } else { "" };
|
||
let per_foot = 12 * denom;
|
||
let feet = ticks / per_foot;
|
||
let rem = ticks % per_foot;
|
||
let whole = rem / denom;
|
||
let frac_str = format_fraction_component(&reduce_fraction(rem % denom, denom), dimfrac);
|
||
|
||
// DIMZIN feet/inch suppression:
|
||
// 0 suppress zero feet & zero inches, 1 include both,
|
||
// 2 include zero feet / suppress zero inches,
|
||
// 3 suppress zero feet / include zero inches.
|
||
let suppress_zero_feet = zin == 0 || zin == 3;
|
||
let suppress_zero_inches = zin == 0 || zin == 2;
|
||
let feet_zero = feet == 0;
|
||
let inches_zero = whole == 0 && frac_str.is_empty();
|
||
let show_feet = !feet_zero || !suppress_zero_feet;
|
||
let show_inches = !inches_zero || !suppress_zero_inches;
|
||
|
||
let feet_part = if show_feet {
|
||
format!("{}'", feet)
|
||
} else {
|
||
String::new()
|
||
};
|
||
let inch_part = if show_inches {
|
||
if frac_str.is_empty() {
|
||
format!("{}\"", whole)
|
||
} else {
|
||
format!("{} {}\"", whole, frac_str)
|
||
}
|
||
} else {
|
||
String::new()
|
||
};
|
||
let body = match (feet_part.is_empty(), inch_part.is_empty()) {
|
||
(false, false) => format!("{}-{}", feet_part, inch_part),
|
||
(false, true) => feet_part,
|
||
(true, false) => inch_part,
|
||
(true, true) => "0\"".to_string(),
|
||
};
|
||
format!("{}{}", sign, body)
|
||
}
|
||
|
||
fn format_fractional(value: f64, precision: usize, dimfrac: i16) -> String {
|
||
let denom = fractional_denominator(precision);
|
||
// Round on the fraction grid first (same carry reasoning as architectural).
|
||
let ticks = (value.abs() * denom as f64).round() as u64;
|
||
let sign = if value < 0.0 && ticks != 0 { "-" } else { "" };
|
||
let whole = ticks / denom;
|
||
let frac_str = format_fraction_component(&reduce_fraction(ticks % denom, denom), dimfrac);
|
||
if frac_str.is_empty() {
|
||
format!("{}{}", sign, whole)
|
||
} else if whole == 0 {
|
||
format!("{}{}", sign, frac_str)
|
||
} else {
|
||
format!("{}{} {}", sign, whole, frac_str)
|
||
}
|
||
}
|
||
|
||
/// Fraction precision maps 0..8 to whole units through 1/256. DIMFRAC controls
|
||
/// only the visual stack form; it must not change the numeric denominator.
|
||
fn fractional_denominator(precision: usize) -> u64 {
|
||
1u64 << precision.min(8)
|
||
}
|
||
|
||
fn format_fraction_component(value: &str, dimfrac: i16) -> String {
|
||
if value.is_empty() || dimfrac == 2 {
|
||
return value.to_string();
|
||
}
|
||
let separator = if dimfrac == 1 { '#' } else { '/' };
|
||
format!("\\S{};", value.replacen('/', &separator.to_string(), 1))
|
||
}
|
||
|
||
/// Reduce a power-of-two fraction numer/denom to display form. Empty for a zero
|
||
/// numerator. Callers strip the whole units first, so `numer < denom` and the
|
||
/// reduced denominator is always ≥ 2 — the carry that used to leak out as a bare
|
||
/// "1/1" is handled upstream now.
|
||
fn reduce_fraction(mut n: u64, mut d: u64) -> String {
|
||
if n == 0 {
|
||
return String::new();
|
||
}
|
||
while n % 2 == 0 && d % 2 == 0 {
|
||
n /= 2;
|
||
d /= 2;
|
||
}
|
||
if d == 1 {
|
||
format!("{}", n)
|
||
} else {
|
||
format!("{}/{}", n, d)
|
||
}
|
||
}
|
||
|
||
/// Format an angular measurement in degrees using the angular style settings.
|
||
fn format_angular_value(measurement_deg: f64, style: Option<&DimStyle>) -> String {
|
||
let (aunit, adec, azin, decimal_separator) = style
|
||
.map(|s| {
|
||
let precision = if s.dimadec < 0 { s.dimdec } else { s.dimadec };
|
||
(s.dimaunit, precision, s.dimazin, s.dimdsep)
|
||
})
|
||
.unwrap_or((0, 2, 0, b'.' as i16));
|
||
let adec = adec.clamp(0, 8) as usize;
|
||
|
||
match aunit {
|
||
// 1 = Degrees / Minutes / Seconds
|
||
1 => format_dms(measurement_deg, adec, azin, decimal_separator),
|
||
// 2 = Gradians
|
||
2 => {
|
||
let g = measurement_deg / 0.9;
|
||
let raw = format!("{:.*}", adec, g);
|
||
format!(
|
||
"{}g",
|
||
swap_decimal_sep(&apply_angular_zero_suppression(&raw, azin), decimal_separator)
|
||
)
|
||
}
|
||
// 3 = Radians
|
||
3 => {
|
||
let r = measurement_deg.to_radians();
|
||
let raw = format!("{:.*}", adec, r);
|
||
format!(
|
||
"{}r",
|
||
swap_decimal_sep(&apply_angular_zero_suppression(&raw, azin), decimal_separator)
|
||
)
|
||
}
|
||
// 4 = Surveyor's units
|
||
4 => format_surveyor(measurement_deg, adec, azin, decimal_separator),
|
||
// 0 or unknown = Decimal Degrees
|
||
_ => {
|
||
let raw = format!("{:.*}", adec, measurement_deg);
|
||
format!(
|
||
"{}°",
|
||
swap_decimal_sep(&apply_angular_zero_suppression(&raw, azin), decimal_separator)
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Surveyor's units (DIMAUNIT 4): a bearing away from north or south toward
|
||
/// east or west, so the quoted angle never exceeds a quarter turn. Due north,
|
||
/// south, east and west have no bearing to quote and are written as the single
|
||
/// letter, which is also what keeps a quarter turn from reading as a
|
||
/// contradictory `N 90d0'0" E`. The bearing is DMS, so it carries DIMADEC,
|
||
/// DIMAZIN and DIMDSEP the same way the DMS format does.
|
||
fn format_surveyor(deg: f64, sec_dec: usize, azin: i16, decimal_separator: i16) -> String {
|
||
// Quantise before choosing a cardinal or a quadrant, so a value a hair
|
||
// under a cardinal direction does not print a quarter-turn bearing.
|
||
let scale = 3600.0 * 10f64.powi(sec_dec.min(8) as i32);
|
||
let turn = 360.0 * scale;
|
||
let ticks = (deg.rem_euclid(360.0) * scale).round().rem_euclid(turn);
|
||
let at = |d: f64| (ticks - d * scale).abs() < 0.5;
|
||
if at(0.0) {
|
||
return "E".into();
|
||
}
|
||
if at(90.0) {
|
||
return "N".into();
|
||
}
|
||
if at(180.0) {
|
||
return "W".into();
|
||
}
|
||
if at(270.0) {
|
||
return "S".into();
|
||
}
|
||
let d = ticks / scale;
|
||
// Measured from the nearer pole, toward the side the angle falls on.
|
||
let (pole, bearing, side) = if d < 90.0 {
|
||
("N", 90.0 - d, "E")
|
||
} else if d < 180.0 {
|
||
("N", d - 90.0, "W")
|
||
} else if d < 270.0 {
|
||
("S", 270.0 - d, "W")
|
||
} else {
|
||
("S", d - 270.0, "E")
|
||
};
|
||
format!(
|
||
"{pole} {} {side}",
|
||
format_dms(bearing, sec_dec, azin, decimal_separator)
|
||
)
|
||
}
|
||
|
||
fn format_dms(deg: f64, sec_dec: usize, azin: i16, decimal_separator: i16) -> String {
|
||
let sign = if deg < 0.0 { "-" } else { "" };
|
||
let scale = 10_u64.pow(sec_dec.min(8) as u32) as f64;
|
||
let total_ticks = (deg.abs() * 3600.0 * scale).round();
|
||
let d = (total_ticks / (3600.0 * scale)).floor();
|
||
let remaining = total_ticks - d * 3600.0 * scale;
|
||
let m = (remaining / (60.0 * scale)).floor();
|
||
let s = (remaining - m * 60.0 * scale) / scale;
|
||
let s_str = format!("{:.*}", sec_dec, s);
|
||
let s_str = swap_decimal_sep(
|
||
&apply_angular_zero_suppression(&s_str, azin),
|
||
decimal_separator,
|
||
);
|
||
format!("{}{:.0}°{:.0}'{}\"", sign, d, m, s_str)
|
||
}
|
||
|
||
/// Apply DIMZIN bit flags to a formatted linear value.
|
||
/// bit 0 (1) suppress 0' (imperial feet) — not applicable for decimal
|
||
/// bit 1 (2) suppress 0" (imperial inches) — not applicable for decimal
|
||
/// bit 2 (4) suppress leading zeros (e.g. ".5" not "0.5")
|
||
/// bit 3 (8) suppress trailing zeros (e.g. "1.5" not "1.50")
|
||
/// Default = 8 (trailing-zero suppression on).
|
||
fn apply_linear_zero_suppression(s: &str, zin: i16) -> String {
|
||
let mut out = s.to_string();
|
||
if zin & 8 != 0 {
|
||
out = strip_trailing_zeros(&out);
|
||
}
|
||
if zin & 4 != 0 {
|
||
out = strip_leading_zero(&out);
|
||
}
|
||
out
|
||
}
|
||
|
||
fn apply_angular_zero_suppression(s: &str, azin: i16) -> String {
|
||
// DIMAZIN: 0=neither, 1=leading, 2=trailing, 3=both.
|
||
let mut out = s.to_string();
|
||
if azin & 2 != 0 {
|
||
out = strip_trailing_zeros(&out);
|
||
}
|
||
if azin & 1 != 0 {
|
||
out = strip_leading_zero(&out);
|
||
}
|
||
out
|
||
}
|
||
|
||
fn strip_trailing_zeros(s: &str) -> String {
|
||
if !s.contains('.') {
|
||
return s.to_string();
|
||
}
|
||
let trimmed = s.trim_end_matches('0').trim_end_matches('.');
|
||
if trimmed.is_empty() || trimmed == "-" {
|
||
"0".to_string()
|
||
} else {
|
||
trimmed.to_string()
|
||
}
|
||
}
|
||
|
||
fn strip_leading_zero(s: &str) -> String {
|
||
// "0.5" → ".5", "-0.5" → "-.5", "0" stays.
|
||
if let Some(rest) = s.strip_prefix("-0.") {
|
||
return format!("-.{rest}");
|
||
}
|
||
if let Some(rest) = s.strip_prefix("0.") {
|
||
return format!(".{rest}");
|
||
}
|
||
s.to_string()
|
||
}
|
||
|
||
fn swap_decimal_sep(s: &str, dsep_code: i16) -> String {
|
||
// DIMDSEP holds an ASCII code (0 means default '.'). 46='.', 44=',', etc.
|
||
if dsep_code <= 0 || dsep_code == 46 {
|
||
return s.to_string();
|
||
}
|
||
let ch = char::from_u32(dsep_code as u32).unwrap_or('.');
|
||
s.replace('.', &ch.to_string())
|
||
}
|
||
|
||
fn vec3_local(v: Vector3) -> Vec3 {
|
||
Vec3::new(v.x as f32, v.y as f32, v.z as f32)
|
||
}
|
||
|
||
/// Style-driven text anchor on the dimension line: a point on the line through
|
||
/// `defpt` parallel to (`ax`,`ay`), slid along it per DIMJUST and lifted by
|
||
/// `perp_off` toward the side the dimension line sits on.
|
||
#[allow(clippy::too_many_arguments)]
|
||
fn text_on_dim_line(
|
||
first: Vector3,
|
||
second: Vector3,
|
||
defpt: Vector3,
|
||
ax: f64,
|
||
ay: f64,
|
||
dimjust: i16,
|
||
perp_off: f64,
|
||
text_w: f64,
|
||
arrow: f64,
|
||
dimtix: bool,
|
||
dimatfit: i16,
|
||
tad: i16,
|
||
horizontal_outside: bool,
|
||
) -> Vector3 {
|
||
// The perpendicular "up" side: the perpendicular of the dimension line
|
||
// normalised so the text reads left-to-right (or bottom-to-top when
|
||
// vertical). DIMTAD "above" (1) and JIS (3) sit on this side.
|
||
let (mut nx, mut ny) = (ax, ay);
|
||
if nx < 0.0 || (nx == 0.0 && ny < 0.0) {
|
||
nx = -nx;
|
||
ny = -ny;
|
||
}
|
||
let px = -ny;
|
||
let py = nx;
|
||
// DIMTAD side:
|
||
// 1 (above) / 3 (JIS) / 0 (centred) → the text-up side, independent of the
|
||
// object — keying it off the object flipped "above" to "below" whenever
|
||
// the dimension ran the opposite way (#144);
|
||
// 4 (below) → the opposite side;
|
||
// 2 (outside) → the side farthest from the defining points (the object).
|
||
let perp_sign = match tad {
|
||
4 => -1.0,
|
||
2 => {
|
||
let off = (defpt.x - first.x) * px + (defpt.y - first.y) * py;
|
||
if off >= 0.0 {
|
||
1.0
|
||
} else {
|
||
-1.0
|
||
}
|
||
}
|
||
_ => 1.0,
|
||
};
|
||
// Along-axis positions of the extension points relative to the dim line.
|
||
let t1 = (first.x - defpt.x) * ax + (first.y - defpt.y) * ay;
|
||
let t2 = (second.x - defpt.x) * ax + (second.y - defpt.y) * ay;
|
||
// DIMJUST: 0=centred, 1/3=near first ext, 2/4=near second ext.
|
||
let mut along = match dimjust {
|
||
1 | 3 => t1,
|
||
2 | 4 => t2,
|
||
_ => (t1 + t2) * 0.5,
|
||
};
|
||
// DIMATFIT / DIMTIX fit: move text according to the selected priority when
|
||
// the combined text-and-arrow envelope cannot fit.
|
||
if dimjust == 0 && !dimtix && text_w > 0.0 {
|
||
let lo = t1.min(t2);
|
||
let hi = t1.max(t2);
|
||
let span = hi - lo;
|
||
let insufficient = text_w + 2.0 * arrow > span;
|
||
let text_outside = insufficient
|
||
&& match dimatfit {
|
||
0 | 2 => true,
|
||
1 | 3 => text_w > span,
|
||
_ => text_w > span,
|
||
};
|
||
if text_outside {
|
||
// Past the two-arrow stub the dimension line grows outside the
|
||
// extension line, then the gap that `text_w` already carries.
|
||
along = hi + 2.0 * arrow + text_w * 0.5;
|
||
if horizontal_outside && ay.abs() > 1e-3 {
|
||
// Horizontal text beside a hook: the line runs out two arrow
|
||
// lengths, the hook turns one arrow toward the text-up side
|
||
// and the text sits against it, centred on the hook.
|
||
let elbow_along = hi + 2.0 * arrow;
|
||
let side = if px >= 0.0 { 1.0 } else { -1.0 };
|
||
let elbow_x = defpt.x + ax * elbow_along;
|
||
let elbow_y = defpt.y + ay * elbow_along;
|
||
let vertical = if tad == 0 { 0.0 } else { perp_off * perp_sign };
|
||
return Vector3::new(
|
||
elbow_x + side * (arrow + text_w * 0.5),
|
||
elbow_y + vertical,
|
||
defpt.z,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
let bx = defpt.x + ax * along;
|
||
let by = defpt.y + ay * along;
|
||
Vector3::new(
|
||
bx + px * perp_off * perp_sign,
|
||
by + py * perp_off * perp_sign,
|
||
defpt.z,
|
||
)
|
||
}
|
||
|
||
fn text_on_single_arrow_dim_line(
|
||
first: Vector3,
|
||
second: Vector3,
|
||
defpt: Vector3,
|
||
ax: f64,
|
||
ay: f64,
|
||
perp_off: f64,
|
||
text_width: f64,
|
||
arrow: f64,
|
||
dimtix: bool,
|
||
dimatfit: i16,
|
||
tad: i16,
|
||
) -> Vector3 {
|
||
let (mut nx, mut ny) = (ax, ay);
|
||
if nx < 0.0 || (nx == 0.0 && ny < 0.0) {
|
||
nx = -nx;
|
||
ny = -ny;
|
||
}
|
||
let (perpendicular_x, perpendicular_y) = (-ny, nx);
|
||
let perpendicular_sign = match tad {
|
||
4 => -1.0,
|
||
2 => {
|
||
let offset = (defpt.x - first.x) * perpendicular_x
|
||
+ (defpt.y - first.y) * perpendicular_y;
|
||
if offset >= 0.0 { 1.0 } else { -1.0 }
|
||
}
|
||
_ => 1.0,
|
||
};
|
||
let first_along = (first.x - defpt.x) * ax + (first.y - defpt.y) * ay;
|
||
let second_along = (second.x - defpt.x) * ax + (second.y - defpt.y) * ay;
|
||
let low = first_along.min(second_along);
|
||
let high = first_along.max(second_along);
|
||
let available = high - low;
|
||
let insufficient = text_width + arrow > available;
|
||
let text_outside = !dimtix
|
||
&& insufficient
|
||
&& match dimatfit {
|
||
0 | 2 => true,
|
||
1 | 3 => text_width > available,
|
||
_ => text_width > available,
|
||
};
|
||
let along = if text_outside {
|
||
high + 2.0 * arrow + text_width * 0.5
|
||
} else {
|
||
(first_along + second_along) * 0.5
|
||
};
|
||
Vector3::new(
|
||
defpt.x + ax * along + perpendicular_x * perp_off * perpendicular_sign,
|
||
defpt.y + ay * along + perpendicular_y * perp_off * perpendicular_sign,
|
||
defpt.z,
|
||
)
|
||
}
|
||
|
||
/// The stored text middle point (group 11) applies to automatic and hand-placed
|
||
/// text alike. A regenerated dimension keeps any non-zero point; only a cleared
|
||
/// point is placed from the style.
|
||
fn stored_text_point(dim: &Dimension) -> Option<Vector3> {
|
||
let p = dim.base().text_middle_point;
|
||
(p.x * p.x + p.y * p.y + p.z * p.z > 1e-16).then_some(p)
|
||
}
|
||
|
||
/// Clear an automatic text position so the dimension is laid out from its
|
||
/// current settings again. A loaded point is kept so a drawing looks as it
|
||
/// was drawn; a new dimension, or one whose style or overrides just changed,
|
||
/// has no valid one. Text the user placed keeps its point (#1412).
|
||
pub(crate) fn reset_automatic_text_position(base: &mut DimensionBase) {
|
||
if !base.text_user_positioned {
|
||
base.text_middle_point = Vector3::new(0.0, 0.0, 0.0);
|
||
}
|
||
}
|
||
|
||
/// 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 {
|
||
match dim {
|
||
Dimension::Radius(d) => d.definition_point,
|
||
Dimension::Diameter(d) => {
|
||
if d.angle_vertex.distance(&text) <= d.definition_point.distance(&text) {
|
||
d.angle_vertex
|
||
} else {
|
||
d.definition_point
|
||
}
|
||
}
|
||
_ => dim.base().text_middle_point,
|
||
}
|
||
}
|
||
|
||
/// 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>,
|
||
text_height: f64,
|
||
dim_scale: f64,
|
||
) -> Vector3 {
|
||
let base = dim.base();
|
||
|
||
// DIMTAD: 0=centred (on the line), 1=above, 4=below. 2 (outside, i.e. the
|
||
// side farthest from the defining points) and 3 (JIS) both resolve to the
|
||
// away-from-object side, which is the same as "above" for 2-D linear dims.
|
||
let dimtad = style.map(|s| s.dimtad).unwrap_or(1);
|
||
// DIMGAP scales with DIMSCALE just like DIMTXT, so the text-to-line gap
|
||
// stays consistent when DIMSCALE != 1.
|
||
let dimgap = style.map(|s| s.dimgap.abs()).unwrap_or(0.0) * dim_scale;
|
||
let dimjust = style.map(|s| s.dimjust).unwrap_or(0);
|
||
// DIMTIX forces the text to stay between the extension lines.
|
||
let dimtix = style.map(|s| s.dimtix).unwrap_or(false);
|
||
let dimatfit = style.map(|s| s.dimatfit).unwrap_or(3);
|
||
// DIMTVP vertical-position multiplier (units of dimtxt). Only honoured when
|
||
// DIMTAD == 0; offsets text perpendicular to the dim line.
|
||
let dimtvp = style.map(|s| s.dimtvp).unwrap_or(0.0);
|
||
let perp_off = if dimtad == 0 {
|
||
dimtvp * text_height
|
||
} else {
|
||
dimension_text_extent_height(dim, style, text_height) * 0.5 + dimgap
|
||
};
|
||
// Rough text width + arrow allowance, used to decide text-outside fit.
|
||
let text_w = dimension_text_cells(dim, style)
|
||
.map(|cells| cells * text_height * CELL_WIDTH + 2.0 * dimgap)
|
||
.unwrap_or(0.0);
|
||
let arrow = style
|
||
.map(|style| style.dimasz * dim_scale)
|
||
.filter(|arrow| *arrow > 1e-9)
|
||
.unwrap_or(text_height);
|
||
// DIMTOH: text that lands outside reads horizontally.
|
||
let horizontal_outside = style.is_some_and(|style| style.dimtoh);
|
||
|
||
if let Some(point) = stored_text_point(dim) {
|
||
return match dim {
|
||
// 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
|
||
// heights before the text, which is DIMASZ plus DIMGAP.
|
||
Dimension::Radius(_) | Dimension::Diameter(_)
|
||
if style.is_some_and(|s| s.dimtmove == 1) =>
|
||
{
|
||
let tip = radial_leader_tip(dim, point);
|
||
let side = if point.x >= tip.x { 1.0 } else { -1.0 };
|
||
Vector3::new(point.x + side * (arrow + text_w * 0.5), point.y, point.z)
|
||
}
|
||
// A diameter whose text the user dragged keeps the grip point on
|
||
// the leader and lifts the text by the DIMTAD offset (#1323).
|
||
Dimension::Diameter(d) if base.text_user_positioned && dimtad != 0 => {
|
||
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);
|
||
// Match the readable orientation used by text_on_dim_line().
|
||
let (mut nx, mut ny) = (dx / len, dy / len);
|
||
if nx < 0.0 || (nx == 0.0 && ny < 0.0) {
|
||
nx = -nx;
|
||
ny = -ny;
|
||
}
|
||
let (px, py) = (-ny, nx);
|
||
let perp_sign = if dimtad == 4 { -1.0 } else { 1.0 };
|
||
Vector3::new(
|
||
point.x + px * perp_off * perp_sign,
|
||
point.y + py * perp_off * perp_sign,
|
||
point.z,
|
||
)
|
||
}
|
||
_ => point,
|
||
};
|
||
}
|
||
|
||
match dim {
|
||
Dimension::Linear(d) => {
|
||
let ax = d.rotation.cos();
|
||
let ay = d.rotation.sin();
|
||
text_on_dim_line(
|
||
d.first_point,
|
||
d.second_point,
|
||
d.definition_point,
|
||
ax,
|
||
ay,
|
||
dimjust,
|
||
perp_off,
|
||
text_w,
|
||
arrow,
|
||
dimtix,
|
||
dimatfit,
|
||
dimtad,
|
||
horizontal_outside,
|
||
)
|
||
}
|
||
Dimension::Aligned(d) => {
|
||
let dx = d.second_point.x - d.first_point.x;
|
||
let dy = d.second_point.y - d.first_point.y;
|
||
let len = (dx * dx + dy * dy).sqrt().max(1e-12);
|
||
text_on_dim_line(
|
||
d.first_point,
|
||
d.second_point,
|
||
d.definition_point,
|
||
dx / len,
|
||
dy / len,
|
||
dimjust,
|
||
perp_off,
|
||
text_w,
|
||
arrow,
|
||
dimtix,
|
||
dimatfit,
|
||
dimtad,
|
||
horizontal_outside,
|
||
)
|
||
}
|
||
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_) => {
|
||
let Some((vertex, start, end, radius)) = angular_dimension_frame(dim) else {
|
||
return base.text_middle_point;
|
||
};
|
||
let span = radius as f64 * (end - start).abs() as f64;
|
||
let insufficient = text_w + arrow * 2.0 > span;
|
||
let move_outside = !dimtix
|
||
&& insufficient
|
||
&& match dimatfit {
|
||
0 | 2 => true,
|
||
1 | 3 => text_w > span,
|
||
_ => text_w > span,
|
||
};
|
||
let angle = if move_outside {
|
||
end as f64 + (text_w * 0.5 + dimgap + arrow) / (radius as f64).max(1.0e-12)
|
||
} else {
|
||
((start + end) * 0.5) as f64
|
||
};
|
||
let radial_offset = if dimtad == 4 { -perp_off } else { perp_off };
|
||
let text_radius = (radius as f64 + radial_offset).max(0.0);
|
||
Vector3::new(
|
||
vertex.x as f64 + angle.cos() * text_radius,
|
||
vertex.y as f64 + angle.sin() * text_radius,
|
||
vertex.z as f64,
|
||
)
|
||
}
|
||
Dimension::Radius(d) => {
|
||
let dx = d.definition_point.x - d.angle_vertex.x;
|
||
let dy = d.definition_point.y - d.angle_vertex.y;
|
||
let radius = dx.hypot(dy).max(1e-12);
|
||
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;
|
||
(
|
||
d.definition_point.x + ux * distance,
|
||
d.definition_point.y + uy * distance,
|
||
)
|
||
} else {
|
||
(
|
||
d.angle_vertex.x + ux * radius * 0.5,
|
||
d.angle_vertex.y + uy * radius * 0.5,
|
||
)
|
||
};
|
||
if dimtad != 0 {
|
||
let sign = if dimtad == 4 { -1.0 } else { 1.0 };
|
||
x += -uy * perp_off * sign;
|
||
y += ux * perp_off * sign;
|
||
}
|
||
Vector3::new(x, y, d.definition_point.z)
|
||
}
|
||
Dimension::LargeRadial(d) => {
|
||
let dx = d.chord_point.x - d.override_center.x;
|
||
let dy = d.chord_point.y - d.override_center.y;
|
||
let length = dx.hypot(dy).max(1.0e-12);
|
||
text_on_single_arrow_dim_line(
|
||
d.override_center,
|
||
d.chord_point,
|
||
d.definition_point,
|
||
dx / length,
|
||
dy / length,
|
||
perp_off,
|
||
text_w,
|
||
arrow,
|
||
dimtix,
|
||
dimatfit,
|
||
dimtad,
|
||
)
|
||
}
|
||
Dimension::Diameter(d) => {
|
||
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,
|
||
d.angle_vertex,
|
||
dx / len,
|
||
dy / len,
|
||
dimjust,
|
||
perp_off,
|
||
text_w,
|
||
arrow,
|
||
dimtix,
|
||
dimatfit,
|
||
dimtad,
|
||
horizontal_outside,
|
||
)
|
||
}
|
||
Dimension::Ordinate(d) => {
|
||
let (x_axis, y_axis) = d.local_axes();
|
||
let text_axis = if d.is_ordinate_type_x { y_axis } else { x_axis };
|
||
let perpendicular = if d.is_ordinate_type_x { x_axis } else { y_axis };
|
||
let delta = d.leader_endpoint - d.feature_location;
|
||
let direction_sign = if delta.dot(&text_axis) < 0.0 { -1.0 } else { 1.0 };
|
||
let perpendicular_sign = if delta.dot(&perpendicular) < 0.0 {
|
||
-1.0
|
||
} else {
|
||
1.0
|
||
};
|
||
let vertical = match dimtad {
|
||
0 => dimtvp * text_height,
|
||
4 => -perp_off,
|
||
_ => perp_off,
|
||
};
|
||
d.leader_endpoint
|
||
+ text_axis * (direction_sign * (text_w * 0.5 + dimgap))
|
||
+ perpendicular * (perpendicular_sign * vertical)
|
||
}
|
||
_ => {
|
||
// Auto-placed text follows the style offset.
|
||
let mid = match dim {
|
||
Dimension::Angular2Ln(d) => d.dimension_arc,
|
||
Dimension::Angular3Pt(d) => d.definition_point,
|
||
Dimension::Arc(d) => d.definition_point,
|
||
_ => base.text_middle_point,
|
||
};
|
||
Vector3::new(mid.x, mid.y + perp_off * perp_sign_default(), mid.z)
|
||
}
|
||
}
|
||
}
|
||
|
||
fn perp_sign_default() -> f64 {
|
||
1.0
|
||
}
|
||
|
||
pub(crate) fn baked_large_radial_geometry(
|
||
dimension: &Dimension,
|
||
document: &CadDocument,
|
||
) -> Option<DimGeom> {
|
||
if !matches!(dimension, Dimension::LargeRadial(_)) {
|
||
return None;
|
||
}
|
||
let style_name = dimension.base().style_name.as_str();
|
||
let effective_style = document
|
||
.dim_styles
|
||
.iter()
|
||
.find(|style| {
|
||
style.name.eq_ignore_ascii_case(style_name)
|
||
|| (style_name.trim().is_empty()
|
||
&& style.name.eq_ignore_ascii_case("Standard"))
|
||
})
|
||
.map(|style| resolved_dimension_style(style, dimension, document));
|
||
let style = effective_style.as_ref();
|
||
let scale = style
|
||
.map(|style| dimension_style_scale(style, 1.0))
|
||
.unwrap_or(1.0);
|
||
let text_height = style.map(|style| style.dimtxt * scale).unwrap_or(2.5 * scale);
|
||
let arrow_size_raw = style.map(|style| style.dimasz * scale).unwrap_or(0.18 * scale);
|
||
let tick_size = style.map(|style| style.dimtsz * scale).unwrap_or(0.0);
|
||
let arrow_size = (arrow_size_raw as f32).max(0.001);
|
||
let arrow = if tick_size > 1.0e-9 {
|
||
ArrowKind::Tick {
|
||
size: (tick_size as f32).max(0.001),
|
||
}
|
||
} else if let Some(style) = style {
|
||
arrow_from_block(document, style.dimldrblk, arrow_size)
|
||
} else {
|
||
ArrowKind::Triangle {
|
||
size: arrow_size,
|
||
filled: true,
|
||
size_mul: 1.0,
|
||
}
|
||
};
|
||
let text = dimension_text_layout(dimension, style, text_height, scale);
|
||
let mut geometry = dimension_geometry(
|
||
dimension,
|
||
&arrow,
|
||
&arrow,
|
||
DimLineParams {
|
||
dimexo: style.map(|style| (style.dimexo * scale) as f32).unwrap_or(0.0),
|
||
dimexe: style.map(|style| (style.dimexe * scale) as f32).unwrap_or(0.0),
|
||
dimdle: style.map(|style| (style.dimdle * scale) as f32).unwrap_or(0.0),
|
||
dimfxl: style.map(|style| (style.dimfxl * scale) as f32).unwrap_or(1.0),
|
||
dimfxlon: style.is_some_and(|style| style.dimfxlon),
|
||
dimsoxd: style.is_some_and(|style| style.dimsoxd),
|
||
dimcen: style.map(|style| (style.dimcen * scale) as f32).unwrap_or(0.09),
|
||
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),
|
||
text_position: text.position,
|
||
horizontal_text: text.horizontal,
|
||
text_movement: style.map(|style| style.dimtmove).unwrap_or(0),
|
||
text_break: text.break_box,
|
||
leader_anchor: None,
|
||
},
|
||
SuppressFlags {
|
||
ext1: style.is_some_and(|style| style.dimse1),
|
||
ext2: style.is_some_and(|style| style.dimse2),
|
||
dim1: style.is_some_and(|style| style.dimsd1),
|
||
dim2: style.is_some_and(|style| style.dimsd2),
|
||
},
|
||
);
|
||
if !style.is_some_and(|style| style.dimse1) {
|
||
if let Some(points) = crate::scene::dimension_assoc::radial_extension_points(
|
||
document,
|
||
dimension.base().common.handle,
|
||
style.map(|style| style.dimexo * scale).unwrap_or(0.0),
|
||
style.map(|style| style.dimexe * scale).unwrap_or(0.0),
|
||
) {
|
||
let points: Vec<_> = points.into_iter().map(vec3_local).collect();
|
||
add_polyline(&mut geometry.ext_lines, &points);
|
||
}
|
||
}
|
||
if style.is_some_and(|style| style.dimtmove == 1) {
|
||
if let Some((anchor, endpoint, under_text)) =
|
||
dimtmove_leader_endpoints(dimension, text)
|
||
{
|
||
if under_text || anchor.distance(endpoint) > text_height as f32 {
|
||
add_segment_with_text_break(
|
||
&mut geometry.dim_lines,
|
||
anchor,
|
||
endpoint,
|
||
text.break_box,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
apply_dimension_breaks(
|
||
document,
|
||
dimension.base().common.handle,
|
||
&mut geometry.dim_lines,
|
||
);
|
||
apply_dimension_breaks(
|
||
document,
|
||
dimension.base().common.handle,
|
||
&mut geometry.ext_lines,
|
||
);
|
||
Some(geometry)
|
||
}
|
||
|
||
/// Build measurement text for a saved dimension block.
|
||
pub(crate) fn baked_dimension_text_entity(
|
||
dim: &Dimension,
|
||
document: &CadDocument,
|
||
anno_scale: f64,
|
||
) -> Option<EntityType> {
|
||
let style_name = dim.base().style_name.as_str();
|
||
let effective_style = document
|
||
.dim_styles
|
||
.iter()
|
||
.find(|style| {
|
||
style.name.eq_ignore_ascii_case(style_name)
|
||
|| (style_name.trim().is_empty()
|
||
&& style.name.eq_ignore_ascii_case("Standard"))
|
||
})
|
||
.map(|style| resolved_dimension_style(style, dim, document));
|
||
let style = effective_style.as_ref();
|
||
let dim_scale = style
|
||
.map(|style| dimension_style_scale(style, anno_scale))
|
||
.unwrap_or(1.0);
|
||
let dim_txt = style
|
||
.map(|s| s.dimtxt * dim_scale)
|
||
.unwrap_or(2.5 * dim_scale);
|
||
let mut ent = dimension_text_entity(dim, dim_txt, style, document, dim_scale)?;
|
||
if let EntityType::Text(t) = &mut ent {
|
||
t.alignment_point = Some(t.insertion_point);
|
||
}
|
||
Some(ent)
|
||
}
|
||
|
||
pub(crate) fn baked_arc_length_symbol_points(
|
||
dim: &Dimension,
|
||
document: &CadDocument,
|
||
anno_scale: f64,
|
||
) -> Vec<Vector3> {
|
||
if !matches!(dim, Dimension::Arc(_)) {
|
||
return Vec::new();
|
||
}
|
||
let style_name = dim.base().style_name.as_str();
|
||
let style = document.dim_styles.iter().find(|style| {
|
||
style.name.eq_ignore_ascii_case(style_name)
|
||
|| (style_name.trim().is_empty() && style.name.eq_ignore_ascii_case("Standard"))
|
||
});
|
||
let dim_scale = style
|
||
.map(|style| {
|
||
if style.dimscale > 1.0e-6 {
|
||
style.dimscale
|
||
} else {
|
||
anno_scale
|
||
}
|
||
})
|
||
.unwrap_or(1.0);
|
||
let text_height = style
|
||
.map(|style| style.dimtxt * dim_scale)
|
||
.unwrap_or(2.5 * dim_scale);
|
||
let symbol_position = crate::entities::dim_override::int(
|
||
&dim.base().common.extended_data,
|
||
crate::entities::dim_override::DIMARCSYM,
|
||
)
|
||
.or_else(|| style.map(|style| style.dimarcsym))
|
||
.unwrap_or(0);
|
||
|
||
arc_length_symbol_points(dim, style, text_height, dim_scale, symbol_position)
|
||
.unwrap_or_default()
|
||
.into_iter()
|
||
.map(|point| Vector3::new(point.x as f64, point.y as f64, point.z as f64))
|
||
.collect()
|
||
}
|
||
|
||
pub(crate) fn dimension_text_grip_position(
|
||
dim: &Dimension,
|
||
document: &CadDocument,
|
||
anno_scale: f64,
|
||
) -> Option<Vector3> {
|
||
// A stored text point is where the text is drawn; the grip goes there.
|
||
if let Some(p) = stored_text_point(dim) {
|
||
return Some(p);
|
||
}
|
||
|
||
// For automatic text, use the same text-building path used by the
|
||
// dimension picture so the grip follows the actual DIMSTYLE placement,
|
||
// including annotation scaling and fit behaviour.
|
||
match baked_dimension_text_entity(dim, document, anno_scale)? {
|
||
EntityType::Text(text) => Some(text.insertion_point),
|
||
EntityType::MText(text) => Some(text.insertion_point),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod dimtad_tests {
|
||
use super::text_on_dim_line;
|
||
use codec::types::Vector3;
|
||
|
||
fn v(x: f64, y: f64) -> Vector3 {
|
||
Vector3::new(x, y, 0.0)
|
||
}
|
||
|
||
// DIMTAD=Above must place text on the geometric "up" side of a horizontal
|
||
// dimension whichever way the dimension runs; DIMTAD=Below flips it. (#144)
|
||
#[test]
|
||
fn above_is_up_regardless_of_direction() {
|
||
let (first, second, defpt) = (v(0.0, 10.0), v(20.0, 10.0), v(0.0, 0.0));
|
||
let perp = 5.0;
|
||
let fwd = text_on_dim_line(first, second, defpt, 1.0, 0.0, 0, perp, 0.0, 1.0, false, 3, 1, false);
|
||
let rev = text_on_dim_line(first, second, defpt, -1.0, 0.0, 0, perp, 0.0, 1.0, false, 3, 1, false);
|
||
assert!(fwd.y > 0.0, "above must be +Y, got {}", fwd.y);
|
||
assert!(rev.y > 0.0, "above must be +Y even reversed, got {}", rev.y);
|
||
let below = text_on_dim_line(first, second, defpt, 1.0, 0.0, 0, perp, 0.0, 1.0, false, 3, 4, false);
|
||
assert!(below.y < 0.0, "below must be -Y, got {}", below.y);
|
||
}
|
||
|
||
// DIMTAD=Outside (2) stays on the side farthest from the measured points,
|
||
// whichever side of the geometry the dimension line is on.
|
||
#[test]
|
||
fn outside_is_away_from_object() {
|
||
let perp = 5.0;
|
||
// Object at y=0, dim line above it (y=10): away → further above.
|
||
let above = text_on_dim_line(
|
||
v(0.0, 0.0),
|
||
v(20.0, 0.0),
|
||
v(0.0, 10.0),
|
||
1.0,
|
||
0.0,
|
||
0,
|
||
perp,
|
||
0.0,
|
||
1.0,
|
||
false,
|
||
3,
|
||
2,
|
||
false,
|
||
);
|
||
assert!(
|
||
above.y > 10.0,
|
||
"outside must clear the object side, got {}",
|
||
above.y
|
||
);
|
||
// Object at y=0, dim line below it (y=-10): away → further below.
|
||
let below = text_on_dim_line(
|
||
v(0.0, 0.0),
|
||
v(20.0, 0.0),
|
||
v(0.0, -10.0),
|
||
1.0,
|
||
0.0,
|
||
0,
|
||
perp,
|
||
0.0,
|
||
1.0,
|
||
false,
|
||
3,
|
||
2,
|
||
false,
|
||
);
|
||
assert!(
|
||
below.y < -10.0,
|
||
"outside must clear the object side, got {}",
|
||
below.y
|
||
);
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod angular_unit_tests {
|
||
use super::format_angular_value;
|
||
use codec::tables::DimStyle;
|
||
|
||
fn style(dimaunit: i16, dimadec: i16) -> DimStyle {
|
||
let mut s = DimStyle::standard();
|
||
s.dimaunit = dimaunit;
|
||
s.dimadec = dimadec;
|
||
s
|
||
}
|
||
|
||
// DIMAUNIT 4 is Surveyor's units. It used to fall through to the decimal
|
||
// branch, so an angular dimension in a drawing authored with surveyor
|
||
// units read as plain degrees while AUNITS=4 elsewhere wrote a bearing.
|
||
#[test]
|
||
fn surveyor_units_write_a_bearing() {
|
||
assert_eq!(format_angular_value(45.0, Some(&style(4, 0))), "N 45\u{b0}0'0\" E");
|
||
assert_eq!(format_angular_value(135.0, Some(&style(4, 0))), "N 45\u{b0}0'0\" W");
|
||
assert_eq!(format_angular_value(200.0, Some(&style(4, 0))), "S 70\u{b0}0'0\" W");
|
||
assert_eq!(format_angular_value(300.0, Some(&style(4, 0))), "S 30\u{b0}0'0\" E");
|
||
// A hair under a cardinal must not quote a quarter-turn bearing.
|
||
assert_eq!(format_angular_value(89.99999, Some(&style(4, 0))), "N");
|
||
assert_eq!(format_angular_value(90.0, Some(&style(4, 0))), "N");
|
||
assert_eq!(format_angular_value(0.0, Some(&style(4, 0))), "E");
|
||
}
|
||
|
||
// The other unit formats are untouched.
|
||
#[test]
|
||
fn other_angular_units_unchanged() {
|
||
assert_eq!(format_angular_value(45.5, Some(&style(1, 0))), "45\u{b0}30'0\"");
|
||
assert_eq!(format_angular_value(90.0, Some(&style(2, 1))), "100.0g");
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod dim_color_override_tests {
|
||
use super::dim_color_index;
|
||
use crate::entities::dim_override::{DIMCLRD, DIMCLRE, DIMCLRT};
|
||
use codec::xdata::{ExtendedData, ExtendedDataRecord, XDataValue};
|
||
|
||
// An ACAD/DSTYLE record holding the overrides the Properties panel writes.
|
||
fn xdata(overrides: &[(i16, i16)]) -> ExtendedData {
|
||
let mut values = vec![
|
||
XDataValue::String("DSTYLE".to_string()),
|
||
XDataValue::ControlString("{".to_string()),
|
||
];
|
||
for (code, aci) in overrides {
|
||
values.push(XDataValue::Integer16(*code));
|
||
values.push(XDataValue::Integer16(*aci));
|
||
}
|
||
values.push(XDataValue::ControlString("}".to_string()));
|
||
let mut record = ExtendedDataRecord::new("ACAD");
|
||
for value in values {
|
||
record.add_value(value);
|
||
}
|
||
let mut xd = ExtendedData::new();
|
||
xd.add_record(record);
|
||
xd
|
||
}
|
||
|
||
// Every dim colour honours its per-object override, not just DIMCLRD.
|
||
// Text and extension-line colours edited on the Properties panel used to
|
||
// show there and render with the style/layer colour anyway. (#898)
|
||
#[test]
|
||
fn override_wins_over_style_for_every_dim_colour() {
|
||
let xd = xdata(&[(DIMCLRD, 1), (DIMCLRE, 3), (DIMCLRT, 5)]);
|
||
assert_eq!(dim_color_index(&xd, DIMCLRD, 256), 1);
|
||
assert_eq!(dim_color_index(&xd, DIMCLRE, 256), 3);
|
||
assert_eq!(dim_color_index(&xd, DIMCLRT, 256), 5);
|
||
}
|
||
|
||
// Only the overridden variable changes; the rest still inherit the style.
|
||
#[test]
|
||
fn absent_override_falls_back_to_the_style_value() {
|
||
let xd = xdata(&[(DIMCLRT, 5)]);
|
||
assert_eq!(dim_color_index(&xd, DIMCLRT, 256), 5);
|
||
assert_eq!(dim_color_index(&xd, DIMCLRD, 7), 7);
|
||
assert_eq!(dim_color_index(&xd, DIMCLRE, 256), 256);
|
||
}
|
||
|
||
// An entity with no override record at all is unaffected.
|
||
#[test]
|
||
fn no_record_uses_the_style_value() {
|
||
let xd = ExtendedData::new();
|
||
assert_eq!(dim_color_index(&xd, DIMCLRT, 2), 2);
|
||
}
|
||
|
||
// ByLayer (256) and ByBlock (0) stay meaningful as override values, so an
|
||
// explicit "back to ByLayer" edit is not mistaken for "no override".
|
||
#[test]
|
||
fn bylayer_override_is_kept() {
|
||
let xd = xdata(&[(DIMCLRT, 256)]);
|
||
assert_eq!(dim_color_index(&xd, DIMCLRT, 5), 256);
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod arch_format_tests {
|
||
use super::{format_architectural, format_engineering, format_fractional};
|
||
|
||
// A 3ft object that measures 35.99" (float noise) must carry the rounded
|
||
// fraction up through inches into feet — not print "2'-11 1"". (Regression
|
||
// for the fraction that reduced to 1/1 and leaked out as a bare "1".)
|
||
#[test]
|
||
fn arch_carries_fraction_up_to_feet() {
|
||
assert_eq!(format_architectural(35.99, 4, 2, 1), "3'-0\"");
|
||
assert_eq!(format_architectural(36.0, 4, 2, 1), "3'-0\"");
|
||
// An exact 15/16 must still render as a fraction, no spurious carry.
|
||
assert_eq!(format_architectural(35.9375, 4, 2, 1), "2'-11 15/16\"");
|
||
}
|
||
|
||
// Carry that stops at inches (11.999" → 12" → 1'-0", not "0'-11 1"").
|
||
#[test]
|
||
fn arch_carries_fraction_up_to_inches() {
|
||
assert_eq!(format_architectural(11.999, 4, 2, 1), "1'-0\"");
|
||
}
|
||
|
||
#[test]
|
||
fn arch_normal_values_unchanged() {
|
||
assert_eq!(format_architectural(30.5, 4, 2, 1), "2'-6 1/2\"");
|
||
assert_eq!(format_architectural(0.0, 4, 2, 1), "0'-0\"");
|
||
assert_eq!(format_architectural(-30.25, 4, 2, 1), "-2'-6 1/4\"");
|
||
}
|
||
|
||
// Same carry bug lived in the plain fractional formatter.
|
||
#[test]
|
||
fn fractional_carries_up() {
|
||
assert_eq!(format_fractional(35.9999, 4, 2), "36");
|
||
assert_eq!(format_fractional(11.999, 4, 2), "12");
|
||
assert_eq!(format_fractional(6.5, 4, 2), "6 1/2");
|
||
}
|
||
|
||
// Engineering units round to DIMDEC before the feet/inches split, so a
|
||
// remainder that rounds up to 12" carries instead of printing "2'-12.00"".
|
||
#[test]
|
||
fn engineering_carries_inches_up_to_feet() {
|
||
assert_eq!(format_engineering(35.999, 2), "3'-0.00\"");
|
||
assert_eq!(format_engineering(11.999, 2), "1'-0.00\"");
|
||
assert_eq!(format_engineering(11.9, 0), "1'-0\"");
|
||
}
|
||
|
||
#[test]
|
||
fn engineering_normal_values_unchanged() {
|
||
assert_eq!(format_engineering(30.5, 2), "2'-6.50\"");
|
||
assert_eq!(format_engineering(36.0, 2), "3'-0.00\"");
|
||
assert_eq!(format_engineering(0.0, 2), "0'-0.00\"");
|
||
assert_eq!(format_engineering(-30.25, 2), "-2'-6.25\"");
|
||
}
|
||
|
||
// A magnitude that rounds away to zero must not keep a lone minus sign.
|
||
#[test]
|
||
fn engineering_negative_zero_has_no_sign() {
|
||
assert_eq!(format_engineering(-0.001, 2), "0'-0.00\"");
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod dimtmove_leader_tests {
|
||
use super::*;
|
||
use codec::entities::DimensionLinear;
|
||
use codec::types::Vector3;
|
||
|
||
fn v(x: f64, y: f64) -> Vector3 {
|
||
Vector3::new(x, y, 0.0)
|
||
}
|
||
|
||
fn layout(position: Vec3, half_width: f32) -> DimensionTextLayout {
|
||
DimensionTextLayout {
|
||
position,
|
||
width: half_width * 2.0,
|
||
break_box: Some(TextBreak {
|
||
center: position,
|
||
half_width,
|
||
half_height: 1.0,
|
||
padding: 0.0,
|
||
rotation: 0.0,
|
||
}),
|
||
horizontal: true,
|
||
}
|
||
}
|
||
|
||
/// Text outside along the axis → the dim line extends underneath the text.
|
||
#[test]
|
||
fn text_outside_extends_dim_line_under_text() {
|
||
let mut d = DimensionLinear::horizontal(v(0.0, 0.0), v(20.0, 0.0));
|
||
d.definition_point = v(0.0, -5.0); // dimension line at y=-5
|
||
let dim = Dimension::Linear(d);
|
||
// text anchor outside on the right: centre x=30, half width 3 → far edge 33
|
||
let txt = vec3_local(v(30.0, -3.0));
|
||
let (anchor, end, under) = dimtmove_leader_endpoints(&dim, layout(txt, 3.0)).expect("leader");
|
||
assert!(under, "outside text extends the dimension line");
|
||
assert!((anchor.x - 20.0).abs() < 1e-4 && (anchor.y + 5.0).abs() < 1e-4, "near end should be the dim line's right end (20,-5), got {anchor:?}");
|
||
assert!((end.x - 33.0).abs() < 1e-4 && (end.y + 5.0).abs() < 1e-4, "far end should be the text's far edge (33,-5), got {end:?}");
|
||
}
|
||
|
||
/// Text outside on the left → extends leftwards.
|
||
#[test]
|
||
fn text_outside_left_extends_leftwards() {
|
||
let mut d = DimensionLinear::horizontal(v(0.0, 0.0), v(20.0, 0.0));
|
||
d.definition_point = v(0.0, -5.0);
|
||
let dim = Dimension::Linear(d);
|
||
let txt = vec3_local(v(-12.0, -3.0));
|
||
let (anchor, end, under) = dimtmove_leader_endpoints(&dim, layout(txt, 2.0)).expect("leader");
|
||
assert!(under);
|
||
assert!((anchor.x - 0.0).abs() < 1e-4, "near end should be the dim line's left end, got {anchor:?}");
|
||
assert!((end.x + 14.0).abs() < 1e-4, "far end should be the text's far edge at -14, got {end:?}");
|
||
}
|
||
|
||
/// Text still inside (only moved up/down) → leader from the dim line to the text.
|
||
#[test]
|
||
fn text_inside_connects_to_text() {
|
||
let mut d = DimensionLinear::horizontal(v(0.0, 0.0), v(20.0, 0.0));
|
||
d.definition_point = v(0.0, -5.0);
|
||
let dim = Dimension::Linear(d);
|
||
let txt = vec3_local(v(10.0, 2.0));
|
||
let (anchor, end, under) = dimtmove_leader_endpoints(&dim, layout(txt, 3.0)).expect("leader");
|
||
assert!(!under, "inside → keep the connect-to-text leader (text break allowed)");
|
||
assert!((anchor.x - 10.0).abs() < 1e-4 && (anchor.y + 5.0).abs() < 1e-4, "anchor should be the dim line point facing the text, got {anchor:?}");
|
||
assert!((end.y - 2.0).abs() < 1e-4, "end should be the text position, got {end:?}");
|
||
}
|
||
|
||
/// Radius dimensions draw no leader (unchanged).
|
||
#[test]
|
||
fn radius_has_no_leader() {
|
||
let r = codec::entities::DimensionRadius::new(v(0.0, 0.0), v(0.0, 5.0));
|
||
assert!(dimtmove_leader_endpoints(&Dimension::Radius(r), layout(vec3_local(v(0.0, 8.0)), 2.0)).is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn extension_does_not_draw_through_text() {
|
||
let mut d = DimensionLinear::horizontal(v(0.0, 0.0), v(20.0, 0.0));
|
||
d.definition_point = v(0.0, -5.0);
|
||
let text = layout(Vec3::new(30.0, -5.0, 0.0), 3.0);
|
||
let (start, end, outside) = dimtmove_leader_endpoints(&Dimension::Linear(d), text).unwrap();
|
||
assert!(outside);
|
||
let mut points = Vec::new();
|
||
add_segment_with_text_break(&mut points, start, end, text.break_box);
|
||
let finite: Vec<_> = points.into_iter().filter(|point| point[0].is_finite()).collect();
|
||
assert_eq!(finite, vec![[20.0, -5.0, 0.0], [27.0, -5.0, 0.0]]);
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod linear_transform_tests {
|
||
use super::*;
|
||
use codec::entities::DimensionLinear;
|
||
use std::f64::consts::FRAC_PI_2;
|
||
|
||
/// A horizontal linear dimension measuring 10 units along X.
|
||
fn horizontal() -> Dimension {
|
||
let mut d =
|
||
DimensionLinear::horizontal(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
|
||
d.definition_point = Vector3::new(0.0, 5.0, 0.0);
|
||
Dimension::Linear(d)
|
||
}
|
||
|
||
fn rotation(dim: &Dimension) -> f64 {
|
||
match dim {
|
||
Dimension::Linear(d) => d.rotation,
|
||
_ => unreachable!("linear dimension expected"),
|
||
}
|
||
}
|
||
|
||
/// The stored and the live value both still read 10.
|
||
fn assert_measures_ten(dim: &Dimension) {
|
||
let stored = dim.base().actual_measurement;
|
||
assert!(
|
||
(stored - 10.0).abs() < 1e-9,
|
||
"stored measurement must stay 10, got {stored}"
|
||
);
|
||
let live = dim.measurement();
|
||
assert!(
|
||
(live - 10.0).abs() < 1e-9,
|
||
"measurement must stay 10, got {live}"
|
||
);
|
||
}
|
||
|
||
/// ROTATE in the drawing plane turns the measured axis with the points.
|
||
/// Left at 0, a horizontal dimension turned 90° measured its now-vertical
|
||
/// points along X and dropped to 0.
|
||
#[test]
|
||
fn rotate_turns_the_measured_axis() {
|
||
let mut dim = horizontal();
|
||
apply_transform(
|
||
&mut dim,
|
||
&EntityTransform::Rotate {
|
||
center: DVec3::ZERO,
|
||
axis: DVec3::Z,
|
||
angle_rad: FRAC_PI_2,
|
||
},
|
||
);
|
||
assert_measures_ten(&dim);
|
||
let turned = rotation(&dim);
|
||
assert!(
|
||
(turned - FRAC_PI_2).abs() < 1e-9,
|
||
"rotation must follow the rotate, got {turned}"
|
||
);
|
||
}
|
||
|
||
/// MIRROR in the drawing plane reflects the measured axis. Mirrored about
|
||
/// y = x, the horizontal dimension becomes a vertical one of the same value.
|
||
#[test]
|
||
fn mirror_reflects_the_measured_axis() {
|
||
let mut dim = horizontal();
|
||
apply_transform(
|
||
&mut dim,
|
||
&EntityTransform::Mirror {
|
||
p1: DVec3::ZERO,
|
||
p2: DVec3::new(1.0, 1.0, 0.0),
|
||
working_normal: DVec3::Z,
|
||
},
|
||
);
|
||
assert_measures_ten(&dim);
|
||
let reflected = rotation(&dim);
|
||
assert!(
|
||
(reflected - FRAC_PI_2).abs() < 1e-9,
|
||
"rotation must be reflected, got {reflected}"
|
||
);
|
||
}
|
||
|
||
/// A zero-length mirror line leaves the points alone, so the axis too.
|
||
#[test]
|
||
fn degenerate_mirror_keeps_the_measured_axis() {
|
||
let mut dim = horizontal();
|
||
apply_transform(
|
||
&mut dim,
|
||
&EntityTransform::Mirror {
|
||
p1: DVec3::new(3.0, 3.0, 0.0),
|
||
p2: DVec3::new(3.0, 3.0, 0.0),
|
||
working_normal: DVec3::Z,
|
||
},
|
||
);
|
||
assert_eq!(rotation(&dim), 0.0);
|
||
assert_measures_ten(&dim);
|
||
}
|
||
|
||
/// The point the dimension line passes through.
|
||
fn definition_point(dim: &Dimension) -> DVec3 {
|
||
let p = dimension_definition_point(dim);
|
||
DVec3::new(p.x, p.y, p.z)
|
||
}
|
||
|
||
fn assert_near(actual: DVec3, expected: DVec3, what: &str) {
|
||
assert!(
|
||
actual.abs_diff_eq(expected, 1e-9),
|
||
"{what}: expected {expected:?}, got {actual:?}"
|
||
);
|
||
}
|
||
|
||
/// ROTATE about an axis off world Z (a working plane other than world XY)
|
||
/// turns the dimension like the geometry it measures. It used to only move
|
||
/// by where the origin went, leaving the dimension line and plane unturned.
|
||
#[test]
|
||
fn rotate_off_world_z_turns_the_dimension() {
|
||
let center = DVec3::new(0.0, 10.0, 0.0);
|
||
let turn = glam::DQuat::from_axis_angle(DVec3::X, FRAC_PI_2);
|
||
let mut dim = horizontal();
|
||
apply_transform(
|
||
&mut dim,
|
||
&EntityTransform::Rotate {
|
||
center,
|
||
axis: DVec3::X,
|
||
angle_rad: FRAC_PI_2,
|
||
},
|
||
);
|
||
assert_near(
|
||
definition_point(&dim),
|
||
turn * (DVec3::new(0.0, 5.0, 0.0) - center) + center,
|
||
"definition point",
|
||
);
|
||
let normal = dim.base().normal;
|
||
assert_near(
|
||
DVec3::new(normal.x, normal.y, normal.z),
|
||
turn * DVec3::Z,
|
||
"normal",
|
||
);
|
||
assert_measures_ten(&dim);
|
||
}
|
||
|
||
/// MIRROR in a working plane off world Z reflects the dimension through
|
||
/// the mirror plane instead of only moving it.
|
||
#[test]
|
||
fn mirror_off_world_z_reflects_the_dimension() {
|
||
// Working plane YZ; the line runs along Z at y = 2, so the mirror
|
||
// plane is y = 2.
|
||
let mut dim = horizontal();
|
||
apply_transform(
|
||
&mut dim,
|
||
&EntityTransform::Mirror {
|
||
p1: DVec3::new(0.0, 2.0, 0.0),
|
||
p2: DVec3::new(0.0, 2.0, 1.0),
|
||
working_normal: DVec3::X,
|
||
},
|
||
);
|
||
assert_near(
|
||
definition_point(&dim),
|
||
DVec3::new(0.0, -1.0, 0.0),
|
||
"definition point",
|
||
);
|
||
assert_measures_ten(&dim);
|
||
}
|
||
|
||
/// Every stroke point the dimension draws, with its low residual added.
|
||
fn stroke_points(dim: &Dimension) -> Vec<DVec3> {
|
||
let document = CadDocument::new();
|
||
let wires = dim.tessellate(
|
||
&document,
|
||
Handle::new(0x40),
|
||
false,
|
||
[1.0; 4],
|
||
1.0,
|
||
1.0,
|
||
&rustc_hash::FxHashSet::default(),
|
||
None,
|
||
[0.0, 0.0, 0.0, 1.0],
|
||
None,
|
||
None,
|
||
);
|
||
let mut points = Vec::new();
|
||
for wire in &wires {
|
||
for (index, high) in wire.points.iter().enumerate() {
|
||
if !high[0].is_finite() {
|
||
continue;
|
||
}
|
||
let low = wire.points_low.get(index).copied().unwrap_or([0.0; 3]);
|
||
points.push(DVec3::new(
|
||
high[0] as f64 + low[0] as f64,
|
||
high[1] as f64 + low[1] as f64,
|
||
high[2] as f64 + low[2] as f64,
|
||
));
|
||
}
|
||
}
|
||
points
|
||
}
|
||
|
||
/// Turned 90° about X into the XZ plane, the dimension has to draw the
|
||
/// same strokes turned with it.
|
||
fn assert_draws_turned_with_its_plane(flat: Dimension) {
|
||
let turn = glam::DQuat::from_axis_angle(DVec3::X, FRAC_PI_2);
|
||
let expected: Vec<DVec3> = stroke_points(&flat).into_iter().map(|p| turn * p).collect();
|
||
assert!(!expected.is_empty(), "the dimension draws no strokes");
|
||
let mut turned = flat;
|
||
apply_transform(
|
||
&mut turned,
|
||
&EntityTransform::Rotate {
|
||
center: DVec3::ZERO,
|
||
axis: DVec3::X,
|
||
angle_rad: FRAC_PI_2,
|
||
},
|
||
);
|
||
let drawn = stroke_points(&turned);
|
||
assert_eq!(drawn.len(), expected.len(), "stroke point count");
|
||
for (index, (got, want)) in drawn.iter().zip(&expected).enumerate() {
|
||
assert!(
|
||
got.abs_diff_eq(*want, 1e-3),
|
||
"point {index}: expected {want:?}, got {got:?}"
|
||
);
|
||
}
|
||
}
|
||
|
||
/// A linear dimension in a working plane off world Z draws in that plane.
|
||
/// Its offset used to be read in world XY only, so once it ran along Z the
|
||
/// dimension line fell onto the measured points and the extension lines
|
||
/// stuck out along world Y.
|
||
#[test]
|
||
fn a_linear_dimension_off_world_z_draws_in_its_own_plane() {
|
||
assert_draws_turned_with_its_plane(horizontal());
|
||
}
|
||
|
||
/// An aligned dimension draws through the same builder and plane.
|
||
#[test]
|
||
fn an_aligned_dimension_off_world_z_draws_in_its_own_plane() {
|
||
let mut d = DimensionAligned::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(6.0, 8.0, 0.0));
|
||
d.definition_point = Vector3::new(2.0, 11.0, 0.0);
|
||
assert_draws_turned_with_its_plane(Dimension::Aligned(d));
|
||
}
|
||
}
|
||
|
||
|
||
#[cfg(test)]
|
||
mod limits_format_tests {
|
||
use super::*;
|
||
use codec::entities::DimensionLinear;
|
||
|
||
/// A horizontal linear dimension measuring 10 units along X.
|
||
fn horizontal() -> Dimension {
|
||
let mut d =
|
||
DimensionLinear::horizontal(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
|
||
d.definition_point = Vector3::new(0.0, 5.0, 0.0);
|
||
Dimension::Linear(d)
|
||
}
|
||
|
||
fn style() -> DimStyle {
|
||
let mut s = DimStyle::standard();
|
||
s.dimdec = 3;
|
||
s.dimtdec = 3;
|
||
// Keep trailing zeros so the expectations read like the drawing.
|
||
s.dimzin = 0;
|
||
s.dimtzin = 0;
|
||
s.dimtp = 0.02;
|
||
s.dimtm = 0.05;
|
||
s
|
||
}
|
||
|
||
// the source application's DIMLIM shows the upper limit over the lower one and nothing
|
||
// else. The renderer used to keep the nominal and hang the stack beside
|
||
// it, so a drawing with limits-only styles grew an extra number on every
|
||
// regenerated dimension.
|
||
#[test]
|
||
fn limits_replace_the_nominal() {
|
||
let mut s = style();
|
||
s.dimlim = true;
|
||
assert_eq!(
|
||
dimension_text_parts(&horizontal(), Some(&s)),
|
||
Some((r"\S10.020^9.950;".to_string(), None))
|
||
);
|
||
}
|
||
|
||
// Stacked limits keep a common number of decimals: with DIMTZIN 8 the
|
||
// zero in 10.020 stays because 9.965 has nothing to drop, while a pair
|
||
// that both end in zero shortens together.
|
||
#[test]
|
||
fn limits_drop_trailing_zeros_only_together() {
|
||
let mut s = style();
|
||
s.dimlim = true;
|
||
s.dimtzin = 8;
|
||
s.dimtm = 0.035;
|
||
assert_eq!(
|
||
dimension_text_parts(&horizontal(), Some(&s)),
|
||
Some((r"\S10.020^9.965;".to_string(), None))
|
||
);
|
||
s.dimtm = 0.08;
|
||
assert_eq!(
|
||
dimension_text_parts(&horizontal(), Some(&s)),
|
||
Some((r"\S10.02^9.92;".to_string(), None))
|
||
);
|
||
}
|
||
|
||
// The offsets apply to the number the reader sees, which for a paper
|
||
// space dimension is the DIMLFAC-scaled distance (and DIMRND-rounded).
|
||
#[test]
|
||
fn limits_are_offsets_from_the_scaled_measurement() {
|
||
let mut s = style();
|
||
s.dimlim = true;
|
||
s.dimlfac = 7.5;
|
||
assert_eq!(
|
||
dimension_text_parts(&horizontal(), Some(&s)),
|
||
Some((r"\S75.020^74.950;".to_string(), None))
|
||
);
|
||
s.dimrnd = 0.5;
|
||
s.dimtm = 0.0;
|
||
assert_eq!(
|
||
dimension_text_parts(&horizontal(), Some(&s)),
|
||
Some((r"\S75.020^75.000;".to_string(), None))
|
||
);
|
||
}
|
||
|
||
// the source application keeps the stack when both offsets are zero: production
|
||
// drawings show `\S11.469^11.469;` for such dimensions.
|
||
#[test]
|
||
fn limits_without_offsets_still_stack() {
|
||
let mut s = style();
|
||
s.dimlim = true;
|
||
s.dimtp = 0.0;
|
||
s.dimtm = 0.0;
|
||
assert_eq!(
|
||
dimension_text_parts(&horizontal(), Some(&s)),
|
||
Some((r"\S10.000^10.000;".to_string(), None))
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn limits_take_precedence_over_deviation() {
|
||
let mut s = style();
|
||
s.dimlim = true;
|
||
s.dimtol = true;
|
||
let (_, suffix) = dimension_text_parts(&horizontal(), Some(&s)).unwrap();
|
||
assert_eq!(suffix, None);
|
||
}
|
||
|
||
#[test]
|
||
fn deviation_keeps_the_nominal_and_a_separate_suffix() {
|
||
let mut s = style();
|
||
s.dimtol = true;
|
||
assert_eq!(
|
||
dimension_text_parts(&horizontal(), Some(&s)),
|
||
Some(("10.000".to_string(), Some(r"\S+0.020^-0.050;".to_string())))
|
||
);
|
||
}
|
||
|
||
// A per-entity DSTYLE override turning limits off (group 72 = 0) is how
|
||
// the reference callouts in a limits-style drawing show a plain value.
|
||
#[test]
|
||
fn limits_off_shows_the_plain_value() {
|
||
let s = style();
|
||
assert_eq!(
|
||
dimension_text_parts(&horizontal(), Some(&s)),
|
||
Some(("10.000".to_string(), None))
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn user_text_substitutes_the_stack_for_the_measurement() {
|
||
let mut s = style();
|
||
s.dimlim = true;
|
||
let mut dim = horizontal();
|
||
dim.base_mut().text = "<> REF".to_string();
|
||
let (value, _) = dimension_text_parts(&dim, Some(&s)).unwrap();
|
||
assert_eq!(value, r"\S10.020^9.950; REF");
|
||
}
|
||
|
||
// Fit decisions size the text by its visible cells: a stack is as wide as
|
||
// its wider half, drawn at DIMTFAC, not as wide as its control codes.
|
||
#[test]
|
||
fn text_cells_measure_the_wider_stack_half() {
|
||
assert_eq!(text_cells("10.000", 0.5), 6.0);
|
||
assert_eq!(text_cells(r"R\S1.020^0.98;", 0.5), 1.0 + 5.0 * 0.5);
|
||
assert_eq!(text_cells(r"\S10.020^9.950; REF", 1.0), 6.0 + 4.0);
|
||
assert_eq!(text_cells(r"\S1.020^0.980;\PCutouts", 1.0), 7.0, "widest line, not the sum");
|
||
assert_eq!(text_lines(r"4X <>\PNominal"), 2);
|
||
assert_eq!(
|
||
text_cells(r"{\fArial|b0|i1|c0|p34;Cutouts}", 1.0),
|
||
7.0,
|
||
"format codes and braces take no room"
|
||
);
|
||
assert_eq!(text_cells(r"\H0.7x;10\L Ref\l", 1.0), 6.0);
|
||
}
|
||
|
||
// A three-point angular dimension whose first extension point coincides
|
||
// with the vertex measures nothing; the stored angle keeps the text.
|
||
#[test]
|
||
fn degenerate_angular_dimension_reports_its_stored_angle() {
|
||
use codec::entities::DimensionAngular3Pt;
|
||
let mut d = DimensionAngular3Pt::default();
|
||
d.angle_vertex = Vector3::new(6.0, 4.85, 0.0);
|
||
d.first_point = d.angle_vertex;
|
||
d.second_point = Vector3::new(7.0, 4.85, 0.0);
|
||
d.definition_point = Vector3::new(8.0, 5.4, 0.0);
|
||
d.base.actual_measurement = std::f64::consts::FRAC_PI_4;
|
||
let dim = Dimension::Angular3Pt(d);
|
||
let mut s = style();
|
||
s.dimadec = 0;
|
||
assert_eq!(
|
||
dimension_text_parts(&dim, Some(&s)),
|
||
Some(("45°".to_string(), None))
|
||
);
|
||
s.dimlim = true;
|
||
s.dimtp = 0.1;
|
||
s.dimtm = 0.1;
|
||
let (value, _) = dimension_text_parts(&dim, Some(&s)).unwrap();
|
||
assert!(value.starts_with(r"\S45"), "{value}");
|
||
}
|
||
|
||
// the source application rounds a printed value half away from zero; Rust's formatter
|
||
// rounds the binary value, which sits a hair below the decimal tie.
|
||
#[test]
|
||
fn decimal_text_rounds_half_away_from_zero() {
|
||
assert_eq!(decimal_text(0.4255, 3), "0.426");
|
||
assert_eq!(decimal_text(0.4085, 3), "0.409");
|
||
assert_eq!(decimal_text(-0.4255, 3), "-0.426");
|
||
assert_eq!(decimal_text(2.5, 0), "3");
|
||
assert_eq!(decimal_text(0.1234, 3), "0.123");
|
||
assert_eq!(decimal_text(0.0000000000004, 12), "0.000000000000");
|
||
}
|
||
|
||
// Alternate units multiply the displayed (DIMLFAC-scaled) value and stack
|
||
// their limits like the primary ones.
|
||
#[test]
|
||
fn alternate_limits_stack_from_the_scaled_value() {
|
||
let mut s = style();
|
||
s.dimlim = true;
|
||
s.dimlfac = 2.0;
|
||
s.dimalt = true;
|
||
s.dimaltf = 25.4;
|
||
s.dimaltd = 2;
|
||
s.dimalttd = 2;
|
||
s.dimaltz = 0;
|
||
s.dimalttz = 0;
|
||
s.dimtp = 0.02;
|
||
s.dimtm = 0.05;
|
||
let (value, _) = dimension_text_parts(&horizontal(), Some(&s)).unwrap();
|
||
assert_eq!(value, r"\S20.020^19.950; [\S508.51^506.73;]");
|
||
}
|
||
|
||
#[test]
|
||
fn a_limits_stack_is_two_halves_tall() {
|
||
let mut s = style();
|
||
assert_eq!(dimension_text_block_height(Some(&s), 2.0), 2.0);
|
||
s.dimlim = true;
|
||
s.dimtfac = 0.5;
|
||
let expected = 2.0
|
||
* 0.5
|
||
* f64::from(
|
||
crate::entities::text_support::STACK_RAISE
|
||
+ crate::entities::text_support::STACK_HALF_SCALE,
|
||
);
|
||
assert_eq!(dimension_text_block_height(Some(&s), 2.0), expected);
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod layout_parity_tests {
|
||
use super::*;
|
||
use codec::entities::{DimensionDiameter, DimensionLinear, DimensionRadius};
|
||
use glam::DVec3;
|
||
|
||
/// A document whose Standard style has imperial sizes and horizontal text.
|
||
fn document() -> CadDocument {
|
||
let mut document = CadDocument::new();
|
||
let style = document
|
||
.dim_styles
|
||
.get_mut("Standard")
|
||
.expect("Standard style");
|
||
style.dimasz = 0.18;
|
||
style.dimtxt = 0.18;
|
||
style.dimgap = 0.09;
|
||
style.dimtih = true;
|
||
style.dimtoh = true;
|
||
style.dimtad = 0;
|
||
style.dimdec = 2;
|
||
document
|
||
}
|
||
|
||
fn style(document: &CadDocument) -> DimStyle {
|
||
document.dim_styles.get("Standard").expect("Standard style").clone()
|
||
}
|
||
|
||
/// Stroke endpoints and arrowhead triangle count of the drawn dimension.
|
||
fn drawn(document: &CadDocument, dim: &Dimension) -> (Vec<DVec3>, usize) {
|
||
let wires = dim.tessellate(
|
||
document,
|
||
Handle::new(0x40),
|
||
false,
|
||
[1.0; 4],
|
||
1.0,
|
||
1.0,
|
||
&rustc_hash::FxHashSet::default(),
|
||
None,
|
||
[0.0, 0.0, 0.0, 1.0],
|
||
None,
|
||
None,
|
||
);
|
||
let mut points = Vec::new();
|
||
let mut fills = 0;
|
||
for wire in &wires {
|
||
for p in wire.points.iter().filter(|p| p[0].is_finite()) {
|
||
points.push(DVec3::new(p[0] as f64, p[1] as f64, p[2] as f64));
|
||
}
|
||
fills += wire.fill_tris.len() / 3;
|
||
}
|
||
(points, fills)
|
||
}
|
||
|
||
fn vertical(top: f64) -> DimensionLinear {
|
||
let mut d = DimensionLinear::default();
|
||
d.first_point = Vector3::new(0.0, 0.0, 0.0);
|
||
d.second_point = Vector3::new(0.0, top, 0.0);
|
||
d.definition_point = Vector3::new(-0.5, top, 0.0);
|
||
d.rotation = std::f64::consts::FRAC_PI_2;
|
||
d
|
||
}
|
||
|
||
// Group 11 is authoritative whenever present, whether or not the user moved
|
||
// the text.
|
||
#[test]
|
||
fn stored_text_point_places_automatic_text() {
|
||
let document = document();
|
||
let mut d = DimensionLinear::horizontal(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
|
||
d.definition_point = Vector3::new(0.0, 5.0, 0.0);
|
||
d.base.text_middle_point = Vector3::new(7.0, 5.2, 0.0);
|
||
d.base.text_user_positioned = false;
|
||
let dim = Dimension::Linear(d);
|
||
let position = dimension_text_pos_f64(&dim, Some(&style(&document)), 0.18, 1.0);
|
||
assert_eq!((position.x, position.y), (7.0, 5.2));
|
||
}
|
||
|
||
// Text pushed outside by a narrow gap sits past the two-arrow stub and a
|
||
// gap, not directly against the arrowhead.
|
||
#[test]
|
||
fn outside_text_clears_the_stub_and_a_gap() {
|
||
let document = document();
|
||
let mut d = DimensionLinear::horizontal(Vector3::new(0.0, 0.0, 0.0), Vector3::new(0.2, 0.0, 0.0));
|
||
d.definition_point = Vector3::new(0.0, 1.0, 0.0);
|
||
let dim = Dimension::Linear(d);
|
||
let s = style(&document);
|
||
let position = dimension_text_pos_f64(&dim, Some(&s), 0.18, 1.0);
|
||
let half_width = dimension_text_cells(&dim, Some(&s)).unwrap() * 0.18 * CELL_WIDTH * 0.5;
|
||
let text_start = position.x - half_width;
|
||
let stub_end = 0.2 + 2.0 * 0.18;
|
||
assert!(
|
||
(text_start - (stub_end + 0.09)).abs() < 1e-6,
|
||
"text starts at {text_start}, expected {}",
|
||
stub_end + 0.09
|
||
);
|
||
}
|
||
|
||
// Horizontal text near the top of a vertical dimension runs into the top
|
||
// arrowhead, so both arrowheads flip outside and stubs grow past the
|
||
// extension lines. Text in the middle leaves them inside.
|
||
#[test]
|
||
fn text_that_hits_an_arrow_moves_the_arrows_outside() {
|
||
let document = document();
|
||
let top = 1.12;
|
||
let mut near_top = vertical(top);
|
||
near_top.base.text_middle_point = Vector3::new(-0.5, 0.98, 0.0);
|
||
let (points, _) = drawn(&document, &Dimension::Linear(near_top));
|
||
assert!(
|
||
points.iter().any(|p| p.y > top + 0.1) && points.iter().any(|p| p.y < -0.1),
|
||
"arrows should be outside with stubs past both extension lines"
|
||
);
|
||
|
||
let mut centred = vertical(top);
|
||
centred.base.text_middle_point = Vector3::new(-0.5, top * 0.5, 0.0);
|
||
let (points, _) = drawn(&document, &Dimension::Linear(centred));
|
||
assert!(
|
||
points.iter().all(|p| p.y <= top + 0.01 && p.y >= -0.01),
|
||
"centred text leaves the arrows inside"
|
||
);
|
||
}
|
||
|
||
// A negative DIMLFAC scales only a paper-space dimension that measures
|
||
// model space through a viewport. The association record carries the
|
||
// applied factor; zero means the dimension reads its sheet distance.
|
||
#[test]
|
||
fn negative_dimlfac_follows_the_recorded_association_factor() {
|
||
use codec::xdata::{ExtendedDataRecord, XDataValue};
|
||
let document = CadDocument::new();
|
||
let mut source = style(&document);
|
||
source.dimlfac = -4.0;
|
||
let mut d = DimensionLinear::horizontal(Vector3::new(0.0, 0.0, 0.0), Vector3::new(4.457, 0.0, 0.0));
|
||
d.definition_point = Vector3::new(0.0, 1.0, 0.0);
|
||
let with_factor = |factor: f64| {
|
||
let mut d = d.clone();
|
||
let mut record = ExtendedDataRecord::new("ACAD_DIMASSOC_CALC_DIMLFAC");
|
||
record.add_value(XDataValue::Real(factor));
|
||
d.base.common.extended_data.add_record(record);
|
||
Dimension::Linear(d)
|
||
};
|
||
let sheet = resolved_dimension_style(&source, &with_factor(0.0), &document);
|
||
assert_eq!(sheet.dimlfac, 1.0, "no factor applied: sheet distance");
|
||
let through_viewport = resolved_dimension_style(&source, &with_factor(-4.0), &document);
|
||
assert_eq!(through_viewport.dimlfac, 4.0, "the recorded factor, made positive");
|
||
let unknown = resolved_dimension_style(&source, &Dimension::Linear(d.clone()), &document);
|
||
assert_eq!(unknown.dimlfac, 1.0, "model space without a record: unscaled");
|
||
let invalid = resolved_dimension_style(&source, &with_factor(f64::NAN), &document);
|
||
assert_eq!(invalid.dimlfac, 1.0, "invalid recorded factor: unscaled");
|
||
}
|
||
|
||
// DIMTMOVE 1: the stored point is where the hook starts. The hook runs
|
||
// one arrow toward the text, and the text begins a gap beyond it.
|
||
#[test]
|
||
fn dimtmove_one_anchors_the_hook_start_at_the_stored_point() {
|
||
let mut document = document();
|
||
document.dim_styles.get_mut("Standard").unwrap().dimtmove = 1;
|
||
let mut d = DimensionRadius::default();
|
||
d.angle_vertex = Vector3::new(0.0, 0.0, 0.0);
|
||
d.definition_point = Vector3::new(1.0, 0.0, 0.0);
|
||
d.base.text_middle_point = Vector3::new(3.0, 1.5, 0.0);
|
||
let dim = Dimension::Radius(d);
|
||
let (points, _) = drawn(&document, &dim);
|
||
let hook: Vec<f64> = points.iter().filter(|p| (p.y - 1.5).abs() < 1e-6).map(|p| p.x).collect();
|
||
let (lo, hi) = (hook.iter().cloned().fold(f64::MAX, f64::min), hook.iter().cloned().fold(f64::MIN, f64::max));
|
||
assert!((lo - 3.0).abs() < 1e-6 && (hi - 3.18).abs() < 1e-6, "hook from the stored point one arrow toward the text: {lo}..{hi}");
|
||
let s = style(&document);
|
||
let position = dimension_text_pos_f64(&dim, Some(&s), 0.18, 1.0);
|
||
let half = dimension_text_cells(&dim, Some(&s)).unwrap() * 0.18 * CELL_WIDTH * 0.5;
|
||
assert!((position.x - half - 0.09 - 3.18).abs() < 1e-6, "text starts a gap past the hook end, got {}", position.x - half);
|
||
}
|
||
|
||
// Text stored well past the second extension line of a horizontal
|
||
// dimension: the dimension line runs out to the text, not just two arrow
|
||
// lengths.
|
||
#[test]
|
||
fn outside_text_on_the_axis_pulls_the_line_out_to_it() {
|
||
let document = document();
|
||
let mut d = DimensionLinear::horizontal(Vector3::new(0.0, 0.0, 0.0), Vector3::new(0.8, 0.0, 0.0));
|
||
d.definition_point = Vector3::new(0.0, 2.0, 0.0);
|
||
d.base.text_middle_point = Vector3::new(2.5, 2.0, 0.0);
|
||
let (points, _) = drawn(&document, &Dimension::Linear(d));
|
||
let furthest = points.iter().map(|p| p.x).fold(f64::MIN, f64::max);
|
||
assert!(
|
||
furthest > 2.0 && furthest < 2.5,
|
||
"line reaches the text's near edge, got {furthest}"
|
||
);
|
||
}
|
||
|
||
// Horizontal text stored above a vertical dimension: the dimension line
|
||
// runs up to the text's height and hooks one arrow toward it.
|
||
#[test]
|
||
fn horizontal_outside_text_gets_a_leg_and_a_hook() {
|
||
let document = document();
|
||
let top = 0.3;
|
||
let mut d = vertical(top);
|
||
d.base.text_middle_point = Vector3::new(-0.5 + 0.9, 1.0, 0.0);
|
||
let (points, _) = drawn(&document, &Dimension::Linear(d));
|
||
assert!(
|
||
points.iter().any(|p| (p.y - 1.0).abs() < 1e-6 && (p.x + 0.5).abs() < 1e-6),
|
||
"the dimension line reaches the text height at the extension line"
|
||
);
|
||
assert!(
|
||
points.iter().any(|p| (p.y - 1.0).abs() < 1e-6 && (p.x - (-0.5 + 0.18)).abs() < 1e-6),
|
||
"a hook one arrow long turns toward the text"
|
||
);
|
||
}
|
||
|
||
// Radius text outside the arc: no line to the centre, a leader from the arc
|
||
// point to a hook the text sits against, arrowhead body pointing outward.
|
||
#[test]
|
||
fn radius_text_outside_draws_a_hooked_leader_only() {
|
||
let document = document();
|
||
let mut d = DimensionRadius::default();
|
||
d.angle_vertex = Vector3::new(0.0, 0.0, 0.0);
|
||
d.definition_point = Vector3::new(1.0, 0.0, 0.0);
|
||
d.base.text_middle_point = Vector3::new(3.0, 0.8, 0.0);
|
||
let (points, fills) = drawn(&document, &Dimension::Radius(d));
|
||
assert!(
|
||
!points.iter().any(|p| p.length() < 1e-6),
|
||
"no dimension line reaches the centre"
|
||
);
|
||
let hook: Vec<_> = points.iter().filter(|p| (p.y - 0.8).abs() < 1e-6).collect();
|
||
assert!(hook.len() >= 2, "a horizontal hook at the text height: {hook:?}");
|
||
assert!(
|
||
points.iter().any(|p| p.x > 1.05 && p.x < 1.3 && p.y > 0.0),
|
||
"arrowhead body lies outside the arc along the leader"
|
||
);
|
||
assert_eq!(fills, 1);
|
||
}
|
||
|
||
// Diameter text outside the circle without DIMTOFL: a single leader from
|
||
// the near side, one arrowhead, and no line across the circle.
|
||
#[test]
|
||
fn diameter_text_outside_draws_one_leader_and_no_chord() {
|
||
let document = document();
|
||
let mut d = DimensionDiameter::default();
|
||
d.angle_vertex = Vector3::new(-1.0, 0.0, 0.0);
|
||
d.definition_point = Vector3::new(1.0, 0.0, 0.0);
|
||
d.base.text_middle_point = Vector3::new(3.0, 0.8, 0.0);
|
||
let (points, fills) = drawn(&document, &Dimension::Diameter(d));
|
||
assert!(
|
||
!points.iter().any(|p| p.x < -0.5),
|
||
"nothing is drawn on the far side of the circle"
|
||
);
|
||
assert_eq!(fills, 1, "one arrowhead");
|
||
assert!(points.iter().any(|p| (p.y - 0.8).abs() < 1e-6), "hook at the text height");
|
||
}
|
||
}
|