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");
|
self.push_undo_snapshot(i, "UCSICON");
|
||||||
let visible = sub != "OFF";
|
let visible = sub != "OFF";
|
||||||
let at_origin = sub == "ORIGIN";
|
let at_origin = sub == "ORIGIN";
|
||||||
|
// Update model-space icon flag.
|
||||||
|
self.show_ucs_icon = visible;
|
||||||
let mut count = 0usize;
|
let mut count = 0usize;
|
||||||
for entity in self.tabs[i].scene.document.entities_mut() {
|
for entity in self.tabs[i].scene.document.entities_mut() {
|
||||||
if let acadrust::EntityType::Viewport(vp) = entity {
|
if let acadrust::EntityType::Viewport(vp) = entity {
|
||||||
|
|
@ -2564,7 +2566,7 @@ impl H7CAD {
|
||||||
}
|
}
|
||||||
self.tabs[i].dirty = true;
|
self.tabs[i].dirty = true;
|
||||||
self.command_line.push_output(&format!(
|
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,
|
polar_mode: bool,
|
||||||
/// Show grid lines in the viewport (F7).
|
/// Show grid lines in the viewport (F7).
|
||||||
show_grid: bool,
|
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 committed by a drawing command — used as ortho/polar base.
|
||||||
last_point: Option<glam::Vec3>,
|
last_point: Option<glam::Vec3>,
|
||||||
/// OS window Id for the floating Layer Properties Manager (None when closed).
|
/// OS window Id for the floating Layer Properties Manager (None when closed).
|
||||||
|
|
@ -313,6 +315,7 @@ impl H7CAD {
|
||||||
ortho_mode: false,
|
ortho_mode: false,
|
||||||
polar_mode: false,
|
polar_mode: false,
|
||||||
show_grid: false,
|
show_grid: false,
|
||||||
|
show_ucs_icon: true,
|
||||||
last_point: None,
|
last_point: None,
|
||||||
layer_window: None,
|
layer_window: None,
|
||||||
main_window: None,
|
main_window: None,
|
||||||
|
|
|
||||||
|
|
@ -58,20 +58,32 @@ impl H7CAD {
|
||||||
vec![]
|
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 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 cam = tab.scene.camera.borrow();
|
||||||
let plane = grid_plane_from_camera(cam.pitch, cam.yaw);
|
let plane = grid_plane_from_camera(cam.pitch, cam.yaw);
|
||||||
Some(overlay::GridParams {
|
Some(overlay::GridParams {
|
||||||
view_proj: cam.view_proj(bounds),
|
view_proj: cam.view_proj(vp_bounds),
|
||||||
bounds,
|
bounds: vp_bounds,
|
||||||
plane,
|
plane,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
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())
|
let nav = container(overlay::nav_toolbar())
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,14 @@ pub struct GridParams {
|
||||||
pub plane: GridPlane,
|
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 ───────────────────────────────────────────────────
|
// ── Selection overlay ───────────────────────────────────────────────────
|
||||||
|
|
||||||
pub fn selection_overlay<'a>(
|
pub fn selection_overlay<'a>(
|
||||||
|
|
@ -153,12 +161,14 @@ pub fn selection_overlay<'a>(
|
||||||
snap: Option<(Point, SnapType)>,
|
snap: Option<(Point, SnapType)>,
|
||||||
grips: Vec<GripMarker>,
|
grips: Vec<GripMarker>,
|
||||||
grid: Option<GridParams>,
|
grid: Option<GridParams>,
|
||||||
|
ucs_icon: Option<UcsIconParams>,
|
||||||
) -> Element<'a, Message> {
|
) -> Element<'a, Message> {
|
||||||
canvas(SelectionCanvas {
|
canvas(SelectionCanvas {
|
||||||
selection,
|
selection,
|
||||||
snap,
|
snap,
|
||||||
grips,
|
grips,
|
||||||
grid,
|
grid,
|
||||||
|
ucs_icon,
|
||||||
})
|
})
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
|
|
@ -170,6 +180,7 @@ struct SelectionCanvas {
|
||||||
snap: Option<(Point, SnapType)>,
|
snap: Option<(Point, SnapType)>,
|
||||||
grips: Vec<GripMarker>,
|
grips: Vec<GripMarker>,
|
||||||
grid: Option<GridParams>,
|
grid: Option<GridParams>,
|
||||||
|
ucs_icon: Option<UcsIconParams>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl canvas::Program<Message> for SelectionCanvas {
|
impl canvas::Program<Message> for SelectionCanvas {
|
||||||
|
|
@ -538,6 +549,11 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||||
frame.stroke(&square, stroke);
|
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()]
|
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),
|
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