Files
OneCAD-Draft/src/modules/draw/modify/rotate.rs
T

331 lines
11 KiB
Rust

// Rotate tool — ribbon definition + interactive command.
//
// Command: ROTATE (RO)
// Requires at least one entity selected before starting.
// Step 1: pick rotation center
// Step 2: specify a relative rotation angle, or choose Reference
//
// Reference rotation: the reference angle may be typed or measured between
// two points. The new absolute angle is then typed or picked from the center;
// the applied rotation is new-angle - reference-angle.
use acadrust::Handle;
use cadkernel::geom2d::{self, Curve as KernelCurve};
use glam::DVec3;
use crate::t;
use crate::command::{CadCommand, CmdResult, DynField, EntityTransform, WorkingPlane};
use crate::modules::draw::defaults;
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
const TAU: f64 = std::f64::consts::TAU;
// ── Ribbon definition ──────────────────────────────────────────────────────
pub fn tool() -> ToolDef {
ToolDef {
id: "ROTATE",
label: "Rotate",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/rotate.svg")),
event: ModuleEvent::Command("ROTATE".to_string()),
}
}
// ── Command implementation ─────────────────────────────────────────────────
enum Step {
Center,
Angle { center: DVec3 },
RefFirst { center: DVec3 },
RefSecond { center: DVec3, first: DVec3 },
RefNew { center: DVec3, ref_angle: f64 },
}
pub struct RotateCommand {
handles: Vec<Handle>,
wire_models: Vec<WireModel>,
step: Step,
default_angle: f64, // degrees
plane: WorkingPlane,
}
impl RotateCommand {
pub fn new(handles: Vec<Handle>, wire_models: Vec<WireModel>) -> Self {
Self {
handles,
wire_models,
step: Step::Center,
default_angle: defaults::get_rotate_angle(),
plane: WorkingPlane::default(),
}
}
fn commit(&self, center: DVec3, angle_rad: f64) -> CmdResult {
defaults::set_rotate_angle(angle_rad.to_degrees());
CmdResult::TransformSelected(
self.handles.clone(),
EntityTransform::Rotate {
center,
axis: self.plane.z,
angle_rad,
},
)
}
fn angle_guide(&self, center: DVec3, radius: f64, angle: f64) -> Vec<WireModel> {
if radius <= f64::EPSILON {
return vec![];
}
let center_local = self.plane.to_local(center);
let reference = self.plane.to_world(DVec3::new(
center_local.x + radius,
center_local.y,
center_local.z,
));
let mut guides = vec![WireModel::solid_f64(
"rotate_reference".into(),
vec![center.to_array(), reference.to_array()],
WireModel::CYAN,
false,
)];
if angle.abs() <= f64::EPSILON {
return guides;
}
let (start, end, reverse) = if angle > 0.0 {
(0.0, angle, false)
} else {
(angle, 0.0, true)
};
let Some(arc) = geom2d::bounded_arc([center_local.x, center_local.y], radius, start, end)
else {
return guides;
};
let mut points = KernelCurve::Arc(arc).tessellate_angle(TAU / 64.0);
if reverse {
points.reverse();
}
guides.push(WireModel::solid_f64(
"rotate_angle".into(),
points
.into_iter()
.map(|point| {
self.plane
.to_world(DVec3::new(point[0], point[1], center_local.z))
.to_array()
})
.collect(),
WireModel::CYAN,
false,
));
guides
}
}
impl CadCommand for RotateCommand {
fn set_working_plane(&mut self, plane: WorkingPlane) {
self.plane = plane;
}
fn name(&self) -> &'static str {
"ROTATE"
}
fn prompt(&self) -> String {
match &self.step {
Step::Center => t!(
"ROTATE Specify rotation center [%{count} objects]:",
count = self.handles.len()
)
.into_owned(),
Step::Angle { .. } => {
let a = format!("{:.4}", self.default_angle);
t!("ROTATE Specify rotation angle <%{a}>:", a = a).into_owned()
}
Step::RefFirst { .. } => {
t!("ROTATE Specify first reference point or type reference angle:").into_owned()
}
Step::RefSecond { .. } => {
t!("ROTATE Specify second reference point:").into_owned()
}
Step::RefNew { ref_angle, .. } => {
let a = format!("{:.1}°", ref_angle.to_degrees());
t!("ROTATE Specify new absolute angle [ref=%{a}]:", a = a).into_owned()
}
}
}
fn options(&self) -> Vec<crate::command::CmdOption> {
use crate::command::CmdOption;
match self.step {
Step::Angle { .. } => vec![CmdOption::new(t!("Reference").as_ref(), "R")],
_ => vec![],
}
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
match &self.step {
Step::Center => {
self.step = Step::Angle { center: pt };
CmdResult::NeedPoint
}
Step::Angle { center } => {
let center = *center;
let Some(angle_rad) = self.plane.angle(center, pt) else {
return CmdResult::NeedPoint;
};
self.commit(center, angle_rad)
}
Step::RefFirst { center } => {
let center = *center;
self.step = Step::RefSecond { center, first: pt };
CmdResult::NeedPoint
}
Step::RefSecond { center, first } => {
let center = *center;
let Some(ref_angle) = self.plane.angle(*first, pt) else {
return CmdResult::NeedPoint;
};
self.step = Step::RefNew { center, ref_angle };
CmdResult::NeedPoint
}
Step::RefNew { center, ref_angle } => {
let center = *center;
let Some(new_angle) = self.plane.angle(center, pt) else {
return CmdResult::NeedPoint;
};
self.commit(center, new_angle - *ref_angle)
}
}
}
fn on_enter(&mut self) -> CmdResult {
// At the normal angle step, Enter uses the stored default angle.
if let Step::Angle { center } = &self.step {
let center = *center;
return self.commit(center, self.default_angle.to_radians());
}
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let t = text.trim();
match &self.step {
Step::Angle { center } => {
let center = *center;
let low = t.to_ascii_lowercase();
if low == "r" || low == "reference" {
self.step = Step::RefFirst { center };
return Some(CmdResult::NeedPoint);
}
// The value already carries the correct sign when it comes
// from dynamic input.
let deg: f64 = t.replace(',', ".").parse().ok()?;
Some(self.commit(center, deg.to_radians()))
}
Step::RefFirst { center } => {
let center = *center;
let ref_deg: f64 = t.replace(',', ".").parse().ok()?;
self.step = Step::RefNew {
center,
ref_angle: ref_deg.to_radians(),
};
Some(CmdResult::NeedPoint)
}
Step::RefNew { center, ref_angle } => {
let (center, ref_angle) = (*center, *ref_angle);
let new_deg: f64 = t.replace(',', ".").parse().ok()?;
Some(self.commit(center, new_deg.to_radians() - ref_angle))
}
Step::Center | Step::RefSecond { .. } => None,
}
}
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
let (center, angle_rad, guide_angle) = match &self.step {
Step::Angle { center } => {
let Some(angle) = self.plane.angle(*center, pt) else {
return vec![];
};
(*center, angle, angle)
}
Step::RefSecond { first, .. } => {
return vec![WireModel::solid(
"rubber_band".into(),
vec![
[first.x as f32, first.y as f32, first.z as f32],
[pt.x as f32, pt.y as f32, pt.z as f32],
],
WireModel::CYAN,
false,
)];
}
Step::RefNew { center, ref_angle } => {
let Some(angle) = self.plane.angle(*center, pt) else {
return vec![];
};
(*center, angle - *ref_angle, angle)
}
_ => return vec![],
};
let mut previews: Vec<_> = self
.wire_models
.iter()
.map(|w| {
w.rotated_about_axis(
center.as_vec3(),
self.plane.z.as_vec3(),
angle_rad as f32,
)
})
.collect();
let radius = self.plane.vector_to_local(pt - center).truncate().length();
previews.extend(self.angle_guide(center, radius, guide_angle));
previews
}
fn dyn_field(&self) -> DynField {
match self.step {
Step::Angle { .. } | Step::RefNew { .. } => DynField::Angle,
Step::RefFirst { .. } => DynField::Scalar,
_ => DynField::Point,
}
}
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec};
// Both normal rotation and Reference's new angle are absolute cursor
// directions from the center. Reference mode subtracts its stored
// reference angle only when previewing or committing the transform.
match self.step {
Step::Angle { center } | Step::RefNew { center, .. } => Some(DynSpec {
anchor: DynAnchor::Point(center),
fields: vec![DynFieldSpec::new(DynRole::Angle)],
guide: DynGuide::None,
ref_point: None,
}),
_ => None,
}
}
fn dyn_commit_as_text(&self) -> bool {
matches!(self.step, Step::Angle { .. } | Step::RefFirst { .. })
}
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
match self.step {
Step::Angle { center } | Step::RefNew { center, .. } => self
.plane
.angle(center, cursor)
.map(|angle| crate::command::dyn_display_angle_deg(angle as f32) as f64),
_ => None,
}
}
}