forked from 1b-agent/opencadstudio-upstream
feat(ucs): interactive UCS icon with origin-snap grips, world-fixed compass
The UCS icon now anchors to the projected UCS origin when it is on-screen (falling back to the bottom-left corner), and becomes a selectable object: hover highlights it, a click shows draggable grips, and dragging the origin grip relocates the UCS while the X/Y tip grips rotate it. Drags snap to geometry/grid through the existing snapper, and the new UCS is persisted on release. The icon stays selectable while parked in the corner. Also make the ViewCube compass (N/E/S/W) world-fixed: the cube body still reorients with the active UCS, but the cardinals project through a camera-only rotation and snap in world frame, so the directions no longer spin with the UCS. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
a502379048
commit
edcf24e324
10 files changed
+525
-41
No files matched your search
@@ -570,6 +570,7 @@ impl ViewCubeText {
|
||||
&mut self,
|
||||
queue: &wgpu::Queue,
|
||||
cam_rotation: Mat4,
|
||||
compass_rotation: Mat4,
|
||||
vp_w: u32,
|
||||
vp_h: u32,
|
||||
cube_px: u32,
|
||||
@@ -677,6 +678,21 @@ impl ViewCubeText {
|
||||
// Painted flat onto the ring band (z = RING_Z) so they foreshorten with
|
||||
// the ring and read upright in plan. Local axes u = +X, v = +Y give
|
||||
// u × v = +Z → never mirrored when seen from above.
|
||||
//
|
||||
// The compass is world-fixed: it projects through `compass_rotation`
|
||||
// (camera only, no UCS), so N/E/S/W stay aligned to world even when the
|
||||
// cube reorients with the active UCS.
|
||||
let project_world = |local: Vec3| -> Option<[f32; 2]> {
|
||||
let world = compass_rotation.transform_point3(local);
|
||||
let clip = view_proj * Vec4::new(world.x, world.y, world.z, 1.0);
|
||||
if clip.w.abs() < 1e-6 {
|
||||
return None;
|
||||
}
|
||||
Some([
|
||||
(clip.x / clip.w + 1.0) * 0.5 * vw,
|
||||
(1.0 - clip.y / clip.w) * 0.5 * vh,
|
||||
])
|
||||
};
|
||||
const CARD_GW: f32 = 0.16; // glyph size in cube-local units
|
||||
const CARD_GH: f32 = 0.22;
|
||||
let cardinals = [
|
||||
@@ -691,7 +707,7 @@ impl ViewCubeText {
|
||||
};
|
||||
let center = Vec3::new(dir.x * R_CARD, dir.y * R_CARD, RING_Z);
|
||||
// Dim a cardinal whose ring point sits behind the cube.
|
||||
let alpha = if cam_rotation.transform_point3(center).z >= -0.15 {
|
||||
let alpha = if compass_rotation.transform_point3(center).z >= -0.15 {
|
||||
1.0
|
||||
} else {
|
||||
0.5
|
||||
@@ -699,10 +715,10 @@ impl ViewCubeText {
|
||||
let color = [1.0, 1.0, 1.0, alpha];
|
||||
let (u0, v0, u1, v1) = glyph_uv(gi, self.atlas_w, self.atlas_h);
|
||||
let corner = |lx: f32, ly: f32| center + Vec3::X * lx + Vec3::Y * ly;
|
||||
let tl = project(corner(-CARD_GW * 0.5, CARD_GH * 0.5));
|
||||
let tr = project(corner(CARD_GW * 0.5, CARD_GH * 0.5));
|
||||
let br = project(corner(CARD_GW * 0.5, -CARD_GH * 0.5));
|
||||
let bl = project(corner(-CARD_GW * 0.5, -CARD_GH * 0.5));
|
||||
let tl = project_world(corner(-CARD_GW * 0.5, CARD_GH * 0.5));
|
||||
let tr = project_world(corner(CARD_GW * 0.5, CARD_GH * 0.5));
|
||||
let br = project_world(corner(CARD_GW * 0.5, -CARD_GH * 0.5));
|
||||
let bl = project_world(corner(-CARD_GW * 0.5, -CARD_GH * 0.5));
|
||||
if let (Some(tl), Some(tr), Some(br), Some(bl)) = (tl, tr, br, bl) {
|
||||
let mk = |pos: [f32; 2], uv: [f32; 2]| TextVertex { pos, uv, color };
|
||||
verts.push(mk(tl, [u0, v0]));
|
||||
@@ -1210,6 +1226,7 @@ impl ViewCubePipeline {
|
||||
&mut self,
|
||||
queue: &wgpu::Queue,
|
||||
cam_rotation: Mat4,
|
||||
compass_rotation: Mat4,
|
||||
vp_w: u32,
|
||||
vp_h: u32,
|
||||
hover: Option<usize>,
|
||||
@@ -1226,7 +1243,7 @@ impl ViewCubePipeline {
|
||||
)),
|
||||
);
|
||||
self.text
|
||||
.update(queue, cam_rotation, vp_w, vp_h, self.cube_px);
|
||||
.update(queue, cam_rotation, compass_rotation, vp_w, vp_h, self.cube_px);
|
||||
}
|
||||
|
||||
pub fn ensure_depth_texture(&mut self, device: &wgpu::Device, size: Size<u32>) {
|
||||
|
||||
@@ -53,6 +53,9 @@ pub struct ViewportData {
|
||||
/// Camera rotation matrix derived from the quaternion.
|
||||
/// Used by the ViewCube pipeline — no gimbal lock.
|
||||
pub(in crate::scene) cam_rotation: Mat4,
|
||||
/// Camera-only rotation (no UCS) for the world-fixed compass cardinals, so
|
||||
/// N/E/S/W stay aligned to world even as the cube reorients with the UCS.
|
||||
pub(in crate::scene) compass_rotation: Mat4,
|
||||
pub(in crate::scene) hover_region: Option<usize>,
|
||||
pub(in crate::scene) show_viewcube: bool,
|
||||
/// Header.fill_mode (FILLMODE): when false, hatch / wipeout / face3d-fill
|
||||
@@ -323,6 +326,7 @@ impl shader::Primitive for Primitive {
|
||||
inner.viewcube.upload(
|
||||
queue,
|
||||
vp.cam_rotation,
|
||||
vp.compass_rotation,
|
||||
(vp.screen_rect.width * bounds.width) as u32,
|
||||
(vp.screen_rect.height * bounds.height) as u32,
|
||||
vp.hover_region,
|
||||
@@ -808,6 +812,7 @@ impl Scene {
|
||||
meshes: self.meshes_arc(),
|
||||
uniforms,
|
||||
cam_rotation: inst.camera.view_rotation_mat() * self.viewcube_ucs_mat(),
|
||||
compass_rotation: inst.camera.view_rotation_mat(),
|
||||
// Only the active viewport gets the hovered-region highlight.
|
||||
hover_region: if inst.active { hover_region } else { None },
|
||||
show_viewcube: inst.active,
|
||||
|
||||
Reference in new issue
Block a user