feat(ucs): orient the ViewCube to the active UCS
The ViewCube showed world Front/Top/Right. It now composes the camera rotation with the active UCS (scene.viewcube_ucs, synced from the tab's active_ucs), so its faces follow the user's coordinate system in model space. Render uniform, hover/click hit-test, and the click→snap direction all go through scene.viewcube_ucs_mat() so they stay in lock-step; identity outside model space leaves paper-viewport navigation unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
88bf2e57dc
commit
d78ed60582
5 changed files with 43 additions and 9 deletions
|
|
@ -3822,6 +3822,8 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Keep the scene's ViewCube UCS in lock-step with active_ucs.
|
||||||
|
self.tabs[i].sync_ucs_to_scene();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Named Views (VIEW command) ────────────────────────────────
|
// ── Named Views (VIEW command) ────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,19 @@ impl DocumentTab {
|
||||||
} else {
|
} else {
|
||||||
Some(u)
|
Some(u)
|
||||||
};
|
};
|
||||||
|
self.sync_ucs_to_scene();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push the active UCS rotation into the scene so the ViewCube composes with
|
||||||
|
/// it. Call after any change to `active_ucs`.
|
||||||
|
pub(super) fn sync_ucs_to_scene(&mut self) {
|
||||||
|
let (_, x, y, z) = self.ucs_xform().axes();
|
||||||
|
self.scene.viewcube_ucs = glam::Mat4::from_cols(
|
||||||
|
x.extend(0.0),
|
||||||
|
y.extend(0.0),
|
||||||
|
z.extend(0.0),
|
||||||
|
glam::Vec4::W,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn new_drawing(n: usize) -> Self {
|
pub(super) fn new_drawing(n: usize) -> Self {
|
||||||
|
|
|
||||||
|
|
@ -3175,10 +3175,9 @@ impl OpenCADStudio {
|
||||||
let (vw, vh) = vp_size;
|
let (vw, vh) = vp_size;
|
||||||
|
|
||||||
if vw > 1.0 && vh > 1.0 {
|
if vw > 1.0 && vh > 1.0 {
|
||||||
let cam = self.tabs[i].scene.camera.borrow();
|
let rot = self.tabs[i].scene.camera.borrow().view_rotation_mat()
|
||||||
if scene::hit_test(p.x, p.y, vw, vh, cam.view_rotation_mat(), VIEWCUBE_PX)
|
* self.tabs[i].scene.viewcube_ucs_mat();
|
||||||
.is_some()
|
if scene::hit_test(p.x, p.y, vw, vh, rot, VIEWCUBE_PX).is_some() {
|
||||||
{
|
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4330,9 +4329,13 @@ impl OpenCADStudio {
|
||||||
Message::ViewCubeSnap(region) => {
|
Message::ViewCubeSnap(region) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let mut region = region;
|
let mut region = region;
|
||||||
|
// The cube is oriented in the active UCS, so its face directions
|
||||||
|
// are UCS-frame — rotate them into world before comparing /
|
||||||
|
// snapping (identity outside model space).
|
||||||
|
let r_ucs = self.tabs[i].scene.viewcube_ucs_mat();
|
||||||
// "Already there → flip to opposite" check: compare the
|
// "Already there → flip to opposite" check: compare the
|
||||||
// current gaze direction with the region's target gaze.
|
// current gaze direction with the region's target gaze.
|
||||||
let target_dir = region.snap_direction();
|
let target_dir = r_ucs.transform_vector3(region.snap_direction());
|
||||||
let cur_dir = {
|
let cur_dir = {
|
||||||
let cam = self.tabs[i].scene.camera.borrow();
|
let cam = self.tabs[i].scene.camera.borrow();
|
||||||
cam.rotation * glam::Vec3::Z
|
cam.rotation * glam::Vec3::Z
|
||||||
|
|
@ -4340,7 +4343,7 @@ impl OpenCADStudio {
|
||||||
if cur_dir.dot(target_dir) > 0.9999 {
|
if cur_dir.dot(target_dir) > 0.9999 {
|
||||||
region = region.opposite();
|
region = region.opposite();
|
||||||
}
|
}
|
||||||
let eye_dir = region.snap_direction();
|
let eye_dir = r_ucs.transform_vector3(region.snap_direction());
|
||||||
|
|
||||||
if self.tabs[i].scene.active_viewport.is_some() {
|
if self.tabs[i].scene.active_viewport.is_some() {
|
||||||
self.tabs[i]
|
self.tabs[i]
|
||||||
|
|
|
||||||
|
|
@ -803,6 +803,10 @@ pub struct Scene {
|
||||||
paper_projected_cache: RefCell<HashMap<Handle, (u64, Vec<WireModel>)>>,
|
paper_projected_cache: RefCell<HashMap<Handle, (u64, Vec<WireModel>)>>,
|
||||||
/// Active layout name — "Model" or a paper space layout name.
|
/// Active layout name — "Model" or a paper space layout name.
|
||||||
pub current_layout: String,
|
pub current_layout: String,
|
||||||
|
/// UCS→world rotation for the ViewCube, kept in sync with the tab's active
|
||||||
|
/// UCS by `DocumentTab::sync_ucs_to_scene`. Identity = WCS. Applied only in
|
||||||
|
/// model space so the cube's faces follow the user's coordinate system.
|
||||||
|
pub viewcube_ucs: glam::Mat4,
|
||||||
/// GPU render data for hatch fills, keyed by the DXF entity Handle.
|
/// GPU render data for hatch fills, keyed by the DXF entity Handle.
|
||||||
pub hatches: HashMap<Handle, HatchModel>,
|
pub hatches: HashMap<Handle, HatchModel>,
|
||||||
/// GPU render data for solid meshes (truck Shell/Solid tessellation).
|
/// GPU render data for solid meshes (truck Shell/Solid tessellation).
|
||||||
|
|
@ -954,6 +958,7 @@ impl Scene {
|
||||||
paper_sheet_cache: RefCell::new(None),
|
paper_sheet_cache: RefCell::new(None),
|
||||||
paper_projected_cache: RefCell::new(HashMap::default()),
|
paper_projected_cache: RefCell::new(HashMap::default()),
|
||||||
current_layout: "Model".to_string(),
|
current_layout: "Model".to_string(),
|
||||||
|
viewcube_ucs: glam::Mat4::IDENTITY,
|
||||||
hatches: HashMap::default(),
|
hatches: HashMap::default(),
|
||||||
meshes: HashMap::default(),
|
meshes: HashMap::default(),
|
||||||
block_meshes: HashMap::default(),
|
block_meshes: HashMap::default(),
|
||||||
|
|
@ -3794,7 +3799,18 @@ impl Scene {
|
||||||
return cam.view_rotation_mat();
|
return cam.view_rotation_mat();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.camera.borrow().view_rotation_mat()
|
self.camera.borrow().view_rotation_mat() * self.viewcube_ucs_mat()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The UCS→world rotation the ViewCube should compose with the camera —
|
||||||
|
/// the active UCS in model space, identity everywhere else. Render,
|
||||||
|
/// hit-test, and click-snap all go through this so they stay in lock-step.
|
||||||
|
pub fn viewcube_ucs_mat(&self) -> glam::Mat4 {
|
||||||
|
if self.current_layout == "Model" {
|
||||||
|
self.viewcube_ucs
|
||||||
|
} else {
|
||||||
|
glam::Mat4::IDENTITY
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the handle of the user viewport whose bounding rectangle contains
|
/// Return the handle of the user viewport whose bounding rectangle contains
|
||||||
|
|
|
||||||
|
|
@ -794,7 +794,7 @@ impl Scene {
|
||||||
images,
|
images,
|
||||||
meshes: self.meshes_arc(),
|
meshes: self.meshes_arc(),
|
||||||
uniforms,
|
uniforms,
|
||||||
cam_rotation: inst.camera.view_rotation_mat(),
|
cam_rotation: inst.camera.view_rotation_mat() * self.viewcube_ucs_mat(),
|
||||||
// Only the active viewport gets the hovered-region highlight.
|
// Only the active viewport gets the hovered-region highlight.
|
||||||
hover_region: if inst.active { hover_region } else { None },
|
hover_region: if inst.active { hover_region } else { None },
|
||||||
show_viewcube: inst.active,
|
show_viewcube: inst.active,
|
||||||
|
|
@ -826,7 +826,7 @@ impl Scene {
|
||||||
cursor: mouse::Cursor,
|
cursor: mouse::Cursor,
|
||||||
) {
|
) {
|
||||||
let pos = cursor.position_in(bounds);
|
let pos = cursor.position_in(bounds);
|
||||||
let cam_rotation = self.camera.borrow().view_rotation_mat();
|
let cam_rotation = self.camera.borrow().view_rotation_mat() * self.viewcube_ucs_mat();
|
||||||
if let Some(p) = pos {
|
if let Some(p) = pos {
|
||||||
let tile = self.active_model_tile_bounds(bounds.width, bounds.height);
|
let tile = self.active_model_tile_bounds(bounds.width, bounds.height);
|
||||||
state.hover_region = hover_id(
|
state.hover_region = hover_id(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue