fix(pick): UTM-precise click / box / lasso selection
Selection projected wire vertices from points (the double-single HIGH half) alone, ignoring points_low, so at UTM scale every selection test ran against coordinates ~0.5 m off — box/crossing/window edges and click hit-tests missed or grabbed the wrong entities, worsening with zoom. hit_test now reconstructs each vertex's absolute f64 from points + points_low (wp64) before projecting, across click_hit, click_hits_all, box_hit and poly_hit (lasso). Hatch hit-tests use the f64 world_origin instead of an f32-cast, and mesh hit-tests (click/box/lasso face picking) reconstruct from verts + verts_low. world_to_screen now takes the f64 point directly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e5c8eddedb
commit
fe5ff376e5
1 changed files with 64 additions and 30 deletions
|
|
@ -7,7 +7,7 @@
|
||||||
use rustc_hash::FxHashMap as HashMap;
|
use rustc_hash::FxHashMap as HashMap;
|
||||||
|
|
||||||
use acadrust::Handle;
|
use acadrust::Handle;
|
||||||
use glam::{Mat4, Vec3};
|
use glam::Mat4;
|
||||||
use iced::{Point, Rectangle};
|
use iced::{Point, Rectangle};
|
||||||
|
|
||||||
use crate::scene::model::hatch_model::HatchModel;
|
use crate::scene::model::hatch_model::HatchModel;
|
||||||
|
|
@ -55,7 +55,7 @@ pub fn click_hit<'a>(
|
||||||
let mut sx1 = f32::MIN;
|
let mut sx1 = f32::MIN;
|
||||||
let mut sy1 = f32::MIN;
|
let mut sy1 = f32::MIN;
|
||||||
for (cx, cy) in [(minx, miny), (maxx, miny), (maxx, maxy), (minx, maxy)] {
|
for (cx, cy) in [(minx, miny), (maxx, miny), (maxx, maxy), (minx, maxy)] {
|
||||||
let s = world_to_screen(Vec3::new(cx, cy, 0.0), view_rot, eye, bounds);
|
let s = world_to_screen(glam::DVec3::new(cx as f64, cy as f64, 0.0), view_rot, eye, bounds);
|
||||||
sx0 = sx0.min(s.x);
|
sx0 = sx0.min(s.x);
|
||||||
sx1 = sx1.max(s.x);
|
sx1 = sx1.max(s.x);
|
||||||
sy0 = sy0.min(s.y);
|
sy0 = sy0.min(s.y);
|
||||||
|
|
@ -68,12 +68,12 @@ pub fn click_hit<'a>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut prev: Option<Point> = None;
|
let mut prev: Option<Point> = None;
|
||||||
for &[px, py, pz] in &wire.points {
|
for (i, &[px, py, pz]) in wire.points.iter().enumerate() {
|
||||||
if px.is_nan() {
|
if px.is_nan() {
|
||||||
prev = None;
|
prev = None;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let cur = world_to_screen(Vec3::new(px, py, pz), view_rot, eye, bounds);
|
let cur = world_to_screen(wp64([px, py, pz], &wire.points_low, i), view_rot, eye, bounds);
|
||||||
if let Some(p0) = prev {
|
if let Some(p0) = prev {
|
||||||
let d = dist_point_to_segment(cursor, p0, cur);
|
let d = dist_point_to_segment(cursor, p0, cur);
|
||||||
if d < best_dist {
|
if d < best_dist {
|
||||||
|
|
@ -103,12 +103,12 @@ pub fn click_hits_all<'a>(
|
||||||
let mut prev: Option<Point> = None;
|
let mut prev: Option<Point> = None;
|
||||||
let mut best_for_wire = CLICK_THRESHOLD_PX;
|
let mut best_for_wire = CLICK_THRESHOLD_PX;
|
||||||
let mut hit = false;
|
let mut hit = false;
|
||||||
for &[px, py, pz] in &wire.points {
|
for (i, &[px, py, pz]) in wire.points.iter().enumerate() {
|
||||||
if px.is_nan() {
|
if px.is_nan() {
|
||||||
prev = None;
|
prev = None;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let cur = world_to_screen(Vec3::new(px, py, pz), view_rot, eye, bounds);
|
let cur = world_to_screen(wp64([px, py, pz], &wire.points_low, i), view_rot, eye, bounds);
|
||||||
if let Some(p0) = prev {
|
if let Some(p0) = prev {
|
||||||
let d = dist_point_to_segment(cursor, p0, cur);
|
let d = dist_point_to_segment(cursor, p0, cur);
|
||||||
if d < best_for_wire {
|
if d < best_for_wire {
|
||||||
|
|
@ -141,18 +141,15 @@ pub fn mesh_click_hit<'a>(
|
||||||
for (handle, mesh) in meshes {
|
for (handle, mesh) in meshes {
|
||||||
let v = &mesh.verts;
|
let v = &mesh.verts;
|
||||||
let idx = &mesh.indices;
|
let idx = &mesh.indices;
|
||||||
|
let lo = &mesh.verts_low;
|
||||||
let mut t = 0;
|
let mut t = 0;
|
||||||
while t + 2 < idx.len() {
|
while t + 2 < idx.len() {
|
||||||
let tri = [
|
let tri = [idx[t] as usize, idx[t + 1] as usize, idx[t + 2] as usize];
|
||||||
v[idx[t] as usize],
|
|
||||||
v[idx[t + 1] as usize],
|
|
||||||
v[idx[t + 2] as usize],
|
|
||||||
];
|
|
||||||
t += 3;
|
t += 3;
|
||||||
let mut sp = [Point::ORIGIN; 3];
|
let mut sp = [Point::ORIGIN; 3];
|
||||||
let mut depth = 0.0f32;
|
let mut depth = 0.0f32;
|
||||||
for (j, w) in tri.iter().enumerate() {
|
for (j, &k) in tri.iter().enumerate() {
|
||||||
let ndc = view_rot.project_point3((Vec3::new(w[0], w[1], w[2]).as_dvec3() - eye).as_vec3());
|
let ndc = view_rot.project_point3((mesh_vert(v[k], lo, k) - eye).as_vec3());
|
||||||
sp[j] = Point::new(
|
sp[j] = Point::new(
|
||||||
(ndc.x + 1.0) * 0.5 * bounds.width,
|
(ndc.x + 1.0) * 0.5 * bounds.width,
|
||||||
(1.0 - ndc.y) * 0.5 * bounds.height,
|
(1.0 - ndc.y) * 0.5 * bounds.height,
|
||||||
|
|
@ -171,12 +168,26 @@ pub fn mesh_click_hit<'a>(
|
||||||
best.map(|(_, h)| h)
|
best.map(|(_, h)| h)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reconstruct a mesh vertex's absolute f64 position from its high/low pair —
|
||||||
|
/// without the low residual the f32 high alone is ~0.5 m off at UTM scale and
|
||||||
|
/// box / lasso / face selection lands on the wrong place.
|
||||||
|
#[inline]
|
||||||
|
fn mesh_vert(hi: [f32; 3], low: &[[f32; 3]], i: usize) -> glam::DVec3 {
|
||||||
|
let l = low.get(i).copied().unwrap_or([0.0; 3]);
|
||||||
|
glam::DVec3::new(
|
||||||
|
hi[0] as f64 + l[0] as f64,
|
||||||
|
hi[1] as f64 + l[1] as f64,
|
||||||
|
hi[2] as f64 + l[2] as f64,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Project a mesh's vertices to screen space.
|
/// Project a mesh's vertices to screen space.
|
||||||
fn project_mesh_verts(mesh: &MeshModel, view_rot: Mat4, eye: glam::DVec3, bounds: Rectangle) -> Vec<Point> {
|
fn project_mesh_verts(mesh: &MeshModel, view_rot: Mat4, eye: glam::DVec3, bounds: Rectangle) -> Vec<Point> {
|
||||||
mesh.verts
|
mesh.verts
|
||||||
.iter()
|
.iter()
|
||||||
.map(|w| {
|
.enumerate()
|
||||||
let ndc = view_rot.project_point3((Vec3::new(w[0], w[1], w[2]).as_dvec3() - eye).as_vec3());
|
.map(|(i, &w)| {
|
||||||
|
let ndc = view_rot.project_point3((mesh_vert(w, &mesh.verts_low, i) - eye).as_vec3());
|
||||||
Point::new(
|
Point::new(
|
||||||
(ndc.x + 1.0) * 0.5 * bounds.width,
|
(ndc.x + 1.0) * 0.5 * bounds.width,
|
||||||
(1.0 - ndc.y) * 0.5 * bounds.height,
|
(1.0 - ndc.y) * 0.5 * bounds.height,
|
||||||
|
|
@ -336,16 +347,23 @@ pub fn box_hit<'a>(
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Low residual parallel to `pts` (empty for the AABB fallback,
|
||||||
|
// whose coarse f32 box doesn't carry one).
|
||||||
|
let low: &[[f32; 3]] = if !wire.points.is_empty() {
|
||||||
|
&wire.points_low
|
||||||
|
} else {
|
||||||
|
&[]
|
||||||
|
};
|
||||||
let mut hit = false;
|
let mut hit = false;
|
||||||
let mut all_inside = true;
|
let mut all_inside = true;
|
||||||
let mut prev: Option<Point> = None;
|
let mut prev: Option<Point> = None;
|
||||||
|
|
||||||
for &[px, py, pz] in pts {
|
for (i, &[px, py, pz]) in pts.iter().enumerate() {
|
||||||
if px.is_nan() {
|
if px.is_nan() {
|
||||||
prev = None;
|
prev = None;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let sp = world_to_screen(Vec3::new(px, py, pz), view_rot, eye, bounds);
|
let sp = world_to_screen(wp64([px, py, pz], low, i), view_rot, eye, bounds);
|
||||||
if crossing {
|
if crossing {
|
||||||
if inside(sp) {
|
if inside(sp) {
|
||||||
hit = true;
|
hit = true;
|
||||||
|
|
@ -424,16 +442,21 @@ pub fn poly_hit<'a>(
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let low: &[[f32; 3]] = if !wire.points.is_empty() {
|
||||||
|
&wire.points_low
|
||||||
|
} else {
|
||||||
|
&[]
|
||||||
|
};
|
||||||
let mut hit = false;
|
let mut hit = false;
|
||||||
let mut all_inside = true;
|
let mut all_inside = true;
|
||||||
let mut prev: Option<Point> = None;
|
let mut prev: Option<Point> = None;
|
||||||
|
|
||||||
for &[px, py, pz] in pts {
|
for (i, &[px, py, pz]) in pts.iter().enumerate() {
|
||||||
if px.is_nan() {
|
if px.is_nan() {
|
||||||
prev = None;
|
prev = None;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let sp = world_to_screen(Vec3::new(px, py, pz), view_rot, eye, bounds);
|
let sp = world_to_screen(wp64([px, py, pz], low, i), view_rot, eye, bounds);
|
||||||
if crossing {
|
if crossing {
|
||||||
if point_in_polygon(sp, poly) {
|
if point_in_polygon(sp, poly) {
|
||||||
hit = true;
|
hit = true;
|
||||||
|
|
@ -469,14 +492,28 @@ pub fn poly_hit<'a>(
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn world_to_screen(world: Vec3, view_rot: Mat4, eye: glam::DVec3, bounds: Rectangle) -> Point {
|
fn world_to_screen(world: glam::DVec3, view_rot: Mat4, eye: glam::DVec3, bounds: Rectangle) -> Point {
|
||||||
let ndc = view_rot.project_point3((world.as_dvec3() - eye).as_vec3());
|
let ndc = view_rot.project_point3((world - eye).as_vec3());
|
||||||
Point::new(
|
Point::new(
|
||||||
(ndc.x + 1.0) * 0.5 * bounds.width,
|
(ndc.x + 1.0) * 0.5 * bounds.width,
|
||||||
(1.0 - ndc.y) * 0.5 * bounds.height,
|
(1.0 - ndc.y) * 0.5 * bounds.height,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reconstruct the absolute-f64 world position of wire vertex `i` from its
|
||||||
|
/// double-single high (`points`) + low (`points_low`) pair. At UTM scale the
|
||||||
|
/// high f32 alone is ~0.5 m off, which throws box / lasso / click selection
|
||||||
|
/// edges off by metres; adding the low residual restores f64 precision.
|
||||||
|
#[inline]
|
||||||
|
fn wp64(hi: [f32; 3], low: &[[f32; 3]], i: usize) -> glam::DVec3 {
|
||||||
|
let l = low.get(i).copied().unwrap_or([0.0; 3]);
|
||||||
|
glam::DVec3::new(
|
||||||
|
hi[0] as f64 + l[0] as f64,
|
||||||
|
hi[1] as f64 + l[1] as f64,
|
||||||
|
hi[2] as f64 + l[2] as f64,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Even-odd ray-casting test: is `p` inside the polygon?
|
/// Even-odd ray-casting test: is `p` inside the polygon?
|
||||||
///
|
///
|
||||||
/// Handles multi-path boundaries: NaN points (used as path separators by
|
/// Handles multi-path boundaries: NaN points (used as path separators by
|
||||||
|
|
@ -609,14 +646,13 @@ fn hatch_contains_screen_point(
|
||||||
// boundary verts are stored as small f32 offsets from
|
// boundary verts are stored as small f32 offsets from
|
||||||
// `world_origin` (f64). Reconstruct offset-rel WCS before
|
// `world_origin` (f64). Reconstruct offset-rel WCS before
|
||||||
// projecting to screen.
|
// projecting to screen.
|
||||||
let ox = hatch.world_origin[0] as f32;
|
let (ox, oy) = (hatch.world_origin[0], hatch.world_origin[1]);
|
||||||
let oy = hatch.world_origin[1] as f32;
|
|
||||||
let screen: Vec<Point> = hatch
|
let screen: Vec<Point> = hatch
|
||||||
.boundary
|
.boundary
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&[x, y]| {
|
.map(|&[x, y]| {
|
||||||
if x.is_finite() && y.is_finite() {
|
if x.is_finite() && y.is_finite() {
|
||||||
world_to_screen(Vec3::new(x + ox, y + oy, 0.0), view_rot, eye, bounds)
|
world_to_screen(glam::DVec3::new(x as f64 + ox, y as f64 + oy, 0.0), view_rot, eye, bounds)
|
||||||
} else {
|
} else {
|
||||||
// Preserve path separators for the NaN-aware
|
// Preserve path separators for the NaN-aware
|
||||||
// point_in_polygon ray-cast.
|
// point_in_polygon ray-cast.
|
||||||
|
|
@ -654,12 +690,11 @@ pub fn box_hit_hatch(
|
||||||
if hatch.boundary.is_empty() {
|
if hatch.boundary.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let ox = hatch.world_origin[0] as f32;
|
let (ox, oy) = (hatch.world_origin[0], hatch.world_origin[1]);
|
||||||
let oy = hatch.world_origin[1] as f32;
|
|
||||||
let screen: Vec<Point> = hatch
|
let screen: Vec<Point> = hatch
|
||||||
.boundary
|
.boundary
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&[x, y]| world_to_screen(Vec3::new(x + ox, y + oy, 0.0), view_rot, eye, bounds))
|
.map(|&[x, y]| world_to_screen(glam::DVec3::new(x as f64 + ox, y as f64 + oy, 0.0), view_rot, eye, bounds))
|
||||||
.collect();
|
.collect();
|
||||||
let hit = if crossing {
|
let hit = if crossing {
|
||||||
screen.iter().any(|&sp| inside(sp))
|
screen.iter().any(|&sp| inside(sp))
|
||||||
|
|
@ -694,12 +729,11 @@ pub fn poly_hit_hatch(
|
||||||
if hatch.boundary.is_empty() {
|
if hatch.boundary.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let ox = hatch.world_origin[0] as f32;
|
let (ox, oy) = (hatch.world_origin[0], hatch.world_origin[1]);
|
||||||
let oy = hatch.world_origin[1] as f32;
|
|
||||||
let screen: Vec<Point> = hatch
|
let screen: Vec<Point> = hatch
|
||||||
.boundary
|
.boundary
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&[x, y]| world_to_screen(Vec3::new(x + ox, y + oy, 0.0), view_rot, eye, bounds))
|
.map(|&[x, y]| world_to_screen(glam::DVec3::new(x as f64 + ox, y as f64 + oy, 0.0), view_rot, eye, bounds))
|
||||||
.collect();
|
.collect();
|
||||||
let hit = if crossing {
|
let hit = if crossing {
|
||||||
screen.iter().any(|&sp| point_in_polygon(sp, poly))
|
screen.iter().any(|&sp| point_in_polygon(sp, poly))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue