feat(viewport): tile-relative input + overlays in unified model

Route every per-frame interaction through the active model tile's screen
rectangle so a tiled VPORTS layout behaves like independent panes while
still sharing one shader widget:

- pick / draw / snap / box+lasso: cursor mapped to tile-local space; the
  hover-activates-tile rule on ViewportMove keeps the active tile under
  the cursor, so the picked camera matches what the user sees.
- pan: switched to Camera::pan_screen(dx, dy, viewport_height) so the
  world-per-pixel rate matches the active tile's height instead of the
  full canvas.
- grid: clipped via Frame::with_clip to the active tile rect; world→screen
  adds the tile origin so the lines align with the panel.
- UCS icon: origin offset by the tile rect so the gizmo sits in the
  active tile's bottom-left corner.
- render-mode picker (+ split buttons) and ViewCube hit-area: positioned
  via active_model_tile_bounds with leading Spaces, mirroring the
  floating-viewport pattern.
- ViewCube hover: driven by the CursorMoved/ViewportMove handlers into a
  new Scene::viewcube_hover cell, so the highlight survives the cube
  hit-area overlay masking events from shader::Program::update.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.7 committed 2026-05-28 09:45:19 +03:00
1 parent 6fe25e4b5f
commit 417a708275
7 files changed
+330 -86

No files matched your search

+18 -6
View File
@@ -558,8 +558,12 @@ impl Scene {
&self,
bounds: Rectangle,
model_render_mode: acadrust::entities::ViewportRenderMode,
hover_region: Option<usize>,
_hover_region: Option<usize>,
) -> Primitive {
// Hover comes from the scene cell driven by the app-level
// `CursorMoved` handler — the cube overlay sits above the shader
// and would otherwise mask the move event from `Program::update`.
let hover_region = self.viewcube_hover.get();
self.selection.borrow_mut().vp_size = (bounds.width, bounds.height);
if bounds.height > 0.0 {
self.set_render_aspect(bounds.width / bounds.height);
@@ -682,10 +686,13 @@ impl Scene {
pub(super) fn build_viewport_primitive(
&self,
vp_handle: Handle,
hover_region: Option<usize>,
_hover_region: Option<usize>,
bounds: Rectangle,
show_viewcube: bool,
) -> Primitive {
// See `build_viewports` — the cube hit-area overlay shadows the
// shader, so hover is sourced from the scene cell.
let hover_region = self.viewcube_hover.get();
let render_mode = match self.document.get_entity(vp_handle) {
Some(EntityType::Viewport(vp)) => vp.render_mode,
_ => acadrust::entities::ViewportRenderMode::Wireframe2D,
@@ -755,6 +762,10 @@ impl Scene {
}
/// Update viewcube hover state from cursor position within `bounds`.
///
/// The cube draws in the top-right of the *active model tile* (which fills
/// the canvas when there is a single tile), so the hover hit-test maps the
/// cursor into that tile's local space and uses the tile's dimensions.
pub(super) fn update_viewcube_state(
&self,
state: &mut CameraState,
@@ -764,11 +775,12 @@ impl Scene {
let pos = cursor.position_in(bounds);
let cam_rotation = self.camera.borrow().view_rotation_mat();
if let Some(p) = pos {
let tile = self.active_model_tile_bounds(bounds.width, bounds.height);
state.hover_region = hover_id(
p.x,
p.y,
bounds.width,
bounds.height,
p.x - tile.x,
p.y - tile.y,
tile.width,
tile.height,
cam_rotation,
VIEWCUBE_PX,
);