feat(viewport): draggable Model-tile dividers

Tiled Model layouts now show the inner edges between panes as 2-px
dividers that act as resize grips: cursor on a divider → resize cue,
press + drag moves the edge (with every tile sharing that edge
updating together), release runs a collapse pass that absorbs any
tile that fell below the viewcube-comfort minimum into the neighbour
with the longest shared contact edge — drag a divider all the way
across to remove a pane.

- Scene grows `model_tile_edges`, `hit_model_tile_edge`,
  `move_model_tile_edge`, `collapse_small_model_tiles`. Edges are
  derived from the existing `model_tiles` rects (no extra storage).
- App carries a `tile_drag` Option; `ViewportLeftPress` starts it
  ahead of tile activation / picking, `ViewportMove` drives the
  edge, `ViewportLeftRelease` collapses + clears.
- `SelectionCanvas` draws the dividers and switches the cursor to
  `Resizing{Horizontally, Vertically}` over them.
This commit is contained in:
Hakan Seven 2026-05-28 18:42:39 +03:00
commit 39eab9a4c4
5 changed files with 550 additions and 2 deletions

View file

@ -6,7 +6,9 @@ use super::{Message, OpenCADStudio, POLY_START_DELAY_MS};
use crate::modules::ModuleEvent;
use crate::scene::grip::{find_hit_grip, find_hit_grip_paper, GripEdit};
use crate::scene::object::GripApply;
use crate::scene::{self, hover_id, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX};
use crate::scene::{
self, hover_id, Scene, TileEdgeOrient, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX,
};
use crate::ui::PropertiesPanel;
use acadrust::types::Color as AcadColor;
use acadrust::{EntityType as AcadEntityType, Handle};
@ -15,6 +17,20 @@ use iced::window;
use iced::{mouse, Task};
const VIEWCUBE_HIT_SIZE: f32 = VIEWCUBE_DRAW_PX;
/// Pixel distance from a Model-tile inner divider that still registers as
/// a resize grip on the press.
const TILE_EDGE_HIT_PX: f32 = 4.0;
/// Normalized minimum tile size before `collapse_small_model_tiles` merges
/// it into a neighbour. Sized to comfortably contain the ViewCube + its
/// padding so a tile that's still GPU-rendered always has room to show
/// the gizmo.
fn tile_min_norm(canvas_w: f32, canvas_h: f32) -> (f32, f32) {
let px = VIEWCUBE_DRAW_PX + 2.0 * VIEWCUBE_PAD + 16.0;
(
(px / canvas_w.max(1.0)).min(0.95),
(px / canvas_h.max(1.0)).min(0.95),
)
}
fn format_size(bytes: u64) -> String {
const KB: f64 = 1024.0;
@ -1646,6 +1662,35 @@ impl OpenCADStudio {
Message::ViewportMove(p) => {
let i = self.active_tab;
// A Model-tile divider drag short-circuits the rest of
// the move handling — the cursor neither pans the camera
// nor updates snap state while the user is resizing
// panes.
if let Some(drag) = self.tile_drag.as_mut() {
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
if vw > 1.0 && vh > 1.0 {
let new_coord = match drag.orient {
TileEdgeOrient::Vertical => (p.x / vw).clamp(0.0, 1.0),
TileEdgeOrient::Horizontal => (p.y / vh).clamp(0.0, 1.0),
};
let (min_w, min_h) = tile_min_norm(vw, vh);
let min_size = match drag.orient {
TileEdgeOrient::Vertical => min_w,
TileEdgeOrient::Horizontal => min_h,
};
self.tabs[i].scene.move_model_tile_edge(
drag.orient,
drag.last_applied,
new_coord,
min_size,
);
drag.last_applied = new_coord;
self.tabs[i].scene.camera_generation += 1;
}
return Task::none();
}
// Keep the ViewCube hover in sync as the cursor leaves the
// hit-area overlay and moves over the rest of the viewport.
// `hover_id` returns None outside the cube box, which clears
@ -2084,6 +2129,33 @@ impl OpenCADStudio {
}
}
// Tiled Model layout: an inner divider near the cursor
// becomes a resize grip. Start the drag and short-circuit
// (no pick / select / camera swap). Released on
// `ViewportLeftRelease`.
if self.tabs[i].active_cmd.is_none()
&& self.tabs[i].scene.current_layout == "Model"
&& vw > 1.0
&& vh > 1.0
{
let canvas_bounds = iced::Rectangle {
x: 0.0,
y: 0.0,
width: vw,
height: vh,
};
if let Some(edge) = self.tabs[i]
.scene
.hit_model_tile_edge(p, canvas_bounds, TILE_EDGE_HIT_PX)
{
self.tile_drag = Some(crate::app::TileDrag {
orient: edge.orient,
last_applied: edge.coord,
});
return Task::none();
}
}
// Tiled Model layout: clicking a non-active tile activates
// it (swapping in its camera) instead of selecting / drawing.
if self.tabs[i].active_cmd.is_none() && vw > 1.0 && vh > 1.0 {
@ -2165,6 +2237,22 @@ impl OpenCADStudio {
Message::ViewportLeftRelease => {
let i = self.active_tab;
// End an in-flight tile-divider drag. Any tile that fell
// below the minimum (viewcube fits comfortably) gets
// absorbed into its longest-contact neighbour, so the
// user can drag a divider all the way to one side to
// remove a pane.
if self.tile_drag.take().is_some() {
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
let (min_w, min_h) = tile_min_norm(vw, vh);
self.tabs[i]
.scene
.collapse_small_model_tiles(min_w, min_h);
self.tabs[i].scene.camera_generation += 1;
return Task::none();
}
let (p, is_click, is_down) = {
let sel = self.tabs[i].scene.selection.borrow();
let p = match sel.last_move_pos {