From 0fdbc80e1668709d54ff10e387b7403a3d655015 Mon Sep 17 00:00:00 2001 From: Christopher Overbeck Date: Thu, 17 Sep 2026 09:06:54 -0500 Subject: [PATCH] fix(dimension): measured cell width, DIMTMOVE 1 hook start, diameter sign, #1323 offset kept - The per-character width estimate is 0.66 text heights, measured by rendering the text of 106 saved dimension pictures with the bundled fonts; the old 0.60 left every fit and hook estimate ten percent narrow. With it, the "text plus DIMGAP must clear the arrowheads" rule reproduces AutoCAD's arrow placement for 1047 of 1078 pictured linear dimensions whose text sits between the extension lines. - DIMTMOVE 1 stores where the leader's hook starts, not where it ends: on 39 such radial dimensions the stored point sits DIMASZ plus DIMGAP before the text. The hook now runs from the stored point one arrow toward the text. - The diameter prefix is U+2205, matching the %%c expansion and the pictures. - A diameter whose text the user dragged keeps the DIMTAD lift from #1323; the four AutoCAD-authored diameters with DIMTAD above in the sample all store the lifted point directly, so this applies only to dragged text. --- src/entities/dimension.rs | 98 ++++++++++++++++++++++++++++++--------- 1 file changed, 75 insertions(+), 23 deletions(-) diff --git a/src/entities/dimension.rs b/src/entities/dimension.rs index 72033e970..b902661cb 100644 --- a/src/entities/dimension.rs +++ b/src/entities/dimension.rs @@ -3825,7 +3825,7 @@ fn tessellate_dimension_inner( &mut geom.dim_lines, point, vec3_local(dim.base().normal), - dim_txt as f32 * 0.6, + dim_txt as f32 * CELL_WIDTH as f32, jog_angle, ); } @@ -4440,7 +4440,7 @@ fn text_fill_rect( // ~0.6 × text_height per character; matches average glyph aspect for // 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 * 0.6 + dimgap * 2.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 @@ -4484,7 +4484,7 @@ fn arc_length_symbol_points( 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 * 0.6; + 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 { @@ -4550,7 +4550,7 @@ fn dimension_text_layout( .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 * 0.6 + gap * 2.0) + .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, @@ -4600,7 +4600,7 @@ struct DimLineParams { horizontal_text: bool, text_movement: i16, text_break: Option, - /// DIMTMOVE 1: the point the file stores is where the leader's hook ends, + /// 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, } @@ -5154,12 +5154,13 @@ fn append_linear_dimension( fn append_radial_leader(g: &mut DimGeom, tip: Vec3, text: Vec3, params: &DimLineParams) { if params.horizontal_text { let side = if text.x >= tip.x { 1.0 } else { -1.0 }; - let text_edge = params.leader_anchor.unwrap_or(Vec3::new( - text.x - side * params.text_width * 0.5, - text.y, - text.z, - )); - let hook_start = text_edge - Vec3::X * (side * params.arrow_len); + 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() { @@ -6181,7 +6182,7 @@ fn dimension_text_is_outside(dim: &Dimension, style: Option<&DimStyle>) -> bool let height = style.dimtxt * scale; let gap = style.dimgap.abs() * scale; let text_width = dimension_text_cells(dim, Some(style)) - .map(|cells| cells * height * 0.6 + gap * 2.0) + .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; @@ -6210,7 +6211,7 @@ fn dimension_text_is_outside(dim: &Dimension, style: Option<&DimStyle>) -> bool let height = style.dimtxt * scale; let gap = style.dimgap.abs() * scale; let text_width = dimension_text_cells(dim, Some(style)) - .map(|cells| cells * height * 0.6 + gap * 2.0) + .map(|cells| cells * height * CELL_WIDTH + gap * 2.0) .unwrap_or(0.0); let arrow = style.dimasz * scale; let insufficient = text_width + arrow > available; @@ -6237,7 +6238,7 @@ fn dimension_text_is_outside(dim: &Dimension, style: Option<&DimStyle>) -> bool let height = style.dimtxt * scale; let gap = style.dimgap.abs() * scale; let text_width = dimension_text_cells(dim, Some(style)) - .map(|cells| cells * height * 0.6 + gap * 2.0) + .map(|cells| cells * height * CELL_WIDTH + gap * 2.0) .unwrap_or(0.0); let arrow = style.dimasz * scale; let insufficient = text_width + arrow > available; @@ -6281,7 +6282,7 @@ fn dimension_text_is_outside(dim: &Dimension, style: Option<&DimStyle>) -> bool let height = style.dimtxt * scale; let gap = style.dimgap.abs() * scale; let text_width = dimension_text_cells(dim, Some(style)) - .map(|cells| cells * height * 0.6 + gap * 2.0) + .map(|cells| cells * height * CELL_WIDTH + gap * 2.0) .unwrap_or(0.0); let arrow = style.dimasz * scale; let span = hi - lo; @@ -6365,7 +6366,9 @@ fn dimension_text_parts( }; let primary_raw = match dim { Dimension::Radius(_) | Dimension::LargeRadial(_) => format!("R{}", value), - Dimension::Diameter(_) => format!("Ø{}", value), + // U+2205 is the diameter sign AutoCAD writes (and what `%%c` resolves + // to); the bundled dimension fonts carry it where Latin Ø is missing. + Dimension::Diameter(_) => format!("∅{}", value), _ => value, }; @@ -6643,13 +6646,13 @@ fn dimension_tolerance_entity( }; // Approximate widths from glyph counts (~0.6 × cell size per char). - let primary_w = primary_value_len as f64 * primary_height * 0.6; + 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 * 0.6; + 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 { @@ -6710,6 +6713,12 @@ fn effective_dimlfac(style: Option<&DimStyle>) -> f64 { } } +/// 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 { @@ -7367,7 +7376,7 @@ fn dimension_text_pos_f64( }; // Rough text width + arrow allowance, used to decide text-outside fit. let text_w = dimension_text_cells(dim, style) - .map(|cells| cells * text_height * 0.6 + 2.0 * dimgap) + .map(|cells| cells * text_height * CELL_WIDTH + 2.0 * dimgap) .unwrap_or(0.0); let arrow = style .map(|style| style.dimasz * dim_scale) @@ -7378,14 +7387,36 @@ fn dimension_text_pos_f64( if let Some(point) = stored_text_point(dim) { return match dim { - // DIMTMOVE 1 stores where the leader's hook ends; the text starts - // there and runs away from the arc. + // 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 * text_w * 0.5, point.y, point.z) + 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, }; @@ -8615,7 +8646,7 @@ mod layout_parity_tests { 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 * 0.6 * 0.5; + 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!( @@ -8675,6 +8706,27 @@ mod layout_parity_tests { assert_eq!(unknown.dimlfac, 1.0, "model space without a record: 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 = 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.