SCALE: Reference option (#17) (#27)

SCALE > Reference printed "not available yet". It now asks for the reference length (typed, or two points) and the new length (typed, or a point measured from the base point), and scales by new / reference. Zero, negative or non-finite lengths are rejected and asked again.
This commit is contained in:
MarekSpodBiedry authored and GitHub committed 2026-10-10 12:57:31 -04:00
1 parent 986307f764
commit a1df2f2c8c
2 files changed
+113 -1

No files matched your search

+74 -1
View File
@@ -1297,6 +1297,10 @@ struct SelectThen {
objs: Vec<Handle>,
pts: Vec<Vec2>,
reference: bool,
/// SCALE Reference: first point of a reference length given by two points.
ref_from: Option<Vec2>,
/// SCALE Reference: the reference length, once known.
ref_len: Option<f64>,
copy_mode: bool,
window: Option<cadcraft_geom::Bounds2>,
ask_erase: bool,
@@ -1310,6 +1314,8 @@ impl SelectThen {
objs: Vec::new(),
pts: Vec::new(),
reference: false,
ref_from: None,
ref_len: None,
copy_mode: false,
window: None,
ask_erase: false,
@@ -1357,6 +1363,54 @@ impl SelectThen {
_ => Ok(Step::Continue),
}
}
/// SCALE Reference: reference length (typed, or two points), then new length (typed, or a
/// point measured from the base point). The scale factor is new length / reference length.
fn scale_reference(&mut self, s: &mut Session, i: Input) -> Result<Step> {
let Some(base) = self.pts.first().copied() else { return Ok(Step::Done) };
match (self.ref_len, self.ref_from, i) {
(None, None, Input::Point(p)) => {
self.ref_from = Some(p);
Ok(Step::Continue)
}
(None, None, Input::Text(t)) => {
self.ref_len = Some(require_length(number(&t))?);
Ok(Step::Continue)
}
(None, None, Input::Enter) => {
self.ref_len = Some(1.0);
Ok(Step::Continue)
}
(None, Some(a), Input::Point(p)) => {
self.ref_len = Some(require_length(Some(a.dist(p)))?);
Ok(Step::Continue)
}
(Some(r), _, Input::Point(p)) => self.scale_by(s, base, base.dist(p) / r),
(Some(r), _, Input::Text(t)) => {
let n = require_length(number(&t))?;
self.scale_by(s, base, n / r)
}
(_, _, Input::Enter) => Ok(Step::Done),
_ => Ok(Step::Continue),
}
}
fn scale_by(&mut self, s: &mut Session, base: Vec2, f: f64) -> Result<Step> {
let f = require_length(Some(f))?;
transform_entities(s, &self.objs, &Mat3::scale_about(base, f), self.copy_mode)?;
s.set_selection(Vec::new());
Ok(Step::Done)
}
}
/// A finite length greater than zero (rejects 0, negatives, NaN and infinity).
fn positive_length(v: Option<f64>) -> Option<f64> {
v.filter(|f| f.is_finite() && *f > 1e-12)
}
/// [`positive_length`], or the error that makes the prompt ask again.
fn require_length(v: Option<f64>) -> Result<f64> {
positive_length(v).ok_or_else(|| EngineError::Other("Requires a positive number.".into()))
}
impl Interactive for SelectThen {
@@ -1406,6 +1460,13 @@ impl Interactive for SelectThen {
let pts_from = if self.op == Op::Stretch { 2 } else { 0 };
let k = n.saturating_sub(pts_from);
let bp = if self.op == Op::Stretch { self.pts.get(2).copied() } else { base };
if self.op == Op::Scale && self.reference {
return match (self.ref_len, self.ref_from) {
(Some(_), _) => Prompt::new("Specify new length", Accept::POINT_OR_NUMBER).base_opt(base),
(None, Some(p)) => Prompt::new("Specify second point", Accept::POINT).base(p),
(None, None) => Prompt::new("Specify reference length", Accept::POINT_OR_NUMBER).default("1"),
};
}
match (self.op, k) {
(Op::Move | Op::Copy | Op::Stretch, 0) => Prompt::new("Specify base point", Accept::POINT).kw(&["Displacement"]),
(Op::Move | Op::Stretch, _) => Prompt::new("Specify second point or <use first point as displacement>", Accept::POINT).base_opt(bp),
@@ -1458,12 +1519,19 @@ impl Interactive for SelectThen {
}
let off = if self.op == Op::Stretch { 2 } else { 0 };
let k = self.pts.len() - off;
if self.op == Op::Scale && self.reference {
return self.scale_reference(s, i);
}
match (self.op, k, i) {
(_, _, Input::Keyword(kw)) if kw == "Copy" => {
self.copy_mode = true;
s.echo("Rotating/scaling a copy of the selected objects.");
Ok(Step::Continue)
}
(Op::Scale, 1, Input::Keyword(kw)) if kw == "Reference" => {
self.reference = true;
Ok(Step::Continue)
}
(_, _, Input::Keyword(kw)) if kw == "Reference" => {
self.reference = true;
s.echo("Reference: not available yet; enter a value.");
@@ -1568,7 +1636,12 @@ impl Interactive for SelectThen {
let m = match self.op {
Op::Move | Op::Copy | Op::Stretch => Mat3::translate(c - base),
Op::Rotate => Mat3::rotate_about(base, base.angle_to(c)),
Op::Scale => Mat3::scale_about(base, base.dist(c).max(1e-9)),
Op::Scale if !self.reference => Mat3::scale_about(base, base.dist(c).max(1e-9)),
Op::Scale => match self.ref_len.and_then(|r| positive_length(Some(base.dist(c) / r))) {
Some(f) => Mat3::scale_about(base, f),
None if self.ref_len.is_some() => return Vec::new(),
None => return self.ref_from.map(|a| vec![line(a, c)]).unwrap_or_default(),
},
Op::Mirror if !self.ask_erase => Mat3::mirror(base, c),
_ => return Vec::new(),
};
+39
View File
@@ -342,6 +342,45 @@ fn copy_rotate_scale_mirror() {
assert_eq!(s.doc().unwrap().model.len(), 5);
}
#[test]
fn scale_reference_typed_and_picked() {
// Issue #17: SCALE ▸ Reference scales by new length / reference length.
fn end(s: &Session) -> Vec2 {
let e = s.doc().unwrap().model.iter().next().unwrap();
let EntityKind::Line(l) = &e.kind else { panic!("expected a line") };
l.b.xy()
}
let mut s = Session::new();
s.execute("line", &json!({"points": [[0, 0], [56, 0]]})).unwrap();
// Typed lengths: 56 → 100.
for t in ["scale", "all", "", "0,0", "r"] {
s.cmdline(t).unwrap();
}
assert_eq!(s.current_prompt().unwrap().message, "Specify reference length");
s.cmdline("56").unwrap();
assert_eq!(s.current_prompt().unwrap().message, "Specify new length");
s.cmdline("100").unwrap();
assert!(s.running.is_none());
assert!(end(&s).near(Vec2::new(100.0, 0.0), 1e-9));
// Reference from two points, a zero new length is rejected, new length picked from the base point.
for t in ["scale", "all", "", "0,0", "r", "0,0", "100,0"] {
s.cmdline(t).unwrap();
}
s.cmdline("0").unwrap();
assert!(s.running.is_some());
s.cmdline("0,25").unwrap();
assert!(s.running.is_none());
assert!(end(&s).near(Vec2::new(25.0, 0.0), 1e-9));
// A zero reference length is rejected and asked again.
for t in ["scale", "all", "", "0,0", "r", "0"] {
s.cmdline(t).unwrap();
}
assert_eq!(s.current_prompt().unwrap().message, "Specify reference length");
s.cmdline("25").unwrap();
s.cmdline("50").unwrap();
assert!(end(&s).near(Vec2::new(50.0, 0.0), 1e-9));
}
#[test]
fn layers_and_properties() {
let mut s = Session::new();