forked from 1b-agent/opencadstudio-upstream
fix(dimension): bake↔live consistency, edits, baseline/continue, grips
Batch 1-3 of the dimension audit (19 issues). The dominant class was the baked *D block diverging from the live render: a reloaded dimension renders from that block, so on save+reopen it silently changed. Bake now matches the live render: - text value/format via the shared style-resolved path (DIMDEC/DIMLFAC/ DIMPOST/units/<>), correct height + rotation ladder, suppression - angular dims bake their swept arc (no longer collapse to a V) - diameter draws edge-to-edge through the centre (live + bake), centre mark - radius/linear/aligned bake arrowheads - group-10 (base.definition_point) synced from the edited geometry so an edited dim no longer shifts on reload Editing / properties: - aligned no longer corrupts ext_line_rotation when a coordinate is edited - DIMTEDIT pins the moved text (text_user_positioned) - angle properties shown/edited in degrees - non-linear text respects the user-positioned gate Commands / geometry: - DIMBASELINE stacks outward (not toward the points) and its preview no longer tilts over non-level points - DIMCONTINUE/DIMBASELINE inherit the base dim's text rotation (UCS chains) - angular extension lines reach the arc (no gap) - text grip sits on the text for auto-placed dims instead of the origin Refs #181. (acadrust DXF round-trip + QDIM/DIMBREAK/DIMJOGLINE/DIMSPACE/ DIMEDIT to follow.) Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
b691854e7a
commit
3c5aa4453a
7 files changed
+591
-131
No files matched your search
+126
-59
@@ -34,11 +34,15 @@ fn base_props(base: &DimensionBase) -> Vec<crate::scene::model::object::Property
|
||||
edit("Text X", "text_x", base.text_middle_point.x),
|
||||
edit("Text Y", "text_y", base.text_middle_point.y),
|
||||
edit("Text Z", "text_z", base.text_middle_point.z),
|
||||
edit("Text Rotation", "text_rotation", base.text_rotation),
|
||||
edit(
|
||||
"Horizontal Dir",
|
||||
"Text Rotation (deg)",
|
||||
"text_rotation",
|
||||
base.text_rotation.to_degrees(),
|
||||
),
|
||||
edit(
|
||||
"Horizontal Dir (deg)",
|
||||
"horizontal_direction",
|
||||
base.horizontal_direction,
|
||||
base.horizontal_direction.to_degrees(),
|
||||
),
|
||||
edit(
|
||||
"Line Spacing",
|
||||
@@ -63,9 +67,9 @@ fn properties(dim: &Dimension) -> PropSection {
|
||||
d.definition_point,
|
||||
));
|
||||
props.push(edit(
|
||||
"Ext Rotation",
|
||||
"Ext Rotation (deg)",
|
||||
"ext_line_rotation",
|
||||
d.ext_line_rotation,
|
||||
d.ext_line_rotation.to_degrees(),
|
||||
));
|
||||
}
|
||||
Dimension::Linear(d) => {
|
||||
@@ -74,11 +78,11 @@ fn properties(dim: &Dimension) -> PropSection {
|
||||
d.second_point,
|
||||
d.definition_point,
|
||||
));
|
||||
props.push(edit("Rotation", "rotation", d.rotation));
|
||||
props.push(edit("Rotation (deg)", "rotation", d.rotation.to_degrees()));
|
||||
props.push(edit(
|
||||
"Ext Rotation",
|
||||
"Ext Rotation (deg)",
|
||||
"ext_line_rotation",
|
||||
d.ext_line_rotation,
|
||||
d.ext_line_rotation.to_degrees(),
|
||||
));
|
||||
}
|
||||
Dimension::Radius(d) => {
|
||||
@@ -217,8 +221,8 @@ fn apply_base_prop(base: &mut DimensionBase, field: &str, value: &str) -> bool {
|
||||
base.text_user_positioned = true;
|
||||
assign_f64(value, &mut base.text_middle_point.z)
|
||||
}
|
||||
"text_rotation" => assign_f64(value, &mut base.text_rotation),
|
||||
"horizontal_direction" => assign_f64(value, &mut base.horizontal_direction),
|
||||
"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,
|
||||
}
|
||||
@@ -232,6 +236,17 @@ fn assign_f64(value: &str, target: &mut f64) -> bool {
|
||||
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) {
|
||||
if apply_base_prop(dim.base_mut(), field, value) {
|
||||
return;
|
||||
@@ -256,7 +271,11 @@ fn apply_linear_fields_aligned(d: &mut DimensionAligned, field: &str, value: &st
|
||||
field,
|
||||
value,
|
||||
);
|
||||
let _ = assign_f64(value, &mut d.ext_line_rotation);
|
||||
// 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) {
|
||||
@@ -269,10 +288,10 @@ fn apply_linear_fields_linear(d: &mut DimensionLinear, field: &str, value: &str)
|
||||
);
|
||||
match field {
|
||||
"rotation" => {
|
||||
let _ = assign_f64(value, &mut d.rotation);
|
||||
let _ = assign_deg(value, &mut d.rotation);
|
||||
}
|
||||
"ext_line_rotation" => {
|
||||
let _ = assign_f64(value, &mut d.ext_line_rotation);
|
||||
let _ = assign_deg(value, &mut d.ext_line_rotation);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -604,7 +623,17 @@ fn apply_to_v3(target: &mut acadrust::types::Vector3, apply: &GripApply) {
|
||||
|
||||
impl Grippable for Dimension {
|
||||
fn grips(&self) -> Vec<GripDef> {
|
||||
let text = dv3(&self.base().text_middle_point);
|
||||
// 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)),
|
||||
@@ -1557,23 +1586,27 @@ fn dimension_geometry(
|
||||
append_center_mark(&mut g, center, params.dimcen, radius);
|
||||
}
|
||||
Dimension::Diameter(d) => {
|
||||
let p1 = lv(d.angle_vertex);
|
||||
let p2 = lv(d.definition_point);
|
||||
add_segment(&mut g.dim_lines, p1, p2);
|
||||
append_arrow(&mut g, p1, normalized_or(p2 - p1, Vec3::X), arrow1);
|
||||
append_arrow(&mut g, p2, normalized_or(p1 - p2, Vec3::X), arrow2);
|
||||
// DIMETER leader: continue past p2 toward the text.
|
||||
// angle_vertex is the circle centre and definition_point a point on
|
||||
// the circle. The diameter line runs edge-to-edge THROUGH the centre
|
||||
// (far edge → near edge), with arrows pointing inward at each edge.
|
||||
let center = lv(d.angle_vertex);
|
||||
let edge = lv(d.definition_point);
|
||||
let far = center * 2.0 - edge;
|
||||
add_segment(&mut g.dim_lines, far, edge);
|
||||
append_arrow(&mut g, edge, normalized_or(far - edge, Vec3::X), arrow1);
|
||||
append_arrow(&mut g, far, normalized_or(edge - far, Vec3::X), arrow2);
|
||||
// Diameter leader: continue past the near edge toward the text.
|
||||
if d.leader_length.abs() > 1e-9 {
|
||||
let text = dimension_text_position(dim);
|
||||
let leader_dir = normalized_or(text - p2, p2 - p1);
|
||||
let leader_dir = normalized_or(text - edge, edge - far);
|
||||
add_segment(
|
||||
&mut g.dim_lines,
|
||||
p2,
|
||||
p2 + leader_dir * (d.leader_length as f32),
|
||||
edge,
|
||||
edge + leader_dir * (d.leader_length as f32),
|
||||
);
|
||||
}
|
||||
let radius = (p2 - p1).length() * 0.5;
|
||||
append_center_mark(&mut g, (p1 + p2) * 0.5, params.dimcen, radius);
|
||||
let radius = (edge - center).length();
|
||||
append_center_mark(&mut g, center, params.dimcen, radius);
|
||||
}
|
||||
Dimension::Angular2Ln(d) => {
|
||||
append_angular_dimension(
|
||||
@@ -1783,13 +1816,18 @@ fn append_angular_dimension(
|
||||
arrow1: &ArrowKind,
|
||||
arrow2: &ArrowKind,
|
||||
) {
|
||||
add_segment(&mut g.ext_lines, vertex, first);
|
||||
add_segment(&mut g.ext_lines, vertex, second);
|
||||
|
||||
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 dir1 = normalized_or(first - vertex, Vec3::X);
|
||||
let dir2 = normalized_or(second - vertex, Vec3::X);
|
||||
add_segment(&mut g.ext_lines, first, vertex + dir1 * radius);
|
||||
add_segment(&mut g.ext_lines, second, vertex + dir2 * radius);
|
||||
|
||||
let start = (first.y - vertex.y).atan2(first.x - vertex.x);
|
||||
let mut end = (second.y - vertex.y).atan2(second.x - vertex.x);
|
||||
@@ -1936,29 +1974,7 @@ fn dimension_text_entity(
|
||||
let pos_f64 = dimension_text_pos_f64(dim, style, text_height, dim_scale);
|
||||
let base = dim.base();
|
||||
|
||||
// DIMTIH/DIMTOH: when set, text is forced horizontal (rotation = 0)
|
||||
// regardless of the dim line angle. Honour explicit base.text_rotation
|
||||
// first, then horizontal_direction override, then DIMTIH/DIMTOH.
|
||||
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 rotation = if base.text_rotation.abs() > 1e-9 {
|
||||
base.text_rotation
|
||||
} else if base.horizontal_direction.abs() > 1e-9 {
|
||||
// horizontal_direction is the in-plane reading direction the writer
|
||||
// baked in (used for vertical / oblique dims).
|
||||
base.horizontal_direction
|
||||
} else if dimtih || dimtoh {
|
||||
0.0
|
||||
} else {
|
||||
let mut r = dimension_text_natural_rotation(dim);
|
||||
// DIMJUST 3/4: text reads along the extension lines, i.e. rotated 90°
|
||||
// from the dimension line.
|
||||
if dimjust == 3 || dimjust == 4 {
|
||||
r += std::f64::consts::FRAC_PI_2;
|
||||
}
|
||||
r
|
||||
};
|
||||
let rotation = dimension_text_rotation(dim, style);
|
||||
|
||||
// Text style resolution priority:
|
||||
// 1. DIMTXSTY by handle (most reliable; survives rename)
|
||||
@@ -2056,6 +2072,31 @@ fn attachment_to_text_align(
|
||||
}
|
||||
}
|
||||
|
||||
/// 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);
|
||||
if base.text_rotation.abs() > 1e-9 {
|
||||
base.text_rotation
|
||||
} else if base.horizontal_direction.abs() > 1e-9 {
|
||||
base.horizontal_direction
|
||||
} else if dimtih || dimtoh {
|
||||
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_natural_rotation(dim: &Dimension) -> f64 {
|
||||
let angle = match dim {
|
||||
Dimension::Linear(d) => d.rotation,
|
||||
@@ -2077,7 +2118,7 @@ fn dimension_text_natural_rotation(dim: &Dimension) -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
fn dimension_text_value(dim: &Dimension, style: Option<&DimStyle>) -> Option<String> {
|
||||
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
|
||||
@@ -2731,13 +2772,12 @@ fn dimension_text_pos_f64(
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
// Non-linear (radius / diameter / angular / ordinate): keep the
|
||||
// saved text point when present for fidelity, else lift the natural
|
||||
// mid point straight up by the style offset.
|
||||
let p = base.text_middle_point;
|
||||
if p.x * p.x + p.y * p.y + p.z * p.z > 1e-16 {
|
||||
return p;
|
||||
}
|
||||
// Non-linear (radius / diameter / angular / ordinate): lift the
|
||||
// natural mid point straight up by the style offset. A user-dragged
|
||||
// text point is already returned by the `use_saved` gate above, so
|
||||
// we must NOT short-circuit on a merely-nonzero text_middle_point
|
||||
// here — that would ignore the style placement for auto-placed dims
|
||||
// and make a re-style a no-op. (#181)
|
||||
let mid = match dim {
|
||||
Dimension::Radius(d) => Vector3::new(
|
||||
(d.angle_vertex.x + d.definition_point.x) * 0.5,
|
||||
@@ -2763,6 +2803,33 @@ fn perp_sign_default() -> f64 {
|
||||
1.0
|
||||
}
|
||||
|
||||
/// Text placement (position, height, rotation) the live renderer uses for
|
||||
/// `dim`, resolving its dimension style from `document`. Shared with the
|
||||
/// dimension-block baking on save so a baked `*D` block places its measurement
|
||||
/// text exactly where OCS draws it on screen — otherwise the text jumps when
|
||||
/// the file is saved and reopened (the reload renders from the baked block).
|
||||
/// `anno_scale` is the annotative scale (1.0 for a plain model-space bake).
|
||||
pub(crate) fn baked_dimension_text(
|
||||
dim: &Dimension,
|
||||
document: &CadDocument,
|
||||
anno_scale: f64,
|
||||
) -> Option<(String, Vector3, f64, f64)> {
|
||||
let style_name = dim.base().style_name.as_str();
|
||||
let 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"))
|
||||
});
|
||||
// None => the dimension's text is suppressed (user_text " "): bake no Text.
|
||||
let value = dimension_text_value(dim, style)?;
|
||||
let dim_scale = style
|
||||
.map(|s| if s.dimscale > 1e-6 { s.dimscale } else { anno_scale })
|
||||
.unwrap_or(1.0);
|
||||
let dim_txt = style.map(|s| s.dimtxt * dim_scale).unwrap_or(2.5 * dim_scale);
|
||||
let pos = dimension_text_pos_f64(dim, style, dim_txt, dim_scale);
|
||||
let rot = dimension_text_rotation(dim, style);
|
||||
Some((value, pos, dim_txt, rot))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod dimtad_tests {
|
||||
use super::text_on_dim_line;
|
||||
|
||||
Reference in new issue
Block a user