fix(viewport): UTM auto-fit + mesh world_offset
Two world_offset bugs in viewport rendering. (1) Solid3D / Region / Body meshes were tessellated straight out of the ACIS SAT data in WCS, while the wire / hatch / face3d pipelines all run in `(WCS - world_offset)` local space. At UTM scale the result was triangles drifting millions of units off-camera every frame. `offset_mesh_lod_set` post-processes the freshly built `MeshLodSet`, subtracting `world_offset` from every vertex and recomputing `world_aabb` so the LOD / cull math agrees with the shared camera space. Applied at the three `tessellate_volume` call sites — load, `add_entity`, and `populate_meshes_from_document`. (2) Paper-space content viewports rendered blank on UTM drawings. The DWG / DXF saved `view_target = (0, 0, 0)` while the model sat around `world_offset`; the CPU projection in `viewport_content_wires` already had a "saved view doesn't overlap content cluster → auto-fit to (world_offset ± local_extent_max)" fallback, but the GPU path read `vp.view_target` literally and culled every entity against the empty WCS rect. `camera_for_viewport` now applies the same overlap test + auto-fit, and `model_wires_for_viewport` derives its frustum AABB / wpp from the resulting camera so both the camera and the cull agree on which area to show. Folding `view_center` directly into `effective_target_wcs` matches the CPU path's `display_center_x = view_target + view_center`, removing the prior `+ view_right * view_center.x` shift that would double-count once auto-fit had overwritten the target.
This commit is contained in:
parent
1eb54ee8a3
commit
74a685d0e8
1 changed files with 93 additions and 29 deletions
122
src/scene/mod.rs
122
src/scene/mod.rs
|
|
@ -216,7 +216,7 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
||||||
let e = doc.get_entity(handle)?;
|
let e = doc.get_entity(handle)?;
|
||||||
let color = tess_util::aci_to_rgba(&e.common().color);
|
let color = tess_util::aci_to_rgba(&e.common().color);
|
||||||
crate::entities::solid3d::tessellate_volume(e, color, facet_res)
|
crate::entities::solid3d::tessellate_volume(e, color, facet_res)
|
||||||
.map(|m| (handle, m))
|
.map(|m| (handle, offset_mesh_lod_set(m, world_offset)))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
@ -477,6 +477,37 @@ fn overlap_len(a: (f32, f32), b: (f32, f32)) -> f32 {
|
||||||
(a.1.min(b.1) - a.0.max(b.0)).max(0.0)
|
(a.1.min(b.1) - a.0.max(b.0)).max(0.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shift every vertex of a freshly tessellated `MeshLodSet` into the
|
||||||
|
/// scene's local f32 space by subtracting `world_offset`. ACIS / SAT
|
||||||
|
/// tessellation hands us WCS coordinates; the wire / hatch / face3d
|
||||||
|
/// paths run in `(WCS - world_offset)` so meshes at large UTM-scale
|
||||||
|
/// origins would otherwise float far away from the rest of the
|
||||||
|
/// geometry. Also recomputes `world_aabb` so per-frame LOD / cull math
|
||||||
|
/// uses the same space.
|
||||||
|
fn offset_mesh_lod_set(mut set: MeshLodSet, world_offset: [f64; 3]) -> MeshLodSet {
|
||||||
|
let [ox, oy, oz] = world_offset;
|
||||||
|
let (fx, fy, fz) = (ox as f32, oy as f32, oz as f32);
|
||||||
|
let mut min_x = f32::INFINITY;
|
||||||
|
let mut min_y = f32::INFINITY;
|
||||||
|
let mut max_x = f32::NEG_INFINITY;
|
||||||
|
let mut max_y = f32::NEG_INFINITY;
|
||||||
|
for lod in &mut set.lods {
|
||||||
|
for v in &mut lod.verts {
|
||||||
|
v[0] -= fx;
|
||||||
|
v[1] -= fy;
|
||||||
|
v[2] -= fz;
|
||||||
|
if v[0] < min_x { min_x = v[0]; }
|
||||||
|
if v[1] < min_y { min_y = v[1]; }
|
||||||
|
if v[0] > max_x { max_x = v[0]; }
|
||||||
|
if v[1] > max_y { max_y = v[1]; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if min_x.is_finite() {
|
||||||
|
set.world_aabb = [min_x, min_y, max_x, max_y];
|
||||||
|
}
|
||||||
|
set
|
||||||
|
}
|
||||||
|
|
||||||
/// Stable hash of a `Camera`'s pose for use as a per-tile cache key.
|
/// Stable hash of a `Camera`'s pose for use as a per-tile cache key.
|
||||||
/// Two cameras with bit-identical target / rotation / distance hash the
|
/// Two cameras with bit-identical target / rotation / distance hash the
|
||||||
/// same; any orbit / pan / zoom on a tile bumps it.
|
/// same; any orbit / pan / zoom on a tile bumps it.
|
||||||
|
|
@ -2624,7 +2655,9 @@ impl Scene {
|
||||||
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_)
|
EntityType::Solid3D(_) | EntityType::Region(_) | EntityType::Body(_)
|
||||||
) {
|
) {
|
||||||
let color = self.render_style(&entity).0;
|
let color = self.render_style(&entity).0;
|
||||||
|
let woff = self.world_offset;
|
||||||
crate::entities::solid3d::tessellate_volume(&entity, color, facet_res)
|
crate::entities::solid3d::tessellate_volume(&entity, color, facet_res)
|
||||||
|
.map(|m| offset_mesh_lod_set(m, woff))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
@ -3463,11 +3496,12 @@ impl Scene {
|
||||||
|
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
let facet_res = self.document.header.facet_resolution;
|
let facet_res = self.document.header.facet_resolution;
|
||||||
|
let woff = self.world_offset;
|
||||||
self.meshes = entries
|
self.meshes = entries
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.filter_map(|(handle, entity, color)| {
|
.filter_map(|(handle, entity, color)| {
|
||||||
crate::entities::solid3d::tessellate_volume(&entity, color, facet_res)
|
crate::entities::solid3d::tessellate_volume(&entity, color, facet_res)
|
||||||
.map(|m| (handle, m))
|
.map(|m| (handle, offset_mesh_lod_set(m, woff)))
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
@ -5275,23 +5309,55 @@ impl Scene {
|
||||||
};
|
};
|
||||||
|
|
||||||
let rotation = camera::yaw_pitch_to_quat(yaw, pitch, 0.0);
|
let rotation = camera::yaw_pitch_to_quat(yaw, pitch, 0.0);
|
||||||
let view_right = rotation * glam::Vec3::X;
|
|
||||||
let view_up = rotation * glam::Vec3::Y;
|
|
||||||
|
|
||||||
// view_target is in raw model/WCS coords; the GPU renderer works in
|
// view_target is in raw model/WCS coords; the GPU renderer works in
|
||||||
// wire-space (model - world_offset), so subtract world_offset here.
|
// wire-space (model - world_offset), so subtract world_offset here.
|
||||||
// view_center is a 2-D DCS offset of the display centre from view_target.
|
// view_center is a 2-D DCS offset of the display centre from view_target.
|
||||||
|
//
|
||||||
|
// UTM / coordinate-shifted drawings often arrive with
|
||||||
|
// `view_target = (0, 0, 0)` and a stale `view_center` from before
|
||||||
|
// the file was geo-referenced; the saved view points at empty
|
||||||
|
// WCS while the actual model sits ~`world_offset` away. The CPU
|
||||||
|
// projection path in `viewport_content_wires` already auto-fits
|
||||||
|
// to the content cluster in that case — apply the same fallback
|
||||||
|
// here so the GPU-rendered viewport shows the model instead of
|
||||||
|
// an empty rectangle.
|
||||||
|
let mut effective_target_wcs = (
|
||||||
|
vp.view_target.x + vp.view_center.x,
|
||||||
|
vp.view_target.y + vp.view_center.y,
|
||||||
|
);
|
||||||
|
let mut effective_view_height = vp.view_height.abs().max(1e-9);
|
||||||
|
let aspect_d = (vp.width / vp.height.max(1.0)).max(1e-9);
|
||||||
|
let cluster_half = self.local_extent_max.max(1.0) as f64;
|
||||||
|
let saved_half_h = effective_view_height * 0.5;
|
||||||
|
let saved_half_w = saved_half_h * aspect_d;
|
||||||
|
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 = effective_target_wcs.0 + saved_half_w >= cluster_min_x
|
||||||
|
&& effective_target_wcs.0 - saved_half_w <= cluster_max_x
|
||||||
|
&& effective_target_wcs.1 + saved_half_h >= cluster_min_y
|
||||||
|
&& effective_target_wcs.1 - saved_half_h <= cluster_max_y;
|
||||||
|
if !saved_overlaps {
|
||||||
|
let margin = 1.05_f64;
|
||||||
|
effective_view_height = cluster_half * 2.0 * margin;
|
||||||
|
effective_target_wcs = (self.world_offset[0], self.world_offset[1]);
|
||||||
|
}
|
||||||
|
|
||||||
let base_target = glam::Vec3::new(
|
let base_target = glam::Vec3::new(
|
||||||
(vp.view_target.x - self.world_offset[0]) as f32,
|
(effective_target_wcs.0 - self.world_offset[0]) as f32,
|
||||||
(vp.view_target.y - self.world_offset[1]) as f32,
|
(effective_target_wcs.1 - self.world_offset[1]) as f32,
|
||||||
(vp.view_target.z - self.world_offset[2]) as f32,
|
(vp.view_target.z - self.world_offset[2]) as f32,
|
||||||
);
|
);
|
||||||
let target =
|
// `effective_target_wcs` already folded `view_center` in above
|
||||||
base_target + view_right * vp.view_center.x as f32 + view_up * vp.view_center.y as f32;
|
// (CPU path does the same via `display_center_*`), so no extra
|
||||||
|
// `view_right * view_center` shift here — that would double-count.
|
||||||
|
let target = base_target;
|
||||||
|
|
||||||
let fov_y = 45.0_f32.to_radians();
|
let fov_y = 45.0_f32.to_radians();
|
||||||
let view_height = if vp.view_height.abs() > 1e-9 {
|
let view_height = if effective_view_height > 1e-9 {
|
||||||
vp.view_height as f32
|
effective_view_height as f32
|
||||||
} else {
|
} else {
|
||||||
vp.height as f32
|
vp.height as f32
|
||||||
};
|
};
|
||||||
|
|
@ -5320,10 +5386,7 @@ impl Scene {
|
||||||
) -> Vec<WireModel> {
|
) -> Vec<WireModel> {
|
||||||
use std::collections::HashSet as HSet;
|
use std::collections::HashSet as HSet;
|
||||||
|
|
||||||
let (frozen, vp_anno_scale, vp_aspect, vp_target, vp_ortho_h) = match self
|
let (frozen, vp_anno_scale, vp_aspect) = match self.document.get_entity(vp_handle) {
|
||||||
.document
|
|
||||||
.get_entity(vp_handle)
|
|
||||||
{
|
|
||||||
Some(EntityType::Viewport(vp)) => {
|
Some(EntityType::Viewport(vp)) => {
|
||||||
let f: HSet<Handle> = vp.frozen_layers.iter().cloned().collect();
|
let f: HSet<Handle> = vp.frozen_layers.iter().cloned().collect();
|
||||||
let vp_scale =
|
let vp_scale =
|
||||||
|
|
@ -5338,27 +5401,28 @@ impl Scene {
|
||||||
} else {
|
} else {
|
||||||
1.0_f32
|
1.0_f32
|
||||||
};
|
};
|
||||||
let half_h = (vp.view_height as f32) * 0.5;
|
(f, anno, aspect)
|
||||||
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,
|
|
||||||
);
|
|
||||||
(f, anno, aspect, target, half_h)
|
|
||||||
}
|
}
|
||||||
_ => (HSet::new(), 1.0_f32, 1.0_f32, glam::Vec3::ZERO, 1.0_f32),
|
_ => (HSet::new(), 1.0_f32, 1.0_f32),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Per-viewport frustum AABB in the same f32 wire space (already
|
// Drive the per-viewport view_aabb / wpp from the *effective* camera
|
||||||
// `world_offset`-subtracted). Matches the margin used by
|
// `camera_for_viewport` produces — it folds in the auto-fit
|
||||||
// `view_world_aabb` so pan inertia doesn't pop edges.
|
// fallback for UTM-style files whose saved `view_target` sits at
|
||||||
|
// empty WCS. Without that, the GPU pass would frustum-cull every
|
||||||
|
// entity (saved-view rect doesn't overlap the offset-subtracted
|
||||||
|
// model cluster) and the viewport would render blank.
|
||||||
|
let Some(cam) = self.camera_for_viewport(vp_handle) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let vp_ortho_h = cam.ortho_size();
|
||||||
let half_w = vp_ortho_h * vp_aspect.max(0.01);
|
let half_w = vp_ortho_h * vp_aspect.max(0.01);
|
||||||
let margin = 1.25_f32;
|
let margin = 1.25_f32;
|
||||||
let view_aabb = Some([
|
let view_aabb = Some([
|
||||||
vp_target.x - half_w * margin,
|
cam.target.x - half_w * margin,
|
||||||
vp_target.y - vp_ortho_h * margin,
|
cam.target.y - vp_ortho_h * margin,
|
||||||
vp_target.x + half_w * margin,
|
cam.target.x + half_w * margin,
|
||||||
vp_target.y + vp_ortho_h * margin,
|
cam.target.y + vp_ortho_h * margin,
|
||||||
]);
|
]);
|
||||||
// World units per on-screen pixel for LOD substitution + curve
|
// World units per on-screen pixel for LOD substitution + curve
|
||||||
// tolerance. Tracks the paper-zoom-driven pixel height the
|
// tolerance. Tracks the paper-zoom-driven pixel height the
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue