fix(modify): repair reference transforms

Refs #609
This commit is contained in:
Hakan Seven 2026-08-01 00:13:24 +03:00
commit 54124dc252
2 changed files with 221 additions and 104 deletions

View file

@ -3,8 +3,11 @@
// Command: ROTATE (RO) // Command: ROTATE (RO)
// Requires at least one entity selected before starting. // Requires at least one entity selected before starting.
// Step 1: pick rotation center // Step 1: pick rotation center
// Step 2: pick reference point (defines the 0° direction) // Step 2: specify a relative rotation angle, or choose Reference
// Step 3: pick destination point → rotates by (dest_angle - ref_angle) //
// 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 acadrust::Handle;
use glam::DVec3; use glam::DVec3;
@ -29,8 +32,10 @@ pub fn tool() -> ToolDef {
enum Step { enum Step {
Center, Center,
RefPoint { center: DVec3 }, Angle { center: DVec3 },
Angle { center: DVec3, ref_angle: f64 }, RefFirst { center: DVec3 },
RefSecond { center: DVec3, first: DVec3 },
RefNew { center: DVec3, ref_angle: f64 },
} }
pub struct RotateCommand { pub struct RotateCommand {
@ -49,6 +54,14 @@ impl RotateCommand {
default_angle: defaults::get_rotate_angle(), default_angle: defaults::get_rotate_angle(),
} }
} }
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, angle_rad },
)
}
} }
impl CadCommand for RotateCommand { impl CadCommand for RotateCommand {
@ -62,56 +75,76 @@ impl CadCommand for RotateCommand {
"ROTATE Specify rotation center [{} objects]:", "ROTATE Specify rotation center [{} objects]:",
self.handles.len() self.handles.len()
), ),
Step::RefPoint { .. } => { Step::Angle { .. } => format!(
"ROTATE Specify reference point (or skip: type angle directly):".into() "ROTATE Specify rotation angle <{:.4}>:",
self.default_angle
),
Step::RefFirst { .. } => {
"ROTATE Specify first reference point or type reference angle:".into()
} }
Step::Angle { ref_angle, .. } => format!( Step::RefSecond { .. } => "ROTATE Specify second reference point:".into(),
"ROTATE Specify destination or type angle in degrees <{:.4}> [ref={:.1}°]:", Step::RefNew { ref_angle, .. } => format!(
self.default_angle, "ROTATE Specify new absolute angle [ref={:.1}°]:",
ref_angle.to_degrees() ref_angle.to_degrees()
), ),
} }
} }
fn options(&self) -> Vec<crate::command::CmdOption> {
use crate::command::CmdOption;
match self.step {
Step::Angle { .. } => vec![CmdOption::new("Reference", "R")],
_ => vec![],
}
}
fn on_point(&mut self, pt: DVec3) -> CmdResult { fn on_point(&mut self, pt: DVec3) -> CmdResult {
match &self.step { match &self.step {
Step::Center => { Step::Center => {
self.step = Step::RefPoint { center: pt }; self.step = Step::Angle { center: pt };
CmdResult::NeedPoint CmdResult::NeedPoint
} }
Step::RefPoint { center } => { Step::Angle { center } => {
let center = *center; let center = *center;
let ref_angle = (pt.y - center.y).atan2(pt.x - center.x); let direction = pt - center;
self.step = Step::Angle { center, ref_angle }; if direction.x.hypot(direction.y) <= f64::EPSILON {
return CmdResult::NeedPoint;
}
let angle_rad = direction.y.atan2(direction.x);
self.commit(center, angle_rad)
}
Step::RefFirst { center } => {
let center = *center;
self.step = Step::RefSecond { center, first: pt };
CmdResult::NeedPoint CmdResult::NeedPoint
} }
Step::Angle { center, ref_angle } => { Step::RefSecond { center, first } => {
let direction = pt - *first;
if direction.x.hypot(direction.y) <= f64::EPSILON {
return CmdResult::NeedPoint;
}
let center = *center; let center = *center;
let ref_angle = *ref_angle; let ref_angle = direction.y.atan2(direction.x);
let dest_angle = (pt.y - center.y).atan2(pt.x - center.x); self.step = Step::RefNew { center, ref_angle };
let delta = dest_angle - ref_angle; CmdResult::NeedPoint
defaults::set_rotate_angle(delta.to_degrees()); }
self.default_angle = delta.to_degrees(); Step::RefNew { center, ref_angle } => {
CmdResult::TransformSelected( let center = *center;
self.handles.clone(), let direction = pt - center;
EntityTransform::Rotate { if direction.x.hypot(direction.y) <= f64::EPSILON {
center, return CmdResult::NeedPoint;
angle_rad: delta, }
}, let new_angle = direction.y.atan2(direction.x);
) self.commit(center, new_angle - *ref_angle)
} }
} }
} }
fn on_enter(&mut self) -> CmdResult { fn on_enter(&mut self) -> CmdResult {
// At Angle step: Enter uses the stored default angle. // At the normal angle step, Enter uses the stored default angle.
if let Step::Angle { center, .. } = &self.step { if let Step::Angle { center } = &self.step {
let center = *center; let center = *center;
let angle_rad = self.default_angle.to_radians(); return self.commit(center, self.default_angle.to_radians());
return CmdResult::TransformSelected(
self.handles.clone(),
EntityTransform::Rotate { center, angle_rad },
);
} }
CmdResult::Cancel CmdResult::Cancel
} }
@ -120,52 +153,67 @@ impl CadCommand for RotateCommand {
} }
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> { fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
// A typed angle rotates directly about the centre. Accept it at the let t = text.trim();
// reference-point step too (the prompt offers "skip: type angle match &self.step {
// directly") — otherwise typing the angle there did nothing and the Step::Angle { center } => {
// command cancelled on the next Enter, so the objects never rotated. let center = *center;
let center = match &self.step { let low = t.to_ascii_lowercase();
Step::RefPoint { center } => *center, if low == "r" || low == "reference" {
Step::Angle { center, .. } => *center, self.step = Step::RefFirst { center };
Step::Center => return None, return Some(CmdResult::NeedPoint);
}; }
// The value already carries the correct sign (the dynamic-input // The value already carries the correct sign when it comes
// layer applies the cursor's side for a bare magnitude). // from dynamic input.
let deg: f64 = text.trim().replace(',', ".").parse().ok()?; let deg: f64 = t.replace(',', ".").parse().ok()?;
defaults::set_rotate_angle(deg); Some(self.commit(center, deg.to_radians()))
Some(CmdResult::TransformSelected( }
self.handles.clone(), Step::RefFirst { center } => {
EntityTransform::Rotate { let center = *center;
center, let ref_deg: f64 = t.replace(',', ".").parse().ok()?;
angle_rad: deg.to_radians(), 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> { fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
let (center, ref_angle) = match &self.step { let (center, angle_rad) = match &self.step {
Step::Angle { center, ref_angle } => (*center, *ref_angle), Step::Angle { center } => {
Step::RefPoint { center } => { let direction = pt - *center;
// Show a reference line from center to cursor only. if direction.x.hypot(direction.y) <= f64::EPSILON {
return vec![];
}
(*center, direction.y.atan2(direction.x))
}
Step::RefSecond { first, .. } => {
return vec![WireModel::solid( return vec![WireModel::solid(
"rubber_band".into(), "rubber_band".into(),
vec![ vec![
[center.x as f32, center.y as f32, center.z as f32], [first.x as f32, first.y as f32, first.z as f32],
[pt.x as f32, pt.y as f32, pt.z as f32], [pt.x as f32, pt.y as f32, pt.z as f32],
], ],
WireModel::CYAN, WireModel::CYAN,
false, false,
)]; )];
} }
Step::RefNew { center, ref_angle } => {
let direction = pt - *center;
if direction.x.hypot(direction.y) <= f64::EPSILON {
return vec![];
}
(*center, direction.y.atan2(direction.x) - *ref_angle)
}
_ => return vec![], _ => return vec![],
}; };
let dest_angle = (pt.y - center.y).atan2(pt.x - center.x);
let angle_rad = dest_angle - ref_angle;
// Track the live SIGNED rotation (relative to the reference) so that
// committing with Enter rotates the way the cursor is dragging — the
// dynamic-input box shows the unsigned magnitude, but the committed
// value must keep its direction (clockwise = negative).
self.default_angle = angle_rad.to_degrees();
// Object ghosts rotated to the new angle. The rotation sweep arc is // Object ghosts rotated to the new angle. The rotation sweep arc is
// drawn by the dynamic-input overlay (polar guide), not here. // drawn by the dynamic-input overlay (polar guide), not here.
self.wire_models self.wire_models
@ -176,41 +224,43 @@ impl CadCommand for RotateCommand {
fn dyn_field(&self) -> DynField { fn dyn_field(&self) -> DynField {
match self.step { match self.step {
Step::Angle { .. } => DynField::Angle, Step::Angle { .. } | Step::RefNew { .. } => DynField::Angle,
Step::RefFirst { .. } => DynField::Scalar,
_ => DynField::Point, _ => DynField::Point,
} }
} }
fn dyn_spec(&self) -> Option<crate::command::DynSpec> { fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec}; use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec};
// Rotation angle is measured about the CENTRE, swept from the reference // Both normal rotation and Reference's new angle are absolute cursor
// direction. The polar guide arc is anchored at the centre and starts // directions from the center. Reference mode subtracts its stored
// at the reference (via ref_point), with the value box centred on it. // reference angle only when previewing or committing the transform.
if let Step::Angle { center, ref_angle } = self.step { match self.step {
let ref_dir = DVec3::new(center.x + ref_angle.cos(), center.y + ref_angle.sin(), center.z); Step::Angle { center } | Step::RefNew { center, .. } => Some(DynSpec {
Some(DynSpec {
anchor: DynAnchor::Point(center), anchor: DynAnchor::Point(center),
fields: vec![DynFieldSpec::new(DynRole::Angle)], fields: vec![DynFieldSpec::new(DynRole::Angle)],
guide: DynGuide::Polar, guide: DynGuide::Polar,
ref_point: Some(ref_dir), ref_point: Some(center + DVec3::X),
}) }),
} else { _ => None,
None
} }
} }
fn dyn_commit_as_text(&self) -> bool { fn dyn_commit_as_text(&self) -> bool {
matches!(self.step, Step::Angle { .. }) matches!(self.step, Step::Angle { .. } | Step::RefFirst { .. })
} }
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> { fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
// The rotation amount = cursor direction from the centre minus the match self.step {
// reference angle, so the box reads the actual rotation. Step::Angle { center } | Step::RefNew { center, .. } => {
if let Step::Angle { center, ref_angle } = &self.step { let direction = cursor - center;
let dest = (cursor.y - center.y).atan2(cursor.x - center.x); (direction.x.hypot(direction.y) > f64::EPSILON).then(|| {
Some(crate::command::dyn_display_angle_deg((dest - ref_angle) as f32) as f64) crate::command::dyn_display_angle_deg(
} else { direction.y.atan2(direction.x) as f32,
None ) as f64
})
}
_ => None,
} }
} }
} }

View file

@ -8,14 +8,14 @@
// selection tracks the cursor from the first move onward. // selection tracks the cursor from the first move onward.
// //
// Reference scaling: type `R` at step 2 to define the factor as // Reference scaling: type `R` at step 2 to define the factor as
// new-length / reference-length: // new-length / reference-length. The reference length may be typed or
// Step 2a: specify reference length (pick a point or type a length) // measured between two points; the new length may be typed or measured
// Step 2b: specify new length (pick a point or type a length) // from the scale base.
use acadrust::Handle; use acadrust::Handle;
use glam::DVec3; use glam::DVec3;
use crate::command::{CadCommand, CmdResult, EntityTransform}; use crate::command::{CadCommand, CmdResult, DynField, EntityTransform};
use crate::modules::draw::defaults; use crate::modules::draw::defaults;
use crate::modules::{IconKind, ModuleEvent, ToolDef}; use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel; use crate::scene::model::wire_model::WireModel;
@ -34,8 +34,11 @@ enum Step {
Base, Base,
/// Default flow: factor is the cursor distance from `base`. /// Default flow: factor is the cursor distance from `base`.
Factor { base: DVec3 }, Factor { base: DVec3 },
/// Reference flow: defining the reference length from `base`. /// Reference flow: waiting for the first of two independent points, or a
RefLen { base: DVec3 }, /// typed reference length.
RefFirst { base: DVec3 },
/// Reference flow: measuring the reference length from `first`.
RefSecond { base: DVec3, first: DVec3 },
/// Reference flow: factor is `cursor_dist / ref_dist` from `base`. /// Reference flow: factor is `cursor_dist / ref_dist` from `base`.
RefNew { base: DVec3, ref_dist: f64 }, RefNew { base: DVec3, ref_dist: f64 },
} }
@ -85,11 +88,12 @@ impl CadCommand for ScaleCommand {
"SCALE Specify scale factor <{:.4}>:", "SCALE Specify scale factor <{:.4}>:",
self.default_factor self.default_factor
), ),
Step::RefLen { .. } => { Step::RefFirst { .. } => {
"SCALE Specify reference length (pick a point or type a length):".into() "SCALE Specify first reference point or type reference length:".into()
} }
Step::RefSecond { .. } => "SCALE Specify second reference point:".into(),
Step::RefNew { ref_dist, .. } => format!( Step::RefNew { ref_dist, .. } => format!(
"SCALE Specify new length (pick a point or type a length) [ref={:.3}]:", "SCALE Specify new length from base or type a length [ref={:.3}]:",
ref_dist ref_dist
), ),
} }
@ -112,18 +116,32 @@ impl CadCommand for ScaleCommand {
} }
Step::Factor { base } => { Step::Factor { base } => {
let base = *base; let base = *base;
let factor = base.distance(pt).max(1e-6); let factor = base.distance(pt);
if factor <= f64::EPSILON {
return CmdResult::NeedPoint;
}
self.commit(base, factor) self.commit(base, factor)
} }
Step::RefLen { base } => { Step::RefFirst { base } => {
let base = *base; let base = *base;
let ref_dist = base.distance(pt).max(1e-6); self.step = Step::RefSecond { base, first: pt };
CmdResult::NeedPoint
}
Step::RefSecond { base, first } => {
let base = *base;
let ref_dist = first.distance(pt);
if ref_dist <= f64::EPSILON {
return CmdResult::NeedPoint;
}
self.step = Step::RefNew { base, ref_dist }; self.step = Step::RefNew { base, ref_dist };
CmdResult::NeedPoint CmdResult::NeedPoint
} }
Step::RefNew { base, ref_dist } => { Step::RefNew { base, ref_dist } => {
let base = *base; let base = *base;
let new_dist = base.distance(pt).max(1e-6); let new_dist = base.distance(pt);
if new_dist <= f64::EPSILON {
return CmdResult::NeedPoint;
}
self.commit(base, new_dist / *ref_dist) self.commit(base, new_dist / *ref_dist)
} }
} }
@ -149,13 +167,13 @@ impl CadCommand for ScaleCommand {
// `R` / `Reference` switches to reference scaling. // `R` / `Reference` switches to reference scaling.
let low = t.to_ascii_lowercase(); let low = t.to_ascii_lowercase();
if low == "r" || low == "reference" { if low == "r" || low == "reference" {
self.step = Step::RefLen { base }; self.step = Step::RefFirst { base };
return Some(CmdResult::NeedPoint); return Some(CmdResult::NeedPoint);
} }
let factor: f64 = t.replace(',', ".").parse().ok()?; let factor: f64 = t.replace(',', ".").parse().ok()?;
(factor > 0.0).then(|| self.commit(base, factor)) (factor > 0.0).then(|| self.commit(base, factor))
} }
Step::RefLen { base } => { Step::RefFirst { base } => {
let base = *base; let base = *base;
let ref_dist: f64 = t.replace(',', ".").parse().ok()?; let ref_dist: f64 = t.replace(',', ".").parse().ok()?;
if ref_dist > 0.0 { if ref_dist > 0.0 {
@ -164,6 +182,7 @@ impl CadCommand for ScaleCommand {
} }
None None
} }
Step::RefSecond { .. } => None,
Step::RefNew { base, ref_dist } => { Step::RefNew { base, ref_dist } => {
let (base, ref_dist) = (*base, *ref_dist); let (base, ref_dist) = (*base, *ref_dist);
let new_len: f64 = t.replace(',', ".").parse().ok()?; let new_len: f64 = t.replace(',', ".").parse().ok()?;
@ -179,21 +198,22 @@ impl CadCommand for ScaleCommand {
Step::Factor { base } => (*base, base.distance(pt).max(1e-6) as f32), Step::Factor { base } => (*base, base.distance(pt).max(1e-6) as f32),
// Reference flow, new-length step: factor = cursor_dist / ref_dist. // Reference flow, new-length step: factor = cursor_dist / ref_dist.
Step::RefNew { base, ref_dist } => { Step::RefNew { base, ref_dist } => {
(*base, (base.distance(pt).max(1e-6) / ref_dist) as f32) (*base, (base.distance(pt) / ref_dist) as f32)
} }
// Reference-length step: rubber-band only, no factor defined yet. // The reference length is measured between two points independent
Step::RefLen { base } => { // of the scale base.
Step::RefSecond { first, .. } => {
return vec![WireModel::solid( return vec![WireModel::solid(
"rubber_band".into(), "rubber_band".into(),
vec![ vec![
[base.x as f32, base.y as f32, base.z as f32], [first.x as f32, first.y as f32, first.z as f32],
[pt.x as f32, pt.y as f32, pt.z as f32], [pt.x as f32, pt.y as f32, pt.z as f32],
], ],
WireModel::CYAN, WireModel::CYAN,
false, false,
)]; )];
} }
Step::Base => return vec![], Step::Base | Step::RefFirst { .. } => return vec![],
}; };
let mut out: Vec<WireModel> = self let mut out: Vec<WireModel> = self
.wire_models .wire_models
@ -211,4 +231,51 @@ impl CadCommand for ScaleCommand {
)); ));
out out
} }
fn dyn_field(&self) -> DynField {
match self.step {
Step::Factor { .. } => DynField::Scalar,
Step::RefFirst { .. } | Step::RefNew { .. } => DynField::Distance,
_ => DynField::Point,
}
}
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec};
match self.step {
Step::Factor { base } => Some(DynSpec {
anchor: DynAnchor::Point(base),
fields: vec![DynFieldSpec::new(DynRole::Factor)],
guide: DynGuide::Radius,
ref_point: None,
}),
Step::RefFirst { base } => Some(DynSpec {
anchor: DynAnchor::Point(base),
fields: vec![DynFieldSpec::new(DynRole::Distance)],
guide: DynGuide::None,
ref_point: None,
}),
Step::RefNew { base, .. } => Some(DynSpec {
anchor: DynAnchor::Point(base),
fields: vec![DynFieldSpec::new(DynRole::Distance)],
guide: DynGuide::Radius,
ref_point: None,
}),
_ => None,
}
}
fn dyn_commit_as_text(&self) -> bool {
matches!(
self.step,
Step::Factor { .. } | Step::RefFirst { .. } | Step::RefNew { .. }
)
}
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
match self.step {
Step::Factor { base } | Step::RefNew { base, .. } => Some(base.distance(cursor)),
_ => None,
}
}
} }