forked from 1b-agent/opencadstudio-upstream
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:
1 parent
e38638502e
commit
a2fb334966
7 files changed
+200
-112
No files matched your search
+11
-7
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user