feat(overlay): relative-to-eye CPU projection for grips and grid (P5)
Route the on-screen chrome that projects absolute world coordinates through the f64 relative-to-eye helpers so it stays glued/jitter-free at UTM scale after the world_offset removal: grips (grips_to_screen) and grip hit-testing (find_hit_grip) now project from the f64 GripDef.world via Camera::project_f64; the model grid and its UCS axes carry the camera eye (DVec3) + rotation-only matrix and a f64 origin, projecting each line relative to the eye. Snap-marker / OST-point / compute_grid_step still use the f32 path (single points / spacing — negligible) and can follow. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
ccc933e7b0
commit
52882749eb
4 changed files with 64 additions and 57 deletions
|
|
@ -3281,8 +3281,8 @@ impl OpenCADStudio {
|
||||||
bounds,
|
bounds,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
let cam = self.tabs[i].scene.camera.borrow();
|
||||||
find_hit_grip(p, &self.tabs[i].selected_grips, vp_mat, bounds)
|
find_hit_grip(p, &self.tabs[i].selected_grips, &cam, bounds)
|
||||||
};
|
};
|
||||||
if let Some((grip_id, is_translate, world)) = grip_hit {
|
if let Some((grip_id, is_translate, world)) = grip_hit {
|
||||||
// The visibility (lookup) grip opens a state
|
// The visibility (lookup) grip opens a state
|
||||||
|
|
@ -8275,8 +8275,8 @@ impl OpenCADStudio {
|
||||||
bounds,
|
bounds,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let vp_mat = self.tabs[i].scene.camera.borrow().view_proj(bounds);
|
let cam = self.tabs[i].scene.camera.borrow();
|
||||||
find_hit_grip(p, &self.tabs[i].selected_grips, vp_mat, bounds)
|
find_hit_grip(p, &self.tabs[i].selected_grips, &cam, bounds)
|
||||||
};
|
};
|
||||||
match hit {
|
match hit {
|
||||||
Some((grip_id, _, _)) => {
|
Some((grip_id, _, _)) => {
|
||||||
|
|
|
||||||
|
|
@ -98,8 +98,8 @@ impl OpenCADStudio {
|
||||||
drop(cam);
|
drop(cam);
|
||||||
grips_to_screen_paper(&tab.selected_grips, tx, ty, half_w, half_h, bounds)
|
grips_to_screen_paper(&tab.selected_grips, tx, ty, half_w, half_h, bounds)
|
||||||
} else {
|
} else {
|
||||||
let vp_mat = tab.scene.camera.borrow().view_proj(bounds);
|
let cam = tab.scene.camera.borrow();
|
||||||
grips_to_screen(&tab.selected_grips, vp_mat, bounds)
|
grips_to_screen(&tab.selected_grips, &cam, bounds)
|
||||||
};
|
};
|
||||||
screen_grips
|
screen_grips
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -140,17 +140,11 @@ impl OpenCADStudio {
|
||||||
// enumeration for both, shared with the renderer.
|
// enumeration for both, shared with the renderer.
|
||||||
// Align the model grid to the active UCS (origin in wire space, world
|
// Align the model grid to the active UCS (origin in wire space, world
|
||||||
// axis directions); paper space stays plain WCS.
|
// axis directions); paper space stays plain WCS.
|
||||||
let (grid_origin, grid_axes) = if is_paper {
|
let (grid_origin, grid_axes): (glam::DVec3, _) = if is_paper {
|
||||||
(glam::Vec3::ZERO, (glam::Vec3::X, glam::Vec3::Y, glam::Vec3::Z))
|
(glam::DVec3::ZERO, (glam::Vec3::X, glam::Vec3::Y, glam::Vec3::Z))
|
||||||
} else {
|
} else {
|
||||||
let (o, ux, uy, uz) = tab.ucs_xform().axes();
|
let (o, ux, uy, uz) = tab.ucs_xform().axes();
|
||||||
let wo = [0.0_f64; 3];
|
(o.as_dvec3(), (ux, uy, uz))
|
||||||
let o_wire = glam::Vec3::new(
|
|
||||||
o.x - wo[0] as f32,
|
|
||||||
o.y - wo[1] as f32,
|
|
||||||
o.z - wo[2] as f32,
|
|
||||||
);
|
|
||||||
(o_wire, (ux, uy, uz))
|
|
||||||
};
|
};
|
||||||
let grid: Vec<overlay::GridParams> = tab
|
let grid: Vec<overlay::GridParams> = tab
|
||||||
.scene
|
.scene
|
||||||
|
|
@ -159,7 +153,8 @@ impl OpenCADStudio {
|
||||||
.map(|(bounds, cam)| {
|
.map(|(bounds, cam)| {
|
||||||
let plane = grid_plane_from_camera(cam.pitch, cam.yaw);
|
let plane = grid_plane_from_camera(cam.pitch, cam.yaw);
|
||||||
overlay::GridParams {
|
overlay::GridParams {
|
||||||
view_proj: cam.view_proj(bounds),
|
view_rot: cam.view_proj_rte(bounds),
|
||||||
|
eye: cam.eye_f64(),
|
||||||
bounds,
|
bounds,
|
||||||
plane,
|
plane,
|
||||||
origin: grid_origin,
|
origin: grid_origin,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! OpenCADStudio-style grip editing.
|
//! OpenCADStudio-style grip editing.
|
||||||
|
|
||||||
use acadrust::Handle;
|
use acadrust::Handle;
|
||||||
use glam::{Mat4, Vec3};
|
use glam::Vec3;
|
||||||
use iced::{Point, Rectangle};
|
use iced::{Point, Rectangle};
|
||||||
|
|
||||||
use crate::scene::model::object::{GripDef, GripShape};
|
use crate::scene::model::object::{GripDef, GripShape};
|
||||||
|
|
@ -34,17 +34,19 @@ pub struct GripEdit {
|
||||||
/// Returns `(grip_id, screen_pos, is_midpoint, shape)` for each grip.
|
/// Returns `(grip_id, screen_pos, is_midpoint, shape)` for each grip.
|
||||||
pub fn grips_to_screen(
|
pub fn grips_to_screen(
|
||||||
grips: &[GripDef],
|
grips: &[GripDef],
|
||||||
view_proj: Mat4,
|
camera: &crate::scene::view::camera::Camera,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
) -> Vec<(usize, Point, bool, GripShape, Option<[f32; 2]>)> {
|
) -> Vec<(usize, Point, bool, GripShape, Option<[f32; 2]>)> {
|
||||||
grips
|
grips
|
||||||
.iter()
|
.iter()
|
||||||
.map(|g| {
|
.map(|g| {
|
||||||
let ndc = view_proj.project_point3(g.world.as_vec3());
|
// Project from the f64 grip position via the relative-to-eye path so
|
||||||
let screen = Point::new(
|
// the grip stays glued to the wire at UTM-scale coordinates (an
|
||||||
bounds.x + (ndc.x + 1.0) * 0.5 * bounds.width,
|
// `as_vec3` cast first would quantize it ~0.5 m off at high zoom).
|
||||||
bounds.y + (1.0 - ndc.y) * 0.5 * bounds.height,
|
let screen = match camera.project_f64(g.world, bounds) {
|
||||||
);
|
Some(p) => Point::new(bounds.x + p.x, bounds.y + p.y),
|
||||||
|
None => Point::new(f32::NAN, f32::NAN),
|
||||||
|
};
|
||||||
(g.id, screen, g.is_midpoint, g.shape, g.dir)
|
(g.id, screen, g.is_midpoint, g.shape, g.dir)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
|
@ -106,18 +108,17 @@ pub fn find_hit_grip_paper(
|
||||||
pub fn find_hit_grip(
|
pub fn find_hit_grip(
|
||||||
cursor: Point,
|
cursor: Point,
|
||||||
grips: &[GripDef],
|
grips: &[GripDef],
|
||||||
view_proj: Mat4,
|
camera: &crate::scene::view::camera::Camera,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
) -> Option<(usize, bool, Vec3)> {
|
) -> Option<(usize, bool, Vec3)> {
|
||||||
let mut best_dist = GRIP_THRESHOLD_PX;
|
let mut best_dist = GRIP_THRESHOLD_PX;
|
||||||
let mut best: Option<(usize, bool, Vec3)> = None;
|
let mut best: Option<(usize, bool, Vec3)> = None;
|
||||||
|
|
||||||
for g in grips {
|
for g in grips {
|
||||||
let ndc = view_proj.project_point3(g.world.as_vec3());
|
let Some(screen) = camera.project_f64(g.world, bounds) else {
|
||||||
let screen = Point::new(
|
continue;
|
||||||
(ndc.x + 1.0) * 0.5 * bounds.width,
|
};
|
||||||
(1.0 - ndc.y) * 0.5 * bounds.height,
|
let screen = Point::new(screen.x, screen.y);
|
||||||
);
|
|
||||||
let dx = screen.x - cursor.x;
|
let dx = screen.x - cursor.x;
|
||||||
let dy = screen.y - cursor.y;
|
let dy = screen.y - cursor.y;
|
||||||
let d = (dx * dx + dy * dy).sqrt();
|
let d = (dx * dx + dy * dy).sqrt();
|
||||||
|
|
|
||||||
|
|
@ -48,13 +48,18 @@ pub enum GridPlane {
|
||||||
/// Passed to the canvas when the GRID display is active.
|
/// Passed to the canvas when the GRID display is active.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct GridParams {
|
pub struct GridParams {
|
||||||
pub view_proj: Mat4,
|
/// Rotation-only view-projection (Camera::view_proj_rte). Grid points are
|
||||||
|
/// made relative to `eye` in f64 before projecting, so the grid stays
|
||||||
|
/// precise / jitter-free at UTM-scale absolute coordinates.
|
||||||
|
pub view_rot: Mat4,
|
||||||
|
/// Camera eye in absolute world f64 — subtracted from each grid point.
|
||||||
|
pub eye: glam::DVec3,
|
||||||
pub bounds: iced::Rectangle,
|
pub bounds: iced::Rectangle,
|
||||||
pub plane: GridPlane,
|
pub plane: GridPlane,
|
||||||
/// Grid origin in render (wire) space and the active UCS axis directions.
|
/// Grid origin in absolute world f64 and the active UCS axis directions.
|
||||||
/// The grid rules along these instead of world X/Y/Z, so it aligns to the
|
/// The grid rules along these instead of world X/Y/Z, so it aligns to the
|
||||||
/// user's coordinate system. Plain WCS passes `(ZERO, X, Y, Z)`.
|
/// user's coordinate system. Plain WCS passes `(ZERO, X, Y, Z)`.
|
||||||
pub origin: Vec3,
|
pub origin: glam::DVec3,
|
||||||
pub axes: (Vec3, Vec3, Vec3),
|
pub axes: (Vec3, Vec3, Vec3),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -314,7 +319,7 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||||
// Project with the full viewport rect `gb` so the grid stays aligned;
|
// Project with the full viewport rect `gb` so the grid stays aligned;
|
||||||
// only the clip is clamped.
|
// only the clip is clamped.
|
||||||
frame.with_clip(clip, |f| {
|
frame.with_clip(clip, |f| {
|
||||||
draw_grid(f, g.view_proj, g.plane, gb, g.origin, g.axes)
|
draw_grid(f, g.view_rot, g.eye, g.plane, gb, g.origin, g.axes)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -864,17 +869,18 @@ const MIN_GRID_PX: f32 = 20.0;
|
||||||
|
|
||||||
fn draw_grid(
|
fn draw_grid(
|
||||||
frame: &mut canvas::Frame,
|
frame: &mut canvas::Frame,
|
||||||
vp: Mat4,
|
view_rot: Mat4,
|
||||||
|
eye: glam::DVec3,
|
||||||
plane: GridPlane,
|
plane: GridPlane,
|
||||||
bounds: iced::Rectangle,
|
bounds: iced::Rectangle,
|
||||||
grid_origin: Vec3,
|
grid_origin: glam::DVec3,
|
||||||
grid_axes: (Vec3, Vec3, Vec3),
|
grid_axes: (Vec3, Vec3, Vec3),
|
||||||
) {
|
) {
|
||||||
// World → canvas screen: include bounds origin so the grid lands in the
|
// World → canvas screen via relative-to-eye: subtract the f64 eye first so
|
||||||
// active tile's rectangle (the screen → world unproject below stays
|
// grid points near the camera stay precise at UTM-scale absolute coords.
|
||||||
// tile-local, which is what feeds the visible-extent computation).
|
let w2s = |world: glam::DVec3| -> Point {
|
||||||
let w2s = |world: Vec3| -> Point {
|
let rel = (world - eye).as_vec3();
|
||||||
let ndc = vp.project_point3(world);
|
let ndc = view_rot.project_point3(rel);
|
||||||
Point::new(
|
Point::new(
|
||||||
bounds.x + (ndc.x + 1.0) * 0.5 * bounds.width,
|
bounds.x + (ndc.x + 1.0) * 0.5 * bounds.width,
|
||||||
bounds.y + (1.0 - ndc.y) * 0.5 * bounds.height,
|
bounds.y + (1.0 - ndc.y) * 0.5 * bounds.height,
|
||||||
|
|
@ -893,8 +899,8 @@ fn draw_grid(
|
||||||
// Adaptive spacing: measure pixels per 1-unit step along each axis,
|
// Adaptive spacing: measure pixels per 1-unit step along each axis,
|
||||||
// then find the smallest power-of-5 multiple that gives ≥ MIN_GRID_PX.
|
// then find the smallest power-of-5 multiple that gives ≥ MIN_GRID_PX.
|
||||||
let o = w2s(grid_origin);
|
let o = w2s(grid_origin);
|
||||||
let a1s = w2s(grid_origin + axis1);
|
let a1s = w2s(grid_origin + axis1.as_dvec3());
|
||||||
let a2s = w2s(grid_origin + axis2);
|
let a2s = w2s(grid_origin + axis2.as_dvec3());
|
||||||
let px1 = ((a1s.x - o.x).powi(2) + (a1s.y - o.y).powi(2)).sqrt();
|
let px1 = ((a1s.x - o.x).powi(2) + (a1s.y - o.y).powi(2)).sqrt();
|
||||||
let px2 = ((a2s.x - o.x).powi(2) + (a2s.y - o.y).powi(2)).sqrt();
|
let px2 = ((a2s.x - o.x).powi(2) + (a2s.y - o.y).powi(2)).sqrt();
|
||||||
let px_per_unit = px1.max(px2);
|
let px_per_unit = px1.max(px2);
|
||||||
|
|
@ -912,11 +918,12 @@ fn draw_grid(
|
||||||
|
|
||||||
// Visible world extent: unproject screen corners (mid-depth approximation)
|
// Visible world extent: unproject screen corners (mid-depth approximation)
|
||||||
// and project them onto the grid axes.
|
// and project them onto the grid axes.
|
||||||
let inv = vp.inverse();
|
let inv = view_rot.inverse();
|
||||||
let unproject = |sx: f32, sy: f32| -> Vec3 {
|
let unproject = |sx: f32, sy: f32| -> glam::DVec3 {
|
||||||
let ndc_x = (sx / bounds.width) * 2.0 - 1.0;
|
let ndc_x = (sx / bounds.width) * 2.0 - 1.0;
|
||||||
let ndc_y = 1.0 - (sy / bounds.height) * 2.0;
|
let ndc_y = 1.0 - (sy / bounds.height) * 2.0;
|
||||||
inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.5))
|
// Unproject in eye-relative space, then add the f64 eye back.
|
||||||
|
eye + inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.5)).as_dvec3()
|
||||||
};
|
};
|
||||||
let corners = [
|
let corners = [
|
||||||
unproject(0.0, 0.0),
|
unproject(0.0, 0.0),
|
||||||
|
|
@ -925,7 +932,10 @@ fn draw_grid(
|
||||||
unproject(bounds.width, bounds.height),
|
unproject(bounds.width, bounds.height),
|
||||||
];
|
];
|
||||||
let range = |ax: Vec3| -> (f32, f32) {
|
let range = |ax: Vec3| -> (f32, f32) {
|
||||||
let vals: Vec<f32> = corners.iter().map(|p| (*p - grid_origin).dot(ax)).collect();
|
let vals: Vec<f32> = corners
|
||||||
|
.iter()
|
||||||
|
.map(|p| (*p - grid_origin).as_vec3().dot(ax))
|
||||||
|
.collect();
|
||||||
(
|
(
|
||||||
vals.iter().cloned().fold(f32::INFINITY, f32::min),
|
vals.iter().cloned().fold(f32::INFINITY, f32::min),
|
||||||
vals.iter().cloned().fold(f32::NEG_INFINITY, f32::max),
|
vals.iter().cloned().fold(f32::NEG_INFINITY, f32::max),
|
||||||
|
|
@ -957,8 +967,8 @@ fn draw_grid(
|
||||||
// Lines parallel to axis2 (varying axis1 position)
|
// Lines parallel to axis2 (varying axis1 position)
|
||||||
for i in n1_s..=n1_e {
|
for i in n1_s..=n1_e {
|
||||||
let v = i as f32 * s;
|
let v = i as f32 * s;
|
||||||
let p0 = w2s(grid_origin + axis1 * v + axis2 * (min2 - s));
|
let p0 = w2s(grid_origin + (axis1 * v + axis2 * (min2 - s)).as_dvec3());
|
||||||
let p1 = w2s(grid_origin + axis1 * v + axis2 * (max2 + s));
|
let p1 = w2s(grid_origin + (axis1 * v + axis2 * (max2 + s)).as_dvec3());
|
||||||
frame.stroke(
|
frame.stroke(
|
||||||
&canvas::Path::new(|b| {
|
&canvas::Path::new(|b| {
|
||||||
b.move_to(p0);
|
b.move_to(p0);
|
||||||
|
|
@ -970,8 +980,8 @@ fn draw_grid(
|
||||||
// Lines parallel to axis1 (varying axis2 position)
|
// Lines parallel to axis1 (varying axis2 position)
|
||||||
for i in n2_s..=n2_e {
|
for i in n2_s..=n2_e {
|
||||||
let v = i as f32 * s;
|
let v = i as f32 * s;
|
||||||
let p0 = w2s(grid_origin + axis2 * v + axis1 * (min1 - s));
|
let p0 = w2s(grid_origin + (axis2 * v + axis1 * (min1 - s)).as_dvec3());
|
||||||
let p1 = w2s(grid_origin + axis2 * v + axis1 * (max1 + s));
|
let p1 = w2s(grid_origin + (axis2 * v + axis1 * (max1 + s)).as_dvec3());
|
||||||
frame.stroke(
|
frame.stroke(
|
||||||
&canvas::Path::new(|b| {
|
&canvas::Path::new(|b| {
|
||||||
b.move_to(p0);
|
b.move_to(p0);
|
||||||
|
|
@ -983,21 +993,22 @@ fn draw_grid(
|
||||||
|
|
||||||
// Coloured axes drawn on top of the grid lines, along the same UCS basis.
|
// Coloured axes drawn on top of the grid lines, along the same UCS basis.
|
||||||
let extent = (min1.abs().max(max1.abs()).max(min2.abs()).max(max2.abs()) + s) * 1.5;
|
let extent = (min1.abs().max(max1.abs()).max(min2.abs()).max(max2.abs()) + s) * 1.5;
|
||||||
draw_axes(frame, vp, bounds, extent.max(10.0), grid_origin, grid_axes);
|
draw_axes(frame, view_rot, eye, bounds, extent.max(10.0), grid_origin, grid_axes);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Coloured UCS axes ──────────────────────────────────────────────────────
|
// ── Coloured UCS axes ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn draw_axes(
|
fn draw_axes(
|
||||||
frame: &mut canvas::Frame,
|
frame: &mut canvas::Frame,
|
||||||
vp: Mat4,
|
view_rot: Mat4,
|
||||||
|
eye: glam::DVec3,
|
||||||
bounds: iced::Rectangle,
|
bounds: iced::Rectangle,
|
||||||
extent: f32,
|
extent: f32,
|
||||||
origin: Vec3,
|
origin: glam::DVec3,
|
||||||
axes: (Vec3, Vec3, Vec3),
|
axes: (Vec3, Vec3, Vec3),
|
||||||
) {
|
) {
|
||||||
let w2s = |world: Vec3| -> Point {
|
let w2s = |world: glam::DVec3| -> Point {
|
||||||
let ndc = vp.project_point3(world);
|
let ndc = view_rot.project_point3((world - eye).as_vec3());
|
||||||
Point::new(
|
Point::new(
|
||||||
bounds.x + (ndc.x + 1.0) * 0.5 * bounds.width,
|
bounds.x + (ndc.x + 1.0) * 0.5 * bounds.width,
|
||||||
bounds.y + (1.0 - ndc.y) * 0.5 * bounds.height,
|
bounds.y + (1.0 - ndc.y) * 0.5 * bounds.height,
|
||||||
|
|
@ -1014,8 +1025,8 @@ fn draw_axes(
|
||||||
let mut line = |dir: Vec3, r: f32, g: f32, b: f32| {
|
let mut line = |dir: Vec3, r: f32, g: f32, b: f32| {
|
||||||
frame.stroke(
|
frame.stroke(
|
||||||
&canvas::Path::new(|p| {
|
&canvas::Path::new(|p| {
|
||||||
p.move_to(w2s(origin - dir * e));
|
p.move_to(w2s(origin - (dir * e).as_dvec3()));
|
||||||
p.line_to(w2s(origin + dir * e));
|
p.line_to(w2s(origin + (dir * e).as_dvec3()));
|
||||||
}),
|
}),
|
||||||
axis_stroke(r, g, b),
|
axis_stroke(r, g, b),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue