forked from 1b-agent/opencadstudio-upstream
feat(3drotate): rotate about an axis picked in space
3DROTATE (ROTATE3D) took the drawing's rotation path, so a polyline only turned in its own plane. It now asks for a base point, an axis — X, Y, Z or a second point — and an angle, and rotates the selection about that axis in space. Fixes #1641 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
e854481f72
commit
f485df7636
24 files changed
+170
-16
No files matched your search
@@ -36,6 +36,8 @@ pub fn tool() -> ToolDef {
|
||||
|
||||
enum Step {
|
||||
Center,
|
||||
/// 3DROTATE: the axis through the base point.
|
||||
Axis { center: DVec3 },
|
||||
Angle { center: DVec3 },
|
||||
RefFirst { center: DVec3 },
|
||||
RefSecond { center: DVec3, first: DVec3 },
|
||||
@@ -48,6 +50,11 @@ pub struct RotateCommand {
|
||||
step: Step,
|
||||
default_angle: f64, // degrees
|
||||
plane: WorkingPlane,
|
||||
/// 3DROTATE: ask for the axis, then measure the angle in the plane square
|
||||
/// to it.
|
||||
spatial: bool,
|
||||
/// The plane is the one square to the chosen axis, not the UCS.
|
||||
axis_chosen: bool,
|
||||
}
|
||||
|
||||
impl RotateCommand {
|
||||
@@ -58,9 +65,35 @@ impl RotateCommand {
|
||||
step: Step::Center,
|
||||
default_angle: defaults::get_rotate_angle(),
|
||||
plane: WorkingPlane::default(),
|
||||
spatial: false,
|
||||
axis_chosen: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 3DROTATE: any objects, about any axis through a base point.
|
||||
pub fn new_3d(handles: Vec<Handle>, wire_models: Vec<WireModel>) -> Self {
|
||||
Self { spatial: true, ..Self::new(handles, wire_models) }
|
||||
}
|
||||
|
||||
/// Turn about `axis` through `center`: the angle is then read in the plane
|
||||
/// square to it, so every later step works as in a plain rotation.
|
||||
fn choose_axis(&mut self, center: DVec3, axis: DVec3) -> CmdResult {
|
||||
let Some(axis) = axis.try_normalize() else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
// The UCS X laid into that plane, or Y when X runs along the axis.
|
||||
let reference = [self.plane.x, self.plane.y]
|
||||
.into_iter()
|
||||
.map(|direction| direction - axis * direction.dot(axis))
|
||||
.find(|direction| direction.length() > 1e-6)
|
||||
.unwrap_or(DVec3::X);
|
||||
let x = reference.normalize();
|
||||
self.plane = WorkingPlane::new(center, x, axis.cross(x));
|
||||
self.axis_chosen = true;
|
||||
self.step = Step::Angle { center };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn commit(&self, center: DVec3, angle_rad: f64) -> CmdResult {
|
||||
defaults::set_rotate_angle(angle_rad.to_degrees());
|
||||
CmdResult::TransformSelected(
|
||||
@@ -138,20 +171,38 @@ impl RotateCommand {
|
||||
|
||||
impl CadCommand for RotateCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
if !self.axis_chosen {
|
||||
self.plane = plane;
|
||||
}
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ROTATE"
|
||||
if self.spatial {
|
||||
"3DROTATE"
|
||||
} else {
|
||||
"ROTATE"
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::Center if self.spatial => t!(
|
||||
"3DROTATE Specify base point [%{count} objects]:",
|
||||
count = self.handles.len()
|
||||
)
|
||||
.into_owned(),
|
||||
Step::Center => t!(
|
||||
"ROTATE Specify rotation center [%{count} objects]:",
|
||||
count = self.handles.len()
|
||||
)
|
||||
.into_owned(),
|
||||
Step::Axis { .. } => {
|
||||
t!("3DROTATE Specify rotation axis [X/Y/Z] or a second point on it <Z>:").into_owned()
|
||||
}
|
||||
Step::Angle { .. } if self.spatial => {
|
||||
let a = format!("{:.4}", self.default_angle);
|
||||
t!("3DROTATE Specify rotation angle <%{a}>:", a = a).into_owned()
|
||||
}
|
||||
Step::Angle { .. } => {
|
||||
let a = format!("{:.4}", self.default_angle);
|
||||
t!("ROTATE Specify rotation angle <%{a}>:", a = a).into_owned()
|
||||
@@ -172,6 +223,7 @@ impl CadCommand for RotateCommand {
|
||||
fn options(&self) -> Vec<crate::command::CmdOption> {
|
||||
use crate::command::CmdOption;
|
||||
match self.step {
|
||||
Step::Axis { .. } => ["X", "Y", "Z"].map(|axis| CmdOption::new(axis, axis)).to_vec(),
|
||||
Step::Angle { .. } => vec![CmdOption::new(t!("Reference").as_ref(), "R")],
|
||||
_ => vec![],
|
||||
}
|
||||
@@ -179,10 +231,18 @@ impl CadCommand for RotateCommand {
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
match &self.step {
|
||||
Step::Center if self.spatial => {
|
||||
self.step = Step::Axis { center: pt };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Center => {
|
||||
self.step = Step::Angle { center: pt };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Axis { center } => {
|
||||
let center = *center;
|
||||
self.choose_axis(center, pt - center)
|
||||
}
|
||||
Step::Angle { center } => {
|
||||
let center = *center;
|
||||
let Some(angle_rad) = self.plane.angle(center, pt) else {
|
||||
@@ -214,6 +274,11 @@ impl CadCommand for RotateCommand {
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
// The axis defaults to the UCS Z.
|
||||
if let Step::Axis { center } = &self.step {
|
||||
let center = *center;
|
||||
return self.choose_axis(center, self.plane.z);
|
||||
}
|
||||
// At the normal angle step, Enter uses the stored default angle.
|
||||
if let Step::Angle { center } = &self.step {
|
||||
let center = *center;
|
||||
@@ -228,6 +293,16 @@ impl CadCommand for RotateCommand {
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let t = text.trim();
|
||||
match &self.step {
|
||||
Step::Axis { center } => {
|
||||
let center = *center;
|
||||
let axis = match t.to_ascii_uppercase().as_str() {
|
||||
"X" => self.plane.x,
|
||||
"Y" => self.plane.y,
|
||||
"Z" => self.plane.z,
|
||||
_ => return None,
|
||||
};
|
||||
Some(self.choose_axis(center, axis))
|
||||
}
|
||||
Step::Angle { center } => {
|
||||
let center = *center;
|
||||
let low = t.to_ascii_lowercase();
|
||||
@@ -260,6 +335,14 @@ impl CadCommand for RotateCommand {
|
||||
|
||||
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
|
||||
let (center, angle_rad, guide_angle) = match &self.step {
|
||||
Step::Axis { center } => {
|
||||
return vec![WireModel::solid_f64(
|
||||
"rotate_axis".into(),
|
||||
vec![center.to_array(), pt.to_array()],
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
)];
|
||||
}
|
||||
Step::Angle { center } => {
|
||||
let Some(angle) = self.plane.angle(*center, pt) else {
|
||||
return vec![];
|
||||
|
||||
Reference in new issue
Block a user