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 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-25 23:46:43 +03:00
commit f00c48bebb
9 changed files with 633 additions and 40 deletions

View file

@ -29,7 +29,8 @@ pub use model::mesh_model::MeshLodSet;
pub use model::object::{GripApply, GripDef};
pub use pipeline::uniforms::Uniforms;
pub use pipeline::viewcube::{
hit_test, hover_id, CubeRegion, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX,
hit_test, hit_test_cardinal, hover_id, CubeRegion, NudgeDir, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD,
VIEWCUBE_PX, VIEWCUBE_REGION_PX,
};
pub use pick::selection::SelectionState;
pub use model::wire_model::WireModel;
@ -3939,6 +3940,45 @@ impl Scene {
}
}
/// Mutate the active viewport's camera through a closure, then re-encode the
/// result to the stored `(view_direction, twist_angle)` — the same decode
/// the ViewCube snap uses. Lets the home / roll / nudge controls drive a
/// floating viewport just like the model camera. Returns `false` if there is
/// no active (unlocked) viewport.
pub fn mutate_active_viewport_camera(
&mut self,
f: impl FnOnce(&mut view::camera::Camera),
) -> bool {
let Some(vp_handle) = self.active_viewport else {
return false;
};
let mut tmp = self.camera_for_viewport(vp_handle).unwrap_or_default();
f(&mut tmp);
let dir = (tmp.rotation * glam::Vec3::Z).normalize_or(glam::Vec3::Z);
let desired_up = (tmp.rotation * glam::Vec3::Y).normalize_or(glam::Vec3::Y);
let pitch = dir.z.clamp(-1.0, 1.0).asin();
let yaw = if dir.x.abs() < 1e-6 && dir.y.abs() < 1e-6 {
0.0
} else {
dir.x.atan2(-dir.y)
};
let up0 = (view::camera::yaw_pitch_to_quat(yaw, pitch, 0.0) * glam::Vec3::Y)
.normalize_or(glam::Vec3::Y);
let roll = up0.cross(desired_up).dot(dir).atan2(up0.dot(desired_up));
let twist = -roll as f64;
if let Some(acadrust::EntityType::Viewport(vp)) = self.document.get_entity_mut(vp_handle) {
if vp.status.locked {
return false;
}
vp.view_direction.x = dir.x as f64;
vp.view_direction.y = dir.y as f64;
vp.view_direction.z = dir.z as f64;
vp.twist_angle = twist;
return true;
}
false
}
/// Render mode of the active paper-space viewport, or `None` when no
/// viewport is active (PSPACE / model layout).
pub fn active_viewport_render_mode(