From de37ee04bf278d971b7491785b818e7f8cdab141 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sat, 16 May 2026 19:32:36 +0300 Subject: [PATCH] fix(scene): viewport auto-fit fallback + f64-precision projection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three changes to make paper-space viewports render correctly on UTM-scale drawings whose viewports were saved with default view_target=(0, 0, 0) and a view_center pointing at empty WCS: 1. **Auto-fit fallback.** When the saved view's WCS rect doesn't overlap the IQR-bounded content cluster (`world_offset ± local_extent_max`), override `view_center` with `world_offset` and recompute `view_height` to fit the cluster into the paper viewport. Matches AutoCAD's silent auto-fit-on-open for stale / uninitialized viewports; without it those viewports rendered blank. 2. **f64 projection inner loop.** The previous f32 path computed `(wire_offset_rel - target_offset_rel).dot(view_right) - view_center` by subtracting values at ~5e6 magnitude (f32 ULP ~0.5 m) to land on a small paper offset. The cancellation produced centimetre-scale jitter on paper output even when the model was clean. Reconstruct the wire WCS coord, subtract the display center, and dot-project in f64; cast to f32 only at the final paper position where magnitudes are bounded by the viewport rect. 3. **examples/inspect_viewports.rs** — diagnostic that dumps every viewport's saved view fields (view_target, view_center, view_height vs paper height, custom_scale, status flags) plus drawing-level insertion_units and model-space extents. Used to verify that acadrust's DWG reader reads view_height correctly; the `view_height == vp.height` patterns observed on real files turn out to be genuine "default 1:1" file content rather than a reader bug (a round-trip test through DwgWriter / DwgReader survives all per-viewport values). Also: switch the `acadrust` patch from `HakanSeven12/acadrust@feat/ expose-blockrecord-is-loaded` (a stale branch with the abandoned is_loaded experiment) to `hakanaktt/acadrust@main`. Upstream main now carries the layout paper-dimensions and mirrored-arc fixes that this branch used to add — plus PR #20's DXF reading fixes that the old branch was missing. Co-Authored-By: Claude Opus 4.7 --- Cargo.lock | 2 +- Cargo.toml | 2 +- examples/inspect_viewports.rs | 126 +++++++++++++++++++++++++++++ src/scene/mod.rs | 144 ++++++++++++++++++++++++++++------ 4 files changed, 247 insertions(+), 27 deletions(-) create mode 100644 examples/inspect_viewports.rs diff --git a/Cargo.lock b/Cargo.lock index 8952111c..b7805e0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,7 +41,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" [[package]] name = "acadrust" version = "0.3.4" -source = "git+https://github.com/HakanSeven12/acadrust?branch=feat%2Fexpose-blockrecord-is-loaded#44ee57604af6f29a3f3ace05c2e49879fb8ae18a" +source = "git+https://github.com/hakanaktt/acadrust?branch=main#ee5f0db7f8159ea39db3c4a85dd58bec9aceb121" dependencies = [ "ahash", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index ac45d852..79e74462 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,4 +23,4 @@ rayon = "1" windows-sys = { version = "0.59", features = ["Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } [patch.crates-io] -acadrust = { git = "https://github.com/HakanSeven12/acadrust", branch = "feat/expose-blockrecord-is-loaded" } +acadrust = { git = "https://github.com/hakanaktt/acadrust", branch = "main" } diff --git a/examples/inspect_viewports.rs b/examples/inspect_viewports.rs new file mode 100644 index 00000000..328c5e0d --- /dev/null +++ b/examples/inspect_viewports.rs @@ -0,0 +1,126 @@ +// Dumps every paper-space viewport's view fields from a DWG/DXF. +// cargo run --release --example inspect_viewports -- +use acadrust::entities::EntityType; +use acadrust::io::dwg::DwgReader; + +fn main() -> Result<(), Box> { + let path = std::env::args().nth(1).expect("usage: inspect_viewports "); + let doc = DwgReader::from_file(&path)?.read()?; + println!("file: {}", path); + println!("total entities: {}", doc.entities().count()); + let unit_name = match doc.header.insertion_units { + 0 => "Unitless", + 1 => "Inches", + 2 => "Feet", + 3 => "Miles", + 4 => "Millimeters", + 5 => "Centimeters", + 6 => "Meters", + 7 => "Kilometers", + _ => "Other", + }; + println!( + "header.insertion_units = {} ({})", + doc.header.insertion_units, unit_name + ); + println!( + "header.model_space_extents min=({:.3}, {:.3}, {:.3}) max=({:.3}, {:.3}, {:.3})", + doc.header.model_space_extents_min.x, + doc.header.model_space_extents_min.y, + doc.header.model_space_extents_min.z, + doc.header.model_space_extents_max.x, + doc.header.model_space_extents_max.y, + doc.header.model_space_extents_max.z, + ); + println!(); + + let viewports: Vec<_> = doc + .entities() + .filter_map(|e| { + if let EntityType::Viewport(v) = e { + Some(v.clone()) + } else { + None + } + }) + .collect(); + println!("viewports: {}\n", viewports.len()); + + for (i, vp) in viewports.iter().enumerate() { + let scale_from_view_height = if vp.view_height.abs() > 1e-9 { + vp.height / vp.view_height + } else { + f64::NAN + }; + println!( + "[{}] id={} handle={:?}", + i, vp.id, vp.common.handle + ); + println!( + " paper center=({:.3}, {:.3}) size={:.3} × {:.3}", + vp.center.x, vp.center.y, vp.width, vp.height + ); + println!( + " view_target=({:.3}, {:.3}, {:.3})", + vp.view_target.x, vp.view_target.y, vp.view_target.z + ); + println!( + " view_direction=({:.3}, {:.3}, {:.3})", + vp.view_direction.x, vp.view_direction.y, vp.view_direction.z + ); + println!( + " view_center=({:.3}, {:.3})", + vp.view_center.x, vp.view_center.y + ); + println!( + " view_height={:.6} == vp.height? {} ⇒ height/view_height = {:.6}", + vp.view_height, + (vp.view_height - vp.height).abs() < 1e-6, + scale_from_view_height + ); + println!( + " custom_scale={:.6} twist={:.4} lens={:.2}", + vp.custom_scale, vp.twist_angle, vp.lens_length + ); + println!( + " status: on={}, locked={}, perspective={}", + vp.status.is_on, vp.status.locked, vp.status.perspective + ); + println!(); + } + + // Summary stats + let n_with_view_eq_height = viewports + .iter() + .filter(|v| (v.view_height - v.height).abs() < 1e-6) + .count(); + let n_with_target_zero = viewports + .iter() + .filter(|v| { + v.view_target.x.abs() < 1e-9 + && v.view_target.y.abs() < 1e-9 + && v.view_target.z.abs() < 1e-9 + }) + .count(); + let unique_view_heights: std::collections::BTreeSet = viewports + .iter() + .map(|v| v.view_height.to_bits()) + .collect(); + + println!("─────── summary ───────"); + println!( + " view_height == vp.height : {}/{}", + n_with_view_eq_height, + viewports.len() + ); + println!( + " view_target == (0, 0, 0) : {}/{}", + n_with_target_zero, + viewports.len() + ); + println!( + " unique view_height values : {}", + unique_view_heights.len() + ); + Ok(()) +} diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 6457b766..69aa2994 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1506,6 +1506,15 @@ impl Scene { } // Fallback: tessellate (first call or paper-space context). + // wire.key_vertices live in offset-rel coords (world_offset + // already subtracted at tessellation time). Add it back so the + // result matches Path 1 above and the caller's expectation — + // callers (auto_fit_viewport) write the centroid directly to + // `Viewport.view_target`, which is a WCS field; storing + // offset-rel coords there silently double-subtracts world_offset + // inside `camera_for_viewport` and points the viewport at the + // wrong location on UTM-scale drawings. + let oz = self.world_offset[2] as f32; for entity in self.document.entities() { let c = entity.common(); if c.owner_handle != model_block || c.invisible { @@ -1514,8 +1523,16 @@ impl Scene { for wire in self.tessellate_one(entity) { for &[x, y, z] in &wire.key_vertices { if x.is_finite() && y.is_finite() && z.is_finite() { - min = min.min(glam::Vec3::new(x, y, z)); - max = max.max(glam::Vec3::new(x, y, z)); + min = min.min(glam::Vec3::new( + x + ox as f32, + y + oy as f32, + z + oz, + )); + max = max.max(glam::Vec3::new( + x + ox as f32, + y + oy as f32, + z + oz, + )); any = true; } } @@ -1618,24 +1635,58 @@ impl Scene { let view_right = cam_frame.rotation * glam::Vec3::X; let view_up = cam_frame.rotation * glam::Vec3::Y; - // ── Scale & viewport parameters ─────────────────────────────── - // view_height is always correct; custom_scale is unreliable in DWG files - // (acadrust DWG reader never populates it, so it stays at 1.0). - let scale = if vp.view_height.abs() > 1e-9 { - (vp.height / vp.view_height) as f32 + // ── Scale, target, view_center — saved-view-then-fallback ───── + // + // Honor the file's saved view (view_target + view_center + + // view_height) whenever the WCS region it points at overlaps + // model content. Some DWG files (typical for AutoCAD + // viewports the user never explicitly panned/zoomed into) + // arrive with view_target = (0, 0, 0) and a stale view_center + // pointing at empty WCS — in that case AutoCAD silently + // auto-fits to the model on first display; mirror that so + // UTM-scale drawings don't open with blank viewports. + let mut effective_view_center = (vp.view_center.x, vp.view_center.y); + let mut effective_view_height = vp.view_height as f32; + + // Project the saved view's WCS rect and test against + // `world_offset`-centered IQR cluster (`local_extent_max`). + let saved_center_wcs_x = vp.view_target.x + vp.view_center.x; + let saved_center_wcs_y = vp.view_target.y + vp.view_center.y; + let saved_half_h = (effective_view_height as f64) * 0.5; + let saved_half_w = saved_half_h * (vp.width / vp.height.max(1.0)); + let cluster_half = self.local_extent_max.max(1.0) as f64; + let cluster_min_x = self.world_offset[0] - cluster_half; + let cluster_max_x = self.world_offset[0] + cluster_half; + let cluster_min_y = self.world_offset[1] - cluster_half; + let cluster_max_y = self.world_offset[1] + cluster_half; + let saved_overlaps = saved_center_wcs_x + saved_half_w >= cluster_min_x + && saved_center_wcs_x - saved_half_w <= cluster_max_x + && saved_center_wcs_y + saved_half_h >= cluster_min_y + && saved_center_wcs_y - saved_half_h <= cluster_max_y + && effective_view_height > 1e-9; + + if !saved_overlaps { + // Saved view points at empty space — auto-fit to the + // outlier-immune content cluster (world_offset ± + // local_extent_max). + let margin = 1.05_f64; + let fit_h = cluster_half * 2.0 * margin; + let fit_w = fit_h * (vp.width / vp.height.max(1.0)); + let scale_w = vp.width / fit_w; + let scale_h = vp.height / fit_h; + let fit_scale = scale_w.min(scale_h).max(1e-12); + effective_view_height = (vp.height / fit_scale) as f32; + effective_view_center = (self.world_offset[0], self.world_offset[1]); + } + + let scale = if effective_view_height.abs() > 1e-9 { + vp.height as f32 / effective_view_height } else if vp.custom_scale.abs() > 1e-9 { vp.custom_scale as f32 } else { 1.0 }; - // view_target is in raw model coords; wire points have world_offset - // subtracted, so bring target into the same wire-space. - let target = glam::Vec3::new( - (vp.view_target.x - self.world_offset[0]) as f32, - (vp.view_target.y - self.world_offset[1]) as f32, - (vp.view_target.z - self.world_offset[2]) as f32, - ); let pcx = vp.center.x as f32; let pcy = vp.center.y as f32; let pcz = vp.center.z as f32; @@ -1663,8 +1714,34 @@ impl Scene { let mut projected: Vec = Vec::new(); + // Precompute precision-stable WCS-space projection inputs in + // f64. The previous f32 inner loop suffered catastrophic + // cancellation on UTM-scale drawings: `(wire_offset_rel - + // target_offset_rel).dot(view_right) - view_center` is a + // small paper offset computed by subtracting two values at + // ~5e6 magnitude — f32 ULP there is ~0.5 m, so paper output + // jittered by cm even when the actual model was clean. + // + // Do everything WCS-relative in f64; cast to f32 only at the + // final paper position. + let display_center_x = vp.view_target.x + effective_view_center.0; + let display_center_y = vp.view_target.y + effective_view_center.1; + let display_center_z = vp.view_target.z; + let view_right_d = ( + view_right.x as f64, + view_right.y as f64, + view_right.z as f64, + ); + let view_up_d = (view_up.x as f64, view_up.y as f64, view_up.z as f64); + let view_fwd = cam_frame.rotation * glam::Vec3::Z; + let view_fwd_d = (view_fwd.x as f64, view_fwd.y as f64, view_fwd.z as f64); + let camera_dist_d = camera_dist as f64; + let scale_d = scale as f64; + let pcx_d = pcx as f64; + let pcy_d = pcy as f64; + let [wo_x, wo_y, wo_z] = self.world_offset; + for wire in model_wires.iter() { - // Project 3-D model points onto view plane → paper space. let projected_pts: Vec<[f32; 3]> = wire .points .iter() @@ -1672,21 +1749,38 @@ impl Scene { if mx.is_nan() || my.is_nan() || mz.is_nan() { return [f32::NAN; 3]; } - let mp = glam::Vec3::new(mx, my, mz) - target; - // view_center is the 2-D DCS offset of the display centre from - // view_target; subtract it so model origin maps to viewport centre. - let u = mp.dot(view_right) - vp.view_center.x as f32; - let v = mp.dot(view_up) - vp.view_center.y as f32; + // wire stored offset-rel; reconstruct WCS in f64 + // then subtract display center in WCS → small + // f64 mp_proj with full precision. + let mp_x = (mx as f64 + wo_x) - display_center_x; + let mp_y = (my as f64 + wo_y) - display_center_y; + let mp_z = (mz as f64 + wo_z) - display_center_z; + let u = mp_x * view_right_d.0 + + mp_y * view_right_d.1 + + mp_z * view_right_d.2; + let v = mp_x * view_up_d.0 + + mp_y * view_up_d.1 + + mp_z * view_up_d.2; if use_perspective { - let d_vd = mp.dot(cam_frame.rotation * glam::Vec3::Z); - let fwd = camera_dist - d_vd; + let d_vd = mp_x * view_fwd_d.0 + + mp_y * view_fwd_d.1 + + mp_z * view_fwd_d.2; + let fwd = camera_dist_d - d_vd; if fwd <= 0.001 { return [f32::NAN; 3]; } - let factor = camera_dist / fwd; - [pcx + u * factor * scale, pcy + v * factor * scale, pcz] + let factor = camera_dist_d / fwd; + [ + (pcx_d + u * factor * scale_d) as f32, + (pcy_d + v * factor * scale_d) as f32, + pcz, + ] } else { - [pcx + u * scale, pcy + v * scale, pcz] + [ + (pcx_d + u * scale_d) as f32, + (pcy_d + v * scale_d) as f32, + pcz, + ] } }) .collect();