How to draw perpendicular lines in cetz?

I’m trying to draw a simple diagram that illustrates the dot product, like this one:

but i’m not seeing how to make the perpendicular line going through the end of “a” to “b”.
I could calc the distance I guess but I was wondering if there’s some geometric way like in the Cetz tutorial when the tangent on the unit circle is drawn?

#cetz.canvas({
  import cetz.draw: *
  scale(4)
  arc((), start: 15deg, stop: 35deg, radius: 5mm, mode: "PIE", fill: color.mix((green, 20%), white), anchor: "origin")
  let orig = (0, 0)
  let (o, a, b) = ((0, 0), (35deg, 1cm), (15deg, 1.25cm))
  line(o, a, mark: (end: ">"), name: "v1")
  line(o, b, mark: (end: ">"), name: "v2")
  line((), ("v1", "|-", "v2"), stroke: red) // doesn't work...
  line((o, 0.7, b), (a: (), b: o, number: -0.5, angle: 90deg), stroke: blue) // almost there but doesn't pass through 'a'
})

You can use the rotate function in combination with intersections.

#import "@preview/cetz:0.4.1"

#cetz.canvas({
  import cetz.draw: *
  scale(4)

  let rotation = 15deg
  let theta = 20deg

  rotate(rotation)
  arc(
    (),
    start: 0deg,
    stop: theta,
    radius: 5mm,
    mode: "PIE",
    anchor: "origin",
  )
  let (o, a, b) = ((0, 0), (theta, 1cm), (0deg, 1.25cm))
  line(o, a, mark: (end: ">"), name: "v1", stroke: blue)
  line(o, b, mark: (end: ">"), name: "v2", stroke: red)

  hide({
    line(
      a,
      (
        // Calculate the x coordinate of `a` from polar coordinates
        a.at(1) * calc.cos(a.at(0)),
        -1,
      ),
      name: "perpendicular line",
    )
  })

  intersections("x", "perpendicular line", "v2")
  line(a, "x.0")
})

Another option is adding a custom coordinate:

#cetz.canvas({
  import cetz.draw: *

  // <point-a> perpendicular <point-b> <point-c>
  register-coordinate-resolver((ctx, coord) => {
    if type(coord) == array and coord.len() >= 2 and coord.at(1) == "perpendicular" {
      import cetz: vector
      let (p, _, a, b) = coord
      (_, p, a, b) = cetz.coordinate.resolve(ctx, p, a, b)

      let ap = vector.sub(p, a)
      let ab = vector.sub(b, a)

      return vector.add(a, vector.scale(ab, vector.dot(ap,ab)/vector.dot(ab,ab)))      
    }
    
    return coord
  })
  
  scale(4)
  
  let orig = (0, 0)
  let (o, a, b) = ((0, 0), (35deg, 1cm), (15deg, 1.25cm))
  cetz.angle.angle(o, b, a, fill: color.mix((green, 20%), white),
    radius: 0.5)
  line(o, a, mark: (end: ">"), name: "v1")
  line(o, b, mark: (end: ">"), name: "v2")
  

  // Because of a bug with `line`, curstom coordinates do not work properly,
  // so we create a named anchor.
  anchor("pt", ("v1.end", "perpendicular", "v2.start", "v2.end"))
  line("v1.end", "pt", stroke: red)
})

(You can use cetz.angle.angle to draw angles between 3 points.)

3 Likes

The new version of cetz (≥ 0.4.2) supports (project: pt, onto: (a, b)) or the short form (pt, "_|_", a, b) for projecting a point onto a line through two points.

4 Likes