Feat: UCS icon — animated XYZ axis tripod in viewport bottom-left corner
Screen-space UCS icon drawn on the existing selection canvas overlay. Axes are projected from world space so the icon rotates with the camera, always showing the correct X/Y/Z orientation relative to the drawing plane. Each axis has its canonical color (X=red, Y=green, Z=blue) with arrowheads. Icon visibility controlled by show_ucs_icon flag (default on); toggled by UCSICON ON/OFF command which also updates paper-space viewport entities. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e38aa6b611
commit
6a4813ce42
4 changed files with 129 additions and 6 deletions
|
|
@ -2552,6 +2552,8 @@ impl H7CAD {
|
|||
self.push_undo_snapshot(i, "UCSICON");
|
||||
let visible = sub != "OFF";
|
||||
let at_origin = sub == "ORIGIN";
|
||||
// Update model-space icon flag.
|
||||
self.show_ucs_icon = visible;
|
||||
let mut count = 0usize;
|
||||
for entity in self.tabs[i].scene.document.entities_mut() {
|
||||
if let acadrust::EntityType::Viewport(vp) = entity {
|
||||
|
|
@ -2564,7 +2566,7 @@ impl H7CAD {
|
|||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(&format!(
|
||||
"UCSICON {sub}: updated {count} viewport(s)."
|
||||
"UCSICON {sub}: updated {count} viewport(s) + model space."
|
||||
));
|
||||
}
|
||||
_ => {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ pub(super) struct H7CAD {
|
|||
polar_mode: bool,
|
||||
/// Show grid lines in the viewport (F7).
|
||||
show_grid: bool,
|
||||
/// Show the UCS icon in the bottom-left corner of model space (UCSICON).
|
||||
show_ucs_icon: bool,
|
||||
/// Last point committed by a drawing command — used as ortho/polar base.
|
||||
last_point: Option<glam::Vec3>,
|
||||
/// OS window Id for the floating Layer Properties Manager (None when closed).
|
||||
|
|
@ -313,6 +315,7 @@ impl H7CAD {
|
|||
ortho_mode: false,
|
||||
polar_mode: false,
|
||||
show_grid: false,
|
||||
show_ucs_icon: true,
|
||||
last_point: None,
|
||||
layer_window: None,
|
||||
main_window: None,
|
||||
|
|
|
|||
|
|
@ -58,20 +58,32 @@ impl H7CAD {
|
|||
vec![]
|
||||
};
|
||||
|
||||
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
||||
let vp_bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
||||
|
||||
let grid = if self.show_grid {
|
||||
let (vw, vh) = tab.scene.selection.borrow().vp_size;
|
||||
let bounds = iced::Rectangle { x: 0.0, y: 0.0, width: vw, height: vh };
|
||||
let cam = tab.scene.camera.borrow();
|
||||
let plane = grid_plane_from_camera(cam.pitch, cam.yaw);
|
||||
Some(overlay::GridParams {
|
||||
view_proj: cam.view_proj(bounds),
|
||||
bounds,
|
||||
view_proj: cam.view_proj(vp_bounds),
|
||||
bounds: vp_bounds,
|
||||
plane,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
overlay::selection_overlay(sel, snap_info, grips, grid)
|
||||
|
||||
let ucs_icon = if self.show_ucs_icon && !is_paper {
|
||||
let cam = tab.scene.camera.borrow();
|
||||
Some(overlay::UcsIconParams {
|
||||
view_proj: cam.view_proj(vp_bounds),
|
||||
bounds: vp_bounds,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
overlay::selection_overlay(sel, snap_info, grips, grid, ucs_icon)
|
||||
};
|
||||
|
||||
let nav = container(overlay::nav_toolbar())
|
||||
|
|
|
|||
|
|
@ -146,6 +146,14 @@ pub struct GridParams {
|
|||
pub plane: GridPlane,
|
||||
}
|
||||
|
||||
/// Parameters for the screen-space UCS icon drawn in the viewport corner.
|
||||
pub struct UcsIconParams {
|
||||
/// View-projection matrix used to project world axis directions to screen.
|
||||
pub view_proj: Mat4,
|
||||
/// Viewport bounds (used for NDC → pixel conversion).
|
||||
pub bounds: iced::Rectangle,
|
||||
}
|
||||
|
||||
// ── Selection overlay ───────────────────────────────────────────────────
|
||||
|
||||
pub fn selection_overlay<'a>(
|
||||
|
|
@ -153,12 +161,14 @@ pub fn selection_overlay<'a>(
|
|||
snap: Option<(Point, SnapType)>,
|
||||
grips: Vec<GripMarker>,
|
||||
grid: Option<GridParams>,
|
||||
ucs_icon: Option<UcsIconParams>,
|
||||
) -> Element<'a, Message> {
|
||||
canvas(SelectionCanvas {
|
||||
selection,
|
||||
snap,
|
||||
grips,
|
||||
grid,
|
||||
ucs_icon,
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
|
|
@ -170,6 +180,7 @@ struct SelectionCanvas {
|
|||
snap: Option<(Point, SnapType)>,
|
||||
grips: Vec<GripMarker>,
|
||||
grid: Option<GridParams>,
|
||||
ucs_icon: Option<UcsIconParams>,
|
||||
}
|
||||
|
||||
impl canvas::Program<Message> for SelectionCanvas {
|
||||
|
|
@ -538,6 +549,11 @@ impl canvas::Program<Message> for SelectionCanvas {
|
|||
frame.stroke(&square, stroke);
|
||||
}
|
||||
|
||||
// ── UCS icon ──────────────────────────────────────────────────────
|
||||
if let Some(ref ucs) = self.ucs_icon {
|
||||
draw_ucs_icon(&mut frame, ucs.view_proj, ucs.bounds);
|
||||
}
|
||||
|
||||
vec![frame.into_geometry()]
|
||||
}
|
||||
}
|
||||
|
|
@ -700,3 +716,93 @@ fn draw_axes(frame: &mut canvas::Frame, vp: Mat4, bounds: iced::Rectangle, exten
|
|||
axis_stroke(0.20, 0.40, 0.90),
|
||||
);
|
||||
}
|
||||
|
||||
// ── UCS icon ──────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Draws a small X/Y/Z axis tripod in the bottom-left corner of the viewport.
|
||||
// The axis directions are projected from world space so the icon rotates with
|
||||
// the camera, matching the orientation of the drawing plane.
|
||||
|
||||
const UCS_ICON_MARGIN: f32 = 50.0; // px from bottom-left corner to icon origin
|
||||
const UCS_ICON_LEN: f32 = 35.0; // axis arm length in screen pixels
|
||||
const UCS_ICON_TIP: f32 = 6.0; // arrowhead size in pixels
|
||||
|
||||
fn draw_ucs_icon(frame: &mut canvas::Frame, vp: Mat4, bounds: iced::Rectangle) {
|
||||
// Project world origin and axis unit vectors to NDC, then to screen px.
|
||||
let w2s = |world: Vec3| -> Point {
|
||||
let ndc = vp.project_point3(world);
|
||||
Point::new(
|
||||
(ndc.x + 1.0) * 0.5 * bounds.width,
|
||||
(1.0 - ndc.y) * 0.5 * bounds.height,
|
||||
)
|
||||
};
|
||||
|
||||
// Origin in screen space — used to compute axis directions.
|
||||
let origin_s = w2s(Vec3::ZERO);
|
||||
let x_tip_s = w2s(Vec3::X);
|
||||
let y_tip_s = w2s(Vec3::Y);
|
||||
let z_tip_s = w2s(Vec3::Z);
|
||||
|
||||
// Icon origin: fixed bottom-left corner.
|
||||
let ox = UCS_ICON_MARGIN;
|
||||
let oy = bounds.height - UCS_ICON_MARGIN;
|
||||
let icon_origin = Point::new(ox, oy);
|
||||
|
||||
// Compute normalized screen-space axis directions, then scale to UCS_ICON_LEN.
|
||||
let axis_dir = |tip: Point| -> Option<Point> {
|
||||
let dx = tip.x - origin_s.x;
|
||||
let dy = tip.y - origin_s.y;
|
||||
let len = (dx * dx + dy * dy).sqrt();
|
||||
if len < 1e-4 {
|
||||
return None;
|
||||
}
|
||||
Some(Point::new(dx / len * UCS_ICON_LEN, dy / len * UCS_ICON_LEN))
|
||||
};
|
||||
|
||||
let draw_axis = |frame: &mut canvas::Frame, dir: Point, r: f32, g: f32, b: f32| {
|
||||
let tip = Point::new(icon_origin.x + dir.x, icon_origin.y + dir.y);
|
||||
let path = canvas::Path::new(|p| {
|
||||
p.move_to(icon_origin);
|
||||
p.line_to(tip);
|
||||
});
|
||||
frame.stroke(
|
||||
&path,
|
||||
canvas::Stroke {
|
||||
width: 2.0,
|
||||
style: canvas::Style::Solid(Color { r, g, b, a: 1.0 }),
|
||||
line_cap: canvas::LineCap::Round,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
// Arrowhead: small filled triangle at tip.
|
||||
let len = (dir.x * dir.x + dir.y * dir.y).sqrt().max(1e-4);
|
||||
let nx = dir.x / len;
|
||||
let ny = dir.y / len;
|
||||
let px = -ny;
|
||||
let py = nx;
|
||||
let tip_l = Point::new(tip.x - nx * UCS_ICON_TIP + px * (UCS_ICON_TIP * 0.4), tip.y - ny * UCS_ICON_TIP + py * (UCS_ICON_TIP * 0.4));
|
||||
let tip_r = Point::new(tip.x - nx * UCS_ICON_TIP - px * (UCS_ICON_TIP * 0.4), tip.y - ny * UCS_ICON_TIP - py * (UCS_ICON_TIP * 0.4));
|
||||
let arrow = canvas::Path::new(|p| {
|
||||
p.move_to(tip);
|
||||
p.line_to(tip_l);
|
||||
p.line_to(tip_r);
|
||||
p.close();
|
||||
});
|
||||
frame.fill(&arrow, Color { r, g, b, a: 1.0 });
|
||||
};
|
||||
|
||||
// Draw Z first (behind), then Y, then X (in front).
|
||||
if let Some(dir) = axis_dir(z_tip_s) {
|
||||
draw_axis(frame, dir, 0.20, 0.40, 0.90); // Z blue
|
||||
}
|
||||
if let Some(dir) = axis_dir(y_tip_s) {
|
||||
draw_axis(frame, dir, 0.20, 0.85, 0.20); // Y green
|
||||
}
|
||||
if let Some(dir) = axis_dir(x_tip_s) {
|
||||
draw_axis(frame, dir, 0.90, 0.20, 0.20); // X red
|
||||
}
|
||||
|
||||
// Small circle at origin.
|
||||
let circle = canvas::Path::circle(icon_origin, 3.0);
|
||||
frame.fill(&circle, Color { r: 0.9, g: 0.9, b: 0.9, a: 0.9 });
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue