From a2fb334966e7222db306b37517ffba4d921d14b0 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 27 Apr 2026 07:41:38 +0300 Subject: [PATCH] fix: world_offset precision fix for large-coordinate DXF files (e.g. Turkish UTM) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/app/cmd_result.rs | 18 +-- src/app/update.rs | 1 + src/modules/insert/solid3d_cmds.rs | 4 +- src/scene/mod.rs | 58 ++++++--- src/scene/pipeline/wire_gpu.rs | 3 +- src/scene/tessellate.rs | 194 ++++++++++++++++++----------- src/scene/truck_tess.rs | 34 +++-- 7 files changed, 200 insertions(+), 112 deletions(-) diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index 843e9ac95..7efe94510 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -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 = 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 = (|| { 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, diff --git a/src/app/update.rs b/src/app/update.rs index 83cc67cb8..978ecc833 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -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(); diff --git a/src/modules/insert/solid3d_cmds.rs b/src/modules/insert/solid3d_cmds.rs index 4a334341b..3f8a1bf89 100644 --- a/src/modules/insert/solid3d_cmds.rs +++ b/src/modules/insert/solid3d_cmds.rs @@ -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 { - 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 { - 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, diff --git a/src/scene/mod.rs b/src/scene/mod.rs index c458d58ce..8cc60b1c4 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -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 = 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 { - 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, active_viewport: Option, + world_offset: [f64; 3], e: &EntityType, ) -> Vec { 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] diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index 54718d0c8..80de1ae8c 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -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]) diff --git a/src/scene/tessellate.rs b/src/scene/tessellate.rs index a87277c93..6c76e139f 100644 --- a/src/scene/tessellate.rs +++ b/src/scene/tessellate.rs @@ -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 { 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 { 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 { 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 { 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 { }) } -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 { diff --git a/src/scene/truck_tess.rs b/src/scene/truck_tess.rs index 90b659f29..2f5b23897 100644 --- a/src/scene/truck_tess.rs +++ b/src/scene/truck_tess.rs @@ -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