diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index c959f870b..57a2b9e6f 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -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: diff --git a/src/app/commands/layerprops.rs b/src/app/commands/layerprops.rs index 3943e0a33..34117610e 100644 --- a/src/app/commands/layerprops.rs +++ b/src/app/commands/layerprops.rs @@ -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 ").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 diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index 42a9dd5a3..1db66f468 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -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", diff --git a/src/app/helpers.rs b/src/app/helpers.rs index d1b3d3fb7..91e925bc0 100644 --- a/src/app/helpers.rs +++ b/src/app/helpers.rs @@ -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 { 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) } diff --git a/src/command.rs b/src/command.rs index 44852bd88..a79b09582 100644 --- a/src/command.rs +++ b/src/command.rs @@ -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 ` 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 ` / +/// `UCS OBJECT ` 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. diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 17536c4f7..05c3409d7 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -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")), diff --git a/src/modules/view/ucs_cmd.rs b/src/modules/view/ucs_cmd.rs index d7d58ed87..61f3c6b11 100644 --- a/src/modules/view/ucs_cmd.rs +++ b/src/modules/view/ucs_cmd.rs @@ -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(),