forked from 1b-agent/opencadstudio-upstream
feat(ui): refine ribbon and view navigation
This commit is contained in:
1 parent
675538eb8d
commit
d1f9055901
14 files changed
+466
-116
No files matched your search
@@ -99,6 +99,7 @@ impl OpenCADStudio {
|
||||
self.ucs_icon_selected = false;
|
||||
self.ucs_icon_hover = false;
|
||||
self.tabs[i].pan_mode = false;
|
||||
self.tabs[i].orbit_mode = false;
|
||||
let _ = self.on_viewport_exit();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@ impl OpenCADStudio {
|
||||
pub(super) fn dispatch_inquiry(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||||
match cmd {
|
||||
"3DORBIT" => {
|
||||
self.command_line
|
||||
.push_info(crate::t!("3D Orbit: drag with right mouse button.").as_ref());
|
||||
self.tabs[i].orbit_mode = true;
|
||||
}
|
||||
|
||||
// ── Selection utilities ───────────────────────────────────────
|
||||
|
||||
@@ -76,9 +76,10 @@ impl OpenCADStudio {
|
||||
// template-property override too (#239).
|
||||
self.restore_add_selected_defaults();
|
||||
}
|
||||
// Starting any command leaves interactive PAN mode (the PAN arm below
|
||||
// re-enables it).
|
||||
// Starting any command leaves interactive navigation modes (their own
|
||||
// command arms below re-enable the selected one).
|
||||
self.tabs[i].pan_mode = false;
|
||||
self.tabs[i].orbit_mode = false;
|
||||
// Reset the last committed point so the first click of the new command
|
||||
// is not constrained by ortho/polar relative to a previous command's endpoint.
|
||||
self.last_point = None;
|
||||
|
||||
@@ -209,6 +209,9 @@ pub(super) struct DocumentTab {
|
||||
/// device with no middle mouse button (a trackpad / web client). Exited
|
||||
/// with Esc or by starting another command.
|
||||
pub(super) pan_mode: bool,
|
||||
/// Interactive 3-D orbit mode. While active, a left-button drag follows
|
||||
/// the same camera-orbit path as Shift + middle-button drag.
|
||||
pub(super) orbit_mode: bool,
|
||||
/// Per-plugin document state (`plugin::BuiltinPlugin` manifest id → state).
|
||||
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
|
||||
pub(super) plugin_state: HashMap<&'static str, Box<dyn Any + Send + Sync>>,
|
||||
@@ -479,6 +482,7 @@ impl DocumentTab {
|
||||
thumbnail_cache_key: None,
|
||||
is_start: false,
|
||||
pan_mode: false,
|
||||
orbit_mode: false,
|
||||
plugin_state: HashMap::new(),
|
||||
suspended_cmd: None,
|
||||
}
|
||||
|
||||
+6
-2
@@ -1893,8 +1893,12 @@ pub enum Message {
|
||||
LayerStateEditorFilter(String),
|
||||
LayerStateEditorSave,
|
||||
LayerStateEditorCancel,
|
||||
CursorMoved(Point),
|
||||
ViewportClick,
|
||||
/// ViewCube-local cursor movement, tagged with the floating viewport that
|
||||
/// owned the overlay when the event was produced (`None` = Model layout).
|
||||
CursorMoved(Point, Option<acadrust::Handle>),
|
||||
/// ViewCube press with the same owner tag, so a stale overlay event can
|
||||
/// never fall through and rotate a different camera.
|
||||
ViewportClick(Option<acadrust::Handle>),
|
||||
ViewportMove(Point),
|
||||
ViewportLeftPress,
|
||||
ViewportLeftRelease,
|
||||
|
||||
@@ -569,16 +569,25 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
self.tabs[i].snap_result = None;
|
||||
return Task::none();
|
||||
}
|
||||
// Leave interactive PAN mode (and end any in-flight pan drag).
|
||||
if self.tabs[self.active_tab].pan_mode {
|
||||
// Leave an interactive navigation mode and end its in-flight
|
||||
// drag. Orbit exits silently; PAN keeps its existing message.
|
||||
if self.tabs[self.active_tab].pan_mode
|
||||
|| self.tabs[self.active_tab].orbit_mode
|
||||
{
|
||||
let i = self.active_tab;
|
||||
let was_pan = self.tabs[i].pan_mode;
|
||||
self.tabs[i].pan_mode = false;
|
||||
self.tabs[i].orbit_mode = false;
|
||||
{
|
||||
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
||||
sel.middle_down = false;
|
||||
sel.middle_last_pos = None;
|
||||
sel.orbit_pivot = None;
|
||||
}
|
||||
self.command_line.push_output(crate::t!("PAN ended.").as_ref());
|
||||
if was_pan {
|
||||
self.command_line.push_output(crate::t!("PAN ended.").as_ref());
|
||||
}
|
||||
self.ribbon.deactivate_tool();
|
||||
return Task::none();
|
||||
}
|
||||
// Grip popup intercepts Escape — dismisses the menu
|
||||
|
||||
@@ -138,7 +138,11 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
|
||||
// dialog owners keep theirs; the command end / modal
|
||||
// close clears those. (#355)
|
||||
let i = self.active_tab;
|
||||
if self.tabs[i].active_cmd.is_none() && self.active_modal.is_none() {
|
||||
if self.tabs[i].active_cmd.is_none()
|
||||
&& self.active_modal.is_none()
|
||||
&& !self.tabs[i].pan_mode
|
||||
&& !self.tabs[i].orbit_mode
|
||||
{
|
||||
self.ribbon.deactivate_tool();
|
||||
}
|
||||
return task;
|
||||
|
||||
@@ -2420,7 +2420,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
// ── Cursor / viewport messages ─────────────────────────────────
|
||||
Message::CursorMoved(p) => self.on_cursor_moved(p),
|
||||
Message::CursorMoved(p, viewport) => self.on_cursor_moved(p, viewport),
|
||||
|
||||
Message::ViewportMove(p) => self.on_viewport_move(p),
|
||||
|
||||
@@ -2587,7 +2587,7 @@ impl OpenCADStudio {
|
||||
|
||||
Message::ViewportScroll(delta) => self.on_viewport_scroll(delta),
|
||||
|
||||
Message::ViewportClick => self.on_viewport_click(),
|
||||
Message::ViewportClick(viewport) => self.on_viewport_click(viewport),
|
||||
|
||||
Message::WindowResized(w, h) => {
|
||||
self.vp_size = ((w - 440.0).max(200.0), h);
|
||||
|
||||
+44
-15
@@ -482,6 +482,12 @@ impl OpenCADStudio {
|
||||
if let Some(session) = self.tabs[i].active_block_edit_session_mut() {
|
||||
session.editor_camera = camera;
|
||||
}
|
||||
} else if self.tabs[i].scene.active_viewport.is_some() {
|
||||
// Floating-viewport navigation writes its camera straight to
|
||||
// the viewport entity. Syncing the separate main camera here
|
||||
// can overwrite the saved Model/Paper view with a camera that
|
||||
// does not own this change.
|
||||
self.tabs[i].dirty = true;
|
||||
} else if self.tabs[i].scene.sync_camera_to_document() {
|
||||
self.tabs[i].dirty = true;
|
||||
}
|
||||
@@ -541,7 +547,11 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(super) fn on_cursor_moved(&mut self, p: Point) -> Task<Message> {
|
||||
pub(super) fn on_cursor_moved(
|
||||
&mut self,
|
||||
p: Point,
|
||||
expected_viewport: Option<acadrust::Handle>,
|
||||
) -> Task<Message> {
|
||||
if self.color_pick_target.is_some() {
|
||||
return Task::none();
|
||||
}
|
||||
@@ -552,6 +562,12 @@ impl OpenCADStudio {
|
||||
// the full canvas in model space, or of the active
|
||||
// viewport's screen rectangle in a paper layout.
|
||||
let i = self.active_tab;
|
||||
if self.tabs[i].scene.active_viewport != expected_viewport
|
||||
|| (expected_viewport.is_none()
|
||||
&& self.tabs[i].scene.current_layout != "Model")
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
||||
let (ox, oy) = match self.tabs[i]
|
||||
.scene
|
||||
@@ -716,7 +732,7 @@ impl OpenCADStudio {
|
||||
// — the requested Zoom=wheel / Pan=MMB / Rotate=Shift+MMB
|
||||
// scheme (#229). Floating viewports and paper keep the
|
||||
// plain MMB pan.
|
||||
if self.shift_down {
|
||||
if self.shift_down || self.tabs[i].orbit_mode {
|
||||
if self.tabs[i].scene.active_viewport.is_some() {
|
||||
// Orbit the floating viewport's own model view.
|
||||
drop(sel);
|
||||
@@ -746,8 +762,12 @@ impl OpenCADStudio {
|
||||
self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p);
|
||||
return Task::none();
|
||||
}
|
||||
// Paper sheet is top-locked: Shift+MMB falls through
|
||||
// to the plain pan below.
|
||||
// Paper sheet is top-locked. Shift+MMB keeps its existing
|
||||
// pan fallback; the explicit orbit tool does nothing here.
|
||||
if self.tabs[i].orbit_mode {
|
||||
sel.middle_last_pos = Some(p);
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
// Pan scale uses the active tile's size (ortho size
|
||||
// is relative to viewport height), so a tiled pane
|
||||
@@ -2185,10 +2205,9 @@ impl OpenCADStudio {
|
||||
};
|
||||
let (vw, vh) = vp_size;
|
||||
|
||||
// PAN mode: a left press begins a pan drag. Reuse the middle-
|
||||
// button pan path (the move handler pans whenever `middle_down`),
|
||||
// so no selection/pick logic runs while panning.
|
||||
if self.tabs[i].pan_mode {
|
||||
// Interactive navigation tools reuse the middle-button movement path,
|
||||
// so no selection/pick logic runs while the left button drives them.
|
||||
if self.tabs[i].orbit_mode || self.tabs[i].pan_mode {
|
||||
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
||||
sel.middle_down = true;
|
||||
sel.middle_last_pos = Some(p);
|
||||
@@ -2357,12 +2376,13 @@ impl OpenCADStudio {
|
||||
pub(super) fn on_viewport_left_release(&mut self) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
|
||||
// PAN mode: end the pan drag but stay in pan mode for the next
|
||||
// drag (exit is Esc / another command). Mirror of the press.
|
||||
if self.tabs[i].pan_mode {
|
||||
// Navigation mode: end this drag but keep the tool armed for the next
|
||||
// left drag (exit is Esc / another command). Mirror of the press.
|
||||
if self.tabs[i].orbit_mode || self.tabs[i].pan_mode {
|
||||
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
||||
sel.middle_down = false;
|
||||
sel.middle_last_pos = None;
|
||||
sel.orbit_pivot = None;
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
@@ -3886,8 +3906,17 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn on_viewport_click(&mut self) -> Task<Message> {
|
||||
pub(super) fn on_viewport_click(
|
||||
&mut self,
|
||||
expected_viewport: Option<acadrust::Handle>,
|
||||
) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
if self.tabs[i].scene.active_viewport != expected_viewport
|
||||
|| (expected_viewport.is_none()
|
||||
&& self.tabs[i].scene.current_layout != "Model")
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
let rot = self.tabs[i].scene.active_view_rotation_mat();
|
||||
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
||||
// The ViewCube draws in the top-right of whichever area
|
||||
@@ -3929,17 +3958,17 @@ impl OpenCADStudio {
|
||||
} else {
|
||||
scene::CubeRegion::Corner(id)
|
||||
};
|
||||
return Task::done(Message::ViewCubeSnap(region));
|
||||
return self.on_view_cube_snap(region);
|
||||
}
|
||||
if let Some(region) = scene::hit_test(cx, cy, w, h, rot, VIEWCUBE_PX) {
|
||||
return Task::done(Message::ViewCubeSnap(region));
|
||||
return self.on_view_cube_snap(region);
|
||||
}
|
||||
// Compass cardinals are world-fixed: hit-test through the camera-
|
||||
// only rotation (strip the UCS) so the target matches the drawn
|
||||
// N/E/S/W, and snap in world frame.
|
||||
let rot_world = rot * self.tabs[i].scene.viewcube_ucs_mat().inverse();
|
||||
if let Some(card) = scene::hit_test_cardinal(cx, cy, w, h, rot_world, VIEWCUBE_PX) {
|
||||
return Task::done(Message::ViewCubeSnapWorld(card.face_region()));
|
||||
return self.on_view_cube_snap_world(card.face_region());
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
+15
-12
@@ -671,7 +671,7 @@ impl OpenCADStudio {
|
||||
dividers,
|
||||
pane_move_rect,
|
||||
pane_drop_rect,
|
||||
tab.pan_mode,
|
||||
tab.pan_mode || tab.orbit_mode,
|
||||
self.ribbon.open_dropdown.is_some(),
|
||||
hover_locked,
|
||||
crosshair_background(tab, is_paper),
|
||||
@@ -886,15 +886,18 @@ impl OpenCADStudio {
|
||||
// both layered ABOVE the viewport mouse_area so they receive
|
||||
// clicks (the shader viewport sits below it). Positioned with
|
||||
// leading Spaces sized to the viewport's screen rectangle.
|
||||
let active_vp_rect: Option<iced::Rectangle> = if is_paper && !tab.is_start {
|
||||
tab.scene.active_viewport.and_then(|h| {
|
||||
let (cw, ch) = tab.scene.selection.borrow().vp_size;
|
||||
tab.scene.viewport_screen_rect(h, (cw, ch))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(rect) = active_vp_rect {
|
||||
let active_vp_rect: Option<(acadrust::Handle, iced::Rectangle)> =
|
||||
if is_paper && !tab.is_start {
|
||||
tab.scene.active_viewport.and_then(|h| {
|
||||
let (cw, ch) = tab.scene.selection.borrow().vp_size;
|
||||
tab.scene
|
||||
.viewport_screen_rect(h, (cw, ch))
|
||||
.map(|rect| (h, rect))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some((active_vp, rect)) = active_vp_rect {
|
||||
// Clip the outline to the visible canvas. Clamping only the origin
|
||||
// (max(0.0)) while keeping the full width/height shifted the whole
|
||||
// outline inward when the viewport ran off the top/left edge, so
|
||||
@@ -964,7 +967,7 @@ impl OpenCADStudio {
|
||||
let cube_x = (rect.x + rect.width - VIEWCUBE_HIT_SIZE - VIEWCUBE_PAD).max(0.0);
|
||||
let cube_y = (rect.y + VIEWCUBE_PAD).max(0.0);
|
||||
|
||||
let controls = iced::widget::pin(viewcube_nav_controls())
|
||||
let controls = iced::widget::pin(viewcube_nav_controls(Some(active_vp)))
|
||||
.position(iced::Point::new(cube_x, cube_y));
|
||||
viewport_stack = viewport_stack.push(controls);
|
||||
|
||||
@@ -1003,7 +1006,7 @@ impl OpenCADStudio {
|
||||
let cube_y = (rect.y + VIEWCUBE_PAD).max(0.0);
|
||||
|
||||
// Cube hit area + nav controls (home / roll / nudge) as one layer.
|
||||
let controls = iced::widget::pin(viewcube_nav_controls())
|
||||
let controls = iced::widget::pin(viewcube_nav_controls(None))
|
||||
.position(iced::Point::new(cube_x, cube_y));
|
||||
viewport_stack = viewport_stack.push(controls);
|
||||
|
||||
|
||||
@@ -52,7 +52,9 @@ fn vc_btn<'a>(content: Element<'a, Message>, size: f32, msg: Message) -> Element
|
||||
|
||||
/// Overlay of home / roll / nudge controls sized to the whole nav region, so
|
||||
/// the caller can position it exactly like the cube hit area.
|
||||
pub(super) fn viewcube_nav_controls<'a>() -> Element<'a, Message> {
|
||||
pub(super) fn viewcube_nav_controls<'a>(
|
||||
viewport: Option<acadrust::Handle>,
|
||||
) -> Element<'a, Message> {
|
||||
use crate::scene::NudgeDir;
|
||||
use crate::ui::icons;
|
||||
let r = VIEWCUBE_REGION_PX;
|
||||
@@ -81,8 +83,8 @@ pub(super) fn viewcube_nav_controls<'a>() -> Element<'a, Message> {
|
||||
.width(iced::Length::Fixed(r))
|
||||
.height(iced::Length::Fixed(r)),
|
||||
)
|
||||
.on_move(Message::CursorMoved)
|
||||
.on_press(Message::ViewportClick);
|
||||
.on_move(move |point| Message::CursorMoved(point, viewport))
|
||||
.on_press(Message::ViewportClick(viewport));
|
||||
|
||||
let controls = stack![
|
||||
cube_hit,
|
||||
|
||||
Reference in new issue
Block a user