Skip to content

Add tangents_to_point for QuadBez - #417

Open
Keavon wants to merge 1 commit into
linebender:mainfrom
Keavon:tangents-to-point-quadratic
Open

Add tangents_to_point for QuadBez#417
Keavon wants to merge 1 commit into
linebender:mainfrom
Keavon:tangents-to-point-quadratic

Conversation

@Keavon

@Keavon Keavon commented Feb 15, 2025

Copy link
Copy Markdown
Contributor

Follows up #288 which added tangents_to_point() to CubicBez, by adding it to QuadBez now as well.

Comment thread src/quadbez.rs
Comment on lines +120 to +127
let a = self.p0.to_vec2() - 2.0 * self.p1.to_vec2() + self.p2.to_vec2();
let b = 2.0 * (self.p1.to_vec2() - self.p0.to_vec2());
let c = self.p0.to_vec2() - p.to_vec2();

// coefficients of x(t) \cross x'(t)
let c2 = a.cross(b);
let c1 = -2.0 * c.cross(a);
let c0 = b.cross(c);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like a double check on the math here, both in terms of its correctness (I'm not certain of it) and its formatting (is it more faithful to the canonical Bézier equations to reverse the orders of the terms, swap the order of the cross product and negate it, etc.?). This mostly arose from pattern recognition based fiddling and I wasn't able to quite grasp its relationship with the Bézier equations on Wikipedia. So please confirm this is right.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be a little clearer:

      // Coefficients of (c + b t + a t^2) x (b + 2 a t) = 0.
      let c2 = b.cross(a);
      let c1 = 2.0 * c.cross(a);
      let c0 = c.cross(b);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd drop the "= 0" in the comment, but otherwise I agree that it's easier to follow this way.

I'd also maybe add a comment like this near the top:

// Reparameterize `self - p` as `a t^2 + b t + c`

@waywardmonkeys

Copy link
Copy Markdown
Contributor

should grow some tests, these are from Codex and would be cleaner if another PR (#556) of mine lands (since they wouldn't have to call .deriv().eval(t).to_vec2() but could call .tangent(t):

  #[test]
  fn quadbez_tangents_to_point_recovers_known_t() {
      let q = QuadBez::new((0.0, 0.0), (1.0, 2.0), (3.0, 0.0));

      for &t in &[0.0, 0.2, 0.5, 0.8, 1.0] {
          let pt = q.eval(t);
          let tan = q.deriv().eval(t).to_vec2();

          // Pick points along the tangent line at t.
          for &scale in &[-2.0, -0.5, 0.5, 3.0] {
              let p = pt + scale * tan;
              let roots = q.tangents_to_point(p);
              assert!(
                  roots.iter().any(|r| (*r - t).abs() <= 1e-12),
                  "expected t={t} in roots {roots:?} for point {p:?}"
              );
          }
      }
  }

  #[test]
  fn quadbez_tangents_to_point_finds_two_symmetric_solutions() {
      let q = QuadBez::new((-1.0, 0.0), (0.0, 1.0), (1.0, 0.0));

      // This point is the intersection of the tangent lines at t = 0.25 and t = 0.75.
      let roots = q.tangents_to_point((0.0, 0.625).into());

      assert_eq!(roots.len(), 2, "expected two roots, got {roots:?}");
      assert!((roots[0] - 0.25).abs() <= 1e-12, "roots = {roots:?}");
      assert!((roots[1] - 0.75).abs() <= 1e-12, "roots = {roots:?}");
  }

  #[test]
  fn quadbez_tangents_to_point_handles_endpoint_tangents() {
      let q = QuadBez::new((0.0, 0.0), (1.0, 2.0), (3.0, 0.0));

      let p0 = q.p0 + 2.0 * q.deriv().eval(0.0).to_vec2();
      let roots0 = q.tangents_to_point(p0);
      assert!(
          roots0.iter().any(|r| r.abs() <= 1e-12),
          "expected root near 0, got {roots0:?}"
      );

      let p1 = q.p2 - 1.5 * q.deriv().eval(1.0).to_vec2();
      let roots1 = q.tangents_to_point(p1);
      assert!(
          roots1.iter().any(|r| (*r - 1.0).abs() <= 1e-12),
          "expected root near 1, got {roots1:?}"
      );
  }

  #[test]
  fn quadbez_tangents_to_point_can_return_no_solutions() {
      let q = QuadBez::new((-1.0, 0.0), (0.0, 1.0), (1.0, 0.0));

      // For this symmetric arch, points on the y-axis above y = 1 have no real
  tangent lines.
      let roots = q.tangents_to_point((0.0, 1.5).into());
      assert!(roots.is_empty(), "expected no roots, got {roots:?}");
  }

  #[test]
  fn quadbez_tangents_to_point_near_linear_curve() {
      let q = QuadBez::new((0.0, 0.0), (0.5, 1e-9), (1.0, 0.0));
      let t = 0.3;
      let p = q.eval(t) + 0.75 * q.deriv().eval(t).to_vec2();
      let roots = q.tangents_to_point(p);

      assert!(
          roots.iter().any(|r| (*r - t).abs() <= 1e-9),
          "expected t={t} in roots {roots:?}"
      );
  }

@waywardmonkeys waywardmonkeys left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs tests and a rebase forward.

Comment thread src/quadbez.rs
Comment on lines +120 to +127
let a = self.p0.to_vec2() - 2.0 * self.p1.to_vec2() + self.p2.to_vec2();
let b = 2.0 * (self.p1.to_vec2() - self.p0.to_vec2());
let c = self.p0.to_vec2() - p.to_vec2();

// coefficients of x(t) \cross x'(t)
let c2 = a.cross(b);
let c1 = -2.0 * c.cross(a);
let c0 = b.cross(c);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would be a little clearer:

      // Coefficients of (c + b t + a t^2) x (b + 2 a t) = 0.
      let c2 = b.cross(a);
      let c1 = 2.0 * c.cross(a);
      let c0 = c.cross(b);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants