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:
parent
52882749eb
commit
96aaf2a293
7 changed files with 187 additions and 177 deletions
|
|
@ -228,14 +228,15 @@ pub(super) fn polar_constrain_near(
|
|||
pt: glam::Vec3,
|
||||
base: glam::Vec3,
|
||||
step_deg: f32,
|
||||
view_proj: glam::Mat4,
|
||||
view_rot: glam::Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: iced::Rectangle,
|
||||
tol_px: f32,
|
||||
xf: &UcsXform,
|
||||
) -> glam::Vec3 {
|
||||
let snapped = polar_constrain(pt, base, step_deg, xf);
|
||||
let to_screen = |w: glam::Vec3| {
|
||||
let ndc = view_proj.project_point3(w);
|
||||
let ndc = view_rot.project_point3((w.as_dvec3() - eye).as_vec3());
|
||||
(
|
||||
(ndc.x + 1.0) * 0.5 * bounds.width,
|
||||
(1.0 - ndc.y) * 0.5 * bounds.height,
|
||||
|
|
|
|||
|
|
@ -2618,7 +2618,7 @@ impl OpenCADStudio {
|
|||
};
|
||||
let cam = self.tabs[i].scene.camera.borrow();
|
||||
let raw_paper = cam.pick_on_target_plane(p, bounds);
|
||||
let vp_mat = cam.view_proj(bounds);
|
||||
let view_rot = cam.view_proj_rte(bounds); let eye = cam.eye_f64();
|
||||
drop(cam);
|
||||
let raw = self.tabs[i].scene.paper_to_model(raw_paper);
|
||||
|
||||
|
|
@ -2648,7 +2648,7 @@ impl OpenCADStudio {
|
|||
self.snapper.from_point = None;
|
||||
let (go, gr) = self.tabs[i].ucs_grid_basis();
|
||||
let snap_hit =
|
||||
self.snapper.snap(raw, p, &all_wires[..], vp_mat, bounds, go, gr);
|
||||
self.snapper.snap(raw, p, &all_wires[..], view_rot, eye, bounds, go, gr);
|
||||
let mut snapped = snap_hit.map(|s| s.world).unwrap_or(raw);
|
||||
self.tabs[i].snap_result = snap_hit;
|
||||
if let Some(s) = self.tabs[i].snap_result.as_mut() {
|
||||
|
|
@ -2666,7 +2666,8 @@ impl OpenCADStudio {
|
|||
snapped,
|
||||
base,
|
||||
self.polar_increment_deg,
|
||||
vp_mat,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
self.snapper.osnap_radius_px,
|
||||
&ucs_xf,
|
||||
|
|
@ -2782,10 +2783,13 @@ impl OpenCADStudio {
|
|||
.borrow()
|
||||
.pick_on_target_plane(p, bounds)
|
||||
};
|
||||
let view_proj = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let (view_rot, eye) = {
|
||||
let cam = self.tabs[i].scene.camera.borrow();
|
||||
(cam.view_proj_rte(bounds), cam.eye_f64())
|
||||
};
|
||||
// Sync grid-snap spacing to the adaptive spacing of the visible grid.
|
||||
self.snapper.grid_spacing =
|
||||
crate::ui::overlay::compute_grid_step(view_proj, bounds);
|
||||
crate::ui::overlay::compute_grid_step(view_rot, bounds);
|
||||
// In MSPACE, map paper-space cursor to model space so that
|
||||
// command previews and snapping work in the correct coordinate space.
|
||||
let cursor_world = self.tabs[i].scene.paper_to_model(cursor_paper);
|
||||
|
|
@ -2818,14 +2822,15 @@ impl OpenCADStudio {
|
|||
cursor_world,
|
||||
p,
|
||||
&all_wires[..],
|
||||
view_proj,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
)
|
||||
} else {
|
||||
let (go, gr) = self.tabs[i].ucs_grid_basis();
|
||||
self.snapper.from_point = self.last_point;
|
||||
self.snapper
|
||||
.snap(cursor_world, p, &all_wires[..], view_proj, bounds, go, gr)
|
||||
.snap(cursor_world, p, &all_wires[..], view_rot, eye, bounds, go, gr)
|
||||
};
|
||||
|
||||
// Object Snap Tracking: update dwell, then align the cursor
|
||||
|
|
@ -2835,7 +2840,8 @@ impl OpenCADStudio {
|
|||
let snap_world = self.tabs[i].snap_result.map(|s| s.world);
|
||||
self.snapper.update_otrack_dwell(
|
||||
snap_world,
|
||||
view_proj,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
Instant::now(),
|
||||
);
|
||||
|
|
@ -2848,7 +2854,8 @@ impl OpenCADStudio {
|
|||
let ucs = self.tabs[i].scene.viewcube_ucs_mat();
|
||||
self.snapper.otrack_snap(
|
||||
cursor_world,
|
||||
view_proj,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
step,
|
||||
self.last_point,
|
||||
|
|
@ -2889,7 +2896,8 @@ impl OpenCADStudio {
|
|||
pt,
|
||||
base,
|
||||
self.polar_increment_deg,
|
||||
view_proj,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
self.snapper.osnap_radius_px,
|
||||
&ucs_xf,
|
||||
|
|
@ -2912,7 +2920,7 @@ impl OpenCADStudio {
|
|||
// last point) so the dynamic-input overlay can place its
|
||||
// guide geometry and labels.
|
||||
let project = |bp: glam::Vec3| {
|
||||
let ndc = view_proj.project_point3(bp);
|
||||
let ndc = view_rot.project_point3((bp.as_dvec3() - eye).as_vec3());
|
||||
iced::Point::new(
|
||||
(ndc.x + 1.0) * 0.5 * bounds.width,
|
||||
(1.0 - ndc.y) * 0.5 * bounds.height,
|
||||
|
|
@ -2948,7 +2956,7 @@ impl OpenCADStudio {
|
|||
});
|
||||
if let Some(pick) = pick {
|
||||
let world = glam::Vec3::new(pick.x as f32, pick.y as f32, effective.z);
|
||||
let ndc = view_proj.project_point3(world);
|
||||
let ndc = view_rot.project_point3((world.as_dvec3() - eye).as_vec3());
|
||||
let screen = iced::Point::new(
|
||||
(ndc.x + 1.0) * 0.5 * bounds.width,
|
||||
(1.0 - ndc.y) * 0.5 * bounds.height,
|
||||
|
|
@ -2989,7 +2997,7 @@ impl OpenCADStudio {
|
|||
p
|
||||
} else if needs_entity {
|
||||
let hover_handle =
|
||||
scene::pick::hit_test::click_hit(p, &all_wires[..], view_proj, bounds)
|
||||
scene::pick::hit_test::click_hit(p, &all_wires[..], view_rot, eye, bounds)
|
||||
.and_then(|s| Scene::handle_from_wire_name(s))
|
||||
.unwrap_or(acadrust::Handle::NULL);
|
||||
let mut p = self.tabs[i]
|
||||
|
|
@ -3440,7 +3448,7 @@ impl OpenCADStudio {
|
|||
};
|
||||
// Convert paper-space → model-space when inside a viewport.
|
||||
let raw = self.tabs[i].scene.paper_to_model(raw_paper);
|
||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let (view_rot, eye) = { let c = self.tabs[i].scene.camera.borrow(); (c.view_proj_rte(bounds), c.eye_f64()) };
|
||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
let needs_tan = self.tabs[i]
|
||||
.active_cmd
|
||||
|
|
@ -3456,11 +3464,11 @@ impl OpenCADStudio {
|
|||
None
|
||||
} else if needs_tan {
|
||||
self.snapper
|
||||
.snap_tangent_only(raw, p, &all_wires[..], vp_mat, bounds)
|
||||
.snap_tangent_only(raw, p, &all_wires[..], view_rot, eye, bounds)
|
||||
} else {
|
||||
let (go, gr) = self.tabs[i].ucs_grid_basis();
|
||||
self.snapper.from_point = self.last_point;
|
||||
self.snapper.snap(raw, p, &all_wires[..], vp_mat, bounds, go, gr)
|
||||
self.snapper.snap(raw, p, &all_wires[..], view_rot, eye, bounds, go, gr)
|
||||
};
|
||||
// snap.world is in paper-space (projected wire coords in MSPACE);
|
||||
// convert to model-space so commands receive consistent coordinates.
|
||||
|
|
@ -3482,7 +3490,7 @@ impl OpenCADStudio {
|
|||
};
|
||||
let ucs = self.tabs[i].scene.viewcube_ucs_mat();
|
||||
self.snapper
|
||||
.otrack_snap(raw, vp_mat, bounds, step, self.last_point, ucs)
|
||||
.otrack_snap(raw, view_rot, eye, bounds, step, self.last_point, ucs)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
|
@ -3505,7 +3513,8 @@ impl OpenCADStudio {
|
|||
pt,
|
||||
base,
|
||||
self.polar_increment_deg,
|
||||
vp_mat,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
self.snapper.osnap_radius_px,
|
||||
&ucs_xf,
|
||||
|
|
@ -3569,9 +3578,9 @@ impl OpenCADStudio {
|
|||
.map(|c| c.needs_entity_pick())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let vp_mat2 = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let (view_rot2, eye2) = { let c = self.tabs[i].scene.camera.borrow(); (c.view_proj_rte(bounds), c.eye_f64()) };
|
||||
let all_wires2 = self.tabs[i].scene.hit_test_wires();
|
||||
let hit = scene::pick::hit_test::click_hit(p, &all_wires2[..], vp_mat2, bounds)
|
||||
let hit = scene::pick::hit_test::click_hit(p, &all_wires2[..], view_rot2, eye2, bounds)
|
||||
.and_then(|s| Scene::handle_from_wire_name(s));
|
||||
if let Some(handle) = hit {
|
||||
// Some commands (e.g. SS_CATCHMENT) need the entity
|
||||
|
|
@ -3759,13 +3768,14 @@ impl OpenCADStudio {
|
|||
if let Some(a) = box_anchor {
|
||||
let crossing = box_crossing;
|
||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let (view_rot, eye) = { let c = self.tabs[i].scene.camera.borrow(); (c.view_proj_rte(bounds), c.eye_f64()) };
|
||||
let mut handles: Vec<Handle> = scene::pick::hit_test::box_hit(
|
||||
a,
|
||||
p,
|
||||
crossing,
|
||||
&all_wires[..],
|
||||
vp_mat,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
)
|
||||
.into_iter()
|
||||
|
|
@ -3776,14 +3786,15 @@ impl OpenCADStudio {
|
|||
p,
|
||||
crossing,
|
||||
&self.tabs[i].scene.visible_hatches_for_click(),
|
||||
vp_mat,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
));
|
||||
handles.extend(
|
||||
self.tabs[i].scene.mesh_box_hit(a, p, crossing, vp_mat, bounds),
|
||||
self.tabs[i].scene.mesh_box_hit(a, p, crossing, view_rot, eye, bounds),
|
||||
);
|
||||
handles.extend(self.tabs[i].scene.block_mesh_box_hit(
|
||||
a, p, crossing, vp_mat, bounds,
|
||||
a, p, crossing, view_rot, eye, bounds,
|
||||
));
|
||||
// Box/lasso accumulates like individual picks
|
||||
// (issue #83): a plain box adds to the current
|
||||
|
|
@ -3818,12 +3829,13 @@ impl OpenCADStudio {
|
|||
};
|
||||
self.tabs[i].scene.selection.borrow_mut().poly_last_crossing = crossing;
|
||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let (view_rot, eye) = { let c = self.tabs[i].scene.camera.borrow(); (c.view_proj_rte(bounds), c.eye_f64()) };
|
||||
let mut handles: Vec<Handle> = scene::pick::hit_test::poly_hit(
|
||||
&poly_pts,
|
||||
crossing,
|
||||
&all_wires[..],
|
||||
vp_mat,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
)
|
||||
.into_iter()
|
||||
|
|
@ -3833,14 +3845,15 @@ impl OpenCADStudio {
|
|||
&poly_pts,
|
||||
crossing,
|
||||
&self.tabs[i].scene.visible_hatches_for_click(),
|
||||
vp_mat,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
));
|
||||
handles.extend(
|
||||
self.tabs[i].scene.mesh_poly_hit(&poly_pts, crossing, vp_mat, bounds),
|
||||
self.tabs[i].scene.mesh_poly_hit(&poly_pts, crossing, view_rot, eye, bounds),
|
||||
);
|
||||
handles.extend(self.tabs[i].scene.block_mesh_poly_hit(
|
||||
&poly_pts, crossing, vp_mat, bounds,
|
||||
&poly_pts, crossing, view_rot, eye, bounds,
|
||||
));
|
||||
// Selection filter: keep only allowed types.
|
||||
handles.retain(|&h| self.tabs[i].scene.passes_selection_filter(h));
|
||||
|
|
@ -3870,7 +3883,7 @@ impl OpenCADStudio {
|
|||
} else {
|
||||
if box_anchor.is_none() {
|
||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let (view_rot, eye) = { let c = self.tabs[i].scene.camera.borrow(); (c.view_proj_rte(bounds), c.eye_f64()) };
|
||||
|
||||
// Selection cycling: where two or more objects
|
||||
// overlap, open a list box to pick which one; a
|
||||
|
|
@ -3882,7 +3895,8 @@ impl OpenCADStudio {
|
|||
let cands: Vec<Handle> = scene::pick::hit_test::click_hits_all(
|
||||
p,
|
||||
&all_wires[..],
|
||||
vp_mat,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
)
|
||||
.into_iter()
|
||||
|
|
@ -3898,13 +3912,14 @@ impl OpenCADStudio {
|
|||
|
||||
if !handled_by_cycling {
|
||||
let hit =
|
||||
scene::pick::hit_test::click_hit(p, &all_wires[..], vp_mat, bounds)
|
||||
scene::pick::hit_test::click_hit(p, &all_wires[..], view_rot, eye, bounds)
|
||||
.and_then(|s| Scene::handle_from_wire_name(s))
|
||||
.or_else(|| {
|
||||
scene::pick::hit_test::click_hit_hatch(
|
||||
p,
|
||||
&self.tabs[i].scene.visible_hatches_for_click(),
|
||||
vp_mat,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
)
|
||||
})
|
||||
|
|
@ -3914,7 +3929,8 @@ impl OpenCADStudio {
|
|||
scene::pick::hit_test::click_hit_insert_hatch(
|
||||
p,
|
||||
&self.tabs[i].scene.insert_hatches_for_click(),
|
||||
vp_mat,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
)
|
||||
})
|
||||
|
|
@ -3923,7 +3939,7 @@ impl OpenCADStudio {
|
|||
// body — top-level solids and block-internal
|
||||
// ones together, front-most wins (a block in
|
||||
// front of a solid resolves to the block).
|
||||
self.tabs[i].scene.solid_click_hit(p, vp_mat, bounds)
|
||||
self.tabs[i].scene.solid_click_hit(p, view_rot, eye, bounds)
|
||||
});
|
||||
// Selection filter: drop a pick whose type is excluded.
|
||||
let hit =
|
||||
|
|
@ -3960,13 +3976,14 @@ impl OpenCADStudio {
|
|||
let a = box_anchor.unwrap();
|
||||
let crossing = box_crossing;
|
||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let (view_rot, eye) = { let c = self.tabs[i].scene.camera.borrow(); (c.view_proj_rte(bounds), c.eye_f64()) };
|
||||
let mut handles: Vec<Handle> = scene::pick::hit_test::box_hit(
|
||||
a,
|
||||
p,
|
||||
crossing,
|
||||
&all_wires[..],
|
||||
vp_mat,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
)
|
||||
.into_iter()
|
||||
|
|
@ -3977,14 +3994,15 @@ impl OpenCADStudio {
|
|||
p,
|
||||
crossing,
|
||||
&self.tabs[i].scene.visible_hatches_for_click(),
|
||||
vp_mat,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
));
|
||||
handles.extend(
|
||||
self.tabs[i].scene.mesh_box_hit(a, p, crossing, vp_mat, bounds),
|
||||
self.tabs[i].scene.mesh_box_hit(a, p, crossing, view_rot, eye, bounds),
|
||||
);
|
||||
handles.extend(self.tabs[i].scene.block_mesh_box_hit(
|
||||
a, p, crossing, vp_mat, bounds,
|
||||
a, p, crossing, view_rot, eye, bounds,
|
||||
));
|
||||
// Selection filter: keep only allowed types.
|
||||
handles.retain(|&h| self.tabs[i].scene.passes_selection_filter(h));
|
||||
|
|
@ -4061,14 +4079,14 @@ impl OpenCADStudio {
|
|||
width: vw,
|
||||
height: vh,
|
||||
};
|
||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let (view_rot, eye) = { let c = self.tabs[i].scene.camera.borrow(); (c.view_proj_rte(bounds), c.eye_f64()) };
|
||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
// Resolve the double-clicked object — its wire, or (for a
|
||||
// block/solid with no wire under the cursor) its shaded
|
||||
// body, which maps to the parent INSERT.
|
||||
let hit = scene::pick::hit_test::click_hit(p, &all_wires[..], vp_mat, bounds)
|
||||
let hit = scene::pick::hit_test::click_hit(p, &all_wires[..], view_rot, eye, bounds)
|
||||
.and_then(|s| Scene::handle_from_wire_name(s))
|
||||
.or_else(|| self.tabs[i].scene.solid_click_hit(p, vp_mat, bounds));
|
||||
.or_else(|| self.tabs[i].scene.solid_click_hit(p, view_rot, eye, bounds));
|
||||
if let Some(handle) = hit {
|
||||
// Any text-bearing entity opens its in-place editor
|
||||
// (plain box or rich MText editor, per type). A
|
||||
|
|
@ -4133,9 +4151,9 @@ impl OpenCADStudio {
|
|||
|
||||
// 1) Try direct wire hit — works when the border is clicked.
|
||||
let hit_vp: Option<acadrust::Handle> = {
|
||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let (view_rot, eye) = { let c = self.tabs[i].scene.camera.borrow(); (c.view_proj_rte(bounds), c.eye_f64()) };
|
||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
scene::pick::hit_test::click_hit(p, &all_wires[..], vp_mat, bounds)
|
||||
scene::pick::hit_test::click_hit(p, &all_wires[..], view_rot, eye, bounds)
|
||||
.and_then(|s| Scene::handle_from_wire_name(s))
|
||||
.and_then(|h| {
|
||||
if let Some(AcadEntityType::Viewport(vp)) =
|
||||
|
|
@ -4416,18 +4434,19 @@ impl OpenCADStudio {
|
|||
height: dwell.tile_size.1,
|
||||
};
|
||||
let p = dwell.point;
|
||||
let view_proj = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
||||
let (view_rot, eye) = { let c = self.tabs[i].scene.camera.borrow(); (c.view_proj_rte(bounds), c.eye_f64()) };
|
||||
let all_wires = self.tabs[i].scene.hit_test_wires();
|
||||
// Mirror the click-selection pick order so the rollover
|
||||
// highlights every selectable object: wire → hatch →
|
||||
// block-internal hatch → shaded 3D solid body.
|
||||
let hovered = scene::pick::hit_test::click_hit(p, &all_wires[..], view_proj, bounds)
|
||||
let hovered = scene::pick::hit_test::click_hit(p, &all_wires[..], view_rot, eye, bounds)
|
||||
.and_then(|s| Scene::handle_from_wire_name(s))
|
||||
.or_else(|| {
|
||||
scene::pick::hit_test::click_hit_hatch(
|
||||
p,
|
||||
&self.tabs[i].scene.visible_hatches_for_click(),
|
||||
view_proj,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
)
|
||||
})
|
||||
|
|
@ -4435,11 +4454,12 @@ impl OpenCADStudio {
|
|||
scene::pick::hit_test::click_hit_insert_hatch(
|
||||
p,
|
||||
&self.tabs[i].scene.insert_hatches_for_click(),
|
||||
view_proj,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
)
|
||||
})
|
||||
.or_else(|| self.tabs[i].scene.solid_click_hit(p, view_proj, bounds));
|
||||
.or_else(|| self.tabs[i].scene.solid_click_hit(p, view_rot, eye, bounds));
|
||||
self.tabs[i].scene.set_hover_highlight(hovered);
|
||||
self.hover_dwell = None;
|
||||
Task::none()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -26,7 +26,8 @@ const CLICK_THRESHOLD_PX: f32 = 8.0;
|
|||
pub fn click_hit<'a>(
|
||||
cursor: Point,
|
||||
wires: &'a [WireModel],
|
||||
view_proj: Mat4,
|
||||
view_rot: Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> Option<&'a str> {
|
||||
let mut best_dist = CLICK_THRESHOLD_PX;
|
||||
|
|
@ -38,7 +39,7 @@ pub fn click_hit<'a>(
|
|||
// on its world x/y, so its world-space AABB projects exactly and we can
|
||||
// reject wires nowhere near the cursor without projecting any of their
|
||||
// points (the dominant per-move cost on 100 k-wire drawings).
|
||||
let z_flat = view_proj.z_axis.x.abs() < 1e-9 && view_proj.z_axis.y.abs() < 1e-9;
|
||||
let z_flat = view_rot.z_axis.x.abs() < 1e-9 && view_rot.z_axis.y.abs() < 1e-9;
|
||||
|
||||
// Q: lazy projection — no Vec allocation per wire; NaN resets the segment chain.
|
||||
for wire in wires {
|
||||
|
|
@ -54,7 +55,7 @@ pub fn click_hit<'a>(
|
|||
let mut sx1 = f32::MIN;
|
||||
let mut sy1 = f32::MIN;
|
||||
for (cx, cy) in [(minx, miny), (maxx, miny), (maxx, maxy), (minx, maxy)] {
|
||||
let s = world_to_screen(Vec3::new(cx, cy, 0.0), view_proj, bounds);
|
||||
let s = world_to_screen(Vec3::new(cx, cy, 0.0), view_rot, eye, bounds);
|
||||
sx0 = sx0.min(s.x);
|
||||
sx1 = sx1.max(s.x);
|
||||
sy0 = sy0.min(s.y);
|
||||
|
|
@ -72,7 +73,7 @@ pub fn click_hit<'a>(
|
|||
prev = None;
|
||||
continue;
|
||||
}
|
||||
let cur = world_to_screen(Vec3::new(px, py, pz), view_proj, bounds);
|
||||
let cur = world_to_screen(Vec3::new(px, py, pz), view_rot, eye, bounds);
|
||||
if let Some(p0) = prev {
|
||||
let d = dist_point_to_segment(cursor, p0, cur);
|
||||
if d < best_dist {
|
||||
|
|
@ -93,7 +94,8 @@ pub fn click_hit<'a>(
|
|||
pub fn click_hits_all<'a>(
|
||||
cursor: Point,
|
||||
wires: &'a [WireModel],
|
||||
view_proj: Mat4,
|
||||
view_rot: Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> Vec<&'a str> {
|
||||
let mut hits: Vec<(f32, &str)> = Vec::new();
|
||||
|
|
@ -106,7 +108,7 @@ pub fn click_hits_all<'a>(
|
|||
prev = None;
|
||||
continue;
|
||||
}
|
||||
let cur = world_to_screen(Vec3::new(px, py, pz), view_proj, bounds);
|
||||
let cur = world_to_screen(Vec3::new(px, py, pz), view_rot, eye, bounds);
|
||||
if let Some(p0) = prev {
|
||||
let d = dist_point_to_segment(cursor, p0, cur);
|
||||
if d < best_for_wire {
|
||||
|
|
@ -131,7 +133,8 @@ pub fn click_hits_all<'a>(
|
|||
pub fn mesh_click_hit<'a>(
|
||||
cursor: Point,
|
||||
meshes: impl Iterator<Item = (Handle, &'a MeshModel)>,
|
||||
view_proj: Mat4,
|
||||
view_rot: Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> Option<Handle> {
|
||||
let mut best: Option<(f32, Handle)> = None;
|
||||
|
|
@ -149,7 +152,7 @@ pub fn mesh_click_hit<'a>(
|
|||
let mut sp = [Point::ORIGIN; 3];
|
||||
let mut depth = 0.0f32;
|
||||
for (j, w) in tri.iter().enumerate() {
|
||||
let ndc = view_proj.project_point3(Vec3::new(w[0], w[1], w[2]));
|
||||
let ndc = view_rot.project_point3((Vec3::new(w[0], w[1], w[2]).as_dvec3() - eye).as_vec3());
|
||||
sp[j] = Point::new(
|
||||
(ndc.x + 1.0) * 0.5 * bounds.width,
|
||||
(1.0 - ndc.y) * 0.5 * bounds.height,
|
||||
|
|
@ -169,11 +172,11 @@ pub fn mesh_click_hit<'a>(
|
|||
}
|
||||
|
||||
/// Project a mesh's vertices to screen space.
|
||||
fn project_mesh_verts(mesh: &MeshModel, view_proj: Mat4, bounds: Rectangle) -> Vec<Point> {
|
||||
fn project_mesh_verts(mesh: &MeshModel, view_rot: Mat4, eye: glam::DVec3, bounds: Rectangle) -> Vec<Point> {
|
||||
mesh.verts
|
||||
.iter()
|
||||
.map(|w| {
|
||||
let ndc = view_proj.project_point3(Vec3::new(w[0], w[1], w[2]));
|
||||
let ndc = view_rot.project_point3((Vec3::new(w[0], w[1], w[2]).as_dvec3() - eye).as_vec3());
|
||||
Point::new(
|
||||
(ndc.x + 1.0) * 0.5 * bounds.width,
|
||||
(1.0 - ndc.y) * 0.5 * bounds.height,
|
||||
|
|
@ -208,7 +211,8 @@ pub fn mesh_box_hit<'a>(
|
|||
b: Point,
|
||||
crossing: bool,
|
||||
meshes: impl Iterator<Item = (Handle, &'a MeshModel)>,
|
||||
view_proj: Mat4,
|
||||
view_rot: Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> Vec<Handle> {
|
||||
let (min_x, max_x) = (a.x.min(b.x), a.x.max(b.x));
|
||||
|
|
@ -222,7 +226,7 @@ pub fn mesh_box_hit<'a>(
|
|||
];
|
||||
let mut out = Vec::new();
|
||||
for (h, mesh) in meshes {
|
||||
let proj = project_mesh_verts(mesh, view_proj, bounds);
|
||||
let proj = project_mesh_verts(mesh, view_rot, eye, bounds);
|
||||
if proj.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -245,7 +249,8 @@ pub fn mesh_poly_hit<'a>(
|
|||
poly: &[Point],
|
||||
crossing: bool,
|
||||
meshes: impl Iterator<Item = (Handle, &'a MeshModel)>,
|
||||
view_proj: Mat4,
|
||||
view_rot: Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> Vec<Handle> {
|
||||
if poly.len() < 3 {
|
||||
|
|
@ -253,7 +258,7 @@ pub fn mesh_poly_hit<'a>(
|
|||
}
|
||||
let mut out = Vec::new();
|
||||
for (h, mesh) in meshes {
|
||||
let proj = project_mesh_verts(mesh, view_proj, bounds);
|
||||
let proj = project_mesh_verts(mesh, view_rot, eye, bounds);
|
||||
if proj.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -285,7 +290,8 @@ pub fn box_hit<'a>(
|
|||
corner_b: Point,
|
||||
crossing: bool,
|
||||
wires: &'a [WireModel],
|
||||
view_proj: Mat4,
|
||||
view_rot: Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> Vec<&'a str> {
|
||||
let min_x = corner_a.x.min(corner_b.x);
|
||||
|
|
@ -339,7 +345,7 @@ pub fn box_hit<'a>(
|
|||
prev = None;
|
||||
continue;
|
||||
}
|
||||
let sp = world_to_screen(Vec3::new(px, py, pz), view_proj, bounds);
|
||||
let sp = world_to_screen(Vec3::new(px, py, pz), view_rot, eye, bounds);
|
||||
if crossing {
|
||||
if inside(sp) {
|
||||
hit = true;
|
||||
|
|
@ -385,7 +391,8 @@ pub fn poly_hit<'a>(
|
|||
poly: &[Point],
|
||||
crossing: bool,
|
||||
wires: &'a [WireModel],
|
||||
view_proj: Mat4,
|
||||
view_rot: Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> Vec<&'a str> {
|
||||
if poly.len() < 3 {
|
||||
|
|
@ -426,7 +433,7 @@ pub fn poly_hit<'a>(
|
|||
prev = None;
|
||||
continue;
|
||||
}
|
||||
let sp = world_to_screen(Vec3::new(px, py, pz), view_proj, bounds);
|
||||
let sp = world_to_screen(Vec3::new(px, py, pz), view_rot, eye, bounds);
|
||||
if crossing {
|
||||
if point_in_polygon(sp, poly) {
|
||||
hit = true;
|
||||
|
|
@ -462,8 +469,8 @@ pub fn poly_hit<'a>(
|
|||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn world_to_screen(world: Vec3, view_proj: Mat4, bounds: Rectangle) -> Point {
|
||||
let ndc = view_proj.project_point3(world);
|
||||
fn world_to_screen(world: Vec3, view_rot: Mat4, eye: glam::DVec3, bounds: Rectangle) -> Point {
|
||||
let ndc = view_rot.project_point3((world.as_dvec3() - eye).as_vec3());
|
||||
Point::new(
|
||||
(ndc.x + 1.0) * 0.5 * bounds.width,
|
||||
(1.0 - ndc.y) * 0.5 * bounds.height,
|
||||
|
|
@ -560,11 +567,12 @@ fn segments_intersect(a: Point, b: Point, c: Point, d: Point) -> bool {
|
|||
pub fn click_hit_hatch(
|
||||
cursor: Point,
|
||||
hatches: &HashMap<Handle, HatchModel>,
|
||||
view_proj: Mat4,
|
||||
view_rot: Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> Option<Handle> {
|
||||
for (&handle, hatch) in hatches {
|
||||
if hatch_contains_screen_point(hatch, cursor, view_proj, bounds) {
|
||||
if hatch_contains_screen_point(hatch, cursor, view_rot, eye, bounds) {
|
||||
return Some(handle);
|
||||
}
|
||||
}
|
||||
|
|
@ -579,11 +587,12 @@ pub fn click_hit_hatch(
|
|||
pub fn click_hit_insert_hatch(
|
||||
cursor: Point,
|
||||
insert_hatches: &[(Handle, HatchModel)],
|
||||
view_proj: Mat4,
|
||||
view_rot: Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> Option<Handle> {
|
||||
for (handle, hatch) in insert_hatches {
|
||||
if hatch_contains_screen_point(hatch, cursor, view_proj, bounds) {
|
||||
if hatch_contains_screen_point(hatch, cursor, view_rot, eye, bounds) {
|
||||
return Some(*handle);
|
||||
}
|
||||
}
|
||||
|
|
@ -593,7 +602,8 @@ pub fn click_hit_insert_hatch(
|
|||
fn hatch_contains_screen_point(
|
||||
hatch: &HatchModel,
|
||||
cursor: Point,
|
||||
view_proj: Mat4,
|
||||
view_rot: Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> bool {
|
||||
// boundary verts are stored as small f32 offsets from
|
||||
|
|
@ -606,7 +616,7 @@ fn hatch_contains_screen_point(
|
|||
.iter()
|
||||
.map(|&[x, y]| {
|
||||
if x.is_finite() && y.is_finite() {
|
||||
world_to_screen(Vec3::new(x + ox, y + oy, 0.0), view_proj, bounds)
|
||||
world_to_screen(Vec3::new(x + ox, y + oy, 0.0), view_rot, eye, bounds)
|
||||
} else {
|
||||
// Preserve path separators for the NaN-aware
|
||||
// point_in_polygon ray-cast.
|
||||
|
|
@ -623,7 +633,8 @@ pub fn box_hit_hatch(
|
|||
corner_b: Point,
|
||||
crossing: bool,
|
||||
hatches: &HashMap<Handle, HatchModel>,
|
||||
view_proj: Mat4,
|
||||
view_rot: Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> Vec<Handle> {
|
||||
let min_x = corner_a.x.min(corner_b.x);
|
||||
|
|
@ -648,7 +659,7 @@ pub fn box_hit_hatch(
|
|||
let screen: Vec<Point> = hatch
|
||||
.boundary
|
||||
.iter()
|
||||
.map(|&[x, y]| world_to_screen(Vec3::new(x + ox, y + oy, 0.0), view_proj, bounds))
|
||||
.map(|&[x, y]| world_to_screen(Vec3::new(x + ox, y + oy, 0.0), view_rot, eye, bounds))
|
||||
.collect();
|
||||
let hit = if crossing {
|
||||
screen.iter().any(|&sp| inside(sp))
|
||||
|
|
@ -669,7 +680,8 @@ pub fn poly_hit_hatch(
|
|||
poly: &[Point],
|
||||
crossing: bool,
|
||||
hatches: &HashMap<Handle, HatchModel>,
|
||||
view_proj: Mat4,
|
||||
view_rot: Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> Vec<Handle> {
|
||||
if poly.len() < 3 {
|
||||
|
|
@ -687,7 +699,7 @@ pub fn poly_hit_hatch(
|
|||
let screen: Vec<Point> = hatch
|
||||
.boundary
|
||||
.iter()
|
||||
.map(|&[x, y]| world_to_screen(Vec3::new(x + ox, y + oy, 0.0), view_proj, bounds))
|
||||
.map(|&[x, y]| world_to_screen(Vec3::new(x + ox, y + oy, 0.0), view_rot, eye, bounds))
|
||||
.collect();
|
||||
let hit = if crossing {
|
||||
screen.iter().any(|&sp| point_in_polygon(sp, poly))
|
||||
|
|
|
|||
|
|
@ -225,65 +225,17 @@ impl Camera {
|
|||
plane_normal: Vec3,
|
||||
plane_point: Vec3,
|
||||
) -> Vec3 {
|
||||
let ndc_x = (screen.x / bounds.width) * 2.0 - 1.0;
|
||||
let ndc_y = 1.0 - (screen.y / bounds.height) * 2.0;
|
||||
let inv = self.view_proj(bounds).inverse();
|
||||
|
||||
let (ray_origin, ray_dir) = match self.projection {
|
||||
Projection::Perspective => {
|
||||
let near_pt = inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.0));
|
||||
let far_pt = inv.project_point3(Vec3::new(ndc_x, ndc_y, 1.0));
|
||||
let dir = (far_pt - near_pt).normalize();
|
||||
(near_pt, dir)
|
||||
}
|
||||
Projection::Orthographic => {
|
||||
let origin = inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.0));
|
||||
let forward = (self.target.as_vec3() - self.eye()).normalize();
|
||||
(origin, forward)
|
||||
}
|
||||
};
|
||||
|
||||
let denom = ray_dir.dot(plane_normal);
|
||||
if denom.abs() < 1e-6 {
|
||||
return plane_point;
|
||||
}
|
||||
let t = (plane_point - ray_origin).dot(plane_normal) / denom;
|
||||
if t < 0.0 {
|
||||
return plane_point;
|
||||
}
|
||||
ray_origin + ray_dir * t
|
||||
// Delegate to the eye-relative f64 unproject so the cursor stays
|
||||
// precise at UTM-scale coordinates (the old full view_proj.inverse()
|
||||
// cancelled catastrophically in f32).
|
||||
self.unproject_on_plane_f64(screen, bounds, plane_normal, plane_point.as_dvec3())
|
||||
.as_vec3()
|
||||
}
|
||||
|
||||
pub fn pick_on_target_plane(&self, screen: Point, bounds: Rectangle) -> Vec3 {
|
||||
let ndc_x = (screen.x / bounds.width) * 2.0 - 1.0;
|
||||
let ndc_y = 1.0 - (screen.y / bounds.height) * 2.0;
|
||||
let inv = self.view_proj(bounds).inverse();
|
||||
|
||||
match self.projection {
|
||||
Projection::Perspective => {
|
||||
let target = self.target.as_vec3();
|
||||
let near_pt = inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.0));
|
||||
let far_pt = inv.project_point3(Vec3::new(ndc_x, ndc_y, 1.0));
|
||||
let dir = (far_pt - near_pt).normalize();
|
||||
let forward = (target - self.eye()).normalize();
|
||||
let denom = dir.dot(forward);
|
||||
if denom.abs() < 1e-6 {
|
||||
return target;
|
||||
}
|
||||
let t = (target - near_pt).dot(forward) / denom;
|
||||
if t < 0.0 {
|
||||
return target;
|
||||
}
|
||||
near_pt + dir * t
|
||||
}
|
||||
Projection::Orthographic => {
|
||||
let target = self.target.as_vec3();
|
||||
let ray_origin = inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.0));
|
||||
let forward = (target - self.eye()).normalize();
|
||||
let t = (target - ray_origin).dot(forward) / forward.dot(forward);
|
||||
ray_origin + forward * t
|
||||
}
|
||||
}
|
||||
let forward = (self.target.as_vec3() - self.eye()).normalize_or(Vec3::NEG_Z);
|
||||
self.unproject_on_plane_f64(screen, bounds, forward, self.target)
|
||||
.as_vec3()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -177,7 +177,8 @@ impl Snapper {
|
|||
pub fn update_otrack_dwell(
|
||||
&mut self,
|
||||
snap_world: Option<Vec3>,
|
||||
view_proj: glam::Mat4,
|
||||
view_rot: glam::Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: iced::Rectangle,
|
||||
now: Instant,
|
||||
) {
|
||||
|
|
@ -202,8 +203,8 @@ impl Snapper {
|
|||
Some(p) => {
|
||||
// Convert to screen to measure pixel distance.
|
||||
let is_same = if let Some(prev) = self.last_snap_world {
|
||||
let dp = world_to_screen(p, view_proj, bounds);
|
||||
let dp2 = world_to_screen(prev, view_proj, bounds);
|
||||
let dp = world_to_screen(p, view_rot, eye, bounds);
|
||||
let dp2 = world_to_screen(prev, view_rot, eye, bounds);
|
||||
let dx = dp.x - dp2.x;
|
||||
let dy = dp.y - dp2.y;
|
||||
(dx * dx + dy * dy).sqrt() < DWELL_PX
|
||||
|
|
@ -259,7 +260,8 @@ impl Snapper {
|
|||
pub fn otrack_snap(
|
||||
&self,
|
||||
cursor_world: Vec3,
|
||||
view_proj: glam::Mat4,
|
||||
view_rot: glam::Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: iced::Rectangle,
|
||||
polar_step_deg: Option<f32>,
|
||||
last_point: Option<Vec3>,
|
||||
|
|
@ -271,11 +273,11 @@ impl Snapper {
|
|||
return None;
|
||||
}
|
||||
|
||||
let cursor_screen = world_to_screen(cursor_world, view_proj, bounds);
|
||||
let cursor_screen = world_to_screen(cursor_world, view_rot, eye, bounds);
|
||||
// Use the same aperture as OSNAP so the catch distance is uniform.
|
||||
let r = self.osnap_radius_px;
|
||||
let screen_dist = |w: Vec3| {
|
||||
let s = world_to_screen(w, view_proj, bounds);
|
||||
let s = world_to_screen(w, view_rot, eye, bounds);
|
||||
((s.x - cursor_screen.x).powi(2) + (s.y - cursor_screen.y).powi(2)).sqrt()
|
||||
};
|
||||
|
||||
|
|
@ -409,7 +411,8 @@ impl Snapper {
|
|||
cursor_world: Vec3,
|
||||
cursor_screen: Point,
|
||||
wires: &[WireModel],
|
||||
view_proj: Mat4,
|
||||
view_rot: Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> Option<SnapResult> {
|
||||
let tmp = Snapper {
|
||||
|
|
@ -433,7 +436,8 @@ impl Snapper {
|
|||
cursor_world,
|
||||
cursor_screen,
|
||||
wires,
|
||||
view_proj,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
Vec3::ZERO,
|
||||
Mat4::IDENTITY,
|
||||
|
|
@ -446,7 +450,8 @@ impl Snapper {
|
|||
cursor_world: Vec3,
|
||||
cursor_screen: Point,
|
||||
wires: &[WireModel],
|
||||
view_proj: Mat4,
|
||||
view_rot: Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: Rectangle,
|
||||
// Grid origin (render/wire space) and UCS→world rotation, so grid snap
|
||||
// lands on the UCS grid the user sees. `(ZERO, IDENTITY)` = world grid.
|
||||
|
|
@ -474,7 +479,7 @@ impl Snapper {
|
|||
// view_proj col-0 x = 2*zoom / viewport_width for an orthographic camera,
|
||||
// so scale_x * (width/2) = pixels per world unit.
|
||||
let world_snap_r = {
|
||||
let s = view_proj.col(0).x.abs() * bounds.width * 0.5;
|
||||
let s = view_rot.col(0).x.abs() * bounds.width * 0.5;
|
||||
if s > 1e-6 {
|
||||
self.osnap_radius_px / s
|
||||
} else {
|
||||
|
|
@ -495,7 +500,7 @@ impl Snapper {
|
|||
};
|
||||
|
||||
let mut try_pt = |world: Vec3, snap_type: SnapType| {
|
||||
let screen = world_to_screen(world, view_proj, bounds);
|
||||
let screen = world_to_screen(world, view_rot, eye, bounds);
|
||||
let d2 = dist2(screen, cursor_screen);
|
||||
// `!(d2 < radius2)` (not `d2 >= radius2`) so a NaN distance from
|
||||
// degenerate geometry is rejected: with priority selection a NaN
|
||||
|
|
@ -672,7 +677,8 @@ impl Snapper {
|
|||
cursor_world,
|
||||
p0,
|
||||
p0 - p1,
|
||||
view_proj,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
self.osnap_radius_px,
|
||||
) {
|
||||
|
|
@ -687,7 +693,8 @@ impl Snapper {
|
|||
cursor_world,
|
||||
p_last,
|
||||
p_last - p_prev,
|
||||
view_proj,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
self.osnap_radius_px,
|
||||
) {
|
||||
|
|
@ -709,7 +716,7 @@ impl Snapper {
|
|||
Some(
|
||||
w.points
|
||||
.iter()
|
||||
.map(|&p| world_to_screen(Vec3::from(p), view_proj, bounds))
|
||||
.map(|&p| world_to_screen(Vec3::from(p), view_rot, eye, bounds))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
})
|
||||
|
|
@ -762,8 +769,8 @@ impl Snapper {
|
|||
for tg in &wire.tangent_geoms {
|
||||
let (world_pt, d2) = match tg {
|
||||
TangentGeom::Line { p1, p2 } => {
|
||||
let sp0 = world_to_screen(Vec3::from(*p1), view_proj, bounds);
|
||||
let sp1 = world_to_screen(Vec3::from(*p2), view_proj, bounds);
|
||||
let sp0 = world_to_screen(Vec3::from(*p1), view_rot, eye, bounds);
|
||||
let sp1 = world_to_screen(Vec3::from(*p2), view_rot, eye, bounds);
|
||||
let d2 = dist2_to_segment(cursor_screen, sp0, sp1);
|
||||
let t = t_on_segment(cursor_screen, sp0, sp1);
|
||||
let w = Vec3::from(*p1) + t * (Vec3::from(*p2) - Vec3::from(*p1));
|
||||
|
|
@ -771,10 +778,11 @@ impl Snapper {
|
|||
}
|
||||
TangentGeom::Circle { center, radius } => {
|
||||
let cv = Vec3::from(*center);
|
||||
let sc = world_to_screen(cv, view_proj, bounds);
|
||||
let sc = world_to_screen(cv, view_rot, eye, bounds);
|
||||
let rim = world_to_screen(
|
||||
Vec3::new(cv.x + radius, cv.y, cv.z),
|
||||
view_proj,
|
||||
view_rot,
|
||||
eye,
|
||||
bounds,
|
||||
);
|
||||
let sr = dist2(sc, rim).sqrt();
|
||||
|
|
@ -797,7 +805,7 @@ impl Snapper {
|
|||
if d2 < radius2 && (rank < best_rank || (rank == best_rank && d2 < best_d2)) {
|
||||
best_rank = rank;
|
||||
best_d2 = d2;
|
||||
let screen_pt = world_to_screen(world_pt, view_proj, bounds);
|
||||
let screen_pt = world_to_screen(world_pt, view_rot, eye, bounds);
|
||||
let tangent_obj = match tg {
|
||||
TangentGeom::Line { p1, p2 } => TangentObject::Line {
|
||||
p1: Vec3::from(*p1),
|
||||
|
|
@ -940,7 +948,8 @@ fn extension_snap(
|
|||
cursor_world: Vec3,
|
||||
origin: Vec3,
|
||||
dir: Vec3,
|
||||
view_proj: Mat4,
|
||||
view_rot: Mat4,
|
||||
eye: glam::DVec3,
|
||||
bounds: Rectangle,
|
||||
radius_px: f32,
|
||||
) -> Option<Vec3> {
|
||||
|
|
@ -953,8 +962,8 @@ fn extension_snap(
|
|||
return None;
|
||||
} // only beyond the endpoint
|
||||
let world_pt = Vec3::new(origin.x + t * dir.x, origin.y + t * dir.y, origin.z);
|
||||
let screen_pt = world_to_screen(world_pt, view_proj, bounds);
|
||||
let cursor_screen = world_to_screen(cursor_world, view_proj, bounds);
|
||||
let screen_pt = world_to_screen(world_pt, view_rot, eye, bounds);
|
||||
let cursor_screen = world_to_screen(cursor_world, view_rot, eye, bounds);
|
||||
if dist2(screen_pt, cursor_screen) > radius_px * radius_px {
|
||||
return None;
|
||||
}
|
||||
|
|
@ -963,8 +972,13 @@ fn extension_snap(
|
|||
|
||||
// ── Projection helpers ────────────────────────────────────────────────────
|
||||
|
||||
fn world_to_screen(world: Vec3, view_proj: Mat4, bounds: Rectangle) -> Point {
|
||||
let ndc = view_proj.project_point3(world);
|
||||
/// Project a world point to screen relative-to-eye: subtract the f64 eye first
|
||||
/// so the result is precise at UTM-scale absolute coordinates (a full
|
||||
/// view-projection with a ~1e7 translation cancels catastrophically in f32).
|
||||
/// `view_rot` is the rotation-only view-projection (Camera::view_proj_rte).
|
||||
fn world_to_screen(world: Vec3, view_rot: Mat4, eye: glam::DVec3, bounds: Rectangle) -> Point {
|
||||
let rel = (world.as_dvec3() - eye).as_vec3();
|
||||
let ndc = view_rot.project_point3(rel);
|
||||
Point::new(
|
||||
(ndc.x + 1.0) * 0.5 * bounds.width,
|
||||
(1.0 - ndc.y) * 0.5 * bounds.height,
|
||||
|
|
|
|||
|
|
@ -69,10 +69,13 @@ pub struct GridParams {
|
|||
/// Returns the smallest power-of-5 multiple of 1.0 that places grid lines at
|
||||
/// least `MIN_GRID_PX` pixels apart. This matches exactly what `draw_grid`
|
||||
/// renders, so callers can sync snap spacing to the visible grid.
|
||||
pub fn compute_grid_step(vp: Mat4, bounds: iced::Rectangle) -> f32 {
|
||||
pub fn compute_grid_step(view_rot: Mat4, bounds: iced::Rectangle) -> f32 {
|
||||
use glam::Vec3;
|
||||
// Only the per-unit screen scale is needed; project small eye-relative
|
||||
// offsets (0 / X / Y) through the rotation-only matrix so this stays correct
|
||||
// and precise at any absolute coordinate (no eye term required).
|
||||
let w2s = |world: Vec3| {
|
||||
let ndc = vp.project_point3(world);
|
||||
let ndc = view_rot.project_point3(world);
|
||||
glam::Vec2::new(
|
||||
(ndc.x + 1.0) * 0.5 * bounds.width,
|
||||
(1.0 - ndc.y) * 0.5 * bounds.height,
|
||||
|
|
|
|||
Loading…
Reference in a new issue