diff --git a/src/entities/attribute.rs b/src/entities/attribute.rs index 692998835..e3c5300d8 100644 --- a/src/entities/attribute.rs +++ b/src/entities/attribute.rs @@ -255,17 +255,19 @@ fn build_attr_truck(input: AttrTextInputs<'_>, document: &acadrust::CadDocument) oblique_angle, ); let (anchor_local_x, anchor_local_y) = - if let Some(([min_x, min_y], [max_x, max_y])) = bounds { + if let Some(b) = bounds { + // Horizontal anchor uses the pen advance box so leading / + // trailing spaces keep their width. let ax = match input.horizontal_alignment { - AHA::Left => min_x, - AHA::Center | AHA::Middle => (min_x + max_x) * 0.5, - AHA::Right | AHA::Aligned | AHA::Fit => max_x, + AHA::Left => 0.0, + AHA::Center | AHA::Middle => b.advance * 0.5, + AHA::Right | AHA::Aligned | AHA::Fit => b.advance, }; let ay = match input.vertical_alignment { AVA::Baseline => 0.0, - AVA::Bottom => min_y, - AVA::Middle => (min_y + max_y) * 0.5, - AVA::Top => max_y, + AVA::Bottom => b.ink_min[1], + AVA::Middle => (b.ink_min[1] + b.ink_max[1]) * 0.5, + AVA::Top => b.ink_max[1], }; (ax, ay) } else { diff --git a/src/entities/table.rs b/src/entities/table.rs index 7699a413d..1d3a18fe0 100644 --- a/src/entities/table.rs +++ b/src/entities/table.rs @@ -22,7 +22,17 @@ impl TruckConvertible for Table { return None; } - let origin = v3(&self.insertion_point); + // Lay the table out in a local frame with the origin at zero. The world + // insertion point is added back as f64 at the widening step below, so + // large coordinates (UTM etc.) keep full precision instead of snapping + // onto the coarse f32 grid — which would collide cell-text baselines and + // overflow the integer border-dedup keys. + let base = [ + self.insertion_point.x, + self.insertion_point.y, + self.insertion_point.z, + ]; + let origin = Vec3::ZERO; let h_raw = v3(&self.horizontal_direction); let h = if h_raw.length_squared() > 1e-10 { h_raw.normalize() @@ -298,22 +308,22 @@ impl TruckConvertible for Table { } } - // Table currently does its layout in glam::Vec3 (f32). The world_offset - // subtraction in tessellate.rs needs f64, so widen at the boundary — - // precision is already limited by the f32 math above (separate fix-up). + // The layout above is in a local f32 frame (small magnitudes). Widen to + // f64 and add the world insertion so the absolute position carries full + // precision; tessellate.rs then applies world_offset. let pts_f64: Vec<[f64; 3]> = pts .into_iter() .map(|[x, y, z]| { if x.is_nan() { [f64::NAN, f64::NAN, f64::NAN] } else { - [x as f64, y as f64, z as f64] + [x as f64 + base[0], y as f64 + base[1], z as f64 + base[2]] } }) .collect(); Some(TruckEntity { object: TruckObject::Lines(pts_f64), - snap_pts: vec![(origin, SnapHint::Insertion)], + snap_pts: vec![(v3(&self.insertion_point), SnapHint::Insertion)], tangent_geoms: vec![], key_vertices: vec![], fill_tris: vec![], diff --git a/src/entities/text.rs b/src/entities/text.rs index d2afc5d26..9feba96fd 100644 --- a/src/entities/text.rs +++ b/src/entities/text.rs @@ -87,15 +87,19 @@ fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity { } else { resolved_style.oblique_angle }; - let anchor = match ( + // Anchor stays f64: large coordinates (UTM etc.) lose ~0.5 units of + // precision when cast to f32, which snaps text baselines onto a coarse + // grid and makes adjacent rows collide. Only the small local offsets + // below are computed in f32. + let anchor: [f64; 2] = match ( &t.horizontal_alignment, &t.vertical_alignment, &t.alignment_point, ) { - (HA::Aligned | HA::Middle | HA::Fit, _, Some(a)) => [a.x as f32, a.y as f32], - (HA::Center | HA::Right, _, Some(a)) => [a.x as f32, a.y as f32], - (_, VA::Bottom | VA::Middle | VA::Top, Some(a)) => [a.x as f32, a.y as f32], - _ => [t.insertion_point.x as f32, t.insertion_point.y as f32], + (HA::Aligned | HA::Middle | HA::Fit, _, Some(a)) => [a.x, a.y], + (HA::Center | HA::Right, _, Some(a)) => [a.x, a.y], + (_, VA::Bottom | VA::Middle | VA::Top, Some(a)) => [a.x, a.y], + _ => [t.insertion_point.x, t.insertion_point.y], }; // Strip %%u/%%o for bounds (they add no width); resolve %%d/%%c/%%p for correct advance. let value_for_bounds = resolve_dxf_special_chars(&t.value); @@ -106,17 +110,20 @@ fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity { width_factor, oblique_angle, ); - let (anchor_local_x, anchor_local_y) = if let Some(([min_x, min_y], [max_x, max_y])) = bounds { + let (anchor_local_x, anchor_local_y) = if let Some(b) = bounds { + // Horizontal anchor uses the pen advance box [0, advance] so leading / + // trailing spaces keep their width instead of snapping to the inked glyphs. let ax = match t.horizontal_alignment { - HA::Left => min_x, - HA::Center | HA::Middle => (min_x + max_x) * 0.5, - HA::Right | HA::Aligned | HA::Fit => max_x, + HA::Left => 0.0, + HA::Center | HA::Middle => b.advance * 0.5, + HA::Right | HA::Aligned | HA::Fit => b.advance, }; + // Vertical anchor uses the inked extent (cap / baseline geometry). let ay = match t.vertical_alignment { VA::Baseline => 0.0, - VA::Bottom => min_y, - VA::Middle => (min_y + max_y) * 0.5, - VA::Top => max_y, + VA::Bottom => b.ink_min[1], + VA::Middle => (b.ink_min[1] + b.ink_max[1]) * 0.5, + VA::Top => b.ink_max[1], }; (ax, ay) } else { @@ -125,7 +132,7 @@ fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity { let (cos_r, sin_r) = (rotation.cos() as f64, rotation.sin() as f64); // Keep origin as f64 — large coordinates (UTM etc.) must not be cast to // f32 here; world_offset subtraction happens later in tessellate.rs. - let anchor_f64 = [anchor[0] as f64, anchor[1] as f64]; + let anchor_f64 = anchor; let origin: [f64; 2] = [ anchor_f64[0] - (anchor_local_x as f64 * cos_r - anchor_local_y as f64 * sin_r), anchor_f64[1] - (anchor_local_x as f64 * sin_r + anchor_local_y as f64 * cos_r), diff --git a/src/entities/text_support.rs b/src/entities/text_support.rs index 0b54453ed..afeda921a 100644 --- a/src/entities/text_support.rs +++ b/src/entities/text_support.rs @@ -55,13 +55,24 @@ pub fn resolve_text_style(style_name: &str, document: &CadDocument) -> ResolvedT } } +pub struct TextLocalBounds { + /// Inked extent (glyph strokes only) — drives vertical alignment, where + /// the cap / baseline geometry is what matters. + pub ink_min: [f32; 2], + pub ink_max: [f32; 2], + /// Pen advance along the baseline, including leading / trailing spaces and + /// inter-glyph spacing. Drives horizontal alignment so spaces in the string + /// keep their width instead of collapsing to the first / last inked glyph. + pub advance: f32, +} + pub fn text_local_bounds( font_name: &str, text: &str, height: f32, width_factor: f32, oblique_angle: f32, -) -> Option<([f32; 2], [f32; 2])> { +) -> Option { if text.is_empty() || height <= 0.0 { return None; } @@ -101,8 +112,16 @@ pub fn text_local_bounds( } } + // Pen advance is measured at the baseline, so oblique shear (which skews x + // only by gy) does not enter it. Valid even for an all-space string. + let advance = cursor_x * scale * wf; + if min_x.is_finite() && min_y.is_finite() && max_x.is_finite() && max_y.is_finite() { - Some(([min_x, min_y], [max_x, max_y])) + Some(TextLocalBounds { + ink_min: [min_x, min_y], + ink_max: [max_x, max_y], + advance, + }) } else { None }