fix(render): preserve f64 precision until world_offset subtraction

Entity converters were casting WCS coordinates to f32 before the renderer
subtracted world_offset, which destroyed sub-unit precision on drawings
sitting at large coordinates (typical UTM-style CAD files). The multileader
case made it visible — glyphs warped under rotation — but every entity
that emits TruckObject::Lines/SegmentedLines/key_vertices/fill_tris was
affected.

Type changes (in src/scene/acad_to_truck.rs):
- TruckObject::Lines / SegmentedLines: Vec<[f32; 3]> → Vec<[f64; 3]>
- TruckEntity::key_vertices / fill_tris: f32 → f64

All ~17 producer files updated to keep WCS coordinates in f64 right up to
the TruckEntity boundary. The consumer in scene/tessellate.rs now performs
`(x - ox) as f32` (f64 subtract, then cast) for points, key_vertices, and
fill_tris — preserving precision near the camera origin even when the
drawing sits at million-unit coordinates.

catmull_rom_pts (multileader spline) is now f64; the scene path widens its
offset-relative f32 ctrl points to f64 around the call and narrows back at
the output.

Notes:
- Table layout still uses glam::Vec3 (f32) internally; widened to f64 only
  at the TruckEntity boundary for type compatibility. Separate cleanup to
  switch its internal math to f64 is tracked.
- snap_pts and tangent_geoms remain f32 — they are UI feedback (snap
  candidates, tangent hints) and the precision is sufficient there.
This commit is contained in:
Hakan Seven 2026-05-12 23:47:40 +03:00
commit 288ccff95a
20 changed files with 371 additions and 360 deletions

View file

@ -39,7 +39,9 @@ use crate::scene::wire_model::{SnapHint, TangentGeom};
// ── TruckConvertible ────────────────────────────────────────────────────────
/// Catmull-Rom spline tessellation through `ctrl` points, `segs_per_span` segments each.
pub(crate) fn catmull_rom_pts(ctrl: &[[f32; 3]], segs_per_span: u32) -> Vec<[f32; 3]> {
/// Operates in f64 so it can be applied to either WCS-direct coordinates (entity path)
/// or offset-relative coordinates (scene path) without precision loss.
pub(crate) fn catmull_rom_pts(ctrl: &[[f64; 3]], segs_per_span: u32) -> Vec<[f64; 3]> {
let n = ctrl.len();
let mut out = Vec::new();
for i in 0..n.saturating_sub(1) {
@ -48,10 +50,10 @@ pub(crate) fn catmull_rom_pts(ctrl: &[[f32; 3]], segs_per_span: u32) -> Vec<[f32
let p2 = ctrl[i + 1];
let p3 = if i + 2 < n { ctrl[i + 2] } else { ctrl[n - 1] };
for j in 0..=segs_per_span {
let t = j as f32 / segs_per_span as f32;
let t = j as f64 / segs_per_span as f64;
let t2 = t * t;
let t3 = t2 * t;
let mut pt = [0.0_f32; 3];
let mut pt = [0.0_f64; 3];
for k in 0..3 {
pt[k] = 0.5
* ((2.0 * p1[k])
@ -66,20 +68,26 @@ pub(crate) fn catmull_rom_pts(ctrl: &[[f32; 3]], segs_per_span: u32) -> Vec<[f32
}
fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckEntity> {
let nan = [f32::NAN; 3];
let p3 = |v: &acadrust::types::Vector3| -> [f32; 3] { [v.x as f32, v.y as f32, v.z as f32] };
let nan = [f64::NAN; 3];
let p3 = |v: &acadrust::types::Vector3| -> [f64; 3] { [v.x, v.y, v.z] };
let arrow_size = ml.arrowhead_size as f32;
let arrow_size = ml.arrowhead_size;
let draw_arrow = arrow_size > 0.0;
let invisible = ml.path_type == MultiLeaderPathType::Invisible;
let mut points: Vec<[f32; 3]> = Vec::new();
let mut points: Vec<[f64; 3]> = Vec::new();
let mut tangents: Vec<TangentGeom> = Vec::new();
let mut key_verts: Vec<[f32; 3]> = Vec::new();
let mut key_verts: Vec<[f64; 3]> = Vec::new();
let mut snap_pts: Vec<(Vec3, SnapHint)> = Vec::new();
let mut first = true;
let node = |arr: [f32; 3]| (Vec3::from(arr), SnapHint::Node);
// snap_pts uses f32 (UI-only); cast at construction.
let node = |arr: [f64; 3]| {
(
Vec3::new(arr[0] as f32, arr[1] as f32, arr[2] as f32),
SnapHint::Node,
)
};
for root in &ml.context.leader_roots {
let cp = &root.connection_point;
@ -98,7 +106,7 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckE
first = false;
// Build the full control-point list: line.points + connection_point
let mut ctrl: Vec<[f32; 3]> = line.points.iter().map(|p| p3(p)).collect();
let mut ctrl: Vec<[f64; 3]> = line.points.iter().map(|p| p3(p)).collect();
let last_f = *ctrl.last().unwrap_or(&cp_f);
let dist = ((last_f[0] - cp_f[0]).powi(2) + (last_f[1] - cp_f[1]).powi(2)).sqrt();
if dist > 1e-9 {
@ -122,9 +130,11 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckE
}
for i in 0..ctrl.len().saturating_sub(1) {
let a = ctrl[i];
let b = ctrl[i + 1];
tangents.push(TangentGeom::Line {
p1: ctrl[i],
p2: ctrl[i + 1],
p1: [a[0] as f32, a[1] as f32, a[2] as f32],
p2: [b[0] as f32, b[1] as f32, b[2] as f32],
});
}
}
@ -138,11 +148,11 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckE
} else {
*cp
};
let dx = (next.x - tip.x) as f32;
let dy = (next.y - tip.y) as f32;
let dx = next.x - tip.x;
let dy = next.y - tip.y;
let dl = (dx * dx + dy * dy).sqrt().max(1e-9);
let (dx, dy) = (dx / dl, dy / dl);
let a = std::f32::consts::PI / 6.0;
let a = std::f64::consts::PI / 6.0;
let (s, c) = a.sin_cos();
points.push(nan);
points.push([
@ -166,11 +176,7 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckE
let d = ml.dogleg_length;
points.push(nan);
points.push(cp_f);
points.push([
(cp.x + dir.x / dl * d) as f32,
(cp.y + dir.y / dl * d) as f32,
cp.z as f32,
]);
points.push([cp.x + dir.x / dl * d, cp.y + dir.y / dl * d, cp.z]);
}
}
@ -192,7 +198,7 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckE
let ins = &ctx.text_location;
let ins_x = ins.x;
let ins_y = ins.y;
let z = ins.z as f32;
let z = ins.z;
// Prefer text_direction (carries through rotations/mirrors); fall back
// to text_rotation when no direction has been set.
let td = ctx.text_direction;
@ -202,7 +208,7 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckE
ctx.text_rotation as f32
};
let (cos_r, sin_r) = (rot.cos(), rot.sin());
snap_pts.push(node([ins_x as f32, ins_y as f32, z]));
snap_pts.push(node([ins_x, ins_y, z]));
// Resolve text style via handle when available, falling back to STANDARD.
let style_name = ctx
@ -267,6 +273,10 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckE
let h_shift_local = -line_w * h_anchor;
let wcs_dx = h_shift_local * cos_r - line_y_local * sin_r;
let wcs_dy = h_shift_local * sin_r + line_y_local * cos_r;
// tessellate_text_ex emits f32 glyph points around `origin`.
// For entity-level WCS output, we accept the f32 cast here since
// glyph offsets are small relative to the insertion point and the
// entity path doesn't apply a world_offset before render.
let origin = [ins_x as f32 + wcs_dx, ins_y as f32 + wcs_dy];
let strokes = crate::scene::cxf::tessellate_text_ex(
origin,
@ -283,7 +293,7 @@ fn to_truck(ml: &MultiLeader, document: &acadrust::CadDocument) -> Option<TruckE
}
points.push(nan);
for &[x, y] in stroke {
points.push([x, y, z]);
points.push([x as f64, y as f64, z]);
}
}
}