forked from 1b-agent/opencadstudio-upstream
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:
1 parent
6fe25e4b5f
commit
417a708275
7 files changed
+330
-86
No files matched your search
+12
-4
@@ -234,12 +234,20 @@ impl Camera {
|
||||
self.target += before - after;
|
||||
}
|
||||
|
||||
pub fn pan(&mut self, delta_x: f32, delta_y: f32) {
|
||||
let speed = self.distance * 0.001;
|
||||
/// Pan so the world point under the cursor tracks it: screen pixels are
|
||||
/// converted to world units via the ortho world-per-pixel scale of a
|
||||
/// viewport `viewport_height` pixels tall. Used by tiled panes where the
|
||||
/// pane height differs from the full canvas.
|
||||
pub fn pan_screen(&mut self, delta_x: f32, delta_y: f32, viewport_height: f32) {
|
||||
let wpp = if viewport_height > 0.0 {
|
||||
(2.0 * self.ortho_size()) / viewport_height
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let cam_right = self.rotation * Vec3::X;
|
||||
let cam_up = self.rotation * Vec3::Y;
|
||||
self.target -= cam_right * delta_x * speed;
|
||||
self.target += cam_up * delta_y * speed;
|
||||
self.target -= cam_right * delta_x * wpp;
|
||||
self.target += cam_up * delta_y * wpp;
|
||||
}
|
||||
|
||||
pub fn fit_to_bounds(&mut self, min: Vec3, max: Vec3) {
|
||||
|
||||
+2
-2
@@ -42,8 +42,8 @@ pub fn grips_to_screen(
|
||||
.map(|g| {
|
||||
let ndc = view_proj.project_point3(g.world);
|
||||
let screen = Point::new(
|
||||
(ndc.x + 1.0) * 0.5 * bounds.width,
|
||||
(1.0 - ndc.y) * 0.5 * bounds.height,
|
||||
bounds.x + (ndc.x + 1.0) * 0.5 * bounds.width,
|
||||
bounds.y + (1.0 - ndc.y) * 0.5 * bounds.height,
|
||||
);
|
||||
(g.id, screen, g.is_midpoint, g.shape)
|
||||
})
|
||||
|
||||
+31
-1
@@ -40,7 +40,9 @@ pub use image_model::ImageModel;
|
||||
pub use mesh_model::MeshLodSet;
|
||||
pub use object::{GripApply, GripDef};
|
||||
pub use pipeline::uniforms::Uniforms;
|
||||
pub use pipeline::viewcube::{hit_test, CubeRegion, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX};
|
||||
pub use pipeline::viewcube::{
|
||||
hit_test, hover_id, CubeRegion, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX,
|
||||
};
|
||||
pub use selection::SelectionState;
|
||||
pub use wire_model::WireModel;
|
||||
|
||||
@@ -548,6 +550,12 @@ pub struct Scene {
|
||||
/// in expand_insert / tessellate_entity. 0 means "not yet set" — culling
|
||||
/// falls back to None.
|
||||
last_world_per_pixel: std::cell::Cell<f32>,
|
||||
/// ViewCube hover region (0..25, face/edge/corner index), driven by the
|
||||
/// `CursorMoved` message that the cube hit-area overlay publishes. Lives
|
||||
/// here so the unified render path can read it for the active viewport
|
||||
/// without depending on the shader widget's internal `Program::State`
|
||||
/// (which can miss events under overlapping overlays).
|
||||
pub viewcube_hover: std::cell::Cell<Option<usize>>,
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
@@ -597,6 +605,7 @@ impl Scene {
|
||||
entity_index_cache: RefCell::new(None),
|
||||
last_render_aspect: std::cell::Cell::new(16.0 / 9.0),
|
||||
last_world_per_pixel: std::cell::Cell::new(0.0),
|
||||
viewcube_hover: std::cell::Cell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4390,6 +4399,27 @@ impl Scene {
|
||||
self.active_model_tile.set(0);
|
||||
}
|
||||
|
||||
/// Screen-pixel rectangle of the active Model tile within a canvas of
|
||||
/// `(vw, vh)`. Full canvas outside the Model layout or for a single
|
||||
/// tile. Used to map cursor coordinates into the active tile so pick /
|
||||
/// pan / ViewCube work per-pane in a tiled layout.
|
||||
pub fn active_model_tile_bounds(&self, vw: f32, vh: f32) -> iced::Rectangle {
|
||||
if self.current_layout != "Model" {
|
||||
return iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
||||
}
|
||||
let tiles = self.model_tiles.borrow();
|
||||
let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1));
|
||||
match tiles.get(active) {
|
||||
Some(t) => iced::Rectangle {
|
||||
x: t.rect.x * vw,
|
||||
y: t.rect.y * vh,
|
||||
width: (t.rect.width * vw).max(1.0),
|
||||
height: (t.rect.height * vh).max(1.0),
|
||||
},
|
||||
None => iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh },
|
||||
}
|
||||
}
|
||||
|
||||
/// The viewports to render this frame, one entry per scissor pass.
|
||||
///
|
||||
/// - **Model layout**: a single full-canvas instance driven by the
|
||||
|
||||
+18
-6
@@ -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,
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user