forked from 1b-agent/opencadstudio-upstream
ucs: dynamic UCS on planar solid faces
With dynamic UCS on (UCSDETECT 1, toggled with F6), a command's first point picked on a planar face of a solid sets the UCS to that face for the rest of the command, so 2D commands draw on the face (#1711). An unsnapped first point lands on the face itself; a snapped point off the face's plane belongs to other geometry and leaves the UCS alone. The previous UCS comes back when the command ends. UCS commands themselves are left out. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
19ecf78bd9
commit
cccea4008b
33 files changed
+193
No files matched your search
@@ -624,6 +624,48 @@ impl OpenCADStudio {
|
||||
Some(key)
|
||||
}
|
||||
|
||||
/// Dynamic UCS: a command's first point picked on a planar solid face sets
|
||||
/// the UCS to that face for the rest of the command. Returns the picked
|
||||
/// point on the face. A snapped point (`snapped`) off the face's plane
|
||||
/// belongs to other geometry, so it leaves the UCS alone.
|
||||
pub(in crate::app) fn apply_dynamic_ucs(
|
||||
&mut self,
|
||||
snapped: Option<glam::DVec3>,
|
||||
) -> Option<glam::DVec3> {
|
||||
let i = self.active_tab;
|
||||
if !self.dynamic_ucs
|
||||
|| self.dynamic_ucs_restore.is_some()
|
||||
|| self.last_point.is_some()
|
||||
|| !self.tabs[i].editing_model_space()
|
||||
|| self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.is_none_or(|command| command.name().starts_with("UCS"))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let cursor = self.tabs[i].scene.selection.borrow().input.last_move_pos?;
|
||||
let (vw, vh) = self.tabs[i].scene.selection.borrow().view.vp_size;
|
||||
let bounds = self.tabs[i].scene.active_model_tile_bounds(vw, vh);
|
||||
let local = iced::Point::new(cursor.x - bounds.x, cursor.y - bounds.y);
|
||||
let (ucs, pick) = self.tabs[i].scene.planar_face_ucs_at(bounds, local)?;
|
||||
if let Some(point) = snapped {
|
||||
let normal = glam::DVec3::new(ucs.x_axis.x, ucs.x_axis.y, ucs.x_axis.z)
|
||||
.cross(glam::DVec3::new(ucs.y_axis.x, ucs.y_axis.y, ucs.y_axis.z))
|
||||
.normalize_or_zero();
|
||||
let off_plane = (point - pick).dot(normal).abs();
|
||||
if off_plane > 1.0e-6 * (1.0 + point.length().max(pick.length())) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let saved = self.tabs[i].active_ucs.take();
|
||||
self.dynamic_ucs_restore = Some((self.tabs[i].id, saved));
|
||||
self.tabs[i].active_ucs = Some(ucs);
|
||||
self.tabs[i].sync_ucs_to_scene();
|
||||
self.push_ucs_to_cmd(i);
|
||||
Some(pick)
|
||||
}
|
||||
|
||||
/// `feed_command`, also reporting whether the step actually took the input.
|
||||
/// Only a `Text` token can come back unclaimed (`on_text_input` returning
|
||||
/// `None`), which is what lets the caller read it as something else — a
|
||||
@@ -632,6 +674,9 @@ impl OpenCADStudio {
|
||||
&mut self,
|
||||
input: StepInput,
|
||||
) -> (Task<Message>, bool) {
|
||||
if matches!(input, StepInput::Point(_)) {
|
||||
let _ = self.apply_dynamic_ucs(None);
|
||||
}
|
||||
let gathering = self.report_selection_found(&input);
|
||||
// A command that takes associative dimensions only keeps just those:
|
||||
// the rest leave the selection and the count of what went is reported,
|
||||
|
||||
@@ -768,6 +768,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"FILEDIA",
|
||||
"TRIMEXTENDMODE",
|
||||
"SELECTIONCYCLING",
|
||||
"UCSDETECT",
|
||||
"SURFACEASSOCIATIVITY",
|
||||
"LAYOUTCREATEVIEWPORT",
|
||||
"FIELDEVAL",
|
||||
|
||||
@@ -1216,6 +1216,7 @@ impl OpenCADStudio {
|
||||
| "FILEDIA"
|
||||
| "TRIMEXTENDMODE"
|
||||
| "SELECTIONCYCLING"
|
||||
| "UCSDETECT"
|
||||
| "SURFACEASSOCIATIVITY"
|
||||
| "UCSORTHO"
|
||||
| "UCSVIEW"
|
||||
@@ -1656,6 +1657,30 @@ impl OpenCADStudio {
|
||||
}
|
||||
return Some(self.finish_dispatch(cmd));
|
||||
}
|
||||
if name == "UCSDETECT" {
|
||||
let current = i16::from(self.dynamic_ucs);
|
||||
let prompt = format!("Enter new value for UCSDETECT <{current}>:");
|
||||
match value
|
||||
.as_deref()
|
||||
.map(|v| v.trim().parse::<i16>().ok().filter(|v| (0..=1).contains(v)))
|
||||
{
|
||||
Some(Some(mode)) => {
|
||||
self.dynamic_ucs = mode == 1;
|
||||
self.persist_settings_if_changed();
|
||||
}
|
||||
Some(None) => {
|
||||
self.command_line
|
||||
.push_error(crate::t!("Requires 0 or 1 only.").as_ref());
|
||||
self.command_line.push_output(&prompt);
|
||||
self.pending_setvar = Some(name.clone());
|
||||
}
|
||||
None => {
|
||||
self.command_line.push_output(&prompt);
|
||||
self.pending_setvar = Some(name.clone());
|
||||
}
|
||||
}
|
||||
return Some(self.finish_dispatch(cmd));
|
||||
}
|
||||
if name == "SELECTIONCYCLING" {
|
||||
// 1 and 2 both turn cycling on; the candidate list
|
||||
// always shows.
|
||||
|
||||
@@ -655,6 +655,12 @@ pub(super) struct OpenCADStudio {
|
||||
/// CATIA navigation: the left button went down while the middle one was
|
||||
/// held, so the middle drag orbits until it is released.
|
||||
nav_left_with_middle: bool,
|
||||
/// UCSDETECT: a command's first point on a planar solid face sets the
|
||||
/// UCS to that face until the command ends.
|
||||
dynamic_ucs: bool,
|
||||
/// The UCS a dynamic UCS replaced, by tab id, restored when that tab's
|
||||
/// command ends.
|
||||
dynamic_ucs_restore: Option<(u64, Option<codec::tables::Ucs>)>,
|
||||
/// Scroll pans and Ctrl+scroll zooms (touchpads); see `scroll_intent`.
|
||||
touchpad_pan: bool,
|
||||
/// Multi-finger gestures on a touch screen.
|
||||
@@ -2663,6 +2669,8 @@ pub enum Message {
|
||||
/// Toggle reversing the mouse-wheel zoom direction (ZOOMWHEEL).
|
||||
ZoomWheelReversedChanged(bool),
|
||||
NavigationStyleChanged(crate::app::settings::NavigationStyle),
|
||||
/// F6 / UCSDETECT: toggle the dynamic UCS.
|
||||
ToggleDynamicUcs,
|
||||
TouchpadPanChanged(bool),
|
||||
/// Options: show the pull-down menus (MENUBAR).
|
||||
MenuBarChanged(bool),
|
||||
@@ -4640,6 +4648,8 @@ impl OpenCADStudio {
|
||||
zoom_wheel_reversed: false,
|
||||
navigation_style: Default::default(),
|
||||
nav_left_with_middle: false,
|
||||
dynamic_ucs: true,
|
||||
dynamic_ucs_restore: None,
|
||||
touchpad_pan: false,
|
||||
touch: Default::default(),
|
||||
touch_offset: None,
|
||||
|
||||
@@ -416,6 +416,9 @@ pub struct UserSettings {
|
||||
pub polar_increment_deg: f32,
|
||||
pub zoom_wheel_reversed: bool,
|
||||
pub navigation_style: NavigationStyle,
|
||||
/// UCSDETECT: a command's first point on a planar solid face sets the
|
||||
/// UCS to that face until the command ends.
|
||||
pub dynamic_ucs: bool,
|
||||
/// Scrolling pans and Ctrl+scroll (a touchpad pinch) zooms, for a
|
||||
/// touchpad that reports two-finger drags as wheel scrolls.
|
||||
pub touchpad_pan: bool,
|
||||
@@ -920,6 +923,7 @@ impl Default for UserSettings {
|
||||
polar_increment_deg: 45.0,
|
||||
zoom_wheel_reversed: false,
|
||||
navigation_style: NavigationStyle::Cad,
|
||||
dynamic_ucs: true,
|
||||
touchpad_pan: false,
|
||||
ui_scale_percent: 100,
|
||||
zoom_factor: 60,
|
||||
|
||||
@@ -41,6 +41,7 @@ pub(super) fn default_bindings() -> BTreeMap<String, String> {
|
||||
("F3".to_string(), "TOGGLEOSNAP"),
|
||||
("F4".to_string(), "TOGGLE3DOSNAP"),
|
||||
("F5".to_string(), "ISOPLANE"),
|
||||
("F6".to_string(), "TOGGLEDUCS"),
|
||||
("F7".to_string(), "GRID"),
|
||||
("F8".to_string(), "ORTHO"),
|
||||
("F9".to_string(), "SNAP"),
|
||||
@@ -185,6 +186,7 @@ pub(super) const INPUT_ACTIONS: &[&str] = &[
|
||||
"COMMANDHISTORY",
|
||||
"TOGGLEOSNAP",
|
||||
"TOGGLE3DOSNAP",
|
||||
"TOGGLEDUCS",
|
||||
"OTRACK",
|
||||
"DYNINPUT",
|
||||
"SELECTALL",
|
||||
@@ -343,6 +345,7 @@ impl OpenCADStudio {
|
||||
"COMMANDHISTORY" => Message::CommandHistoryToggle,
|
||||
"TOGGLEOSNAP" => Message::ToggleSnapEnabled,
|
||||
"TOGGLE3DOSNAP" => Message::ToggleSnap3dEnabled,
|
||||
"TOGGLEDUCS" => Message::ToggleDynamicUcs,
|
||||
"OTRACK" => Message::ToggleOTrack,
|
||||
"DYNINPUT" => Message::ToggleDynInput,
|
||||
"SELECTALL" => Message::SelectAllShortcut,
|
||||
|
||||
@@ -779,6 +779,7 @@ impl OpenCADStudio {
|
||||
polar_increment_deg: self.polar_increment_deg,
|
||||
zoom_wheel_reversed: self.zoom_wheel_reversed,
|
||||
navigation_style: self.navigation_style,
|
||||
dynamic_ucs: self.dynamic_ucs,
|
||||
touchpad_pan: self.touchpad_pan,
|
||||
ui_scale_percent: self.ui_scale_percent,
|
||||
zoom_factor: self.zoom_factor,
|
||||
@@ -915,6 +916,7 @@ impl OpenCADStudio {
|
||||
self.polar_increment_deg = s.polar_increment_deg;
|
||||
self.zoom_wheel_reversed = s.zoom_wheel_reversed;
|
||||
self.navigation_style = s.navigation_style;
|
||||
self.dynamic_ucs = s.dynamic_ucs;
|
||||
self.touchpad_pan = s.touchpad_pan;
|
||||
self.ui_scale_percent = crate::app::settings::clamp_ui_scale(s.ui_scale_percent);
|
||||
self.zoom_factor = crate::app::settings::clamp_zoom_factor(s.zoom_factor);
|
||||
|
||||
@@ -374,6 +374,7 @@ impl OpenCADStudio {
|
||||
// document's selection.
|
||||
let before = self.tabs.get(self.active_tab).map(|tab| tab.id);
|
||||
let task = self.update_message(msg);
|
||||
self.restore_dynamic_ucs_after_command();
|
||||
if self.tabs.get(self.active_tab).map(|tab| tab.id) != before {
|
||||
self.sync_underlay_tab();
|
||||
}
|
||||
@@ -396,6 +397,25 @@ impl OpenCADStudio {
|
||||
task
|
||||
}
|
||||
|
||||
/// A dynamic UCS lasts for the command that set it: once that tab's
|
||||
/// command has ended, the UCS it replaced comes back.
|
||||
fn restore_dynamic_ucs_after_command(&mut self) {
|
||||
let Some((tab_id, _)) = self.dynamic_ucs_restore else {
|
||||
return;
|
||||
};
|
||||
let Some(i) = self.tabs.iter().position(|tab| tab.id == tab_id) else {
|
||||
self.dynamic_ucs_restore = None;
|
||||
return;
|
||||
};
|
||||
if self.tabs[i].active_cmd.is_some() {
|
||||
return;
|
||||
}
|
||||
if let Some((_, saved)) = self.dynamic_ucs_restore.take() {
|
||||
self.tabs[i].active_ucs = saved;
|
||||
self.tabs[i].sync_ucs_to_scene();
|
||||
}
|
||||
}
|
||||
|
||||
/// The Frames list on the ribbon shows the active drawing's frame state
|
||||
/// (FRAME), however it was changed: the list, a command, undo or another
|
||||
/// drawing.
|
||||
@@ -9245,6 +9265,18 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::ToggleDynamicUcs => {
|
||||
self.dynamic_ucs = !self.dynamic_ucs;
|
||||
self.command_line.push_output(if self.dynamic_ucs {
|
||||
crate::t!("Dynamic UCS: on")
|
||||
} else {
|
||||
crate::t!("Dynamic UCS: off")
|
||||
}
|
||||
.as_ref());
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::NavigationStyleChanged(style) => {
|
||||
self.navigation_style = style;
|
||||
self.persist_settings_if_changed();
|
||||
|
||||
@@ -4325,6 +4325,7 @@ impl OpenCADStudio {
|
||||
};
|
||||
|
||||
let snap_taken = self.tabs[i].snap_result.take();
|
||||
let snapped = snap_taken.is_some();
|
||||
let tangent_obj_at_click = snap_taken.and_then(|s| s.tangent_obj);
|
||||
|
||||
let world_pt = {
|
||||
@@ -5019,6 +5020,11 @@ impl OpenCADStudio {
|
||||
self.finish_command_click(i);
|
||||
return Task::none();
|
||||
}
|
||||
// An unsnapped first point on a solid face lands on the face.
|
||||
let world_pt = match self.apply_dynamic_ucs(snapped.then_some(world_pt)) {
|
||||
Some(on_face) if !snapped => on_face,
|
||||
_ => world_pt,
|
||||
};
|
||||
self.last_point = Some(world_pt);
|
||||
// The one-shot snap override is spent by this pick —
|
||||
// restore the running osnap configuration (#337).
|
||||
|
||||
@@ -5074,6 +5074,8 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static
|
||||
"CAD: Shift + middle button orbits" => Some(("common", "nav-style-cad")),
|
||||
"Blender: middle button orbits, Shift pans" => Some(("common", "nav-style-blender")),
|
||||
"CATIA: middle + left button orbits" => Some(("common", "nav-style-catia")),
|
||||
"Dynamic UCS: on" => Some(("common", "dynamic-ucs-on")),
|
||||
"Dynamic UCS: off" => Some(("common", "dynamic-ucs-off")),
|
||||
"Touchpad: scroll pans, Ctrl+scroll or pinch zooms" => Some(("common", "touchpad-scroll-pans-ctrl-scroll-or-pinch-zooms")),
|
||||
"Interface scale" => Some(("common", "interface-scale")),
|
||||
"Zoom factor" => Some(("common", "zoom-factor")),
|
||||
|
||||
@@ -11363,6 +11363,25 @@ impl Scene {
|
||||
)
|
||||
}
|
||||
|
||||
/// The UCS of the planar solid face under `local` (relative to `bounds`),
|
||||
/// Z along the face normal, and the picked point on the face.
|
||||
pub fn planar_face_ucs_at(
|
||||
&mut self,
|
||||
bounds: iced::Rectangle,
|
||||
local: iced::Point,
|
||||
) -> Option<(codec::tables::Ucs, glam::DVec3)> {
|
||||
let camera = self.camera.borrow();
|
||||
let view_rot = camera.view_proj_rte(bounds);
|
||||
let eye = camera.eye();
|
||||
drop(camera);
|
||||
let (handle, pick) = self.solid_click_hit_point(local, view_rot, eye, bounds)?;
|
||||
self.restore_solid_models(&[handle]);
|
||||
let body = self.solid_models.get(&handle)?;
|
||||
let face = crate::scene::model::solid_model::nearest_planar_face(body, pick.to_array())?;
|
||||
crate::scene::model::solid_model::planar_face_ucs(body, face, pick.to_array())
|
||||
.map(|ucs| (ucs, pick))
|
||||
}
|
||||
|
||||
/// The nearest solid or mesh surface under `local` (relative to `bounds`).
|
||||
pub fn surface_pivot_at(
|
||||
&self,
|
||||
|
||||
Reference in new issue
Block a user