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 <noreply@anthropic.com>
This commit is contained in:
parent
68fe77c133
commit
de37ee04bf
4 changed files with 247 additions and 27 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
126
examples/inspect_viewports.rs
Normal file
126
examples/inspect_viewports.rs
Normal 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(())
|
||||
}
|
||||
144
src/scene/mod.rs
144
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<WireModel> = 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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue