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
@@ -85,8 +85,11 @@ impl OpenCADStudio {
|
||||
self.push_undo_snapshot(i, "UCSICON");
|
||||
let visible = sub != "OFF";
|
||||
let at_origin = sub == "ORIGIN";
|
||||
// Update model-space icon flag.
|
||||
// Update model-space icon flags.
|
||||
self.show_ucs_icon = visible;
|
||||
if sub == "NOORIGIN" || sub == "ORIGIN" {
|
||||
self.ucs_icon_at_origin = at_origin;
|
||||
}
|
||||
let mut count = 0usize;
|
||||
for entity in self.tabs[i].scene.document.entities_mut() {
|
||||
if let acadrust::EntityType::Viewport(vp) = entity {
|
||||
|
||||
@@ -176,6 +176,16 @@ impl DocumentTab {
|
||||
super::helpers::UcsXform::from_active(self.active_ucs.as_ref())
|
||||
}
|
||||
|
||||
/// World-space UCS origin in full f64 precision (stored f64 in the header /
|
||||
/// viewport). Used to anchor the UCS icon and as the cursor-pick plane point
|
||||
/// — `UcsXform`'s origin is f32 and would quantize the anchor at UTM scale.
|
||||
pub(super) fn ucs_origin_world(&self) -> glam::DVec3 {
|
||||
self.active_ucs
|
||||
.as_ref()
|
||||
.map(|u| glam::dvec3(u.origin.x, u.origin.y, u.origin.z))
|
||||
.unwrap_or(glam::DVec3::ZERO)
|
||||
}
|
||||
|
||||
/// True when the active pane edits **model-space** geometry: the Model tab,
|
||||
/// or inside a floating viewport (MSPACE). The UCS applies in both; plain
|
||||
/// paper space (no active viewport) is excluded. Single predicate so every
|
||||
|
||||
@@ -39,6 +39,17 @@ pub struct TileDrag {
|
||||
pub last_applied: f32,
|
||||
}
|
||||
|
||||
/// Which UCS-icon grip is being dragged. `Origin` slides the UCS origin within
|
||||
/// its own plane; `XAxis`/`YAxis` rotate the UCS so that axis points at the
|
||||
/// cursor (the other in-plane axis follows, Z fixed). The cursor is mapped onto
|
||||
/// the UCS plane every move, so no extra drag-start state is needed.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum UcsGripKind {
|
||||
Origin,
|
||||
XAxis,
|
||||
YAxis,
|
||||
}
|
||||
|
||||
/// Cursor dwelling over a grip. When `started.elapsed() >=` the popup
|
||||
/// threshold the multi-functional menu opens (`grip_popup`).
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -258,6 +269,13 @@ pub(super) struct OpenCADStudio {
|
||||
/// edge, updated on move, cleared on release (which also runs the
|
||||
/// collapse pass).
|
||||
tile_drag: Option<TileDrag>,
|
||||
/// Cursor is hovering over the UCS icon body — drives the hover highlight.
|
||||
ucs_icon_hover: bool,
|
||||
/// UCS icon is selected (clicked): its grips are shown and draggable.
|
||||
ucs_icon_selected: bool,
|
||||
/// Active direct-drag of a UCS icon grip (origin slide or axis rotate).
|
||||
/// Set on press over a grip, updated on move, committed on release.
|
||||
ucs_grip_drag: Option<UcsGripKind>,
|
||||
/// `true` once the user has reshaped the dynamic-input field set via
|
||||
/// the `,` separator during the current command iteration. Tells
|
||||
/// `sync_dyn_fields` to preserve the user's chosen shape instead of
|
||||
@@ -302,6 +320,9 @@ pub(super) struct OpenCADStudio {
|
||||
qselect: Option<QSelectState>,
|
||||
/// Show the UCS icon in the bottom-left corner of model space (UCSICON).
|
||||
show_ucs_icon: bool,
|
||||
/// Anchor the UCS icon to the projected UCS origin when it is on-screen,
|
||||
/// falling back to the corner otherwise (UCSICON ORigin / NOorigin).
|
||||
ucs_icon_at_origin: bool,
|
||||
/// Whether the ViewCube 3D gizmo is visible in model space (NAVVCUBE).
|
||||
show_viewcube: bool,
|
||||
/// Whether the Properties panel is shown on the left (PROPERTIES).
|
||||
@@ -1123,6 +1144,9 @@ pub enum Message {
|
||||
ViewportScroll(mouse::ScrollDelta),
|
||||
ViewportExit,
|
||||
ViewCubeSnap(CubeRegion),
|
||||
/// World-frame view snap from a compass cardinal (N/E/S/W), bypassing the
|
||||
/// UCS so the compass stays world-aligned.
|
||||
ViewCubeSnapWorld(CubeRegion),
|
||||
/// ViewCube home button → jump to the default isometric view.
|
||||
ViewCubeHome,
|
||||
/// ViewCube roll arrow → roll the view 90° (true = clockwise).
|
||||
@@ -1783,6 +1807,9 @@ impl OpenCADStudio {
|
||||
default_paper_bg_color: None,
|
||||
awaiting_vports: false,
|
||||
tile_drag: None,
|
||||
ucs_icon_hover: false,
|
||||
ucs_icon_selected: false,
|
||||
ucs_grip_drag: None,
|
||||
dyn_user_reshaped: false,
|
||||
grip_hover: None,
|
||||
grip_popup: None,
|
||||
@@ -1794,6 +1821,7 @@ impl OpenCADStudio {
|
||||
grip_original: None,
|
||||
qselect: None,
|
||||
show_ucs_icon: true,
|
||||
ucs_icon_at_origin: true,
|
||||
show_viewcube: true,
|
||||
show_properties: true,
|
||||
show_file_tabs: true,
|
||||
|
||||
@@ -402,6 +402,18 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||
self.text_inline_cancel();
|
||||
return self.post_editor_closed(false);
|
||||
}
|
||||
// UCS icon: Esc ends any grip drag and clears the selection
|
||||
// (only when no command owns Escape).
|
||||
if self.tabs[self.active_tab].active_cmd.is_none()
|
||||
&& (self.ucs_grip_drag.is_some() || self.ucs_icon_selected)
|
||||
{
|
||||
let i = self.active_tab;
|
||||
self.ucs_grip_drag = None;
|
||||
self.ucs_icon_selected = false;
|
||||
self.ucs_icon_hover = false;
|
||||
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 {
|
||||
let i = self.active_tab;
|
||||
|
||||
@@ -1079,6 +1079,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
Message::ViewCubeSnap(region) => self.on_view_cube_snap(region),
|
||||
Message::ViewCubeSnapWorld(region) => self.on_view_cube_snap_world(region),
|
||||
|
||||
Message::ViewCubeHome => {
|
||||
let i = self.active_tab;
|
||||
|
||||
+250
-6
@@ -20,6 +20,54 @@ use acadrust::{EntityType as AcadEntityType, Handle};
|
||||
use iced::time::Instant;
|
||||
use iced::{mouse, Point, Task};
|
||||
|
||||
/// Pixel radius for grabbing a UCS-icon grip (origin dot or an axis tip).
|
||||
const UCS_GRIP_HIT_PX: f32 = 9.0;
|
||||
/// Pixel reach for hovering/clicking the icon body (origin, tips, or an arm).
|
||||
const UCS_ICON_PICK_PX: f32 = 10.0;
|
||||
/// Pixel half-width for clicking an icon arm (line segment).
|
||||
const UCS_ICON_ARM_PX: f32 = 6.0;
|
||||
|
||||
fn pt_pt_d2(a: Point, b: Point) -> f32 {
|
||||
(a.x - b.x).powi(2) + (a.y - b.y).powi(2)
|
||||
}
|
||||
|
||||
/// Squared distance from `p` to the segment `a`–`b`.
|
||||
fn pt_seg_d2(p: Point, a: Point, b: Point) -> f32 {
|
||||
let (vx, vy) = (b.x - a.x, b.y - a.y);
|
||||
let len2 = vx * vx + vy * vy;
|
||||
let t = if len2 > 1e-6 {
|
||||
(((p.x - a.x) * vx + (p.y - a.y) * vy) / len2).clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
pt_pt_d2(p, Point::new(a.x + t * vx, a.y + t * vy))
|
||||
}
|
||||
|
||||
/// Which UCS grip (if any) the cursor `p` is on.
|
||||
fn ucs_grip_under(p: Point, h: &crate::ui::overlay::UcsIconHit) -> Option<crate::app::UcsGripKind> {
|
||||
let r2 = UCS_GRIP_HIT_PX * UCS_GRIP_HIT_PX;
|
||||
if pt_pt_d2(p, h.origin) <= r2 {
|
||||
Some(crate::app::UcsGripKind::Origin)
|
||||
} else if pt_pt_d2(p, h.tips[0]) <= r2 {
|
||||
Some(crate::app::UcsGripKind::XAxis)
|
||||
} else if pt_pt_d2(p, h.tips[1]) <= r2 {
|
||||
Some(crate::app::UcsGripKind::YAxis)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// True when `p` is over the icon body (origin, a tip, or an arm) — the pick
|
||||
/// region for hover-highlight and select.
|
||||
fn over_ucs_icon(p: Point, h: &crate::ui::overlay::UcsIconHit) -> bool {
|
||||
let pick2 = UCS_ICON_PICK_PX * UCS_ICON_PICK_PX;
|
||||
if pt_pt_d2(p, h.origin) <= pick2 || h.tips.iter().any(|t| pt_pt_d2(p, *t) <= pick2) {
|
||||
return true;
|
||||
}
|
||||
let arm2 = UCS_ICON_ARM_PX * UCS_ICON_ARM_PX;
|
||||
h.tips.iter().any(|t| pt_seg_d2(p, h.origin, *t) <= arm2)
|
||||
}
|
||||
|
||||
|
||||
impl OpenCADStudio {
|
||||
/// Write PDSIZE from the dialog buffer with the current relative/absolute
|
||||
@@ -460,6 +508,14 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
// UCS icon grip drag: map the cursor onto the UCS plane and
|
||||
// slide the origin / rotate the axis. Short-circuits pan & snap.
|
||||
if let Some(kind) = self.ucs_grip_drag {
|
||||
self.drag_ucs_grip(i, kind, p);
|
||||
self.tabs[i].scene.selection.borrow_mut().last_move_pos = Some(p);
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
// Keep the ViewCube hover in sync as the cursor leaves the
|
||||
// hit-area overlay and moves over the rest of the viewport.
|
||||
// `hover_id` returns None outside the cube box, which clears
|
||||
@@ -488,6 +544,13 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
||||
// popup menu. See scene::model::object::GripMenuItem.
|
||||
self.update_grip_hover(i, p);
|
||||
|
||||
// UCS icon hover highlight (suppressed mid grip-drag).
|
||||
self.ucs_icon_hover = self.ucs_grip_drag.is_none()
|
||||
&& self
|
||||
.ucs_icon_hit_info(i, svw, svh)
|
||||
.map(|h| over_ucs_icon(p, &h))
|
||||
.unwrap_or(false);
|
||||
|
||||
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
||||
sel.last_move_pos = Some(p);
|
||||
|
||||
@@ -1203,6 +1266,138 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Screen positions of the UCS-icon grips (origin + axis tips, absolute px)
|
||||
/// for the active pane, or `None` when the icon is not shown / not anchored
|
||||
/// at its on-screen origin / a command is active. The single source of truth
|
||||
/// for both hover and grip hit-testing, so they match what is drawn.
|
||||
fn ucs_icon_hit_info(&self, i: usize, vw: f32, vh: f32) -> Option<crate::ui::overlay::UcsIconHit> {
|
||||
if !self.show_ucs_icon || self.tabs[i].active_cmd.is_some() {
|
||||
return None;
|
||||
}
|
||||
let tab = &self.tabs[i];
|
||||
let (_, ux, uy, uz) = tab.ucs_xform().axes();
|
||||
// Project through whichever pane owns the icon — a floating viewport's
|
||||
// own camera, else the active model tile. Bare paper space shows none.
|
||||
let (cam, bounds) = if let Some((c, full)) = tab.scene.viewport_edit_frame((vw, vh)) {
|
||||
(c, full)
|
||||
} else if tab.scene.current_layout == "Model" {
|
||||
(
|
||||
tab.scene.camera.borrow().clone(),
|
||||
tab.scene.active_model_tile_bounds(vw, vh),
|
||||
)
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
// Mirror the draw: anchor at the projected origin only when ORigin mode
|
||||
// is on AND the origin is on-screen; otherwise `None` parks it in the
|
||||
// corner — still selectable/draggable there.
|
||||
let os = if self.ucs_icon_at_origin {
|
||||
cam.project(tab.ucs_origin_world(), bounds)
|
||||
.map(|q| Point::new(bounds.x + q.x, bounds.y + q.y))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
crate::ui::overlay::ucs_icon_hit(cam.view_proj_rte(bounds), bounds, (ux, uy, uz), os)
|
||||
}
|
||||
|
||||
/// Apply one frame of a UCS-icon grip drag: map the cursor onto the active
|
||||
/// UCS plane (so the move stays in-plane) and either slide the origin there
|
||||
/// or rotate the chosen axis to point at it, keeping a right-handed frame
|
||||
/// with Z fixed. Live — the commit (persist) happens on release.
|
||||
fn drag_ucs_grip(&mut self, i: usize, kind: crate::app::UcsGripKind, p_full: Point) {
|
||||
let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size;
|
||||
if vw <= 1.0 || vh <= 1.0 {
|
||||
return;
|
||||
}
|
||||
// Same pane framing as the rest of the move handler: pane-local cursor
|
||||
// and origin-zero bounds, with the floating-viewport camera when inside
|
||||
// one. `cursor_model_point` handles the paper→model round-trip.
|
||||
let edit_frame = self.tabs[i].scene.viewport_edit_frame((vw, vh));
|
||||
let tile_b = match &edit_frame {
|
||||
Some((_, full)) => *full,
|
||||
None => self.tabs[i].scene.active_model_tile_bounds(vw, vh),
|
||||
};
|
||||
let edit_cam = edit_frame.map(|(cam, _)| cam);
|
||||
let p = Point::new(p_full.x - tile_b.x, p_full.y - tile_b.y);
|
||||
let bounds = iced::Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: tile_b.width,
|
||||
height: tile_b.height,
|
||||
};
|
||||
let raw = self.cursor_model_point(i, &edit_cam, p, bounds);
|
||||
|
||||
// Object/grid snap, same path as an entity grip or command drag: the
|
||||
// dragged UCS point sticks to endpoints/midpoints/grid under the cursor,
|
||||
// and the snap marker is published via `snap_result`.
|
||||
let (view_rot, eye) = match &edit_cam {
|
||||
Some(cam) => (cam.view_proj_rte(bounds), cam.eye()),
|
||||
None => {
|
||||
let cam = self.tabs[i].scene.camera.borrow();
|
||||
(cam.view_proj_rte(bounds), cam.eye())
|
||||
}
|
||||
};
|
||||
let all_wires = if let (Some(_), Some(h)) =
|
||||
(&edit_cam, self.tabs[i].scene.active_viewport)
|
||||
{
|
||||
self.tabs[i].scene.model_wires_for_viewport_arc(h, bounds.height)
|
||||
} else {
|
||||
self.tabs[i].scene.hit_test_wires()
|
||||
};
|
||||
self.snapper.grid_spacing = crate::ui::overlay::compute_grid_step(view_rot, bounds);
|
||||
// No rubber-band origin (perp/extension feet don't apply to a free drag).
|
||||
self.snapper.from_point = None;
|
||||
let (go, gr) = self.tabs[i].ucs_grid_basis();
|
||||
let snap_hit = self
|
||||
.snapper
|
||||
.snap(raw, p, &all_wires[..], view_rot, eye, bounds, go, gr);
|
||||
let world = snap_hit.map(|s| s.world).unwrap_or(raw);
|
||||
self.tabs[i].snap_result = snap_hit;
|
||||
if let Some(s) = self.tabs[i].snap_result.as_mut() {
|
||||
// Snap marker is pane-local; lift it to absolute canvas px.
|
||||
s.screen.x += tile_b.x;
|
||||
s.screen.y += tile_b.y;
|
||||
}
|
||||
|
||||
use acadrust::types::Vector3;
|
||||
let v3 = |d: glam::DVec3| Vector3::new(d.x, d.y, d.z);
|
||||
{
|
||||
let ucs = self.tabs[i]
|
||||
.active_ucs
|
||||
.get_or_insert_with(|| acadrust::tables::Ucs::new("*ACTIVE*"));
|
||||
match kind {
|
||||
crate::app::UcsGripKind::Origin => {
|
||||
ucs.origin = v3(world);
|
||||
}
|
||||
crate::app::UcsGripKind::XAxis | crate::app::UcsGripKind::YAxis => {
|
||||
let o = glam::dvec3(ucs.origin.x, ucs.origin.y, ucs.origin.z);
|
||||
let x = glam::dvec3(ucs.x_axis.x, ucs.x_axis.y, ucs.x_axis.z);
|
||||
let y = glam::dvec3(ucs.y_axis.x, ucs.y_axis.y, ucs.y_axis.z);
|
||||
let z = x.cross(y).normalize_or(glam::DVec3::Z);
|
||||
// Direction from origin to cursor, flattened into the UCS
|
||||
// plane; bail on a degenerate (cursor on the origin).
|
||||
let dir = world - o;
|
||||
let d = dir - z * dir.dot(z);
|
||||
if d.length_squared() < 1e-12 {
|
||||
return;
|
||||
}
|
||||
let d = d.normalize();
|
||||
// Z fixed; the dragged axis = d, the other = right-handed
|
||||
// completion (z×x = y, y×z = x).
|
||||
let (nx, ny) = match kind {
|
||||
crate::app::UcsGripKind::XAxis => (d, z.cross(d)),
|
||||
_ => (d.cross(z), d),
|
||||
};
|
||||
ucs.x_axis = v3(nx);
|
||||
ucs.y_axis = v3(ny);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.tabs[i].sync_ucs_to_scene();
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].scene.camera_generation += 1;
|
||||
}
|
||||
|
||||
pub(super) fn on_viewport_left_press(&mut self) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
// A click in the viewport dismisses any open ribbon dropdown
|
||||
@@ -1278,6 +1473,30 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
||||
}
|
||||
}
|
||||
|
||||
// UCS icon: click to select (grips appear), then drag a grip to
|
||||
// move/rotate. A click off the icon clears the selection. Works
|
||||
// in the corner too (parked icon), not just at the origin.
|
||||
if self.show_ucs_icon && self.tabs[i].active_cmd.is_none() {
|
||||
if let Some(hit) = self.ucs_icon_hit_info(i, vw, vh) {
|
||||
// Already selected → a press on a grip starts the drag.
|
||||
if self.ucs_icon_selected {
|
||||
if let Some(kind) = ucs_grip_under(p, &hit) {
|
||||
self.ucs_grip_drag = Some(kind);
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
// Press on the icon body selects it (and shows grips).
|
||||
if over_ucs_icon(p, &hit) {
|
||||
self.ucs_icon_selected = true;
|
||||
self.ucs_icon_hover = true;
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
// Press elsewhere drops the selection, then falls through to
|
||||
// the normal pick / box-select so the click still lands.
|
||||
self.ucs_icon_selected = false;
|
||||
}
|
||||
|
||||
// Tiled Model layout: an inner divider near the cursor
|
||||
// becomes a resize grip. Start the drag and short-circuit
|
||||
// (no pick / select / camera swap). Released on
|
||||
@@ -1438,6 +1657,19 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
// Commit a UCS icon grip drag: persist the new UCS so it
|
||||
// round-trips, and clear the lingering press state.
|
||||
if self.ucs_grip_drag.take().is_some() {
|
||||
self.tabs[i].persist_active_ucs();
|
||||
self.tabs[i].snap_result = None;
|
||||
self.snapper.from_point = None;
|
||||
let mut sel = self.tabs[i].scene.selection.borrow_mut();
|
||||
sel.left_down = false;
|
||||
sel.left_press_pos = None;
|
||||
sel.left_dragging = false;
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
let (p, is_click, is_down) = {
|
||||
let sel = self.tabs[i].scene.selection.borrow();
|
||||
let p = match sel.last_move_pos {
|
||||
@@ -2435,19 +2667,31 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task<Message> {
|
||||
if let Some(region) = scene::hit_test(cx, cy, w, h, rot, VIEWCUBE_PX) {
|
||||
return Task::done(Message::ViewCubeSnap(region));
|
||||
}
|
||||
if let Some(card) = scene::hit_test_cardinal(cx, cy, w, h, rot, VIEWCUBE_PX) {
|
||||
return Task::done(Message::ViewCubeSnap(card.face_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()));
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(super) fn on_view_cube_snap(&mut self, region: CubeRegion) -> Task<Message> {
|
||||
// The cube is oriented in the active UCS, so snap in the UCS frame.
|
||||
let r_ucs = self.tabs[self.active_tab].scene.viewcube_ucs_mat();
|
||||
self.snap_view_region(region, r_ucs)
|
||||
}
|
||||
|
||||
/// Compass cardinals are world-fixed, so they snap in the world frame
|
||||
/// (no UCS composition).
|
||||
pub(super) fn on_view_cube_snap_world(&mut self, region: CubeRegion) -> Task<Message> {
|
||||
self.snap_view_region(region, glam::Mat4::IDENTITY)
|
||||
}
|
||||
|
||||
fn snap_view_region(&mut self, region: CubeRegion, r_ucs: glam::Mat4) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
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
|
||||
// current gaze direction with the region's target gaze.
|
||||
let target_dir = r_ucs.transform_vector3(region.snap_direction());
|
||||
|
||||
@@ -215,18 +215,33 @@ impl OpenCADStudio {
|
||||
None
|
||||
} else if let Some((vp_cam, full)) = tab.scene.viewport_edit_frame((vw, vh)) {
|
||||
let (_, ux, uy, uz) = tab.ucs_xform().axes();
|
||||
let origin_screen = self.ucs_icon_at_origin.then(|| {
|
||||
vp_cam
|
||||
.project(tab.ucs_origin_world(), full)
|
||||
.map(|p| iced::Point::new(full.x + p.x, full.y + p.y))
|
||||
}).flatten();
|
||||
Some(crate::ui::overlay::UcsIconParams {
|
||||
view_proj: vp_cam.view_proj_rte(full),
|
||||
bounds: full,
|
||||
axes: (ux, uy, uz),
|
||||
origin_screen,
|
||||
hover: self.ucs_icon_hover,
|
||||
selected: self.ucs_icon_selected,
|
||||
})
|
||||
} else if !is_paper {
|
||||
let cam = tab.scene.camera.borrow();
|
||||
let (_, ux, uy, uz) = tab.ucs_xform().axes();
|
||||
let origin_screen = self.ucs_icon_at_origin.then(|| {
|
||||
cam.project(tab.ucs_origin_world(), vp_bounds)
|
||||
.map(|p| iced::Point::new(vp_bounds.x + p.x, vp_bounds.y + p.y))
|
||||
}).flatten();
|
||||
Some(crate::ui::overlay::UcsIconParams {
|
||||
view_proj: cam.view_proj_rte(vp_bounds),
|
||||
bounds: vp_bounds,
|
||||
axes: (ux, uy, uz),
|
||||
origin_screen,
|
||||
hover: self.ucs_icon_hover,
|
||||
selected: self.ucs_icon_selected,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
||||
Reference in new issue
Block a user