diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index e62e16961..ea9c5c5de 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -462,13 +462,19 @@ impl OpenCADStudio { // clearing Z afterwards shifts the picked point on screen in an // oblique view. Plain paper space still uses the camera target plane. let plane = if self.tabs[i].editing_model_space() { - Some(match self.tabs[i].active_ucs.as_ref() { - Some(ucs) => ( - ucs_z_axis(ucs), - glam::DVec3::new(ucs.origin.x, ucs.origin.y, ucs.origin.z), - ), - None => (glam::DVec3::Z, glam::DVec3::ZERO), - }) + self.tabs[i] + .active_cmd + .as_ref() + .and_then(|command| command.cursor_plane()) + .or_else(|| { + Some(match self.tabs[i].active_ucs.as_ref() { + Some(ucs) => ( + ucs_z_axis(ucs), + glam::DVec3::new(ucs.origin.x, ucs.origin.y, ucs.origin.z), + ), + None => (glam::DVec3::Z, glam::DVec3::ZERO), + }) + }) } else { None }; @@ -1908,12 +1914,17 @@ impl OpenCADStudio { && !command.needs_tangent_pick() && !command.is_selection_gathering() }); + let uses_command_cursor_plane = self.tabs[i] + .active_cmd + .as_ref() + .and_then(|command| command.cursor_plane()) + .is_some(); let axis_lock = if let Some(base) = self.last_point { self.active_axis_lock( i, cursor_world, base, - wants_point && !is_window_corner, + wants_point && !is_window_corner && !uses_command_cursor_plane, ) } else { self.axis_lock_dir = None; @@ -2040,7 +2051,7 @@ impl OpenCADStudio { let osnap_locked = self.tabs[i] .snap_result .is_some_and(|s| s.snap_type != crate::snap::SnapType::Grid); - if !osnap_locked && !is_window_corner { + if !osnap_locked && !is_window_corner && !uses_command_cursor_plane { if let Some(base) = self.last_point { let ucs_xf = self.tabs[i].ucs_xform(); if self.ortho_mode { @@ -2070,7 +2081,10 @@ impl OpenCADStudio { }; // Clamp to world XY only when no UCS is active; with a // UCS the point already lies on the UCS XY plane. - if self.tabs[i].active_cmd.is_some() && self.tabs[i].active_ucs.is_none() { + if self.tabs[i].active_cmd.is_some() + && self.tabs[i].active_ucs.is_none() + && !uses_command_cursor_plane + { pt.z = 0.0; } pt @@ -3367,7 +3381,14 @@ impl OpenCADStudio { let mut pt = snap_hit.map(|s| s.world).unwrap_or(raw); // When no UCS is active clamp to world XY; with a UCS the point is // already constrained to that plane by the ray–plane intersection. - if self.tabs[i].active_ucs.is_none() { + let uses_command_cursor_plane = self.tabs[i] + .active_cmd + .as_ref() + .and_then(|command| command.cursor_plane()) + .is_some(); + let clamp_world_xy = + self.tabs[i].active_ucs.is_none() && !uses_command_cursor_plane; + if clamp_world_xy { pt.z = 0.0; } let axis_lock = if let Some(base) = self.last_point { @@ -3375,7 +3396,10 @@ impl OpenCADStudio { i, raw, base, - !needs_entity_click && !needs_tan && !is_window_corner, + !needs_entity_click + && !needs_tan + && !is_window_corner + && !uses_command_cursor_plane, ) } else { self.axis_lock_dir = None; @@ -3383,7 +3407,7 @@ impl OpenCADStudio { }; if let (Some(dir), Some(base)) = (axis_lock, self.last_point) { pt = axis_lock_apply(pt, base, dir); - if self.tabs[i].active_ucs.is_none() { + if clamp_world_xy { pt.z = 0.0; } } @@ -3403,11 +3427,12 @@ impl OpenCADStudio { }; if let Some(h) = otrack { pt = h.aligned; - if self.tabs[i].active_ucs.is_none() { + if clamp_world_xy { pt.z = 0.0; } } else if axis_lock.is_none() && !is_window_corner + && !uses_command_cursor_plane && !snap_hit.is_some_and(|s| s.snap_type != crate::snap::SnapType::Grid) { // Object snap wins over ortho/polar — a snapped point diff --git a/src/command.rs b/src/command.rs index 25520107d..cc25ff607 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1778,6 +1778,12 @@ pub trait CadCommand: Send { None } + /// Override the model-space plane onto which viewport cursor rays are + /// projected for this command step. + fn cursor_plane(&self) -> Option<(DVec3, DVec3)> { + None + } + /// Mid-command Ctrl+Z: a multi-point drawing command can take the undo /// itself (PLINE pops its last vertex) instead of the document undo /// swallowing the whole in-progress object. `None` (default) lets the diff --git a/src/modules/insert/solid3d_cmds.rs b/src/modules/insert/solid3d_cmds.rs index 63ea91bbe..21b8c8fdf 100644 --- a/src/modules/insert/solid3d_cmds.rs +++ b/src/modules/insert/solid3d_cmds.rs @@ -749,27 +749,13 @@ impl RevolveCommand { Some(self.axis_start + axis * (reference - self.axis_start).dot(axis)) } - fn profile_normal(&self) -> Option { - self.preview_profiles.iter().find_map(|(_, entity)| { - crate::scene::model::sweep_model::extrusion_profile_of(entity) - .and_then(|(profile, _)| profile.plane.normal()) - .map(DVec3::from_array) - .and_then(DVec3::try_normalize) - }) - } - fn cursor_angle(&self, cursor: DVec3, full_at_zero: bool) -> Option { + let axis = (self.axis_end - self.axis_start).try_normalize()?; let anchor = self.angle_anchor()?; let reference = (self.profile_reference()? - anchor).try_normalize()?; - // The picked cursor is projected onto the active/profile construction - // plane. Measuring it about the 3-D revolution axis collapses almost - // every ordinary mouse pick to 0 or 180 degrees, and 0 is the full-turn - // shortcut. Measure the scalar input in the profile plane instead; - // the resulting angle is still applied about the requested 3-D axis. - let normal = self.profile_normal().unwrap_or(self.working_plane.z); let cursor_direction = cursor - anchor; - let radial = (cursor_direction - normal * cursor_direction.dot(normal)).try_normalize()?; - let mut angle = normal + let radial = (cursor_direction - axis * cursor_direction.dot(axis)).try_normalize()?; + let mut angle = axis .dot(reference.cross(radial)) .atan2(reference.dot(radial)); if angle < 0.0 { @@ -1170,6 +1156,15 @@ impl CadCommand for RevolveCommand { fn dyn_auto_sign_angle(&self) -> bool { false } + fn cursor_plane(&self) -> Option<(DVec3, DVec3)> { + if !matches!(self.step, RevolveStep::Angle | RevolveStep::StartAngle) { + return None; + } + Some(( + (self.axis_end - self.axis_start).try_normalize()?, + self.angle_anchor()?, + )) + } fn dyn_live_value(&self, cursor: DVec3) -> Option { let full_at_zero = self.step == RevolveStep::Angle; self.cursor_angle(cursor, full_at_zero).map(|angle| { @@ -1381,4 +1376,34 @@ inventory::submit!(crate::command::CommandRegistration { }); inventory::submit!(crate::command::CommandRegistration { names: &["LOFT"] }); inventory::submit!(crate::command::CommandRegistration { names: &["REVOLVE"] }); + +#[cfg(test)] +mod revolve_tests { + use super::*; + + #[test] + fn cursor_angle_is_measured_about_the_model_axis() { + let profile = EntityType::Line(acadrust::entities::Line::from_coords( + 2.0, 0.0, 0.0, 2.0, 4.0, 0.0, + )); + let mut command = RevolveCommand::new([1.0; 4], 0); + command.set_preselection(vec![(Handle::new(1), profile)]); + command.axis_start = DVec3::ZERO; + command.axis_end = DVec3::Y; + command.step = RevolveStep::Angle; + + let anchor = command.angle_anchor().unwrap(); + let quarter = anchor - DVec3::Z * 2.0; + let angle = command.cursor_angle(quarter, false).unwrap(); + assert!((angle - std::f64::consts::FRAC_PI_2).abs() < 1e-12); + + let displaced_along_axis = quarter + DVec3::Y * 100.0; + let angle = command.cursor_angle(displaced_along_axis, false).unwrap(); + assert!((angle - std::f64::consts::FRAC_PI_2).abs() < 1e-12); + + let (normal, origin) = command.cursor_plane().unwrap(); + assert!((normal - DVec3::Y).length_squared() < 1e-24); + assert!((origin - anchor).length_squared() < 1e-24); + } +} inventory::submit!(crate::command::CommandRegistration { names: &["SWEEP"] });