feat(snap,pick): eye-relative CPU projection — drop full view_proj at UTM

Route the remaining absolute-coordinate CPU projection through the
relative-to-eye path so it no longer multiplies by a view_proj matrix
carrying a ~1e7 translation (which cancels catastrophically in f32 after
world_offset removal):
- camera pick_on_plane / pick_on_target_plane delegate to
  unproject_on_plane_f64 (rotation-only inverse + f64 eye)
- snapper (world_to_screen), hit_test (click_hit / mesh / hatch), the
  scene mesh/solid hit methods, grid + UCS axes, and polar_constrain_near
  all take view_rot + eye and project relative-to-eye
- compute_grid_step measures the per-unit scale via the rotation-only
  matrix (no eye term needed)

view_proj remains only for conservative CPU frustum-cull / scissor and the
screen-space viewcube / UCS-icon directions. NOTE: SnapResult.world and
the command-point chain are still Vec3 (f32) — a ~0.5 m quantization at
extreme UTM zoom remains; the command layer goes f64 next.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.7 committed 2026-06-23 23:33:21 +03:00
1 parent 52882749eb
commit 96aaf2a293
7 files changed
+187 -177

No files matched your search

+20 -12
View File
@@ -2501,14 +2501,15 @@ impl Scene {
pub fn mesh_click_hit(
&self,
cursor: iced::Point,
view_proj: glam::Mat4,
view_rot: glam::Mat4,
eye: glam::DVec3,
bounds: iced::Rectangle,
) -> Option<Handle> {
let iter = self
.meshes
.iter()
.filter_map(|(h, set)| set.lods.first().map(|m| (*h, m)));
pick::hit_test::mesh_click_hit(cursor, iter, view_proj, bounds)
pick::hit_test::mesh_click_hit(cursor, iter, view_rot, eye, bounds)
}
/// True when any handle resolves to an ACIS volume entity (3D solid /
@@ -2532,14 +2533,15 @@ impl Scene {
a: iced::Point,
b: iced::Point,
crossing: bool,
view_proj: glam::Mat4,
view_rot: glam::Mat4,
eye: glam::DVec3,
bounds: iced::Rectangle,
) -> Vec<Handle> {
let iter = self
.meshes
.iter()
.filter_map(|(h, set)| set.lods.first().map(|m| (*h, m)));
pick::hit_test::mesh_box_hit(a, b, crossing, iter, view_proj, bounds)
pick::hit_test::mesh_box_hit(a, b, crossing, iter, view_rot, eye, bounds)
}
/// Top-level solid handles caught by a lasso polygon.
@@ -2547,14 +2549,15 @@ impl Scene {
&self,
poly: &[iced::Point],
crossing: bool,
view_proj: glam::Mat4,
view_rot: glam::Mat4,
eye: glam::DVec3,
bounds: iced::Rectangle,
) -> Vec<Handle> {
let iter = self
.meshes
.iter()
.filter_map(|(h, set)| set.lods.first().map(|m| (*h, m)));
pick::hit_test::mesh_poly_hit(poly, crossing, iter, view_proj, bounds)
pick::hit_test::mesh_poly_hit(poly, crossing, iter, view_rot, eye, bounds)
}
/// Front-most solid under the cursor across BOTH top-level solid meshes
@@ -2565,7 +2568,8 @@ impl Scene {
pub fn solid_click_hit(
&self,
cursor: iced::Point,
view_proj: glam::Mat4,
view_rot: glam::Mat4,
eye: glam::DVec3,
bounds: iced::Rectangle,
) -> Option<Handle> {
// Block-internal instances must be owned (transformed copies); keep
@@ -2597,7 +2601,7 @@ impl Scene {
.iter()
.filter_map(|(h, set)| set.lods.first().map(|m| (*h, m)));
let blk = block_owned.iter().map(|(h, m)| (*h, m));
pick::hit_test::mesh_click_hit(cursor, top.chain(blk), view_proj, bounds)
pick::hit_test::mesh_click_hit(cursor, top.chain(blk), view_rot, eye, bounds)
}
/// Parent INSERT handles whose block-internal solid meshes fall in a
@@ -2609,7 +2613,8 @@ impl Scene {
a: iced::Point,
b: iced::Point,
crossing: bool,
view_proj: glam::Mat4,
view_rot: glam::Mat4,
eye: glam::DVec3,
bounds: iced::Rectangle,
) -> Vec<Handle> {
if self.block_meshes.is_empty() {
@@ -2635,7 +2640,8 @@ impl Scene {
b,
crossing,
std::iter::once((ins.common.handle, m)),
view_proj,
view_rot,
eye,
bounds,
)
.is_empty()
@@ -2653,7 +2659,8 @@ impl Scene {
&self,
poly: &[iced::Point],
crossing: bool,
view_proj: glam::Mat4,
view_rot: glam::Mat4,
eye: glam::DVec3,
bounds: iced::Rectangle,
) -> Vec<Handle> {
if self.block_meshes.is_empty() {
@@ -2678,7 +2685,8 @@ impl Scene {
poly,
crossing,
std::iter::once((ins.common.handle, m)),
view_proj,
view_rot,
eye,
bounds,
)
.is_empty()