feat(viewcube): add compass ring, cardinals, and nav controls

Build the ViewCube out into a full navigation widget:

- Compass ring painted in the cube's ground plane (z = base) so it
  reads as a ground compass and foreshortens with the view.
- N/E/S/W cardinals painted flat onto the ring band; clicking one snaps
  to that side elevation. Added S/W glyphs to the bitmap atlas.
- Home button (top-down view), roll arrows (90° roll about the view
  axis), and four nudge triangles (90° tip/spin) as SVG iced controls
  laid out around the cube. New home.svg.
- WCS / named-UCS selector under the cube, switching the active UCS.

The cube hit area now shares one stack with the control buttons, so a
click on a button is caught by the button while a click on the cube
falls through. Face clicks snap to the highlighted region rather than a
cursor position the viewport's move handler can overwrite.

Camera gains home_view / roll_by / nudge_90; floating viewports drive
the same ops through mutate_active_viewport_camera, which re-encodes the
result to the stored view direction + twist.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-25 23:46:43 +03:00
1 parent 6c4b981424
commit f00c48bebb
9 files changed
+633 -40

No files matched your search

+12
View File
@@ -12,6 +12,8 @@ use iced::{Color, Element, Length, Theme};
const TRI_DOWN: &[u8] = include_bytes!("../../assets/icons/ui/tri_down.svg");
const TRI_UP: &[u8] = include_bytes!("../../assets/icons/ui/tri_up.svg");
const TRI_RIGHT: &[u8] = include_bytes!("../../assets/icons/ui/tri_right.svg");
const TRI_LEFT: &[u8] = include_bytes!("../../assets/icons/ui/tri_left.svg");
const HOME: &[u8] = include_bytes!("../../assets/icons/ui/home.svg");
const UNDO: &[u8] = include_bytes!("../../assets/icons/ui/undo.svg");
const REDO: &[u8] = include_bytes!("../../assets/icons/ui/redo.svg");
@@ -166,6 +168,16 @@ pub fn arrow_right<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> {
icon(TRI_RIGHT, size, color)
}
/// Leftward caret.
pub fn arrow_left<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> {
icon(TRI_LEFT, size, color)
}
/// House glyph — the ViewCube "home view" button.
pub fn home<'a, M: 'a>(size: f32, color: Color) -> Element<'a, M> {
icon(HOME, size, color)
}
/// Caret that flips up/down with `open`.
pub fn arrow_toggle<'a, M: 'a>(open: bool, size: f32, color: Color) -> Element<'a, M> {
if open {
+8 -8
View File
@@ -223,13 +223,13 @@ impl canvas::Program<Message> for SelectionCanvas {
}
if self.show_viewcube {
if let Some(pos) = cursor.position_in(bounds) {
use crate::scene::{VIEWCUBE_DRAW_PX, VIEWCUBE_PAD};
let vc_x = bounds.width - VIEWCUBE_DRAW_PX - VIEWCUBE_PAD;
use crate::scene::{VIEWCUBE_PAD, VIEWCUBE_REGION_PX};
let vc_x = bounds.width - VIEWCUBE_REGION_PX - VIEWCUBE_PAD;
let vc_y = VIEWCUBE_PAD;
if pos.x >= vc_x
&& pos.x <= vc_x + VIEWCUBE_DRAW_PX
&& pos.x <= vc_x + VIEWCUBE_REGION_PX
&& pos.y >= vc_y
&& pos.y <= vc_y + VIEWCUBE_DRAW_PX
&& pos.y <= vc_y + VIEWCUBE_REGION_PX
{
return mouse::Interaction::None;
}
@@ -757,14 +757,14 @@ impl canvas::Program<Message> for SelectionCanvas {
// ── CAD crosshair cursor ──────────────────────────────────────────────
let over_viewcube = self.show_viewcube && {
use crate::scene::{VIEWCUBE_DRAW_PX, VIEWCUBE_PAD};
use crate::scene::{VIEWCUBE_PAD, VIEWCUBE_REGION_PX};
cursor.position_in(bounds).map_or(false, |pos| {
let vc_x = bounds.width - VIEWCUBE_DRAW_PX - VIEWCUBE_PAD;
let vc_x = bounds.width - VIEWCUBE_REGION_PX - VIEWCUBE_PAD;
let vc_y = VIEWCUBE_PAD;
pos.x >= vc_x
&& pos.x <= vc_x + VIEWCUBE_DRAW_PX
&& pos.x <= vc_x + VIEWCUBE_REGION_PX
&& pos.y >= vc_y
&& pos.y <= vc_y + VIEWCUBE_DRAW_PX
&& pos.y <= vc_y + VIEWCUBE_REGION_PX
})
};
// Over a Model-tile divider the OS cursor switches to a resize