mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-14 14:26:45 -04:00
LINE: resolve deferred TAN first point with typed @distance<angle> (#788)
* LINE: resolve deferred TAN first point with typed @distance<angle> * LINE: collapse the nested if-let (clippy collapsible_if) Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
5 files changed
+69
-1
No files matched your search
@@ -428,7 +428,21 @@ impl Interactive for LineM {
|
||||
&& let Some(first) = self.first
|
||||
{
|
||||
match i {
|
||||
Input::Point(p) => return self.resolve_first(s, first, snap::LineEnd::Point(p)),
|
||||
Input::Point(p) => {
|
||||
// A deferred TAN first point with a typed `@d<a` next point: resolve
|
||||
// exactly from the direction and distance the user gave.
|
||||
if let Some((dist, ang)) = s.last_polar.take()
|
||||
&& let Some(t) = snap::tangent_point_in_direction(&first.curve, ang, first.at)
|
||||
{
|
||||
let e = t + Vec2::new(ang.cos(), ang.sin()) * dist;
|
||||
self.handles.push(s.add_entity(line(t, e))?);
|
||||
self.pts = vec![t, e];
|
||||
s.last_point = e;
|
||||
self.first = None;
|
||||
return Ok(Step::Continue);
|
||||
}
|
||||
return self.resolve_first(s, first, snap::LineEnd::Point(p));
|
||||
}
|
||||
Input::Deferred(d) => return self.resolve_first(s, first, snap::LineEnd::Deferred(d)),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -574,6 +574,11 @@ pub struct Session {
|
||||
/// The last segment given at a point prompt (its base point and the point): polar angles
|
||||
/// relative to the last segment (POLARMODE bit 1) are measured from it.
|
||||
pub last_segment: Option<(Vec2, Vec2)>,
|
||||
/// The last typed `@distance<angle` input, as `(distance, angle in radians)`.
|
||||
/// Set by [`Session::typed`] and consumed by commands that resolve a deferred
|
||||
/// tangent first point (e.g. [`cmd::draw::LineM`]) when the next point is typed
|
||||
/// rather than clicked. Cleared at [`Session::start`].
|
||||
pub last_polar: Option<(f64, f64)>,
|
||||
/// Point modifiers pending at the current point prompt (`FROM`, `M2P`, `.x`, `<a`…).
|
||||
pub point_mods: Vec<pointmod::Frame>,
|
||||
/// Cursor position in world coordinates (from the UI; used for direct distance entry).
|
||||
@@ -635,6 +640,7 @@ impl Session {
|
||||
last_command: None,
|
||||
last_point: Vec2::ZERO,
|
||||
last_segment: None,
|
||||
last_polar: None,
|
||||
point_mods: Vec::new(),
|
||||
cursor: Vec2::ZERO,
|
||||
cursor_deferred: None,
|
||||
@@ -830,6 +836,7 @@ impl Session {
|
||||
|
||||
/// Start a command by name or alias as if typed at the command line.
|
||||
pub fn start(&mut self, name: &str) -> Result<()> {
|
||||
self.last_polar = None;
|
||||
let name = name.trim();
|
||||
let (transparent, name) = match name.strip_prefix('\'') {
|
||||
Some(n) => (true, n),
|
||||
@@ -1320,6 +1327,7 @@ impl Session {
|
||||
}
|
||||
if prompt.accept.point {
|
||||
if let Some(p) = prompt::parse_point_with(tt, self.last_point, &self.angle_settings()) {
|
||||
self.last_polar = prompt::parse_polar_text(tt, &self.angle_settings());
|
||||
return self.input(Input::Point(p));
|
||||
}
|
||||
// Direct distance entry along the tracking path the cursor is on, from the point it
|
||||
|
||||
@@ -51,6 +51,16 @@ pub struct Prompt {
|
||||
pub picker: Option<Picker>,
|
||||
}
|
||||
|
||||
/// Parse `@distance<angle` (relative polar), returning `(distance, angle in radians)`.
|
||||
/// Returns `None` for any other form.
|
||||
pub fn parse_polar_text(text: &str, angles: &crate::units::AngleSettings) -> Option<(f64, f64)> {
|
||||
let body = text.trim().strip_prefix('@')?; // relative form only
|
||||
let (d, a) = body.split_once('<')?;
|
||||
let dist = d.trim().parse::<f64>().ok()?;
|
||||
let angle = angles.amount(a.trim())?;
|
||||
(dist.is_finite() && angle.is_finite()).then_some((dist, angle))
|
||||
}
|
||||
|
||||
/// A visual chooser for a prompt ([`Prompt::picker`]).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
@@ -96,6 +96,21 @@ pub struct Deferred {
|
||||
pub at: Vec2,
|
||||
}
|
||||
|
||||
/// The point on `curve` whose tangent has direction `dir`, picking the solution
|
||||
/// nearest `hint`. Used to resolve a deferred TAN first point when the next point
|
||||
/// is typed as `@distance<angle`.
|
||||
pub fn tangent_point_in_direction(curve: &SnapCurve, dir: f64, hint: Vec2) -> Option<Vec2> {
|
||||
match curve {
|
||||
SnapCurve::Circle { center, radius } => {
|
||||
let n = dir + std::f64::consts::FRAC_PI_2;
|
||||
let c1 = *center + Vec2::new(n.cos(), n.sin()) * *radius;
|
||||
let c2 = *center - Vec2::new(n.cos(), n.sin()) * *radius;
|
||||
Some(if c1.dist(hint) < c2.dist(hint) { c1 } else { c2 })
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// One end of a line being solved: a known point or a deferred snap.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum LineEnd {
|
||||
|
||||
@@ -983,6 +983,27 @@ fn line_first_point_deferred_tangent() {
|
||||
assert!(line_ends(&s)[1].0.near(b, 1e-12));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_deferred_tangent_with_typed_polar() {
|
||||
let mut s = Session::new();
|
||||
s.cmdline("circle 0,0 5").unwrap();
|
||||
s.execute("osnap", &json!({"modes": ["tan"]})).unwrap();
|
||||
s.cmdline("line").unwrap();
|
||||
snap_click(&mut s, Vec2::new(0.1, 5.2)); // deferred TAN, near the top of the circle
|
||||
s.cmdline("@150<218").unwrap();
|
||||
s.cmdline("").unwrap();
|
||||
let ends = line_ends(&s);
|
||||
assert_eq!(ends.len(), 1);
|
||||
let (a, b) = ends[0];
|
||||
// The start is the tangent point on the circle.
|
||||
assert!((a.len() - 5.0).abs() < 1e-9, "start not on circle: {a:?}");
|
||||
// The typed direction and distance are exact.
|
||||
let v = b - a;
|
||||
assert!((v.len() - 150.0).abs() < 1e-9, "length was {}", v.len());
|
||||
let angle = v.y.atan2(v.x).to_degrees().rem_euclid(360.0);
|
||||
assert!((angle - 218.0).abs() < 1e-9, "angle was {angle}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_belt_tangent_to_tangent() {
|
||||
let mut s = Session::new();
|
||||
|
||||
Reference in new issue
Block a user