fix: world_offset precision fix for large-coordinate DXF files (e.g. Turkish UTM)

Subtracts the DXF header extents centroid (world_offset) from all coordinates
in f64 BEFORE casting to f32. This eliminates the ~0.5 m ULP error that occurs
at 4,000,000 m UTM coordinates and caused geometry to disappear on zoom-out.

- Scene: adds world_offset field + compute_and_set_world_offset() method
- tessellate.rs: threads world_offset through all f64→f32 conversions
  (legacy_geometry, dimension_geometry, solid_wire_fallback, multileader)
- truck_tess.rs: to_local() helper already in place from prior session
- mod.rs: tessellate_entity() and entity_aabb() apply world_offset
- update.rs: compute_and_set_world_offset() called after doc load
- cmd_result.rs / solid3d_cmds.rs: pass correct offset to truck tessellators

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Sonnet 4.6 committed 2026-04-27 07:41:38 +03:00
1 parent e38638502e
commit a2fb334966
7 files changed
+200 -112

No files matched your search

+11 -7
View File
@@ -954,6 +954,7 @@ impl H7CAD {
let entity_opt = self.tabs[i].scene.document.get_entity(handle).cloned();
if let Some(entity) = entity_opt {
let truck_entity = entity.to_truck_entity(&self.tabs[i].scene.document);
let woff = self.tabs[i].scene.world_offset;
let result = truck_entity.and_then(|te| {
match te.object {
TruckObject::Contour(wire) => {
@@ -961,7 +962,7 @@ impl H7CAD {
let face = builder::try_attach_plane(&[wire]).ok()?;
// tsweep(Face) → Solid
let solid = builder::tsweep(&face, TruckVec3::new(0.0, 0.0, height as f64));
match truck_tess::tessellate_solid(&solid) {
match truck_tess::tessellate_solid(&solid, woff) {
truck_tess::TruckTessResult::Mesh { verts, normals, indices } => {
Some(crate::scene::mesh_model::MeshModel {
name: String::new(),
@@ -1008,6 +1009,7 @@ impl H7CAD {
let entity_opt = self.tabs[i].scene.document.get_entity(handle).cloned();
if let Some(entity) = entity_opt {
let truck_entity = entity.to_truck_entity(&self.tabs[i].scene.document);
let woff = self.tabs[i].scene.world_offset;
let result = truck_entity.and_then(|te| {
let wire: Option<truck_modeling::Wire> = match te.object {
TruckObject::Contour(w) => Some(w),
@@ -1023,7 +1025,7 @@ impl H7CAD {
let dir = (axis_end - axis_start).normalize();
let axis = TruckVec3::new(dir.x as f64, dir.z as f64, dir.y as f64);
let shell = builder::rsweep(&wire, origin, axis, Rad(angle_deg.to_radians() as f64));
match truck_tess::tessellate_shell(&shell) {
match truck_tess::tessellate_shell(&shell, woff) {
truck_tess::TruckTessResult::Mesh { verts, normals, indices } => {
Some(crate::scene::mesh_model::MeshModel {
name: String::new(),
@@ -1066,6 +1068,7 @@ impl H7CAD {
let profile_ent = self.tabs[i].scene.document.get_entity(profile_handle).cloned();
let path_ent = self.tabs[i].scene.document.get_entity(path_handle).cloned();
let woff = self.tabs[i].scene.world_offset;
let result = profile_ent.zip(path_ent).and_then(|(prof_e, path_e)| {
let prof_truck = prof_e.to_truck_entity(&self.tabs[i].scene.document)?;
@@ -1093,7 +1096,7 @@ impl H7CAD {
// wire we get a Solid, otherwise a Shell.
if let Ok(face) = builder::try_attach_plane(&[profile_wire.clone()]) {
let solid = builder::tsweep(&face, dir);
match truck_tess::tessellate_solid(&solid) {
match truck_tess::tessellate_solid(&solid, woff) {
truck_tess::TruckTessResult::Mesh { verts, normals, indices } =>
Some(crate::scene::mesh_model::MeshModel {
name: String::new(), verts, normals, indices, color, selected: false,
@@ -1102,7 +1105,7 @@ impl H7CAD {
}
} else {
let shell = builder::tsweep(&profile_wire, dir);
match truck_tess::tessellate_shell(&shell) {
match truck_tess::tessellate_shell(&shell, woff) {
truck_tess::TruckTessResult::Mesh { verts, normals, indices } =>
Some(crate::scene::mesh_model::MeshModel {
name: String::new(), verts, normals, indices, color, selected: false,
@@ -1126,7 +1129,7 @@ impl H7CAD {
);
if let Ok(face) = builder::try_attach_plane(&[profile_wire.clone()]) {
let solid = builder::tsweep(&face, dir);
match truck_tess::tessellate_solid(&solid) {
match truck_tess::tessellate_solid(&solid, woff) {
truck_tess::TruckTessResult::Mesh { verts, normals, indices } =>
Some(crate::scene::mesh_model::MeshModel {
name: String::new(), verts, normals, indices, color, selected: false,
@@ -1135,7 +1138,7 @@ impl H7CAD {
}
} else {
let shell = builder::tsweep(&profile_wire, dir);
match truck_tess::tessellate_shell(&shell) {
match truck_tess::tessellate_shell(&shell, woff) {
truck_tess::TruckTessResult::Mesh { verts, normals, indices } =>
Some(crate::scene::mesh_model::MeshModel {
name: String::new(), verts, normals, indices, color, selected: false,
@@ -1190,6 +1193,7 @@ impl H7CAD {
}
}
let woff = self.tabs[i].scene.world_offset;
let result: Option<crate::scene::mesh_model::MeshModel> = (|| {
if wires.len() < 2 { return None; }
@@ -1210,7 +1214,7 @@ impl H7CAD {
}
let shell = truck_modeling::Shell::from(all_faces);
match truck_tess::tessellate_shell(&shell) {
match truck_tess::tessellate_shell(&shell, woff) {
truck_tess::TruckTessResult::Mesh { verts, normals, indices } =>
Some(crate::scene::mesh_model::MeshModel {
name: String::new(), verts, normals, indices, color, selected: false,
+1
View File
@@ -73,6 +73,7 @@ impl H7CAD {
}
}
self.tabs[i].scene.compute_and_set_world_offset();
self.tabs[i].scene.populate_hatches_from_document();
self.tabs[i].scene.populate_images_from_document();
self.tabs[i].scene.populate_meshes_from_document();
+2 -2
View File
@@ -25,7 +25,7 @@ use crate::scene::truck_tess;
// ── Tessellation helper ────────────────────────────────────────────────────
fn solid_to_mesh(solid: &truck_modeling::Solid, color: [f32; 4], name: &str) -> Option<MeshModel> {
match truck_tess::tessellate_solid(solid) {
match truck_tess::tessellate_solid(solid, [0.0; 3]) {
truck_tess::TruckTessResult::Mesh { verts, normals, indices } => Some(MeshModel {
name: name.to_string(),
verts, normals, indices,
@@ -37,7 +37,7 @@ fn solid_to_mesh(solid: &truck_modeling::Solid, color: [f32; 4], name: &str) ->
}
fn shell_to_mesh(shell: &truck_modeling::Shell, color: [f32; 4], name: &str) -> Option<MeshModel> {
match truck_tess::tessellate_shell(shell) {
match truck_tess::tessellate_shell(shell, [0.0; 3]) {
truck_tess::TruckTessResult::Mesh { verts, normals, indices } => Some(MeshModel {
name: name.to_string(),
verts, normals, indices,
+43 -15
View File
@@ -123,6 +123,9 @@ pub struct Scene {
pub bg_color: [f32; 4],
/// Custom paper-space background fill color for Wipeout entities.
pub paper_bg_color: [f32; 4],
/// Scene centroid subtracted from all coordinates before f32 conversion.
/// Eliminates f32 precision loss at large world coordinates (e.g. UTM 4,000,000 m).
pub world_offset: [f64; 3],
}
impl Scene {
@@ -153,6 +156,7 @@ impl Scene {
active_viewport: None,
bg_color: [0.11, 0.11, 0.11, 1.0],
paper_bg_color: [1.0, 1.0, 1.0, 1.0],
world_offset: [0.0; 3],
}
}
@@ -160,6 +164,26 @@ impl Scene {
self.geometry_epoch = GEOMETRY_EPOCH.fetch_add(1, Ordering::Relaxed);
}
/// Compute scene centroid from DXF header extents and store as world_offset.
/// Must be called after `self.document` is set so all geometry is offset-corrected.
pub fn compute_and_set_world_offset(&mut self) {
let h = &self.document.header;
let min = h.model_space_extents_min;
let max = h.model_space_extents_max;
// Sentinel: DXF files with no geometry have EXTMIN = 1e20, EXTMAX = -1e20.
// Fall back to no offset so the scene stays at the origin.
let valid = min.x < max.x && min.y < max.y;
self.world_offset = if valid {
[
(min.x + max.x) * 0.5,
(min.y + max.y) * 0.5,
(min.z + max.z) * 0.5,
]
} else {
[0.0; 3]
};
}
/// Public accessor for the block-record handle of the current layout.
/// Used by external callers (e.g. `commit_entity`) that need the handle
/// without going through private API.
@@ -582,9 +606,10 @@ impl Scene {
let doc = &self.document;
let sel = &self.selected;
let avp = self.active_viewport;
let woff = self.world_offset;
let mut wires: Vec<WireModel> = visible
.into_par_iter()
.flat_map(|e| tessellate_entity(doc, sel, avp, e))
.flat_map(|e| tessellate_entity(doc, sel, avp, woff, e))
.collect();
// Apply draw order via the cached index (O(1) block lookup).
@@ -642,7 +667,7 @@ impl Scene {
/// Full tessellation pipeline for one entity.
fn tessellate_one(&self, e: &EntityType) -> Vec<WireModel> {
tessellate_entity(&self.document, &self.selected, self.active_viewport, e)
tessellate_entity(&self.document, &self.selected, self.active_viewport, self.world_offset, e)
}
fn model_space_block_handle(&self) -> Handle {
@@ -2638,6 +2663,7 @@ fn tessellate_entity(
document: &acadrust::CadDocument,
selected: &HashSet<Handle>,
active_viewport: Option<Handle>,
world_offset: [f64; 3],
e: &EntityType,
) -> Vec<WireModel> {
let h = e.common().handle;
@@ -2663,9 +2689,9 @@ fn tessellate_entity(
(0.0_f32, [0.0f32; 8])
};
let mut wire = tessellate::tessellate(
document, h, e, sel, color, pattern_length, pattern, 1.5,
document, h, e, sel, color, pattern_length, pattern, 1.5, world_offset,
);
wire.aabb = entity_aabb(e);
wire.aabb = entity_aabb(e, world_offset);
return vec![wire];
}
@@ -2675,9 +2701,9 @@ fn tessellate_entity(
let lt_name = render::linetype_name_for(document, e);
if let EntityType::Dimension(dim) = e {
let aabb = entity_aabb(e);
let aabb = entity_aabb(e, world_offset);
let mut wires = tessellate::tessellate_dimension(
document, h, dim, sel, entity_color, line_weight_px,
document, h, dim, sel, entity_color, line_weight_px, world_offset,
);
for w in &mut wires {
w.aci = aci;
@@ -2703,7 +2729,7 @@ fn tessellate_entity(
document, &sub,
ins_color, ins_pat_len, ins_pat, ins_lw_px,
);
let sub_aabb = entity_aabb(&sub);
let sub_aabb = entity_aabb(&sub, world_offset);
let mut wire = tessellate::tessellate(
document,
h,
@@ -2713,6 +2739,7 @@ fn tessellate_entity(
sub_pattern_length,
sub_pattern,
sub_line_weight_px,
world_offset,
);
wire.name = h.value().to_string();
wire.aci = sub_aci;
@@ -2722,9 +2749,9 @@ fn tessellate_entity(
.collect();
}
let aabb = entity_aabb(e);
let aabb = entity_aabb(e, world_offset);
let mut base = tessellate::tessellate(
document, h, e, sel, entity_color, pattern_length, pattern, line_weight_px,
document, h, e, sel, entity_color, pattern_length, pattern, line_weight_px, world_offset,
);
base.aci = aci;
base.aabb = aabb;
@@ -2748,15 +2775,16 @@ fn tessellate_entity(
vec![base]
}
fn entity_aabb(e: &acadrust::EntityType) -> [f32; 4] {
fn entity_aabb(e: &acadrust::EntityType, world_offset: [f64; 3]) -> [f32; 4] {
let bbox = e.as_entity().bounding_box();
let min_x = bbox.min.x as f32;
let min_y = bbox.min.y as f32;
let max_x = bbox.max.x as f32;
let max_y = bbox.max.y as f32;
let [ox, oy, _] = world_offset;
let min_x = (bbox.min.x - ox) as f32;
let min_y = (bbox.min.y - oy) as f32;
let max_x = (bbox.max.x - ox) as f32;
let max_y = (bbox.max.y - oy) as f32;
// A degenerate box (min == max == 0) means bounding_box() returned Default —
// use UNBOUNDED so the wire is never wrongly pre-rejected.
if min_x == 0.0 && min_y == 0.0 && max_x == 0.0 && max_y == 0.0 {
if min_x == max_x && min_y == max_y {
return WireModel::UNBOUNDED_AABB;
}
[min_x, min_y, max_x, max_y]
+1 -2
View File
@@ -119,8 +119,7 @@ impl WireGpu {
}
/// Creates a ghost copy with `alpha` applied on top of the wire's own alpha.
/// Used for the X-ray pass: selected wires rendered at reduced opacity through
/// occluding geometry.
#[allow(dead_code)]
pub fn new_ghost(device: &wgpu::Device, wire: &WireModel, alpha: f32) -> Self {
let [r, g, b, a] = wire.color;
Self::build(device, wire, [r, g, b, a * alpha])
+120 -74
View File
@@ -51,6 +51,7 @@ pub fn tessellate(
pattern_length: f32,
pattern: [f32; 8],
line_weight_px: f32,
world_offset: [f64; 3],
) -> WireModel {
let color = if selected {
WireModel::SELECTED
@@ -102,7 +103,7 @@ pub fn tessellate(
// ── Standard topology objects ─────────────────────────────────
TruckObject::Point(v) => {
let result = tessellate_vertex(&v);
let result = tessellate_vertex(&v, world_offset);
match result {
TruckTessResult::Point([x, y, z]) => {
let s = 0.1_f32;
@@ -131,7 +132,7 @@ pub fn tessellate(
}
TruckObject::Curve(e) => {
if let TruckTessResult::Lines(points) = tessellate_edge(&e) {
if let TruckTessResult::Lines(points) = tessellate_edge(&e, world_offset) {
return WireModel {
name,
points,
@@ -150,7 +151,7 @@ pub fn tessellate(
}
TruckObject::Contour(w) => {
if let TruckTessResult::Lines(points) = tessellate_wire(&w) {
if let TruckTessResult::Lines(points) = tessellate_wire(&w, world_offset) {
return WireModel {
name,
points,
@@ -190,14 +191,14 @@ pub fn tessellate(
// As a wire fallback, render the pre-computed edge wires
// stored in the entity when present (e.g. from SOLVIEW output
// or when the SAT kernel cannot parse the ACIS data).
let wire_pts = solid_wire_fallback(entity);
let wire_pts = solid_wire_fallback(entity, world_offset);
return WireModel::solid(name, wire_pts, color, selected);
}
}
}
// ── Legacy fallback for Viewport and other unhandled types ────────────
let (points, snap_pts, tangent_geoms, key_vertices) = legacy_geometry(entity);
let (points, snap_pts, tangent_geoms, key_vertices) = legacy_geometry(entity, world_offset);
WireModel {
name,
points,
@@ -221,6 +222,7 @@ pub fn tessellate_dimension(
selected: bool,
entity_color: [f32; 4],
line_weight_px: f32,
world_offset: [f64; 3],
) -> Vec<WireModel> {
let color = if selected {
WireModel::SELECTED
@@ -243,7 +245,7 @@ pub fn tessellate_dimension(
)
})
.unwrap_or((0.12, 0.0, 0.0));
let points = dimension_geometry(dim, arrow_size, dimexo, dimexe);
let points = dimension_geometry(dim, arrow_size, dimexo, dimexe, world_offset);
let key_vertices = points
.iter()
.copied()
@@ -275,6 +277,7 @@ pub fn tessellate_dimension(
0.0,
[0.0; 8],
line_weight_px,
world_offset,
);
wire.name = name;
wires.push(wire);
@@ -390,13 +393,15 @@ fn tessellate_multileader(
selected: bool,
entity_color: [f32; 4],
line_weight_px: f32,
world_offset: [f64; 3],
) -> Vec<WireModel> {
let color = if selected { WireModel::SELECTED } else { entity_color };
let name = handle.value().to_string();
let nan = [f32::NAN; 3];
let [ox, oy, oz] = world_offset;
let to_f32 = |v: &acadrust::types::Vector3| -> [f32; 3] {
[v.x as f32, v.y as f32, v.z as f32]
[(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32]
};
let arrow_size = ml.arrowhead_size as f32;
@@ -462,9 +467,9 @@ fn tessellate_multileader(
let dlen = (dir.x * dir.x + dir.y * dir.y).sqrt().max(1e-9);
let dl = ml.dogleg_length;
let end = [
(cp.x + dir.x / dlen * dl) as f32,
(cp.y + dir.y / dlen * dl) as f32,
cp.z as f32,
(cp.x + dir.x / dlen * dl - ox) as f32,
(cp.y + dir.y / dlen * dl - oy) as f32,
(cp.z - oz) as f32,
];
points.push(nan);
points.push(cp_f);
@@ -502,7 +507,7 @@ fn tessellate_multileader(
mtext.common.layer = ml.common.layer.clone();
let mut w = tessellate(
document, handle, &EntityType::MText(mtext),
selected, entity_color, 0.0, [0.0; 8], line_weight_px,
selected, entity_color, 0.0, [0.0; 8], line_weight_px, world_offset,
);
w.name = name;
wires.push(w);
@@ -519,10 +524,11 @@ pub fn tessellate_mesh(
entity: &EntityType,
selected: bool,
color: [f32; 4],
world_offset: [f64; 3],
) -> Option<MeshModel> {
let te = convert(entity, document)?;
let result = match te.object {
TruckObject::Volume(solid) => tessellate_solid(&solid),
TruckObject::Volume(solid) => tessellate_solid(&solid, world_offset),
_ => return None,
};
truck_tess::tess_to_mesh_model(
@@ -553,12 +559,13 @@ type Geometry = (
Vec<[f32; 3]>,
);
fn legacy_geometry(entity: &EntityType) -> Geometry {
fn legacy_geometry(entity: &EntityType, world_offset: [f64; 3]) -> Geometry {
let [ox, oy, oz] = world_offset;
match entity {
EntityType::Viewport(vp) => {
let cx = vp.center.x as f32;
let cy = vp.center.y as f32;
let cz = vp.center.z as f32;
let cx = (vp.center.x - ox) as f32;
let cy = (vp.center.y - oy) as f32;
let cz = (vp.center.z - oz) as f32;
let hw = (vp.width / 2.0) as f32;
let hh = (vp.height / 2.0) as f32;
let pts = vec![
@@ -572,9 +579,9 @@ fn legacy_geometry(entity: &EntityType) -> Geometry {
}
EntityType::Insert(ins) => {
let ip = Vec3::new(
ins.insert_point.x as f32,
ins.insert_point.y as f32,
ins.insert_point.z as f32,
(ins.insert_point.x - ox) as f32,
(ins.insert_point.y - oy) as f32,
(ins.insert_point.z - oz) as f32,
);
let s = 0.1_f32;
let pts = vec![
@@ -586,7 +593,7 @@ fn legacy_geometry(entity: &EntityType) -> Geometry {
(pts, vec![(ip, SnapHint::Insertion)], vec![], vec![])
}
EntityType::Hatch(h) => {
let elev = h.elevation as f32;
let elev = (h.elevation - oz) as f32;
let mut pts: Vec<[f32; 3]> = Vec::new();
let mut key_verts: Vec<[f32; 3]> = Vec::new();
for path in &h.paths {
@@ -595,7 +602,7 @@ fn legacy_geometry(entity: &EntityType) -> Geometry {
acadrust::entities::BoundaryEdge::Polyline(poly) => {
let start_idx = pts.len();
for v in &poly.vertices {
let p = [v.x as f32, v.y as f32, elev];
let p = [(v.x - ox) as f32, (v.y - oy) as f32, elev];
pts.push(p);
key_verts.push(p);
}
@@ -604,8 +611,8 @@ fn legacy_geometry(entity: &EntityType) -> Geometry {
}
}
acadrust::entities::BoundaryEdge::Line(ln) => {
let p0 = [ln.start.x as f32, ln.start.y as f32, elev];
let p1 = [ln.end.x as f32, ln.end.y as f32, elev];
let p0 = [(ln.start.x - ox) as f32, (ln.start.y - oy) as f32, elev];
let p1 = [(ln.end.x - ox) as f32, (ln.end.y - oy) as f32, elev];
pts.push(p0);
pts.push(p1);
key_verts.push(p0);
@@ -623,11 +630,11 @@ fn legacy_geometry(entity: &EntityType) -> Geometry {
EntityType::Ole2Frame(ole) => {
// OLE objects carry a bounding rectangle in model space.
// Render a simple X-through-rectangle placeholder.
let x0 = ole.upper_left_corner.x as f32;
let y0 = ole.lower_right_corner.y as f32;
let x1 = ole.lower_right_corner.x as f32;
let y1 = ole.upper_left_corner.y as f32;
let z = ole.upper_left_corner.z as f32;
let x0 = (ole.upper_left_corner.x - ox) as f32;
let y0 = (ole.lower_right_corner.y - oy) as f32;
let x1 = (ole.lower_right_corner.x - ox) as f32;
let y1 = (ole.upper_left_corner.y - oy) as f32;
let z = (ole.upper_left_corner.z - oz) as f32;
if (x1 - x0).abs() < 1e-6 && (y1 - y0).abs() < 1e-6 {
// Degenerate / unknown size — show a small cross.
let s = 0.5_f32;
@@ -656,7 +663,8 @@ fn legacy_geometry(entity: &EntityType) -> Geometry {
/// AutoCAD stores explicit wire geometry (from SOLVIEW / 3DPLOT) alongside the
/// ACIS data. We use this as a visible fallback when the SAT tessellator
/// produces no mesh (e.g. binary SAB data or unsupported geometry).
fn solid_wire_fallback(entity: &EntityType) -> Vec<[f32; 3]> {
fn solid_wire_fallback(entity: &EntityType, world_offset: [f64; 3]) -> Vec<[f32; 3]> {
let [ox, oy, oz] = world_offset;
let wires: &[acadrust::entities::Wire] = match entity {
EntityType::Solid3D(s) => &s.wires,
EntityType::Region(r) => &r.wires,
@@ -673,12 +681,8 @@ fn solid_wire_fallback(entity: &EntityType) -> Vec<[f32; 3]> {
if wire.points.len() < 2 {
continue;
}
for (i, v) in wire.points.iter().enumerate() {
if i > 0 {
// Connect segments: repeat previous point then add current so
// the wire renderer draws a continuous polyline per wire.
}
pts.push([v.x as f32, v.y as f32, v.z as f32]);
for v in &wire.points {
pts.push([(v.x - ox) as f32, (v.y - oy) as f32, (v.z - oz) as f32]);
}
// NaN sentinel separates distinct wire segments.
pts.push([f32::NAN, f32::NAN, f32::NAN]);
@@ -686,34 +690,41 @@ fn solid_wire_fallback(entity: &EntityType) -> Vec<[f32; 3]> {
pts
}
fn dimension_geometry(dim: &Dimension, arrow_size: f32, dimexo: f32, dimexe: f32) -> Vec<[f32; 3]> {
fn dimension_geometry(
dim: &Dimension,
arrow_size: f32,
dimexo: f32,
dimexe: f32,
world_offset: [f64; 3],
) -> Vec<[f32; 3]> {
let lv = |v| vec3_local(v, world_offset);
let mut points = Vec::new();
match dim {
Dimension::Aligned(d) => {
let first = vec3(d.first_point);
let second = vec3(d.second_point);
let def = vec3(d.definition_point);
let first = lv(d.first_point);
let second = lv(d.second_point);
let def = lv(d.definition_point);
let axis = normalized_or(second - first, Vec3::X);
append_linear_dimension(&mut points, first, second, def, axis, arrow_size, dimexo, dimexe);
}
Dimension::Linear(d) => {
let first = vec3(d.first_point);
let second = vec3(d.second_point);
let def = vec3(d.definition_point);
let first = lv(d.first_point);
let second = lv(d.second_point);
let def = lv(d.definition_point);
let axis = Vec3::new(d.rotation.cos() as f32, d.rotation.sin() as f32, 0.0);
append_linear_dimension(&mut points, first, second, def, normalized_or(axis, Vec3::X), arrow_size, dimexo, dimexe);
}
Dimension::Radius(d) => {
let center = vec3(d.angle_vertex);
let point = vec3(d.definition_point);
let text = dimension_text_position(dim);
let center = lv(d.angle_vertex);
let point = lv(d.definition_point);
let text = dimension_text_position(dim, world_offset);
add_segment(&mut points, center, point);
add_segment(&mut points, point, text);
append_arrow(&mut points, point, normalized_or(center - point, Vec3::X), arrow_size);
}
Dimension::Diameter(d) => {
let p1 = vec3(d.angle_vertex);
let p2 = vec3(d.definition_point);
let p1 = lv(d.angle_vertex);
let p2 = lv(d.definition_point);
add_segment(&mut points, p1, p2);
append_arrow(&mut points, p1, normalized_or(p2 - p1, Vec3::X), arrow_size);
append_arrow(&mut points, p2, normalized_or(p1 - p2, Vec3::X), arrow_size);
@@ -721,26 +732,26 @@ fn dimension_geometry(dim: &Dimension, arrow_size: f32, dimexo: f32, dimexe: f32
Dimension::Angular2Ln(d) => {
append_angular_dimension(
&mut points,
vec3(d.angle_vertex),
vec3(d.first_point),
vec3(d.second_point),
vec3(d.dimension_arc),
lv(d.angle_vertex),
lv(d.first_point),
lv(d.second_point),
lv(d.dimension_arc),
arrow_size,
);
}
Dimension::Angular3Pt(d) => {
append_angular_dimension(
&mut points,
vec3(d.angle_vertex),
vec3(d.first_point),
vec3(d.second_point),
vec3(d.definition_point),
lv(d.angle_vertex),
lv(d.first_point),
lv(d.second_point),
lv(d.definition_point),
arrow_size,
);
}
Dimension::Ordinate(d) => {
add_segment(&mut points, vec3(d.feature_location), vec3(d.definition_point));
add_segment(&mut points, vec3(d.definition_point), vec3(d.leader_endpoint));
add_segment(&mut points, lv(d.feature_location), lv(d.definition_point));
add_segment(&mut points, lv(d.definition_point), lv(d.leader_endpoint));
}
}
points
@@ -859,18 +870,17 @@ fn add_polyline(points: &mut Vec<[f32; 3]>, polyline: &[Vec3]) {
fn dimension_text_entity(dim: &Dimension) -> Option<Text> {
let value = dimension_text_value(dim)?;
let pos = dimension_text_position(dim);
// Use f64 position directly to avoid f32 round-trip precision loss at large
// coordinates (e.g. Turkish UTM ~4,000,000 m). tessellate() will apply
// world_offset when rendering this synthetic Text entity.
let pos_f64 = dimension_text_pos_f64(dim);
let base = dim.base();
// acadrust's DXF reader never parses group code 53 (text rotation), so
// base.text_rotation is always 0 for DXF files. Fall back to the natural
// axis-aligned rotation derived from geometry; only use the stored value
// when it represents a genuine user override (non-zero).
let rotation = if base.text_rotation.abs() > 1e-9 {
base.text_rotation
} else {
dimension_text_natural_rotation(dim)
};
let mut text = Text::with_value(value, Vector3::new(pos.x as f64, pos.y as f64, pos.z as f64))
let mut text = Text::with_value(value, pos_f64)
.with_height(dimension_text_height(dim))
.with_rotation(rotation);
text.style = base.style_name.clone();
@@ -919,20 +929,21 @@ fn dimension_text_value(dim: &Dimension) -> Option<String> {
})
}
fn dimension_text_position(dim: &Dimension) -> Vec3 {
fn dimension_text_position(dim: &Dimension, world_offset: [f64; 3]) -> Vec3 {
let lv = |v| vec3_local(v, world_offset);
let base = dim.base();
let pos = vec3(base.text_middle_point);
let pos = lv(base.text_middle_point);
if pos.length_squared() > 1e-8 {
return pos;
}
match dim {
Dimension::Aligned(d) => (vec3(d.first_point) + vec3(d.second_point)) * 0.5,
Dimension::Linear(d) => (vec3(d.first_point) + vec3(d.second_point)) * 0.5,
Dimension::Radius(d) => (vec3(d.angle_vertex) + vec3(d.definition_point)) * 0.5,
Dimension::Diameter(d) => (vec3(d.angle_vertex) + vec3(d.definition_point)) * 0.5,
Dimension::Angular2Ln(d) => vec3(d.dimension_arc),
Dimension::Angular3Pt(d) => vec3(d.definition_point),
Dimension::Ordinate(d) => vec3(d.leader_endpoint),
Dimension::Aligned(d) => (lv(d.first_point) + lv(d.second_point)) * 0.5,
Dimension::Linear(d) => (lv(d.first_point) + lv(d.second_point)) * 0.5,
Dimension::Radius(d) => (lv(d.angle_vertex) + lv(d.definition_point)) * 0.5,
Dimension::Diameter(d) => (lv(d.angle_vertex) + lv(d.definition_point)) * 0.5,
Dimension::Angular2Ln(d) => lv(d.dimension_arc),
Dimension::Angular3Pt(d) => lv(d.definition_point),
Dimension::Ordinate(d) => lv(d.leader_endpoint),
}
}
@@ -941,8 +952,43 @@ fn dimension_text_height(dim: &Dimension) -> f64 {
if scale.is_finite() { scale } else { 0.25 }
}
fn vec3(v: Vector3) -> Vec3 {
Vec3::new(v.x as f32, v.y as f32, v.z as f32)
fn vec3_local(v: Vector3, off: [f64; 3]) -> Vec3 {
Vec3::new((v.x - off[0]) as f32, (v.y - off[1]) as f32, (v.z - off[2]) as f32)
}
/// Returns the text position of a dimension in DXF world-space (f64, no offset applied).
/// Used when building a synthetic Text entity so tessellate() can apply world_offset itself.
fn dimension_text_pos_f64(dim: &Dimension) -> Vector3 {
let base = dim.base();
let p = base.text_middle_point;
if p.x * p.x + p.y * p.y + p.z * p.z > 1e-16 {
return p;
}
match dim {
Dimension::Aligned(d) => Vector3::new(
(d.first_point.x + d.second_point.x) * 0.5,
(d.first_point.y + d.second_point.y) * 0.5,
(d.first_point.z + d.second_point.z) * 0.5,
),
Dimension::Linear(d) => Vector3::new(
(d.first_point.x + d.second_point.x) * 0.5,
(d.first_point.y + d.second_point.y) * 0.5,
(d.first_point.z + d.second_point.z) * 0.5,
),
Dimension::Radius(d) => Vector3::new(
(d.angle_vertex.x + d.definition_point.x) * 0.5,
(d.angle_vertex.y + d.definition_point.y) * 0.5,
(d.angle_vertex.z + d.definition_point.z) * 0.5,
),
Dimension::Diameter(d) => Vector3::new(
(d.angle_vertex.x + d.definition_point.x) * 0.5,
(d.angle_vertex.y + d.definition_point.y) * 0.5,
(d.angle_vertex.z + d.definition_point.z) * 0.5,
),
Dimension::Angular2Ln(d) => d.dimension_arc,
Dimension::Angular3Pt(d) => d.definition_point,
Dimension::Ordinate(d) => d.leader_endpoint,
}
}
fn normalized_or(v: Vec3, fallback: Vec3) -> Vec3 {
+22 -12
View File
@@ -37,16 +37,26 @@ pub enum TruckTessResult {
},
}
// ── Shared offset helper ──────────────────────────────────────────────────
/// Convert a truck f64 point to local f32 by subtracting world_offset in f64
/// before truncating. This preserves sub-unit decimal precision even when the
/// raw world coordinates are in the millions (e.g. Turkish UTM).
#[inline]
fn to_local(x: f64, y: f64, z: f64, off: [f64; 3]) -> [f32; 3] {
[(x - off[0]) as f32, (y - off[1]) as f32, (z - off[2]) as f32]
}
// ── Vertex ────────────────────────────────────────────────────────────────
pub fn tessellate_vertex(v: &Vertex) -> TruckTessResult {
pub fn tessellate_vertex(v: &Vertex, offset: [f64; 3]) -> TruckTessResult {
let p = v.point();
TruckTessResult::Point([p.x as f32, p.y as f32, p.z as f32])
TruckTessResult::Point(to_local(p.x, p.y, p.z, offset))
}
// ── Edge ──────────────────────────────────────────────────────────────────
pub fn tessellate_edge(e: &Edge) -> TruckTessResult {
pub fn tessellate_edge(e: &Edge, offset: [f64; 3]) -> TruckTessResult {
// oriented_curve() respects the edge direction (inverts if needed).
let curve = e.oriented_curve();
let (t0, t1) = curve.range_tuple();
@@ -55,17 +65,17 @@ pub fn tessellate_edge(e: &Edge) -> TruckTessResult {
let (_, pts) = curve.parameter_division((t0, t1), CURVE_TOL);
let lines = pts
.iter()
.map(|p| [p.x as f32, p.y as f32, p.z as f32])
.map(|p| to_local(p.x, p.y, p.z, offset))
.collect();
TruckTessResult::Lines(lines)
}
// ── Wire ──────────────────────────────────────────────────────────────────
pub fn tessellate_wire(w: &Wire) -> TruckTessResult {
pub fn tessellate_wire(w: &Wire, offset: [f64; 3]) -> TruckTessResult {
let mut pts: Vec<[f32; 3]> = Vec::new();
for edge in w.edge_iter() {
if let TruckTessResult::Lines(ep) = tessellate_edge(edge) {
if let TruckTessResult::Lines(ep) = tessellate_edge(edge, offset) {
if pts.is_empty() {
pts = ep;
} else {
@@ -81,17 +91,17 @@ pub fn tessellate_wire(w: &Wire) -> TruckTessResult {
// ── Shell ─────────────────────────────────────────────────────────────────
#[allow(dead_code)]
pub fn tessellate_shell(s: &Shell) -> TruckTessResult {
pub fn tessellate_shell(s: &Shell, offset: [f64; 3]) -> TruckTessResult {
let meshed = s.triangulation(MESH_TOL);
polygon_to_result(meshed.to_polygon())
polygon_to_result(meshed.to_polygon(), offset)
}
// ── Solid ─────────────────────────────────────────────────────────────────
#[allow(dead_code)]
pub fn tessellate_solid(s: &Solid) -> TruckTessResult {
pub fn tessellate_solid(s: &Solid, offset: [f64; 3]) -> TruckTessResult {
let meshed = s.triangulation(MESH_TOL);
polygon_to_result(meshed.to_polygon())
polygon_to_result(meshed.to_polygon(), offset)
}
// ── MeshModel helper ──────────────────────────────────────────────────────
@@ -122,11 +132,11 @@ pub fn tess_to_mesh_model(
// ── Internal ─────────────────────────────────────────────────────────────
fn polygon_to_result(mesh: PolygonMesh) -> TruckTessResult {
fn polygon_to_result(mesh: PolygonMesh, offset: [f64; 3]) -> TruckTessResult {
let verts: Vec<[f32; 3]> = mesh
.positions()
.iter()
.map(|p| [p.x as f32, p.y as f32, p.z as f32])
.map(|p| to_local(p.x, p.y, p.z, offset))
.collect();
// Per-vertex normals: if the mesh has normals, map each triangle vertex's