fix: UCS FACE/OBJECT pick from the interactive prompt; reuse ocs_axes

Follow-up to #1341:
- interactive UCS offers Face / OBject; bare UCS OBJECT starts an entity pick
- one UcsPickCommand serves both FACE and OBJECT
- ucs_from_normal calls the existing arbitrary-axis helper instead of a copy
- prompt catalogue key and en-US text follow the new option list

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Fable 5.1 committed 2026-09-18 18:42:44 +03:00
1 parent 3ba4ab9e7f
commit 9978713acc
7 files changed
+50 -43

No files matched your search

+1 -1
View File
@@ -4198,7 +4198,7 @@ view =
.cmd-not-yet-implemented = __ocs_fmt_0__: not yet implemented.
.viewport-s-in-layout = __ocs_fmt_0__ viewport(s) in layout "__ocs_fmt_1__":
.ucs-option-world-view-3point-z-x-y-origin-save-delete-or-name = UCS option [World/View/3Point/Z/X/Y/Origin/Save/Delete] or name:
.ucs-option-world-view-3point-z-x-y-origin-save-delete-or-name = UCS option [Face/OBject/World/View/3Point/Z/X/Y/Origin/Save/Delete] or name:
.ucs-specify-new-origin = UCS specify new origin:
.ucs-specify-point-on-positive-x-axis = UCS specify point on positive X axis:
.ucs-specify-point-in-positive-xy-plane = UCS specify point in positive XY plane:
+8 -4
View File
@@ -338,7 +338,7 @@ impl OpenCADStudio {
// surface point, so both entry points share the
// plane construction below.
None => {
let picker = crate::command::UcsFaceCommand;
let picker = crate::command::UcsPickCommand { face: true };
self.command_line.push_info(&picker.prompt());
self.tabs[i].active_cmd = Some(Box::new(picker));
}
@@ -386,9 +386,13 @@ impl OpenCADStudio {
})
.map(acadrust::Handle::new);
match target {
None => self
.command_line
.push_error(crate::t!("Usage: UCS OBJECT <handle>").as_ref()),
// Bare `UCS OBJECT` starts the pick; the click
// comes back through this arm with the handle.
None => {
let picker = crate::command::UcsPickCommand { face: false };
self.command_line.push_info(&picker.prompt());
self.tabs[i].active_cmd = Some(Box::new(picker));
}
Some(handle) => {
let built = self.tabs[i]
.scene
+2 -1
View File
@@ -739,9 +739,10 @@ inventory::submit!(crate::command::CommandRegistration {
"SELSIM",
// Draw a new object of the same type as the selected one. (#239)
"ADDSELECTED",
// Outline of origin planes, open sketch and solid bodies.
"BROWSER",
// Open and close a sketch: a drawing plane, a square-on view and
// snapping, held together until the profile is done.
"BROWSER",
"CREATESKETCH",
"FINISHSKETCH",
"SHEETSET",
+7 -22
View File
@@ -249,35 +249,20 @@ pub(super) fn ucs_z_axis(ucs: &Ucs) -> glam::DVec3 {
/// Build a UCS whose Z axis is `normal`, anchored at `origin`.
///
/// X and Y come from the Arbitrary Axis Algorithm, the same rule DWG uses to
/// turn an extrusion direction into an object coordinate system. Picking the
/// world axis that is *least* parallel to the normal keeps the cross product
/// well conditioned, and makes the result stable: the same face always yields
/// the same X axis, so a sketch drawn on it does not spin when the UCS is
/// rebuilt.
///
/// Returns `None` when `normal` is degenerate and defines no plane.
/// X and Y come from the arbitrary-axis algorithm in
/// [`crate::scene::view::transform::ocs_axes`], the same frame entities get
/// from their extrusion direction, so the same face always yields the same X
/// axis. Returns `None` when `normal` is degenerate and defines no plane.
pub(super) fn ucs_from_normal(origin: glam::DVec3, normal: glam::DVec3) -> Option<Ucs> {
let z = normal.normalize_or_zero();
if z.length_squared() < 1e-12 {
return None;
}
// 1/64 is the threshold the DWG spec names for "close enough to the world
// Z axis that using it as the reference would be ill conditioned".
let reference = if z.x.abs() < 1.0 / 64.0 && z.y.abs() < 1.0 / 64.0 {
glam::DVec3::Y
} else {
glam::DVec3::Z
};
let x = reference.cross(z).normalize_or_zero();
if x.length_squared() < 1e-12 {
return None;
}
let y = z.cross(x).normalize_or_zero();
let ((xx, xy, xz), (yx, yy, yz)) = crate::scene::view::transform::ocs_axes((z.x, z.y, z.z));
let mut ucs = Ucs::new("*ACTIVE*");
ucs.origin = acadrust::types::Vector3::new(origin.x, origin.y, origin.z);
ucs.x_axis = acadrust::types::Vector3::new(x.x, x.y, x.z);
ucs.y_axis = acadrust::types::Vector3::new(y.x, y.y, y.z);
ucs.x_axis = acadrust::types::Vector3::new(xx, xy, xz);
ucs.y_axis = acadrust::types::Vector3::new(yx, yy, yz);
Some(ucs)
}
+21 -11
View File
@@ -347,23 +347,30 @@ impl CadCommand for ValuePromptCommand {
}
}
/// Interactive front-end for `UCS FACE` — Fusion's "create sketch on a face".
/// Interactive pick for `UCS FACE` and `UCS OBJECT`.
///
/// Picks one face of a solid and hands the resulting plane to the inline
/// `UCS FACE <handle> <x,y,z>` handler via [`CmdResult::Dispatch`], so the
/// plane construction lives in exactly one place whether the user clicked a
/// face or typed the arguments. `entity_pick_uses_surface_point` is what
/// makes the click land *on the solid's surface* rather than on the current
/// drawing plane, which is the whole point: the pick coordinate is the face.
pub struct UcsFaceCommand;
/// Hands the picked entity to the inline `UCS FACE <handle> <x,y,z>` /
/// `UCS OBJECT <handle>` handler via [`CmdResult::Dispatch`], so the plane
/// construction lives in one place whether the user clicked or typed the
/// arguments. For a face the click lands *on the solid's surface*
/// (`entity_pick_uses_surface_point`) and that point becomes the plane
/// origin; for an object only the handle matters.
pub struct UcsPickCommand {
/// `true` picks a solid face; `false` picks a planar entity.
pub face: bool,
}
impl CadCommand for UcsFaceCommand {
impl CadCommand for UcsPickCommand {
fn name(&self) -> &'static str {
"UCS"
}
fn prompt(&self) -> String {
crate::t!("UCS FACE Select a face to draw on:").into_owned()
if self.face {
crate::t!("UCS FACE Select a face to draw on:").into_owned()
} else {
crate::t!("UCS OBJECT Select object to align UCS:").into_owned()
}
}
fn needs_entity_pick(&self) -> bool {
@@ -371,7 +378,7 @@ impl CadCommand for UcsFaceCommand {
}
fn entity_pick_uses_surface_point(&self) -> bool {
true
self.face
}
fn entity_pick_highlights_hover(&self) -> bool {
@@ -382,6 +389,9 @@ impl CadCommand for UcsFaceCommand {
if handle.is_null() {
return CmdResult::NeedPoint;
}
if !self.face {
return CmdResult::Dispatch(format!("UCS OBJECT {:X}", handle.value()));
}
// Hex handle and comma-separated coordinates are what the inline
// parser reads back. Full `{}` precision, not a rounded format: the
// point has to stay on the face for the planar-face lookup.
+1 -1
View File
@@ -4149,7 +4149,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
"TORUS Specify second end point of tube diameter:" => Some(("model", "torus-specify-second-end-point-of-tube-diameter")),
"2Point" => Some(("model", "value-2point")),
"Cube" => Some(("model", "cube")),
"UCS option [World/View/3Point/Z/X/Y/Origin/Save/Delete] or name:" => Some(("view", "ucs-option-world-view-3point-z-x-y-origin-save-delete-or-name")),
"UCS option [Face/OBject/World/View/3Point/Z/X/Y/Origin/Save/Delete] or name:" => Some(("view", "ucs-option-world-view-3point-z-x-y-origin-save-delete-or-name")),
"UCS specify new origin:" => Some(("view", "ucs-specify-new-origin")),
"UCS specify point on positive X axis:" => Some(("view", "ucs-specify-point-on-positive-x-axis")),
"UCS specify point in positive XY plane:" => Some(("view", "ucs-specify-point-in-positive-xy-plane")),
+10 -3
View File
@@ -24,9 +24,13 @@ impl UcsCommand {
Self::default()
}
/// Options that take no further argument and execute immediately.
/// Options that take no further typed argument: they execute immediately
/// or, for Face / OBject, start their own entity pick.
fn is_zero_arg(opt: &str) -> bool {
matches!(opt, "W" | "WORLD" | "VIEW" | "V" | "LIST" | "?")
matches!(
opt,
"W" | "WORLD" | "VIEW" | "V" | "LIST" | "?" | "FACE" | "F" | "OBJECT" | "OB"
)
}
/// Options whose argument is a coordinate (so a click is accepted too).
@@ -50,7 +54,10 @@ impl CadCommand for UcsCommand {
fn prompt(&self) -> String {
match self.option.as_deref() {
None => t!("UCS option [World/View/3Point/Z/X/Y/Origin/Save/Delete] or name:").into_owned(),
None => t!(
"UCS option [Face/OBject/World/View/3Point/Z/X/Y/Origin/Save/Delete] or name:"
)
.into_owned(),
Some("Z") => t!("UCS rotation angle about Z (degrees):").into_owned(),
Some("X") => t!("UCS rotation angle about X (degrees):").into_owned(),
Some("Y") => t!("UCS rotation angle about Y (degrees):").into_owned(),