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:
Hakan SevenandClaude Opus 4.8 committed 2026-06-24 17:06:59 +03:00
1 parent 4aa8e6681f
commit a66d873e51
5 files changed
+428 -172

No files matched your search

+22 -5
View File
@@ -3,7 +3,7 @@ use super::document::{DynComponent, DynFieldEntry};
use super::helpers::grid_plane_from_camera;
use super::history::history_dropdown_labels;
use super::{Message, OpenCADStudio};
use crate::scene::pick::grip::{grips_to_screen, grips_to_screen_paper};
use crate::scene::pick::grip::{grips_to_screen, grips_to_screen_paper, grips_to_screen_rte};
use crate::scene::view::viewport_pane::ViewportPane;
use crate::scene::{VIEWCUBE_DRAW_PX, VIEWCUBE_PAD};
use crate::ui::overlay;
@@ -86,9 +86,26 @@ impl OpenCADStudio {
// they must use the active tile's screen rectangle (with
// its canvas offset) — not the whole canvas — or they
// land in the wrong place in a tiled layout.
let bounds = tab.scene.active_model_tile_bounds(vw, vh);
// Inside a floating viewport the pane is the viewport's own
// rect + camera; otherwise the active model tile.
let edit_frame = tab.scene.viewport_edit_frame((vw, vh));
let bounds = match &edit_frame {
Some((_, full)) => *full,
None => tab.scene.active_model_tile_bounds(vw, vh),
};
let sel_h = tab.selected_handle;
let screen_grips = if is_paper {
// In-viewport grips are model-space; project them with the
// viewport camera so they sit on the wire the GPU draws.
// Paper entities use the 2-D paper transform; the model tab
// uses the model camera.
let screen_grips = if let Some((cam, _)) = &edit_frame {
grips_to_screen_rte(
&tab.selected_grips,
cam.view_proj_rte(bounds),
cam.eye_f64(),
bounds,
)
} else if is_paper {
let cam = tab.scene.camera.borrow();
let aspect = if vh > 0.0 { vw / vh } else { 1.0 };
let half_h = cam.ortho_size();
@@ -388,7 +405,7 @@ impl OpenCADStudio {
let bar = viewport_controls(
tab.render_mode,
self.show_grid,
self.snapper.is_on(crate::snap::SnapType::Grid),
self.snapper.grid_snap(),
true,
);
// Position the bar at the active model tile's top-left corner so
@@ -464,7 +481,7 @@ impl OpenCADStudio {
iced::widget::opaque(viewport_controls(
vp_mode,
self.show_grid,
self.snapper.is_on(crate::snap::SnapType::Grid),
self.snapper.grid_snap(),
false,
)),
],