feat(viewport): unify in-viewport editing on the model-space path
Replace the leaky paper-projection approach to MSPACE editing with one coordinate adapter that routes all interaction through the viewport's own camera — the exact camera+rect the GPU draws the content with. - Add Scene::viewport_edit_frame(canvas) -> (camera_for_viewport, viewport_screen_rect): the unified adapter. Inside a floating viewport the viewport is the active pane, so snap/pick/grip/preview/DYN run in model space and land on the pixels the GPU draws. Tracks pan/zoom/twist exactly (twist handled by camera_from_view); crop is pixel-preserving so full-rect projection + cursor offset matches. - Route snap-move, click-pick, grip-drag, grip hit-test, grip render, hover (HoverDwellTick), selection (single/box/poly/cycle) and command entity-pick through it via pick_view / cursor_model_point helpers and an edit_cam-aware "active pane = viewport rect" override, so the existing tile offset plumbing just works. Fixes wrong-object hover/selection and stale snap after pan/zoom. - DYN: project anchors with the viewport camera and shift to canvas space so the guide/labels sit correctly (also fixes a latent tiled-model bug). - Separate grid snap from object snap entirely: new Snapper.grid_snap_on flag, Grid removed from the osnap mode set, evaluated independently of the osnap master. Object snap never catches grid; grid corners are picked only when grid snap is on, at lowest priority. Default off. - grip.rs: project_rte + grips_to_screen_rte / find_hit_grip_rte taking (view_rot, eye) so grips project through the composed view. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
4aa8e6681f
commit
a66d873e51
5 changed files with 427 additions and 171 deletions
|
|
@ -3677,10 +3677,10 @@ impl Scene {
|
|||
|
||||
/// Inverse of [`paper_to_model`]: map a model-space point to the paper
|
||||
/// sheet through the active viewport. Returns the input unchanged when
|
||||
/// there is no active viewport. Used to place overlays (dynamic-input
|
||||
/// anchors, guides) that are stored in model coords while editing inside a
|
||||
/// viewport — projecting the raw UTM model point with the paper camera puts
|
||||
/// them millions of pixels away.
|
||||
/// there is no active viewport. Kept as the inverse companion to
|
||||
/// `paper_to_model`; in-viewport overlays now project via the viewport
|
||||
/// camera ([`viewport_edit_frame`]) rather than mapping onto the sheet.
|
||||
#[allow(dead_code)]
|
||||
pub fn model_to_paper(&self, model_pt: glam::DVec3) -> glam::DVec3 {
|
||||
let vp_handle = match self.active_viewport {
|
||||
Some(h) => h,
|
||||
|
|
@ -3698,6 +3698,33 @@ impl Scene {
|
|||
)
|
||||
}
|
||||
|
||||
/// In-viewport (MSPACE) editing frame: the active floating viewport's own
|
||||
/// camera — *exactly* the one the GPU renders its content with
|
||||
/// ([`camera_for_viewport`]) — together with the viewport's full screen
|
||||
/// rectangle in canvas pixels ([`viewport_screen_rect`]).
|
||||
///
|
||||
/// This is the unified editing adapter (the "süzgeç"). Inside a viewport,
|
||||
/// editing IS model-space: treat the returned camera as *the* camera, the
|
||||
/// returned rect as *the* pane, and the cursor relative to that rect — then
|
||||
/// the existing model-space snap / hit-test / grip / preview / plane-pick
|
||||
/// code runs unchanged and lands on the same pixels the GPU draws. Results
|
||||
/// come back as model coordinates directly (no paper round-trip).
|
||||
///
|
||||
/// Because the camera is the real GPU camera, this tracks the viewport's
|
||||
/// pan / zoom / twist / oblique view correctly — unlike a linear
|
||||
/// paper-projection, whose auto-fit / saved-view / crop divergence left the
|
||||
/// snap stale after pan/zoom. Returns `None` when not editing inside a
|
||||
/// floating viewport, or the camera / rect cannot be derived.
|
||||
pub fn viewport_edit_frame(
|
||||
&self,
|
||||
canvas_px: (f32, f32),
|
||||
) -> Option<(view::camera::Camera, iced::Rectangle)> {
|
||||
let vp_handle = self.active_viewport?;
|
||||
let cam = self.camera_for_viewport(vp_handle)?;
|
||||
let full = self.viewport_screen_rect(vp_handle, canvas_px)?;
|
||||
Some((cam, full))
|
||||
}
|
||||
|
||||
/// Fold the active viewport's saved view onto the effective camera (the
|
||||
/// auto-fit centre for stale UTM views) and persist it into `view_target` /
|
||||
/// `view_height`. Called on entering MSPACE so pan/zoom, paper↔model and the
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
//! OpenCADStudio-style grip editing.
|
||||
|
||||
use acadrust::Handle;
|
||||
use glam::DVec3;
|
||||
use glam::{DVec3, Mat4, Vec2};
|
||||
use iced::{Point, Rectangle};
|
||||
|
||||
use crate::scene::model::object::{GripDef, GripShape};
|
||||
|
|
@ -129,3 +129,68 @@ pub fn find_hit_grip(
|
|||
}
|
||||
best
|
||||
}
|
||||
|
||||
/// Project an f64 world point with an explicit relative-to-eye `(view_rot,
|
||||
/// eye)` pair — the camera-less form of `Camera::project_f64`. Used by the
|
||||
/// in-viewport editing path, which supplies a *composed* model→screen view
|
||||
/// (see `Scene::composed_viewport_view`) instead of a real camera.
|
||||
fn project_rte(world: DVec3, view_rot: Mat4, eye: DVec3, bounds: Rectangle) -> Option<Vec2> {
|
||||
let rel = (world - eye).as_vec3();
|
||||
let clip = view_rot * rel.extend(1.0);
|
||||
if clip.w.abs() < 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let ndc = clip.truncate() / clip.w;
|
||||
Some(Vec2::new(
|
||||
(ndc.x * 0.5 + 0.5) * bounds.width,
|
||||
(0.5 - ndc.y * 0.5) * bounds.height,
|
||||
))
|
||||
}
|
||||
|
||||
/// Relative-to-eye variant of [`grips_to_screen`] taking a `(view_rot, eye)`
|
||||
/// pair instead of a `Camera`, so the composed in-viewport view can project
|
||||
/// model-space grips exactly like the GPU renders the viewport content.
|
||||
pub fn grips_to_screen_rte(
|
||||
grips: &[GripDef],
|
||||
view_rot: Mat4,
|
||||
eye: DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> Vec<(usize, Point, bool, GripShape, Option<[f32; 2]>)> {
|
||||
grips
|
||||
.iter()
|
||||
.map(|g| {
|
||||
let screen = match project_rte(g.world, view_rot, eye, 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)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Relative-to-eye variant of [`find_hit_grip`] taking a `(view_rot, eye)`
|
||||
/// pair instead of a `Camera`, for in-viewport (MSPACE) grip hit-testing.
|
||||
pub fn find_hit_grip_rte(
|
||||
cursor: Point,
|
||||
grips: &[GripDef],
|
||||
view_rot: Mat4,
|
||||
eye: DVec3,
|
||||
bounds: Rectangle,
|
||||
) -> Option<(usize, bool, DVec3)> {
|
||||
let mut best_dist = GRIP_THRESHOLD_PX;
|
||||
let mut best: Option<(usize, bool, DVec3)> = None;
|
||||
|
||||
for g in grips {
|
||||
let Some(screen) = project_rte(g.world, view_rot, eye, bounds) else {
|
||||
continue;
|
||||
};
|
||||
let dx = screen.x - cursor.x;
|
||||
let dy = screen.y - cursor.y;
|
||||
let d = (dx * dx + dy * dy).sqrt();
|
||||
if d < best_dist {
|
||||
best_dist = d;
|
||||
best = Some((g.id, g.is_midpoint, g.world));
|
||||
}
|
||||
}
|
||||
best
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue