forked from 1b-agent/opencadstudio-upstream
fix(text): keep text anchors in f64 and honour space advance
Single-line TEXT and ATTRIB/ATTDEF cast the insertion/alignment point to
f32 before computing the stroke origin. At large coordinates (UTM ~4.5M)
the f32 ulp is ~0.5, snapping text baselines onto a coarse grid so
adjacent rows collapse onto each other and overlap. Keep the anchor in
f64 throughout.
Horizontal alignment now uses the pen advance box [0, advance] instead of
the inked min_x/max_x, so leading/trailing spaces keep their width and
space-aligned columns no longer shift. text_local_bounds returns
TextLocalBounds { ink_min, ink_max, advance }.
TABLE laid out its whole grid in an f32 world frame; switch to a local
f32 frame plus an f64 base added at the widening step, fixing cell-text
baseline snap and the i32 overflow in the border-dedup keys.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
33314a5889
commit
5291a3fb82
4 files changed
+66
-28
No files matched your search
@@ -255,17 +255,19 @@ fn build_attr_truck(input: AttrTextInputs<'_>, document: &acadrust::CadDocument)
|
|||||||
oblique_angle,
|
oblique_angle,
|
||||||
);
|
);
|
||||||
let (anchor_local_x, anchor_local_y) =
|
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 {
|
let ax = match input.horizontal_alignment {
|
||||||
AHA::Left => min_x,
|
AHA::Left => 0.0,
|
||||||
AHA::Center | AHA::Middle => (min_x + max_x) * 0.5,
|
AHA::Center | AHA::Middle => b.advance * 0.5,
|
||||||
AHA::Right | AHA::Aligned | AHA::Fit => max_x,
|
AHA::Right | AHA::Aligned | AHA::Fit => b.advance,
|
||||||
};
|
};
|
||||||
let ay = match input.vertical_alignment {
|
let ay = match input.vertical_alignment {
|
||||||
AVA::Baseline => 0.0,
|
AVA::Baseline => 0.0,
|
||||||
AVA::Bottom => min_y,
|
AVA::Bottom => b.ink_min[1],
|
||||||
AVA::Middle => (min_y + max_y) * 0.5,
|
AVA::Middle => (b.ink_min[1] + b.ink_max[1]) * 0.5,
|
||||||
AVA::Top => max_y,
|
AVA::Top => b.ink_max[1],
|
||||||
};
|
};
|
||||||
(ax, ay)
|
(ax, ay)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+16
-6
@@ -22,7 +22,17 @@ impl TruckConvertible for Table {
|
|||||||
return None;
|
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_raw = v3(&self.horizontal_direction);
|
||||||
let h = if h_raw.length_squared() > 1e-10 {
|
let h = if h_raw.length_squared() > 1e-10 {
|
||||||
h_raw.normalize()
|
h_raw.normalize()
|
||||||
@@ -298,22 +308,22 @@ impl TruckConvertible for Table {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Table currently does its layout in glam::Vec3 (f32). The world_offset
|
// The layout above is in a local f32 frame (small magnitudes). Widen to
|
||||||
// subtraction in tessellate.rs needs f64, so widen at the boundary —
|
// f64 and add the world insertion so the absolute position carries full
|
||||||
// precision is already limited by the f32 math above (separate fix-up).
|
// precision; tessellate.rs then applies world_offset.
|
||||||
let pts_f64: Vec<[f64; 3]> = pts
|
let pts_f64: Vec<[f64; 3]> = pts
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|[x, y, z]| {
|
.map(|[x, y, z]| {
|
||||||
if x.is_nan() {
|
if x.is_nan() {
|
||||||
[f64::NAN, f64::NAN, f64::NAN]
|
[f64::NAN, f64::NAN, f64::NAN]
|
||||||
} else {
|
} 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();
|
.collect();
|
||||||
Some(TruckEntity {
|
Some(TruckEntity {
|
||||||
object: TruckObject::Lines(pts_f64),
|
object: TruckObject::Lines(pts_f64),
|
||||||
snap_pts: vec![(origin, SnapHint::Insertion)],
|
snap_pts: vec![(v3(&self.insertion_point), SnapHint::Insertion)],
|
||||||
tangent_geoms: vec![],
|
tangent_geoms: vec![],
|
||||||
key_vertices: vec![],
|
key_vertices: vec![],
|
||||||
fill_tris: vec![],
|
fill_tris: vec![],
|
||||||
|
|||||||
+20
-13
@@ -87,15 +87,19 @@ fn to_truck(t: &Text, document: &acadrust::CadDocument) -> TruckEntity {
|
|||||||
} else {
|
} else {
|
||||||
resolved_style.oblique_angle
|
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.horizontal_alignment,
|
||||||
&t.vertical_alignment,
|
&t.vertical_alignment,
|
||||||
&t.alignment_point,
|
&t.alignment_point,
|
||||||
) {
|
) {
|
||||||
(HA::Aligned | HA::Middle | HA::Fit, _, Some(a)) => [a.x as f32, a.y as f32],
|
(HA::Aligned | HA::Middle | HA::Fit, _, Some(a)) => [a.x, a.y],
|
||||||
(HA::Center | HA::Right, _, Some(a)) => [a.x as f32, a.y as f32],
|
(HA::Center | HA::Right, _, Some(a)) => [a.x, a.y],
|
||||||
(_, VA::Bottom | VA::Middle | VA::Top, Some(a)) => [a.x as f32, a.y as f32],
|
(_, VA::Bottom | VA::Middle | VA::Top, Some(a)) => [a.x, a.y],
|
||||||
_ => [t.insertion_point.x as f32, t.insertion_point.y as f32],
|
_ => [t.insertion_point.x, t.insertion_point.y],
|
||||||
};
|
};
|
||||||
// Strip %%u/%%o for bounds (they add no width); resolve %%d/%%c/%%p for correct advance.
|
// 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);
|
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,
|
width_factor,
|
||||||
oblique_angle,
|
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 {
|
let ax = match t.horizontal_alignment {
|
||||||
HA::Left => min_x,
|
HA::Left => 0.0,
|
||||||
HA::Center | HA::Middle => (min_x + max_x) * 0.5,
|
HA::Center | HA::Middle => b.advance * 0.5,
|
||||||
HA::Right | HA::Aligned | HA::Fit => max_x,
|
HA::Right | HA::Aligned | HA::Fit => b.advance,
|
||||||
};
|
};
|
||||||
|
// Vertical anchor uses the inked extent (cap / baseline geometry).
|
||||||
let ay = match t.vertical_alignment {
|
let ay = match t.vertical_alignment {
|
||||||
VA::Baseline => 0.0,
|
VA::Baseline => 0.0,
|
||||||
VA::Bottom => min_y,
|
VA::Bottom => b.ink_min[1],
|
||||||
VA::Middle => (min_y + max_y) * 0.5,
|
VA::Middle => (b.ink_min[1] + b.ink_max[1]) * 0.5,
|
||||||
VA::Top => max_y,
|
VA::Top => b.ink_max[1],
|
||||||
};
|
};
|
||||||
(ax, ay)
|
(ax, ay)
|
||||||
} else {
|
} 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);
|
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
|
// Keep origin as f64 — large coordinates (UTM etc.) must not be cast to
|
||||||
// f32 here; world_offset subtraction happens later in tessellate.rs.
|
// 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] = [
|
let origin: [f64; 2] = [
|
||||||
anchor_f64[0] - (anchor_local_x as f64 * cos_r - anchor_local_y as f64 * sin_r),
|
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),
|
anchor_f64[1] - (anchor_local_x as f64 * sin_r + anchor_local_y as f64 * cos_r),
|
||||||
|
|||||||
@@ -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(
|
pub fn text_local_bounds(
|
||||||
font_name: &str,
|
font_name: &str,
|
||||||
text: &str,
|
text: &str,
|
||||||
height: f32,
|
height: f32,
|
||||||
width_factor: f32,
|
width_factor: f32,
|
||||||
oblique_angle: f32,
|
oblique_angle: f32,
|
||||||
) -> Option<([f32; 2], [f32; 2])> {
|
) -> Option<TextLocalBounds> {
|
||||||
if text.is_empty() || height <= 0.0 {
|
if text.is_empty() || height <= 0.0 {
|
||||||
return None;
|
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() {
|
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 {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user