forked from 1b-agent/opencadstudio-upstream
feat(viewport): model-space tiled split (▤ / ▥ buttons)
Model layout artık tiled viewport destekliyor. Scene.model_tiles normalized ekran rect'i + kamera tutan tile listesi; active_viewports model layout'ta her tile için bir ViewportInstance üretiyor (aktif tile canlı scene.camera'yı, inaktif tile'lar saklı snapshot'ı kullanıyor). Render-mode picker'ın yanına iki buton eklendi: ▤ aktif tile'ı yatay (üst/alt), ▥ dikey (sol/sağ) ikiye böler. split_active_model_tile bölmeyi yapıyor; her iki yarı aktif kameranın kopyasını alıyor. set_active_model_tile_at (cursor → aktif tile, kamera swap) ve reset_model_tiles eklendi ama henüz bağlanmadı — aktif tile cursor seçimi + pick yönlendirme sonraki adımda. Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
1 parent
c83726c037
commit
c45146bbda
4 files changed
+205
-13
No files matched your search
@@ -355,6 +355,9 @@ pub enum Message {
|
||||
CommandBackspace,
|
||||
/// TAB pressed: move focus to the next dynamic-input field (wraps).
|
||||
DynTabNext,
|
||||
/// Split the active Model viewport in two. `true` → horizontal divider
|
||||
/// (top / bottom); `false` → vertical divider (left / right).
|
||||
SplitModelViewport(bool),
|
||||
/// Recall previous command in history (↑ arrow key).
|
||||
CommandHistoryPrev,
|
||||
/// Recall next command in history (↓ arrow key).
|
||||
|
||||
@@ -916,6 +916,13 @@ impl OpenCADStudio {
|
||||
self.focus_cmd_input()
|
||||
}
|
||||
|
||||
Message::SplitModelViewport(horizontal) => {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].scene.split_active_model_tile(horizontal);
|
||||
self.tabs[i].scene.camera_generation += 1;
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::CommandHistoryPrev => {
|
||||
// While autocomplete is showing suggestions, ↑ walks up
|
||||
// that list. Otherwise it falls back to recall history.
|
||||
|
||||
+47
-1
@@ -483,8 +483,54 @@ impl OpenCADStudio {
|
||||
// viewport drawing / selection is unaffected. In a paper layout
|
||||
// the active viewport gets its own picker (below) instead.
|
||||
if !is_paper && !tab.is_start {
|
||||
// Render-mode picker + viewport-split buttons (horizontal /
|
||||
// vertical divider of the active Model tile).
|
||||
let split_btn = |glyph: &'static str, horizontal: bool| {
|
||||
button(text(glyph).size(13).color(Color {
|
||||
r: 0.85,
|
||||
g: 0.85,
|
||||
b: 0.85,
|
||||
a: 1.0,
|
||||
}))
|
||||
.on_press(Message::SplitModelViewport(horizontal))
|
||||
.padding([4, 8])
|
||||
.style(|_: &Theme, status| iced::widget::button::Style {
|
||||
background: Some(Background::Color(match status {
|
||||
iced::widget::button::Status::Hovered => Color {
|
||||
r: 0.20,
|
||||
g: 0.20,
|
||||
b: 0.20,
|
||||
a: 0.85,
|
||||
},
|
||||
_ => Color {
|
||||
r: 0.10,
|
||||
g: 0.10,
|
||||
b: 0.10,
|
||||
a: 0.75,
|
||||
},
|
||||
})),
|
||||
border: Border {
|
||||
color: Color {
|
||||
r: 0.35,
|
||||
g: 0.35,
|
||||
b: 0.35,
|
||||
a: 1.0,
|
||||
},
|
||||
width: 1.0,
|
||||
radius: 4.0.into(),
|
||||
},
|
||||
text_color: Color {
|
||||
r: 0.85,
|
||||
g: 0.85,
|
||||
b: 0.85,
|
||||
a: 1.0,
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
};
|
||||
let bar = row![info, split_btn("▤", true), split_btn("▥", false)].spacing(4);
|
||||
viewport_stack = viewport_stack.push(
|
||||
container(iced::widget::opaque(info))
|
||||
container(iced::widget::opaque(bar))
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.align_x(iced::alignment::Horizontal::Left)
|
||||
|
||||
+148
-12
@@ -427,8 +427,23 @@ pub struct ViewportInstance {
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
/// One pane of the Model-space tiled viewport layout: the normalized screen
|
||||
/// rectangle it fills and the camera it last had. The active tile uses the
|
||||
/// live `Scene::camera` (so orbit/pan/zoom drive it); inactive tiles keep a
|
||||
/// snapshot here, swapped in when they become active.
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ModelTile {
|
||||
pub(crate) rect: iced::Rectangle,
|
||||
pub(crate) camera: Camera,
|
||||
}
|
||||
|
||||
pub struct Scene {
|
||||
pub camera: Rc<RefCell<Camera>>,
|
||||
/// Model-space tiled viewport layout. One full-window tile by default;
|
||||
/// the split buttons / VPORTS subdivide the active tile.
|
||||
pub(crate) model_tiles: RefCell<Vec<ModelTile>>,
|
||||
/// Index of the active model tile (camera input + overlays target it).
|
||||
pub(crate) active_model_tile: std::cell::Cell<usize>,
|
||||
pub selection: Rc<RefCell<SelectionState>>,
|
||||
/// The CAD document — single source of truth for all entities.
|
||||
pub document: CadDocument,
|
||||
@@ -539,6 +554,16 @@ impl Scene {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
camera: Rc::new(RefCell::new(Camera::default())),
|
||||
model_tiles: RefCell::new(vec![ModelTile {
|
||||
rect: iced::Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: 1.0,
|
||||
height: 1.0,
|
||||
},
|
||||
camera: Camera::default(),
|
||||
}]),
|
||||
active_model_tile: std::cell::Cell::new(0),
|
||||
selection: Rc::new(RefCell::new(SelectionState::default())),
|
||||
document: CadDocument::new(),
|
||||
selected: HashSet::new(),
|
||||
@@ -4256,6 +4281,102 @@ impl Scene {
|
||||
|
||||
// ── Paper-space coordinate helpers ───────────────────────────────────
|
||||
|
||||
/// Split the active Model tile in two. `horizontal` → a horizontal
|
||||
/// divider (top / bottom halves); otherwise a vertical divider (left /
|
||||
/// right). Both halves inherit the active tile's current camera; the
|
||||
/// active tile stays the first half. No-op outside the Model layout.
|
||||
pub fn split_active_model_tile(&self, horizontal: bool) {
|
||||
if self.current_layout != "Model" {
|
||||
return;
|
||||
}
|
||||
let cam_now = self.camera.borrow().clone();
|
||||
let mut tiles = self.model_tiles.borrow_mut();
|
||||
let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1));
|
||||
let r = tiles[active].rect;
|
||||
let (a, b) = if horizontal {
|
||||
(
|
||||
iced::Rectangle { height: r.height / 2.0, ..r },
|
||||
iced::Rectangle {
|
||||
y: r.y + r.height / 2.0,
|
||||
height: r.height / 2.0,
|
||||
..r
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(
|
||||
iced::Rectangle { width: r.width / 2.0, ..r },
|
||||
iced::Rectangle {
|
||||
x: r.x + r.width / 2.0,
|
||||
width: r.width / 2.0,
|
||||
..r
|
||||
},
|
||||
)
|
||||
};
|
||||
tiles[active] = ModelTile {
|
||||
rect: a,
|
||||
camera: cam_now.clone(),
|
||||
};
|
||||
tiles.insert(
|
||||
active + 1,
|
||||
ModelTile {
|
||||
rect: b,
|
||||
camera: cam_now,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Make the Model tile containing normalized point `(nx, ny)` active,
|
||||
/// swapping cameras so the live `Scene::camera` follows the new tile.
|
||||
/// Returns `true` when the active tile changed. No-op outside Model.
|
||||
pub fn set_active_model_tile_at(&self, nx: f32, ny: f32) -> bool {
|
||||
if self.current_layout != "Model" {
|
||||
return false;
|
||||
}
|
||||
let new = {
|
||||
let tiles = self.model_tiles.borrow();
|
||||
tiles.iter().position(|t| {
|
||||
nx >= t.rect.x
|
||||
&& nx < t.rect.x + t.rect.width
|
||||
&& ny >= t.rect.y
|
||||
&& ny < t.rect.y + t.rect.height
|
||||
})
|
||||
};
|
||||
let Some(new) = new else { return false };
|
||||
let old = self.active_model_tile.get();
|
||||
if new == old {
|
||||
return false;
|
||||
}
|
||||
// Stash the live camera into the outgoing tile, load the incoming.
|
||||
let incoming = {
|
||||
let mut tiles = self.model_tiles.borrow_mut();
|
||||
if let Some(t) = tiles.get_mut(old) {
|
||||
t.camera = self.camera.borrow().clone();
|
||||
}
|
||||
tiles.get(new).map(|t| t.camera.clone())
|
||||
};
|
||||
if let Some(cam) = incoming {
|
||||
*self.camera.borrow_mut() = cam;
|
||||
}
|
||||
self.active_model_tile.set(new);
|
||||
// Caller bumps camera_generation (it needs &mut Scene).
|
||||
true
|
||||
}
|
||||
|
||||
/// Reset the Model layout to a single full-window tile.
|
||||
pub fn reset_model_tiles(&self) {
|
||||
let cam_now = self.camera.borrow().clone();
|
||||
*self.model_tiles.borrow_mut() = vec![ModelTile {
|
||||
rect: iced::Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: 1.0,
|
||||
height: 1.0,
|
||||
},
|
||||
camera: cam_now,
|
||||
}];
|
||||
self.active_model_tile.set(0);
|
||||
}
|
||||
|
||||
/// The viewports to render this frame, one entry per scissor pass.
|
||||
///
|
||||
/// - **Model layout**: a single full-canvas instance driven by the
|
||||
@@ -4271,18 +4392,33 @@ impl Scene {
|
||||
model_mode: acadrust::entities::ViewportRenderMode,
|
||||
) -> Vec<ViewportInstance> {
|
||||
if self.current_layout == "Model" {
|
||||
return vec![ViewportInstance {
|
||||
handle: Handle::NULL,
|
||||
screen_rect: iced::Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: canvas_w,
|
||||
height: canvas_h,
|
||||
},
|
||||
camera: self.camera.borrow().clone(),
|
||||
render_mode: model_mode,
|
||||
active: true,
|
||||
}];
|
||||
let tiles = self.model_tiles.borrow();
|
||||
let active = self.active_model_tile.get().min(tiles.len().saturating_sub(1));
|
||||
return tiles
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, tile)| {
|
||||
// The active tile renders the live camera (orbit/pan act
|
||||
// on it); inactive tiles use their stored snapshot.
|
||||
let camera = if i == active {
|
||||
self.camera.borrow().clone()
|
||||
} else {
|
||||
tile.camera.clone()
|
||||
};
|
||||
ViewportInstance {
|
||||
handle: Handle::NULL,
|
||||
screen_rect: iced::Rectangle {
|
||||
x: tile.rect.x * canvas_w,
|
||||
y: tile.rect.y * canvas_h,
|
||||
width: tile.rect.width * canvas_w,
|
||||
height: tile.rect.height * canvas_h,
|
||||
},
|
||||
camera,
|
||||
render_mode: model_mode,
|
||||
active: i == active,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
let layout_block = self.current_layout_block_handle();
|
||||
let mut out: Vec<ViewportInstance> = Vec::new();
|
||||
|
||||
Reference in new issue
Block a user