fix(scene): viewport auto-fit fallback + f64-precision projection

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 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.7 committed 2026-05-16 19:32:36 +03:00
1 parent 68fe77c133
commit de37ee04bf
4 files changed
+247 -27

No files matched your search

+126
View File
@@ -0,0 +1,126 @@
// Dumps every paper-space viewport's view fields from a DWG/DXF.
// cargo run --release --example inspect_viewports -- <path>
use acadrust::entities::EntityType;
use acadrust::io::dwg::DwgReader;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args().nth(1).expect("usage: inspect_viewports <file>");
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<u64> = 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(())
}